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}