001package com.streamconverter.command.impl.analysis;
002
003import com.streamconverter.command.AbstractStreamCommand;
004import java.io.IOException;
005import java.io.InputStream;
006import java.io.OutputStream;
007import java.time.Instant;
008import java.util.*;
009import java.util.stream.Collectors;
010import javax.xml.parsers.DocumentBuilder;
011import javax.xml.parsers.DocumentBuilderFactory;
012import org.w3c.dom.Document;
013import org.w3c.dom.Element;
014import org.w3c.dom.NodeList;
015
016/**
017 * PMD XML レポートを AI 可読性の高い Markdown 形式に変換するコマンド
018 *
019 * <p>StreamConverter アーキテクチャに基づく実装例として、InputStreamからOutputStreamへの
020 * 純粋な変換処理を提供します。PMDの冗長なXMLレポートをMarkdown要約形式に変換し、 AI分析や人間による可読性を向上させます。
021 *
022 * <p><strong>変換仕様:</strong>
023 *
024 * <ul>
025 *   <li>ルール別違反統計(上位20位)
026 *   <li>ファイル別問題統計(上位15ファイル)
027 *   <li>優先度分布と影響度分析
028 *   <li>標準化されたISO-8601タイムスタンプ
029 * </ul>
030 *
031 * <p><strong>使用例:</strong>
032 *
033 * <pre>
034 * // StreamConverter パイプラインでの使用
035 * StreamConverter converter = new StreamConverter(
036 *     new PmdXmlToMarkdownCommand()
037 * );
038 * converter.run(pmdXmlInputStream, markdownOutputStream);
039 * </pre>
040 */
041public class PmdXmlToMarkdownCommand extends AbstractStreamCommand {
042  /** Creates a new command instance. */
043  public PmdXmlToMarkdownCommand() {}
044
045  /**
046   * PMD XML InputStream を Markdown OutputStream に変換
047   *
048   * @param input PMD XML レポートの入力ストリーム
049   * @param output Markdown レポートの出力ストリーム
050   * @throws IOException XML解析エラーまたはI/O例外の場合
051   */
052  @Override
053  protected void executeInternal(InputStream input, OutputStream output) throws IOException {
054    try {
055      // StreamConverter原則: InputStreamから読み取り、OutputStreamに書き込み
056      List<PmdViolation> violations = parseXmlStream(input);
057      String markdownReport = generateMarkdownReport(violations);
058      output.write(markdownReport.getBytes("UTF-8"));
059
060    } catch (Exception e) {
061      throw new IOException("Failed to convert PMD XML to Markdown: " + e.getMessage(), e);
062    }
063  }
064
065  @Override
066  protected String getCommandDetails() {
067    return "PmdXmlToMarkdownCommand: Converts PMD XML reports to AI-readable Markdown format";
068  }
069
070  /**
071   * PMD XML ストリームからバイオレーション情報を解析
072   *
073   * @param input PMD XML入力ストリーム
074   * @return 解析されたバイオレーションのリスト
075   * @throws Exception XML解析エラーの場合
076   */
077  private List<PmdViolation> parseXmlStream(InputStream input) throws Exception {
078    DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
079    Document doc = builder.parse(input);
080
081    NodeList fileNodes = doc.getElementsByTagName("file");
082    List<PmdViolation> violations = new ArrayList<>();
083
084    for (int i = 0; i < fileNodes.getLength(); i++) {
085      Element fileElement = (Element) fileNodes.item(i);
086      String fileName = fileElement.getAttribute("name");
087
088      NodeList violationNodes = fileElement.getElementsByTagName("violation");
089      for (int j = 0; j < violationNodes.getLength(); j++) {
090        Element violationElement = (Element) violationNodes.item(j);
091
092        PmdViolation violation =
093            new PmdViolation(
094                extractRelativePath(fileName),
095                Integer.parseInt(violationElement.getAttribute("beginline")),
096                violationElement.getAttribute("rule"),
097                violationElement.getAttribute("ruleset"),
098                Integer.parseInt(violationElement.getAttribute("priority")),
099                violationElement.getTextContent().trim(),
100                violationElement.getAttribute("class"),
101                violationElement.getAttribute("method"),
102                violationElement.getAttribute("variable"));
103        violations.add(violation);
104      }
105    }
106
107    return violations;
108  }
109
110  /** StreamConverterプロジェクト内の相対パスを抽出 */
111  private String extractRelativePath(String fullPath) {
112    int index = fullPath.indexOf("streamconverter-");
113    return index != -1 ? fullPath.substring(index) : fullPath;
114  }
115
116  /**
117   * バイオレーション情報から AI 可読 Markdown レポートを生成
118   *
119   * @param violations 解析されたバイオレーション情報
120   * @return Markdown形式のレポート文字列
121   */
122  private String generateMarkdownReport(List<PmdViolation> violations) {
123    StringBuilder md = new StringBuilder();
124
125    // ヘッダー情報(ISO-8601標準形式)
126    md.append("# PMD Code Quality Analysis Report\n\n");
127    md.append("**Generated**: ").append(Instant.now().toString()).append("\n");
128    md.append("**Total Violations**: ").append(violations.size()).append("\n\n");
129
130    // 違反数上位のルール分析
131    generateTopRulesSection(md, violations);
132
133    // ファイル別問題統計
134    generateFileStatisticsSection(md, violations);
135
136    // 優先度分布分析
137    generatePriorityDistributionSection(md, violations);
138
139    return md.toString();
140  }
141
142  /** 上位ルール違反セクションを生成 */
143  private void generateTopRulesSection(StringBuilder md, List<PmdViolation> violations) {
144    Map<String, Long> ruleStats =
145        violations.stream()
146            .collect(Collectors.groupingBy(PmdViolation::rule, Collectors.counting()));
147
148    md.append("## 🎯 Top Code Smell Rules\n\n");
149    md.append("| Rank | Rule | Count | Category |\n");
150    md.append("|------|------|-------|----------|\n");
151
152    int rank = 1;
153    ruleStats.entrySet().stream()
154        .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
155        .limit(20)
156        .forEach(
157            entry -> {
158              String category =
159                  violations.stream()
160                      .filter(v -> v.rule().equals(entry.getKey()))
161                      .findFirst()
162                      .map(PmdViolation::ruleset)
163                      .orElse("Unknown");
164              md.append(
165                  String.format(
166                      "| %d | %s | %d | %s |\n", rank, entry.getKey(), entry.getValue(), category));
167            });
168  }
169
170  /** ファイル統計セクションを生成 */
171  private void generateFileStatisticsSection(StringBuilder md, List<PmdViolation> violations) {
172    Map<String, Long> fileStats =
173        violations.stream()
174            .collect(Collectors.groupingBy(PmdViolation::file, Collectors.counting()));
175
176    md.append("\n## 📁 Files with Most Issues\n\n");
177    md.append("| File | Violations |\n");
178    md.append("|------|------------|\n");
179
180    fileStats.entrySet().stream()
181        .sorted(Map.Entry.<String, Long>comparingByValue().reversed())
182        .limit(15)
183        .forEach(
184            entry -> {
185              md.append(
186                  String.format(
187                      "| %s | %d |\n",
188                      entry.getKey().replaceAll(".*/(\\w+\\.java)", "$1"), entry.getValue()));
189            });
190  }
191
192  /** 優先度分布セクションを生成 */
193  private void generatePriorityDistributionSection(
194      StringBuilder md, List<PmdViolation> violations) {
195    Map<Integer, Long> priorityStats =
196        violations.stream()
197            .collect(Collectors.groupingBy(PmdViolation::priority, Collectors.counting()));
198
199    md.append("\n## ⚡ Priority Distribution\n\n");
200    md.append("| Priority | Count | Description |\n");
201    md.append("|----------|-------|-------------|\n");
202
203    priorityStats.entrySet().stream()
204        .sorted(Map.Entry.comparingByKey())
205        .forEach(
206            entry -> {
207              String desc =
208                  switch (entry.getKey()) {
209                    case 1 -> "🔴 High - Critical issues";
210                    case 2 -> "🟡 Medium - Important issues";
211                    case 3 -> "🟢 Low - Minor issues";
212                    case 4 -> "ℹ️ Info - Informational";
213                    default -> "❓ Unknown";
214                  };
215              md.append(
216                  String.format("| %d | %d | %s |\n", entry.getKey(), entry.getValue(), desc));
217            });
218  }
219
220  /**
221   * PMD違反情報を表すレコードクラス
222   *
223   * @param file ファイルパス
224   * @param line 行番号
225   * @param rule ルール名
226   * @param ruleset ルールセット
227   * @param priority 優先度
228   * @param description 説明
229   * @param className クラス名
230   * @param method メソッド名
231   * @param variable 変数名
232   */
233  public record PmdViolation(
234      String file,
235      int line,
236      String rule,
237      String ruleset,
238      int priority,
239      String description,
240      String className,
241      String method,
242      String variable) {}
243}