001package com.streamconverter.sloc.command; 002 003import com.streamconverter.command.AbstractStreamCommand; 004import com.streamconverter.security.SecureXmlConfiguration; 005import com.streamconverter.sloc.ModuleSloc; 006import java.io.ByteArrayInputStream; 007import java.io.IOException; 008import java.io.InputStream; 009import java.io.ObjectOutputStream; 010import java.io.OutputStream; 011import java.io.SequenceInputStream; 012import java.nio.charset.StandardCharsets; 013import javax.xml.stream.XMLInputFactory; 014import javax.xml.stream.XMLStreamConstants; 015import javax.xml.stream.XMLStreamException; 016import javax.xml.stream.XMLStreamReader; 017 018/** 019 * JaCoCo XML レポートから LINE カウンターを抽出して {@link ModuleSloc} オブジェクトとして出力するコマンド。 020 * 021 * <p>{@link ModuleXmlConcatCommand} が出力する連結ストリームを入力として受け取り、 各 {@code <report>} 要素の name 022 * 属性をモジュール名として、report 直下の {@code <counter type="LINE">} を抽出して {@link ObjectOutputStream} で {@link 023 * ModuleSloc} を書き出す。 024 * 025 * <p>入力ストリームは複数の {@code <report>} 要素を持つため、パース前に {@code <jacoco-reports>} ラッパーで 包んで Well-formed な 026 * XML として扱う。ラッパー要素名は JaCoCo の仕様に依存しない独自名を使用している。 {@link ModuleXmlConcatCommand} が XML宣言・DOCTYPE宣言を 027 * 除去した上で {@code <report>} 要素だけを渡すため、ここでは宣言の除去は不要。 028 * 029 * <p>ラッパー要素名は JaCoCo の仕様に依存しない独自名のため、JaCoCo のルート要素名が変わっても影響を受けない。 030 */ 031public class JacocoXmlToModuleSlocCommand extends AbstractStreamCommand { 032 033 private static final byte[] WRAPPER_OPEN = 034 "<?xml version=\"1.0\" encoding=\"UTF-8\"?><jacoco-reports>".getBytes(StandardCharsets.UTF_8); 035 private static final byte[] WRAPPER_CLOSE = "</jacoco-reports>".getBytes(StandardCharsets.UTF_8); 036 037 @Override 038 public void execute(InputStream input, OutputStream output) throws IOException { 039 XMLInputFactory factory = SecureXmlConfiguration.createSecureXMLInputFactory(); 040 InputStream wrapped = 041 new SequenceInputStream( 042 new SequenceInputStream(new ByteArrayInputStream(WRAPPER_OPEN), input), 043 new ByteArrayInputStream(WRAPPER_CLOSE)); 044 try (ObjectOutputStream oos = new ObjectOutputStream(output)) { 045 XMLStreamReader reader = factory.createXMLStreamReader(wrapped); 046 try { 047 parseAll(reader, oos); 048 } finally { 049 reader.close(); 050 } 051 } catch (IOException e) { 052 throw e; 053 } catch (XMLStreamException e) { 054 throw new IOException( 055 "Failed to parse JaCoCo XML at " + e.getLocation() + ": " + e.getMessage(), e); 056 } 057 } 058 059 private void parseAll(XMLStreamReader reader, ObjectOutputStream oos) 060 throws XMLStreamException, IOException { 061 String currentModule = null; 062 boolean lineCounterFound = false; 063 int depth = 0; 064 065 while (reader.hasNext()) { 066 int event = reader.next(); 067 if (event == XMLStreamConstants.START_ELEMENT) { 068 depth++; 069 String localName = reader.getLocalName(); 070 if (depth == 2 && "report".equals(localName)) { 071 currentModule = reader.getAttributeValue(null, "name"); 072 lineCounterFound = false; 073 } else if (depth == 3 074 && "counter".equals(localName) 075 && "LINE".equals(reader.getAttributeValue(null, "type")) 076 && currentModule != null) { 077 try { 078 int missed = Integer.parseInt(reader.getAttributeValue(null, "missed")); 079 int covered = Integer.parseInt(reader.getAttributeValue(null, "covered")); 080 oos.writeObject(new ModuleSloc(currentModule, missed + covered, covered, missed)); 081 lineCounterFound = true; 082 } catch (NumberFormatException e) { 083 throw new IOException( 084 "Invalid LINE counter attribute in JaCoCo XML for module " 085 + currentModule 086 + ": " 087 + e.getMessage(), 088 e); 089 } 090 } 091 } else if (event == XMLStreamConstants.END_ELEMENT) { 092 if (depth == 2 && currentModule != null) { 093 if (!lineCounterFound) { 094 log.warn( 095 "No LINE counter found in JaCoCo report for module '{}'. " 096 + "Check JaCoCo XML format or coverage configuration.", 097 currentModule); 098 oos.writeObject(new ModuleSloc(currentModule, 0, 0, 0)); 099 } 100 currentModule = null; 101 } 102 depth--; 103 } 104 } 105 } 106}