001/* 002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.benchmarking.common.report; 017 018import de.cuioss.benchmarking.common.output.OutputDirectoryStructure; 019import de.cuioss.benchmarking.common.util.JsonSerializationHelper; 020import de.cuioss.tools.logging.CuiLogger; 021 022import java.io.IOException; 023import java.io.InputStream; 024import java.nio.charset.StandardCharsets; 025import java.nio.file.Files; 026import java.nio.file.Path; 027import java.time.Instant; 028import java.util.LinkedHashMap; 029import java.util.Map; 030 031import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Files.Data.BENCHMARK_DATA_JSON; 032import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Files.Html.ERROR_404; 033import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Files.Support.ROBOTS_TXT; 034import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Files.Support.SITEMAP_XML; 035import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Api.*; 036import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Defaults.N_A; 037import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.JsonFields.*; 038import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Templates.NOT_FOUND_FORMAT; 039import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Templates.PATH_PREFIX; 040import static de.cuioss.benchmarking.common.util.BenchmarkingLogMessages.INFO; 041 042/** 043 * Generates GitHub Pages ready deployment structure from benchmark artifacts. 044 * Since all report files are now written directly to gh-pages-ready by other generators, 045 * this class only needs to generate additional deployment assets (404.html, robots.txt, sitemap.xml). 046 * <p> 047 * NO COPYING is performed - everything else is already in gh-pages-ready. 048 */ 049public class GitHubPagesGenerator { 050 051 private static final CuiLogger LOGGER = new CuiLogger(GitHubPagesGenerator.class); 052 public static final String CREATED_API_ENDPOINT_S = "Created API endpoint: %s"; 053 054 /** 055 * Generates deployment-specific assets for GitHub Pages. 056 * This assumes all report files are already written to gh-pages-ready directory. 057 * 058 * @param structure the output directory structure 059 * @throws IOException if file operations fail 060 */ 061 public void generateDeploymentAssets(OutputDirectoryStructure structure) throws IOException { 062 LOGGER.info(INFO.PREPARING_GITHUB_PAGES); 063 064 // Ensure deployment directory exists 065 structure.ensureDirectories(); 066 067 Path deployDir = structure.getDeploymentDir(); 068 LOGGER.info(INFO.DEPLOY_DIRECTORY, deployDir); 069 070 // Generate deployment-specific pages 071 generate404Page(deployDir); 072 generateRobotsTxt(deployDir); 073 generateSitemap(deployDir); 074 075 // Generate API endpoints 076 generateApiEndpoints(structure); 077 078 LOGGER.info(INFO.GITHUB_PAGES_READY); 079 } 080 081 /** 082 * Generates a 404 error page. 083 */ 084 private void generate404Page(Path deployDir) throws IOException { 085 LOGGER.debug("Generating additional pages"); 086 String html404 = loadTemplate(ERROR_404); 087 Files.writeString(deployDir.resolve(ERROR_404), html404); 088 } 089 090 /** 091 * Generates robots.txt for search engines. 092 */ 093 private void generateRobotsTxt(Path deployDir) throws IOException { 094 String robotsTxt = loadTemplate(ROBOTS_TXT); 095 Files.writeString(deployDir.resolve(ROBOTS_TXT), robotsTxt); 096 } 097 098 /** 099 * Generates a basic sitemap.xml. 100 */ 101 private void generateSitemap(Path deployDir) throws IOException { 102 String sitemap = loadTemplate(SITEMAP_XML); 103 Files.writeString(deployDir.resolve(SITEMAP_XML), sitemap); 104 } 105 106 /** 107 * Generates API endpoints for programmatic access. 108 */ 109 private void generateApiEndpoints(OutputDirectoryStructure structure) throws IOException { 110 LOGGER.debug("Creating API endpoints"); 111 112 Path deployDir = structure.getDeploymentDir(); 113 Path apiDir = structure.getApiDir(); 114 115 // Create API structure 116 createLatestEndpoint(deployDir, apiDir); 117 createBenchmarksEndpoint(deployDir, apiDir); 118 createStatusEndpoint(deployDir, apiDir); 119 } 120 121 private void createLatestEndpoint(Path deployDir, Path apiDir) throws IOException { 122 Path latestFile = apiDir.resolve(LATEST_JSON); 123 Path benchmarkDataFile = deployDir.resolve("data").resolve(BENCHMARK_DATA_JSON); 124 125 Map<String, Object> latestData = new LinkedHashMap<>(); 126 latestData.put(TIMESTAMP, Instant.now().toString()); 127 latestData.put(STATUS, STATUS_SUCCESS); 128 129 Map<String, Object> summary = new LinkedHashMap<>(); 130 if (Files.exists(benchmarkDataFile)) { 131 String content = Files.readString(benchmarkDataFile); 132 @SuppressWarnings("unchecked") Map<String, Object> data = JsonSerializationHelper.jsonToMap(content); 133 // Count benchmarks from the benchmarks list 134 Object benchmarks = data.get(BENCHMARKS); 135 int count = (benchmarks instanceof java.util.List<?> list) ? list.size() : 0; 136 summary.put(TOTAL_BENCHMARKS, count); 137 // Get performance grade from overview 138 Object overview = data.get(OVERVIEW); 139 if (overview instanceof Map<?, ?> overviewMap) { 140 Object grade = overviewMap.get(PERFORMANCE_GRADE); 141 summary.put(PERFORMANCE_GRADE_KEY, grade != null ? grade : N_A); 142 } else { 143 summary.put(PERFORMANCE_GRADE_KEY, N_A); 144 } 145 } else { 146 summary.put(TOTAL_BENCHMARKS, 0); 147 summary.put(PERFORMANCE_GRADE_KEY, N_A); 148 } 149 latestData.put(SUMMARY, summary); 150 151 Map<String, String> links = new LinkedHashMap<>(); 152 links.put(BENCHMARKS, API_BENCHMARKS_PATH); 153 links.put("badges", BADGES_PATH); 154 latestData.put(LINKS, links); 155 156 Files.writeString(latestFile, JsonSerializationHelper.toJson(latestData)); 157 LOGGER.debug(CREATED_API_ENDPOINT_S, latestFile); 158 } 159 160 private void createBenchmarksEndpoint(Path deployDir, Path apiDir) throws IOException { 161 Path benchmarksFile = apiDir.resolve(BENCHMARKS_JSON); 162 Path benchmarkDataFile = deployDir.resolve("data").resolve(BENCHMARK_DATA_JSON); 163 164 if (Files.exists(benchmarkDataFile)) { 165 // Extract benchmark data from benchmark-data.json 166 String content = Files.readString(benchmarkDataFile); 167 @SuppressWarnings("unchecked") Map<String, Object> data = JsonSerializationHelper.jsonToMap(content); 168 Map<String, Object> benchmarksData = new LinkedHashMap<>(); 169 benchmarksData.put(BENCHMARKS, data.getOrDefault(BENCHMARKS, new LinkedHashMap<>())); 170 benchmarksData.put(GENERATED, Instant.now().toString()); 171 172 Files.writeString(benchmarksFile, JsonSerializationHelper.toJson(benchmarksData)); 173 } else { 174 Map<String, Object> benchmarksData = new LinkedHashMap<>(); 175 benchmarksData.put(BENCHMARKS, new LinkedHashMap<>()); 176 benchmarksData.put(GENERATED, Instant.now().toString()); 177 178 Files.writeString(benchmarksFile, JsonSerializationHelper.toJson(benchmarksData)); 179 } 180 181 LOGGER.debug(CREATED_API_ENDPOINT_S, benchmarksFile); 182 } 183 184 private void createStatusEndpoint(Path deployDir, Path apiDir) throws IOException { 185 Path statusFile = apiDir.resolve(STATUS_JSON); 186 Path benchmarkDataFile = deployDir.resolve("data").resolve(BENCHMARK_DATA_JSON); 187 188 String now = Instant.now().toString(); 189 Map<String, Object> statusData = new LinkedHashMap<>(); 190 statusData.put(STATUS, STATUS_HEALTHY); 191 statusData.put(TIMESTAMP, now); 192 193 String lastRun = now; 194 if (Files.exists(benchmarkDataFile)) { 195 String content = Files.readString(benchmarkDataFile); 196 @SuppressWarnings("unchecked") Map<String, Object> data = JsonSerializationHelper.jsonToMap(content); 197 Object metadata = data.get(METADATA); 198 if (metadata instanceof Map<?, ?> metadataMap) { 199 Object ts = metadataMap.get(TIMESTAMP); 200 if (ts instanceof String tsString) { 201 lastRun = tsString; 202 } 203 } 204 } 205 statusData.put(LAST_RUN, lastRun); 206 207 Files.writeString(statusFile, JsonSerializationHelper.toJson(statusData)); 208 LOGGER.debug(CREATED_API_ENDPOINT_S, statusFile); 209 } 210 211 /** 212 * Loads a template from the classpath resources. 213 * 214 * @param templateName the name of the template file 215 * @return the template content as a string 216 * @throws IOException if the template cannot be loaded 217 */ 218 private String loadTemplate(String templateName) throws IOException { 219 String resourcePath = PATH_PREFIX + templateName; 220 try (InputStream is = getClass().getResourceAsStream(resourcePath)) { 221 if (is == null) { 222 throw new IOException(NOT_FOUND_FORMAT.formatted(resourcePath)); 223 } 224 return new String(is.readAllBytes(), StandardCharsets.UTF_8); 225 } 226 } 227 228}