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}