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.output;
017
018import lombok.Getter;
019
020import java.io.IOException;
021import java.io.UncheckedIOException;
022import java.nio.file.Files;
023import java.nio.file.Path;
024import java.util.Comparator;
025import java.util.Objects;
026
027/**
028 * Manages the output directory structure for benchmark results.
029 * Provides clear separation between deployable content (gh-pages-ready)
030 * and non-deployable raw data (history, prometheus, wrk).
031 * <p>
032 * Directory structure:
033 * <pre>
034 * benchmark-results/
035 * ├── gh-pages-ready/         (deploymentDir, htmlDir) - All content deployable to GitHub Pages
036 * │   ├── data/               (dataDir) - JSON data files
037 * │   ├── badges/             (badgesDir) - Badge JSON files
038 * │   └── api/                (apiDir) - API endpoint JSON files
039 * ├── history/                (historyDir) - Historical archive data (NOT deployed)
040 * ├── prometheus/             (prometheusRawDir) - Raw Prometheus metrics (NOT deployed)
041 * └── wrk/                    (wrkDir) - WRK raw results (NOT deployed)
042 * </pre>
043 */
044public class OutputDirectoryStructure {
045
046    /** Root benchmark-results directory */
047    @Getter
048    private final Path benchmarkResultsDir;
049
050    /**
051     * Deployment directory (gh-pages-ready) - all content in this directory
052     * is deployable to GitHub Pages
053     */
054    @Getter
055    private final Path deploymentDir;
056
057    /**
058     * HTML directory (same as deployment directory) - HTML files should be
059     * written directly to this directory
060     */
061    @Getter
062    private final Path htmlDir;
063
064    /** Data directory (gh-pages-ready/data) - JSON data files location */
065    @Getter
066    private final Path dataDir;
067
068    /** Badges directory (gh-pages-ready/badges) - Badge JSON files location */
069    @Getter
070    private final Path badgesDir;
071
072    /** API directory (gh-pages-ready/api) - API endpoint JSON files location */
073    @Getter
074    private final Path apiDir;
075
076    // Non-deployed directories (in benchmark-results root)
077    private final Path historyDir;           // benchmark-results/history
078    private final Path prometheusRawDir;     // benchmark-results/prometheus
079    private final Path wrkDir;               // benchmark-results/wrk (WRK module only)
080
081    /**
082     * Creates a new output directory structure.
083     *
084     * @param benchmarkResultsDir the root benchmark-results directory
085     * @throws NullPointerException if benchmarkResultsDir is null
086     */
087    public OutputDirectoryStructure(Path benchmarkResultsDir) {
088        this.benchmarkResultsDir = Objects.requireNonNull(benchmarkResultsDir,
089                "benchmarkResultsDir must not be null");
090        this.deploymentDir = benchmarkResultsDir.resolve("gh-pages-ready");
091        this.htmlDir = deploymentDir;
092        this.dataDir = deploymentDir.resolve("data");
093        this.badgesDir = deploymentDir.resolve("badges");
094        this.apiDir = deploymentDir.resolve("api");
095
096        // Non-deployed directories stay in root
097        this.historyDir = benchmarkResultsDir.resolve("history");
098        this.prometheusRawDir = benchmarkResultsDir.resolve("prometheus");
099        this.wrkDir = benchmarkResultsDir.resolve("wrk");
100    }
101
102    /**
103     * Ensures deployment directories exist, creating them if necessary.
104     * Non-deployed directories (history, prometheus, wrk) are created only when accessed.
105     *
106     * @throws IOException if directory creation fails
107     */
108    public void ensureDirectories() throws IOException {
109        // Create deployment directories (always needed)
110        Files.createDirectories(deploymentDir);
111        Files.createDirectories(dataDir);
112        Files.createDirectories(badgesDir);
113        Files.createDirectories(apiDir);
114
115        // Non-deployed directories are created on-demand by individual getters
116        // to avoid creating unused directories in modules that don't need them
117    }
118
119    /**
120     * Gets the history directory (benchmark-results/history).
121     * Historical archive data should be stored in this directory.
122     * This directory is NOT deployed to GitHub Pages.
123     * Creates the directory if it doesn't exist.
124     *
125     * @return the history directory path
126     * @throws IOException if directory creation fails
127     */
128    public Path getHistoryDir() throws IOException {
129        Files.createDirectories(historyDir);
130        return historyDir;
131    }
132
133    /**
134     * Gets the raw Prometheus metrics directory (benchmark-results/prometheus).
135     * Raw Prometheus data should be stored in this directory.
136     * This directory is NOT deployed to GitHub Pages.
137     * Creates the directory if it doesn't exist.
138     *
139     * @return the raw Prometheus directory path
140     * @throws IOException if directory creation fails
141     */
142    public Path getPrometheusRawDir() throws IOException {
143        Files.createDirectories(prometheusRawDir);
144        return prometheusRawDir;
145    }
146
147    /**
148     * Gets the WRK directory (benchmark-results/wrk).
149     * WRK raw results should be stored in this directory.
150     * This directory is NOT deployed to GitHub Pages.
151     * Creates the directory if it doesn't exist.
152     *
153     * @return the WRK directory path
154     * @throws IOException if directory creation fails
155     */
156    public Path getWrkDir() throws IOException {
157        Files.createDirectories(wrkDir);
158        return wrkDir;
159    }
160
161    /**
162     * Checks if the deployment directory exists.
163     *
164     * @return true if the deployment directory exists, false otherwise
165     */
166    public boolean isDeploymentDirectoryExists() {
167        return Files.exists(deploymentDir);
168    }
169
170    /**
171     * Cleans the deployment directory by deleting it and recreating it.
172     * This ensures a fresh start for report generation.
173     *
174     * @throws IOException if deletion or creation fails
175     */
176    public void cleanDeploymentDirectory() throws IOException {
177        if (Files.exists(deploymentDir)) {
178            deleteDirectoryRecursively(deploymentDir);
179        }
180        Files.createDirectories(deploymentDir);
181        Files.createDirectories(dataDir);
182        Files.createDirectories(badgesDir);
183        Files.createDirectories(apiDir);
184    }
185
186    /**
187     * Recursively deletes a directory and all its contents.
188     *
189     * @param path the directory to delete
190     * @throws IOException if deletion fails
191     */
192    private void deleteDirectoryRecursively(Path path) throws IOException {
193        if (Files.exists(path)) {
194            try (var stream = Files.walk(path)) {
195                stream.sorted(Comparator.reverseOrder())
196                        .forEach(p -> {
197                            try {
198                                Files.delete(p);
199                            } catch (IOException e) {
200                                throw new UncheckedIOException("Failed to delete: " + p, e);
201                            }
202                        });
203            }
204        }
205    }
206
207    @Override
208    public String toString() {
209        return "OutputDirectoryStructure{" +
210                "benchmarkResultsDir=" + benchmarkResultsDir +
211                ", deploymentDir=" + deploymentDir +
212                ", dataDir=" + dataDir +
213                ", badgesDir=" + badgesDir +
214                ", apiDir=" + apiDir +
215                ", historyDir=" + historyDir +
216                ", prometheusRawDir=" + prometheusRawDir +
217                ", wrkDir=" + wrkDir +
218                '}';
219    }
220}