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}