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}