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}