ReportView.java

package pro.verron.officestamper;

import com.fasterxml.jackson.core.type.TypeReference;
import picocli.CommandLine;
import pro.verron.officestamper.api.OfficeStamperException;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

import static java.nio.file.Files.writeString;

/// Subcommand that generates an HTML viewer for a traceability report.
@CommandLine.Command(name = "report-view",
                     description = "Generate an HTML viewer for a "
                                   + "traceability report")
public class ReportView
        implements Runnable {

    @CommandLine.Option(names = {"-i", "--input"},
                        required = true,
                        description = "JSON traceability report") private Path input;
    @CommandLine.Option(names = {"-o", "--output"},
                        defaultValue = "traceability.html",
                        description = "Output HTML file") private Path output;

    /// Default constructor.
    public ReportView() {}

    @Override
    public void run() {
        try {
            var mapper = SerializationUtils.newMapper();
            var root = mapper.readTree(input.toFile());
            var resolutionsNode = root.get("resolutions");
            if (resolutionsNode == null) {
                // Fallback for old format or if it was just a list
                resolutionsNode = root.isArray() ? root : null;
            }

            if (resolutionsNode == null) throw new OfficeStamperException(
                    "Could not find resolutions in report");

            List<TraceabilityReport.Resolution> resolutions =
                    mapper.convertValue(
                            resolutionsNode,
                            new TypeReference<>() {});
            var html = generateHtml(resolutions);
            writeString(output, html);
        } catch (IOException e) {
            throw new OfficeStamperException(e);
        }
    }

    private String generateHtml(List<TraceabilityReport.Resolution> resolutions) {
        StringBuilder sb = new StringBuilder();
        sb.append("<html><head><title>Traceability Report</title><style>");
        sb.append("table { border-collapse: collapse; width: 100%; "
                  + "font-family: sans-serif; }");
        sb.append("th, td { border: 1px solid #ddd; padding: 8px; "
                  + "text-align: left; vertical-align: top; }");
        sb.append("th { background-color: #4CAF50; color: white; }");
        sb.append("tr:nth-child(even) { background-color: #f9f9f9; }");
        sb.append("tr:hover { background-color: #f1f1f1; }");
        sb.append("ul { margin: 0; padding-left: 20px; }");
        sb.append("</style></head><body>");
        sb.append("<h1>Office Stamper Traceability Report</h1>");
        sb.append("<table><tr><th>Expression</th><th>Resolved "
                  + "Value</th><th>Nesting Context</th></tr>");
        for (var res : resolutions) {
            sb.append("<tr>");
            sb.append("<td><code>")
              .append(res.expression())
              .append("</code></td>");
            sb.append("<td>")
              .append(res.value())
              .append("</td>");
            sb.append("<td><ul>");
            for (var ctx : res.contextStack()) {
                sb.append("<li>")
                  .append(ctx)
                  .append("</li>");
            }
            sb.append("</ul></td>");
            sb.append("</tr>");
        }
        sb.append("</table></body></html>");
        return sb.toString();
    }
}