001package com.streamconverter.command.impl.analysis; 002 003import com.fasterxml.jackson.databind.ObjectMapper; 004import com.fasterxml.jackson.databind.SerializationFeature; 005import com.streamconverter.command.AbstractStreamCommand; 006import com.streamconverter.command.impl.analysis.PmdXmlToMarkdownCommand.PmdViolation; 007import java.io.IOException; 008import java.io.InputStream; 009import java.io.OutputStream; 010import java.time.Instant; 011import java.util.*; 012import javax.xml.parsers.DocumentBuilder; 013import javax.xml.parsers.DocumentBuilderFactory; 014import org.w3c.dom.Document; 015import org.w3c.dom.Element; 016import org.w3c.dom.NodeList; 017 018/** 019 * PMD XML レポートを JSON 形式に変換するコマンド 020 * 021 * <p>StreamConverter アーキテクチャに準拠したJSON変換実装例です。 Jackson ObjectMapperを使用して型安全で構造化されたJSON出力を生成し、 022 * プログラムによる解析やAPI連携に適したデータ形式を提供します。 023 * 024 * <p><strong>出力JSON構造:</strong> 025 * 026 * <pre> 027 * { 028 * "summary": { 029 * "totalViolations": 2083, 030 * "generatedAt": "2025-08-19T16:45:00.123Z" 031 * }, 032 * "violations": [ 033 * { 034 * "file": "streamconverter-core/src/.../Example.java", 035 * "line": 42, 036 * "rule": "MethodArgumentCouldBeFinal", 037 * "category": "Code Style", 038 * "priority": 3, 039 * "description": "Parameter 'input' is not assigned...", 040 * "class": "Example", 041 * "method": "process", 042 * "variable": "input" 043 * } 044 * ] 045 * } 046 * </pre> 047 * 048 * <p><strong>特徴:</strong> 049 * 050 * <ul> 051 * <li>Jackson ObjectMapper による型安全なJSON生成 052 * <li>ISO-8601 標準タイムスタンプ 053 * <li>構造化されたサマリー情報 054 * <li>プリティプリント対応 055 * </ul> 056 * 057 * <p><strong>使用例:</strong> 058 * 059 * <pre> 060 * // StreamConverter パイプラインでの使用 061 * StreamConverter converter = new StreamConverter( 062 * new PmdXmlToJsonCommand() 063 * ); 064 * converter.run(pmdXmlInputStream, jsonOutputStream); 065 * </pre> 066 */ 067public class PmdXmlToJsonCommand extends AbstractStreamCommand { 068 069 private final ObjectMapper objectMapper; 070 071 /** 072 * Creates a new command instance. 073 * 074 * <p>Initializes the {@link ObjectMapper} with the following configuration: 075 * 076 * <ul> 077 * <li>Enables pretty-printing of JSON output ({@link SerializationFeature#INDENT_OUTPUT}). 078 * <li>Disables writing dates as timestamps, using ISO-8601 format instead ({@link 079 * SerializationFeature#WRITE_DATES_AS_TIMESTAMPS}). 080 * </ul> 081 * 082 * This configuration affects the formatting and date representation in the generated JSON 083 * reports. 084 */ 085 public PmdXmlToJsonCommand() { 086 this.objectMapper = 087 new ObjectMapper() 088 .enable(SerializationFeature.INDENT_OUTPUT) // Pretty print 089 .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // ISO-8601 090 } 091 092 /** 093 * PMD XML InputStream を JSON OutputStream に変換 094 * 095 * @param input PMD XML レポートの入力ストリーム 096 * @param output JSON レポートの出力ストリーム 097 * @throws IOException XML解析エラーまたはI/O例外の場合 098 */ 099 @Override 100 protected void executeInternal(InputStream input, OutputStream output) throws IOException { 101 try { 102 // StreamConverter原則: ストリーム間変換 103 List<PmdViolation> violations = parseXmlStream(input); 104 PmdJsonReport report = createJsonReport(violations); 105 106 // Jackson による型安全なJSON生成 107 objectMapper.writeValue(output, report); 108 109 } catch (Exception e) { 110 throw new IOException("Failed to convert PMD XML to JSON: " + e.getMessage(), e); 111 } 112 } 113 114 @Override 115 protected String getCommandDetails() { 116 return "PmdXmlToJsonCommand: Converts PMD XML reports to structured JSON using Jackson ObjectMapper"; 117 } 118 119 /** PMD XML ストリームからバイオレーション情報を解析 */ 120 private List<PmdViolation> parseXmlStream(InputStream input) throws Exception { 121 DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); 122 Document doc = builder.parse(input); 123 124 NodeList fileNodes = doc.getElementsByTagName("file"); 125 List<PmdViolation> violations = new ArrayList<>(); 126 127 for (int i = 0; i < fileNodes.getLength(); i++) { 128 Element fileElement = (Element) fileNodes.item(i); 129 String fileName = fileElement.getAttribute("name"); 130 131 NodeList violationNodes = fileElement.getElementsByTagName("violation"); 132 for (int j = 0; j < violationNodes.getLength(); j++) { 133 Element violationElement = (Element) violationNodes.item(j); 134 135 PmdViolation violation = 136 new PmdViolation( 137 extractRelativePath(fileName), 138 Integer.parseInt(violationElement.getAttribute("beginline")), 139 violationElement.getAttribute("rule"), 140 violationElement.getAttribute("ruleset"), 141 Integer.parseInt(violationElement.getAttribute("priority")), 142 violationElement.getTextContent().trim(), 143 violationElement.getAttribute("class"), 144 violationElement.getAttribute("method"), 145 violationElement.getAttribute("variable")); 146 violations.add(violation); 147 } 148 } 149 150 return violations; 151 } 152 153 /** StreamConverterプロジェクト内の相対パスを抽出 */ 154 private String extractRelativePath(String fullPath) { 155 int index = fullPath.indexOf("streamconverter-"); 156 return index != -1 ? fullPath.substring(index) : fullPath; 157 } 158 159 /** 構造化されたJSON レポートオブジェクトを作成 */ 160 private PmdJsonReport createJsonReport(List<PmdViolation> violations) { 161 PmdJsonSummary summary = 162 new PmdJsonSummary( 163 violations.size(), Instant.now().toString() // ISO-8601 標準形式 164 ); 165 166 return new PmdJsonReport(summary, violations); 167 } 168 169 /** 170 * JSON レポートのルートオブジェクト 171 * 172 * @param summary サマリー情報 173 * @param violations バイオレーション一覧 174 */ 175 public static record PmdJsonReport(PmdJsonSummary summary, List<PmdViolation> violations) { 176 public PmdJsonReport { 177 violations = List.copyOf(violations); 178 } 179 } 180 181 /** 182 * JSON レポートのサマリー情報 183 * 184 * @param totalViolations 総違反数 185 * @param generatedAt 生成日時 186 */ 187 public static record PmdJsonSummary(int totalViolations, String generatedAt) {} 188}