001package com.streamconverter.analysis;
002
003import java.io.IOException;
004import java.nio.file.Files;
005import java.nio.file.Path;
006import java.nio.file.Paths;
007import java.util.*;
008import java.util.stream.Collectors;
009import javax.xml.parsers.DocumentBuilder;
010import javax.xml.parsers.DocumentBuilderFactory;
011import org.slf4j.Logger;
012import org.slf4j.LoggerFactory;
013import org.w3c.dom.Document;
014import org.w3c.dom.Element;
015import org.w3c.dom.NodeList;
016
017/**
018 * PMD XMLレポートをAI可読形式に変換するユーティリティクラス
019 *
020 * <p>PMDの生XMLレポートは構造化されているが冗長で、AI解析には適していない。 このクラスは以下の形式に変換する:
021 *
022 * <ul>
023 *   <li>Markdown要約レポート - AI可読性の高いサマリー
024 *   <li>CSVレポート - スプレッドシート分析用
025 *   <li>JSON構造化レポート - プログラム解析用
026 * </ul>
027 */
028public class PmdReportConverter {
029  private static final Logger LOG = LoggerFactory.getLogger(PmdReportConverter.class);
030
031  /** Creates a new converter. */
032  public PmdReportConverter() {}
033
034  /**
035   * CLI entry point for converting a PMD XML report.
036   *
037   * @param args arguments where args[0] is the XML file and args[1] is the optional output
038   *     directory
039   * @throws Exception if conversion fails
040   */
041  public static void main(String[] args) throws Exception {
042    if (args.length < 1) {
043      if (LOG.isErrorEnabled()) {
044        LOG.error("Usage: PmdReportConverter <pmd-xml-file> [output-dir]");
045      }
046      System.exit(1);
047    }
048
049    String xmlFile = args[0];
050    String outputDir = args.length > 1 ? args[1] : "build/reports/pmd/converted";
051
052    PmdReportConverter converter = new PmdReportConverter();
053    converter.convertReport(xmlFile, outputDir);
054  }
055
056  /**
057   * Converts the given PMD XML report into multiple formats.
058   *
059   * @param xmlFilePath path to the PMD XML file
060   * @param outputDir directory where converted reports will be written
061   * @throws Exception if processing fails
062   */
063  public void convertReport(String xmlFilePath, String outputDir) throws Exception {
064    Path xmlPath = Paths.get(xmlFilePath);
065    Path outputPath = Paths.get(outputDir);
066    Files.createDirectories(outputPath);
067
068    // XML解析
069    List<PmdViolation> violations = parseXmlReport(xmlPath);
070
071    // 各形式で出力
072    generateMarkdownReport(violations, outputPath.resolve("pmd-summary.md"));
073    generateCsvReport(violations, outputPath.resolve("pmd-violations.csv"));
074    generateJsonReport(violations, outputPath.resolve("pmd-report.json"));
075
076    if (LOG.isInfoEnabled()) {
077      LOG.info("✅ PMD報告書変換完了:");
078    }
079    if (LOG.isInfoEnabled()) {
080      LOG.info("   📄 Markdown: {}", outputPath.resolve("pmd-summary.md"));
081    }
082    if (LOG.isInfoEnabled()) {
083      LOG.info("   📊 CSV: {}", outputPath.resolve("pmd-violations.csv"));
084    }
085    if (LOG.isInfoEnabled()) {
086      LOG.info("   🔗 JSON: {}", outputPath.resolve("pmd-report.json"));
087    }
088  }
089
090  private List<PmdViolation> parseXmlReport(Path xmlPath) throws Exception {
091    DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
092    Document doc = builder.parse(xmlPath.toFile());
093
094    NodeList fileNodes = doc.getElementsByTagName("file");
095    List<PmdViolation> violations = new ArrayList<>();
096
097    for (int i = 0; i < fileNodes.getLength(); i++) {
098      Element fileElement = (Element) fileNodes.item(i);
099      String fileName = fileElement.getAttribute("name");
100
101      NodeList violationNodes = fileElement.getElementsByTagName("violation");
102      for (int j = 0; j < violationNodes.getLength(); j++) {
103        Element violationElement = (Element) violationNodes.item(j);
104
105        PmdViolation violation =
106            new PmdViolation(
107                extractRelativePath(fileName),
108                Integer.parseInt(violationElement.getAttribute("beginline")),
109                violationElement.getAttribute("rule"),
110                violationElement.getAttribute("ruleset"),
111                Integer.parseInt(violationElement.getAttribute("priority")),
112                violationElement.getTextContent().trim(),
113                violationElement.getAttribute("class"),
114                violationElement.getAttribute("method"),
115                violationElement.getAttribute("variable"));
116        violations.add(violation);
117      }
118    }
119
120    return violations;
121  }
122
123  private String extractRelativePath(String fullPath) {
124    // StreamConverterプロジェクト内の相対パスを抽出
125    int index = fullPath.indexOf("streamconverter-");
126    return index != -1 ? fullPath.substring(index) : fullPath;
127  }
128
129  private void generateMarkdownReport(List<PmdViolation> violations, Path outputPath)
130      throws IOException {
131    StringBuilder md = new StringBuilder();
132
133    md.append("# PMD Code Quality Analysis Report\n\n");
134    md.append("**Generated**: ").append(new Date()).append("\n");
135    md.append("**Total Violations**: ").append(violations.size()).append("\n\n");
136
137    // 違反数上位のルール
138    Map<String, Long> ruleStats =
139        violations.stream()
140            .collect(Collectors.groupingBy(PmdViolation::rule, Collectors.counting()));
141
142    md.append("## 🎯 Top Code Smell Rules\n\n");
143    md.append("| Rank | Rule | Count | Category |\n");
144    md.append("|------|------|-------|----------|\n");
145
146    ruleStats.entrySet().stream()
147        .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
148        .limit(20)
149        .forEach(
150            entry -> {
151              String category =
152                  violations.stream()
153                      .filter(v -> v.rule().equals(entry.getKey()))
154                      .findFirst()
155                      .map(PmdViolation::ruleset)
156                      .orElse("Unknown");
157              md.append(
158                  String.format(
159                      "| | %s | %d | %s |\n", entry.getKey(), entry.getValue(), category));
160            });
161
162    // ファイル別統計
163    Map<String, Long> fileStats =
164        violations.stream()
165            .collect(Collectors.groupingBy(PmdViolation::file, Collectors.counting()));
166
167    md.append("\n## 📁 Files with Most Issues\n\n");
168    md.append("| File | Violations |\n");
169    md.append("|------|------------|\n");
170
171    fileStats.entrySet().stream()
172        .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
173        .limit(15)
174        .forEach(
175            entry -> {
176              md.append(
177                  String.format(
178                      "| %s | %d |\n",
179                      entry.getKey().replaceAll(".*/(\\w+\\.java)", "$1"), entry.getValue()));
180            });
181
182    // 優先度別統計
183    Map<Integer, Long> priorityStats =
184        violations.stream()
185            .collect(Collectors.groupingBy(PmdViolation::priority, Collectors.counting()));
186
187    md.append("\n## ⚡ Priority Distribution\n\n");
188    md.append("| Priority | Count | Description |\n");
189    md.append("|----------|-------|-------------|\n");
190    priorityStats.entrySet().stream()
191        .sorted(Map.Entry.comparingByKey())
192        .forEach(
193            entry -> {
194              String desc =
195                  switch (entry.getKey()) {
196                    case 1 -> "🔴 High - Critical issues";
197                    case 2 -> "🟡 Medium - Important issues";
198                    case 3 -> "🟢 Low - Minor issues";
199                    case 4 -> "ℹ️ Info - Informational";
200                    default -> "❓ Unknown";
201                  };
202              md.append(
203                  String.format("| %d | %d | %s |\n", entry.getKey(), entry.getValue(), desc));
204            });
205
206    Files.writeString(outputPath, md.toString());
207  }
208
209  private void generateCsvReport(List<PmdViolation> violations, Path outputPath)
210      throws IOException {
211    StringBuilder csv = new StringBuilder();
212    csv.append("File,Line,Rule,Category,Priority,Description,Class,Method,Variable\n");
213
214    violations.forEach(
215        v -> {
216          csv.append(
217              String.format(
218                  "\"%s\",%d,\"%s\",\"%s\",%d,\"%s\",\"%s\",\"%s\",\"%s\"\n",
219                  v.file(),
220                  v.line(),
221                  v.rule(),
222                  v.ruleset(),
223                  v.priority(),
224                  v.description().replace("\"", "\"\""),
225                  v.className(),
226                  v.method(),
227                  v.variable()));
228        });
229
230    Files.writeString(outputPath, csv.toString());
231  }
232
233  private void generateJsonReport(List<PmdViolation> violations, Path outputPath)
234      throws IOException {
235    StringBuilder json = new StringBuilder();
236    json.append("{\n");
237    json.append("  \"summary\": {\n");
238    json.append("    \"totalViolations\": ").append(violations.size()).append(",\n");
239    json.append("    \"generatedAt\": \"").append(new Date()).append("\"\n");
240    json.append("  },\n");
241    json.append("  \"violations\": [\n");
242
243    for (int i = 0; i < violations.size(); i++) {
244      PmdViolation v = violations.get(i);
245      json.append("    {\n");
246      json.append("      \"file\": \"").append(v.file()).append("\",\n");
247      json.append("      \"line\": ").append(v.line()).append(",\n");
248      json.append("      \"rule\": \"").append(v.rule()).append("\",\n");
249      json.append("      \"category\": \"").append(v.ruleset()).append("\",\n");
250      json.append("      \"priority\": ").append(v.priority()).append(",\n");
251      json.append("      \"description\": \"")
252          .append(v.description().replace("\"", "\\\""))
253          .append("\",\n");
254      json.append("      \"class\": \"").append(v.className()).append("\",\n");
255      json.append("      \"method\": \"").append(v.method()).append("\",\n");
256      json.append("      \"variable\": \"").append(v.variable()).append("\"\n");
257      json.append("    }").append(i < violations.size() - 1 ? "," : "").append("\n");
258    }
259
260    json.append("  ]\n");
261    json.append("}\n");
262
263    Files.writeString(outputPath, json.toString());
264  }
265
266  /**
267   * PMD違反情報を表すレコードクラス
268   *
269   * @param file ファイルパス
270   * @param line 行番号
271   * @param rule 違反ルール
272   * @param ruleset ルールセット
273   * @param priority 優先度
274   * @param description 説明
275   * @param className クラス名
276   * @param method メソッド名
277   * @param variable 変数名
278   */
279  public record PmdViolation(
280      String file,
281      int line,
282      String rule,
283      String ruleset,
284      int priority,
285      String description,
286      String className,
287      String method,
288      String variable) {}
289}