JsonReporter.java
/*******************************************************************************
* Copyright (c) 2025 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.mining.report;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import org.sandbox.mining.report.MiningReport.MatchEntry;
/**
* Generates a JSON report from mining results.
*/
public class JsonReporter {
/**
* Generates a JSON report string from the given mining report.
* Uses manual JSON generation to avoid adding a JSON library dependency.
*
* @param report the mining report
* @return the JSON content
*/
public String generate(MiningReport report) {
StringBuilder sb = new StringBuilder();
sb.append("{\n");
// Summary
sb.append(" \"summary\": {\n");
sb.append(" \"totalMatches\": ").append(report.getMatches().size()).append(",\n");
sb.append(" \"repositories\": {\n");
var fileCounts = report.getFileCounts();
int repoIndex = 0;
for (var entry : fileCounts.entrySet()) {
sb.append(" ").append(jsonString(entry.getKey())).append(": {\n");
sb.append(" \"files\": ").append(entry.getValue()).append(",\n");
var repoMatches = report.getMatchesByRepo().getOrDefault(entry.getKey(), List.of());
sb.append(" \"matches\": ").append(repoMatches.size()).append(",\n");
sb.append(" \"rules\": ").append(report.getDistinctRuleCount(entry.getKey())).append("\n");
sb.append(" }");
if (++repoIndex < fileCounts.size()) {
sb.append(",");
}
sb.append("\n");
}
sb.append(" }\n");
sb.append(" },\n");
// Matches
sb.append(" \"matches\": [\n");
List<MatchEntry> matches = report.getMatches();
for (int i = 0; i < matches.size(); i++) {
MatchEntry match = matches.get(i);
sb.append(" {\n");
sb.append(" \"repository\": ").append(jsonString(match.repoName())).append(",\n");
sb.append(" \"hintFile\": ").append(jsonString(match.hintFile())).append(",\n");
sb.append(" \"rule\": ").append(jsonString(match.ruleName())).append(",\n");
sb.append(" \"file\": ").append(jsonString(match.filePath())).append(",\n");
sb.append(" \"line\": ").append(match.line()).append(",\n");
sb.append(" \"matchedCode\": ").append(jsonString(match.matchedCode()));
if (match.suggestedReplacement() != null) {
sb.append(",\n \"suggestedReplacement\": ").append(jsonString(match.suggestedReplacement()));
}
sb.append("\n }");
if (i < matches.size() - 1) {
sb.append(",");
}
sb.append("\n");
}
sb.append(" ],\n");
// Errors
sb.append(" \"errors\": {\n");
var errors = report.getErrors();
int errorIndex = 0;
for (var errorEntry : errors.entrySet()) {
sb.append(" ").append(jsonString(errorEntry.getKey())).append(": ")
.append(jsonString(errorEntry.getValue()));
if (++errorIndex < errors.size()) {
sb.append(",");
}
sb.append("\n");
}
sb.append(" }\n");
sb.append("}\n");
return sb.toString();
}
/**
* Writes the JSON report to a file.
*
* @param report the mining report
* @param outputDir the output directory
* @throws IOException if file writing fails
*/
public void write(MiningReport report, Path outputDir) throws IOException {
Files.createDirectories(outputDir);
String content = generate(report);
Files.writeString(outputDir.resolve("report.json"), content, StandardCharsets.UTF_8);
}
private static String jsonString(String value) {
if (value == null) {
return "null";
}
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r")
.replace("\t", "\\t") + "\"";
}
}