001package com.streamconverter.pmd.command;
002
003import com.streamconverter.command.AbstractStreamCommand;
004import com.streamconverter.pmd.PmdViolation;
005import com.streamconverter.security.SecureXmlConfiguration;
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.ObjectOutputStream;
009import java.io.OutputStream;
010import javax.xml.stream.XMLInputFactory;
011import javax.xml.stream.XMLStreamConstants;
012import javax.xml.stream.XMLStreamException;
013import javax.xml.stream.XMLStreamReader;
014
015/**
016 * PMD XML レポートを {@link PmdViolation} オブジェクトのストリームに変換するコマンド。
017 *
018 * <p>入力: PMD XML レポートの {@link InputStream}
019 *
020 * <p>出力: {@link ObjectOutputStream} で {@link PmdViolation} を順次 writeObject
021 *
022 * <p>StAX({@link XMLStreamReader})による1件ずつのストリーム処理を行うため、大規模リポジトリでも OOM が発生しない。
023 *
024 * <p>PMD XML 構造: {@code <pmd><file name="..."><violation ...>テキスト</violation></file></pmd>}
025 */
026public class PmdXmlToViolationsCommand extends AbstractStreamCommand {
027
028  @Override
029  public void execute(InputStream input, OutputStream output) throws IOException {
030    XMLInputFactory factory = SecureXmlConfiguration.createSecureXMLInputFactory();
031    try (ObjectOutputStream oos = new ObjectOutputStream(output)) {
032      XMLStreamReader reader = factory.createXMLStreamReader(input);
033      try {
034        parseAll(reader, oos);
035      } finally {
036        reader.close();
037      }
038    } catch (IOException e) {
039      throw e;
040    } catch (XMLStreamException e) {
041      throw new IOException(
042          "Failed to parse PMD XML at " + e.getLocation() + ": " + e.getMessage(), e);
043    }
044  }
045
046  private void parseAll(XMLStreamReader reader, ObjectOutputStream oos)
047      throws XMLStreamException, IOException {
048    String currentFile = null;
049    String currentRule = null;
050    String currentRuleset = null;
051    int currentLine = 0;
052    int currentPriority = 0;
053    String currentClass = null;
054    String currentMethod = null;
055    String currentVariable = null;
056    StringBuilder currentDescription = null;
057
058    while (reader.hasNext()) {
059      int event = reader.next();
060      if (event == XMLStreamConstants.START_ELEMENT) {
061        String localName = reader.getLocalName();
062        if ("file".equals(localName)) {
063          currentFile = extractRelativePath(reader.getAttributeValue(null, "name"));
064        } else if ("violation".equals(localName) && currentFile != null) {
065          currentRule = reader.getAttributeValue(null, "rule");
066          currentRuleset = reader.getAttributeValue(null, "ruleset");
067          currentLine =
068              parseIntOrZero(reader.getAttributeValue(null, "beginline"), "beginline", currentFile);
069          currentPriority =
070              parseIntOrZero(reader.getAttributeValue(null, "priority"), "priority", currentFile);
071          currentClass = nullToEmpty(reader.getAttributeValue(null, "class"));
072          currentMethod = nullToEmpty(reader.getAttributeValue(null, "method"));
073          currentVariable = nullToEmpty(reader.getAttributeValue(null, "variable"));
074          currentDescription = new StringBuilder();
075        }
076      } else if ((event == XMLStreamConstants.CHARACTERS || event == XMLStreamConstants.CDATA)
077          && currentDescription != null) {
078        currentDescription.append(reader.getText());
079      } else if (event == XMLStreamConstants.END_ELEMENT) {
080        String localName = reader.getLocalName();
081        if ("violation".equals(localName) && currentDescription != null) {
082          oos.writeObject(
083              new PmdViolation(
084                  currentFile,
085                  currentLine,
086                  currentRule,
087                  currentRuleset,
088                  currentPriority,
089                  currentDescription.toString().strip(),
090                  currentClass,
091                  currentMethod,
092                  currentVariable));
093          currentDescription = null;
094        } else if ("file".equals(localName)) {
095          currentFile = null;
096        }
097      }
098    }
099  }
100
101  /**
102   * フルパスから {@code streamconverter-} 以降の相対パスを抽出する。
103   *
104   * <p>例: {@code /home/user/streamconverter-core/src/Foo.java} → {@code
105   * streamconverter-core/src/Foo.java}
106   */
107  private static String extractRelativePath(String fullPath) {
108    if (fullPath == null) {
109      return "";
110    }
111    int idx = fullPath.indexOf("streamconverter-");
112    return idx >= 0 ? fullPath.substring(idx) : fullPath;
113  }
114
115  private int parseIntOrZero(String value, String attributeName, String context) {
116    if (value == null || value.isEmpty()) {
117      return 0;
118    }
119    try {
120      return Integer.parseInt(value);
121    } catch (NumberFormatException e) {
122      log.warn(
123          "Invalid integer for attribute '{}' in '{}': '{}' — defaulting to 0",
124          attributeName,
125          context,
126          value);
127      return 0;
128    }
129  }
130
131  private static String nullToEmpty(String value) {
132    return value != null ? value : "";
133  }
134}