/*
* SonarQube
* Copyright (C) 2009-2017 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.scanner.genericcoverage;
import com.google.common.base.Preconditions;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.xml.stream.XMLStreamException;
import org.codehaus.staxmate.in.SMHierarchicCursor;
import org.codehaus.staxmate.in.SMInputCursor;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.api.batch.sensor.SensorContext;
import org.sonar.api.batch.sensor.coverage.NewCoverage;
import org.sonar.api.utils.StaxParser;
public class GenericCoverageReportParser {
private static final String LINE_NUMBER_ATTR = "lineNumber";
private static final String COVERED_ATTR = "covered";
private static final String BRANCHES_TO_COVER_ATTR = "branchesToCover";
private static final String COVERED_BRANCHES_ATTR = "coveredBranches";
private static final int MAX_STORED_UNKNOWN_FILE_PATHS = 5;
private int numberOfUnknownFiles;
private final List<String> firstUnknownFiles = new ArrayList<>();
private final Set<String> matchedFileKeys = new HashSet<>();
public void parse(java.io.File reportFile, SensorContext context) {
try (InputStream inputStream = new FileInputStream(reportFile)) {
parse(inputStream, context);
} catch (Exception e) {
throw new IllegalStateException("Error during parsing of coverage report " + reportFile, e);
}
}
void parse(InputStream inputStream, SensorContext context) throws XMLStreamException {
new StaxParser(rootCursor -> {
rootCursor.advance();
parseRootNode(rootCursor, context);
}).parse(inputStream);
}
private void parseRootNode(SMHierarchicCursor rootCursor, SensorContext context) throws XMLStreamException {
checkElementName(rootCursor, "coverage");
String version = rootCursor.getAttrValue("version");
if (!"1".equals(version)) {
throw new IllegalStateException("Unknown report version: " + version + ". This parser only handles version 1.");
}
parseFiles(rootCursor.childElementCursor(), context);
}
private void parseFiles(SMInputCursor fileCursor, SensorContext context) throws XMLStreamException {
while (fileCursor.getNext() != null) {
checkElementName(fileCursor, "file");
String filePath = mandatoryAttribute(fileCursor, "path");
InputFile inputFile = context.fileSystem().inputFile(context.fileSystem().predicates().hasPath(filePath));
if (inputFile == null) {
numberOfUnknownFiles++;
if (numberOfUnknownFiles <= MAX_STORED_UNKNOWN_FILE_PATHS) {
firstUnknownFiles.add(filePath);
}
continue;
}
Preconditions.checkState(
inputFile.language() != null,
"Line %s of report refers to a file with an unknown language: %s",
fileCursor.getCursorLocation().getLineNumber(),
filePath);
matchedFileKeys.add(inputFile.absolutePath());
NewCoverage newCoverage = context.newCoverage().onFile(inputFile);
SMInputCursor lineToCoverCursor = fileCursor.childElementCursor();
while (lineToCoverCursor.getNext() != null) {
parseLineToCover(lineToCoverCursor, newCoverage);
}
newCoverage.save();
}
}
private static void parseLineToCover(SMInputCursor cursor, NewCoverage newCoverage)
throws XMLStreamException {
checkElementName(cursor, "lineToCover");
String lineNumberAsString = mandatoryAttribute(cursor, LINE_NUMBER_ATTR);
int lineNumber = intValue(lineNumberAsString, cursor, LINE_NUMBER_ATTR, 1);
boolean covered = getCoveredValue(cursor);
newCoverage.lineHits(lineNumber, covered ? 1 : 0);
String branchesToCoverAsString = cursor.getAttrValue(BRANCHES_TO_COVER_ATTR);
if (branchesToCoverAsString != null) {
int branchesToCover = intValue(branchesToCoverAsString, cursor, BRANCHES_TO_COVER_ATTR, 0);
String coveredBranchesAsString = cursor.getAttrValue(COVERED_BRANCHES_ATTR);
int coveredBranches = 0;
if (coveredBranchesAsString != null) {
coveredBranches = intValue(coveredBranchesAsString, cursor, COVERED_BRANCHES_ATTR, 0);
if (coveredBranches > branchesToCover) {
throw new IllegalStateException("\"coveredBranches\" should not be greater than \"branchesToCover\" on line " + cursor.getCursorLocation().getLineNumber());
}
}
newCoverage.conditions(lineNumber, branchesToCover, coveredBranches);
}
}
private static boolean getCoveredValue(SMInputCursor cursor) throws XMLStreamException {
String coveredAsString = mandatoryAttribute(cursor, COVERED_ATTR);
if (!"true".equalsIgnoreCase(coveredAsString) && !"false".equalsIgnoreCase(coveredAsString)) {
throw new IllegalStateException(expectedMessage("boolean value", COVERED_ATTR, coveredAsString, cursor.getCursorLocation().getLineNumber()));
}
return Boolean.parseBoolean(coveredAsString);
}
static void checkElementName(SMInputCursor cursor, String expectedName) throws XMLStreamException {
String elementName = cursor.getLocalName();
if (!expectedName.equals(elementName)) {
throw new IllegalStateException("Unknown XML node, expected \"" + expectedName + "\" but got \"" + elementName + "\" at line " + cursor.getCursorLocation().getLineNumber());
}
}
static String mandatoryAttribute(SMInputCursor cursor, String attributeName) throws XMLStreamException {
String attributeValue = cursor.getAttrValue(attributeName);
if (attributeValue == null) {
throw new IllegalStateException(
"Missing attribute \"" + attributeName + "\" in element \"" + cursor.getLocalName() + "\" at line " + cursor.getCursorLocation().getLineNumber());
}
return attributeValue;
}
static int intValue(String stringValue, SMInputCursor cursor, String attributeName, int minimum) throws XMLStreamException {
int intValue;
try {
intValue = Integer.valueOf(stringValue);
} catch (NumberFormatException e) {
throw new IllegalStateException(expectedMessage("integer value", attributeName, stringValue, cursor.getCursorLocation().getLineNumber()), e);
}
if (intValue < minimum) {
throw new IllegalStateException("Value of attribute \"" + attributeName + "\" at line " + cursor.getCursorLocation().getLineNumber() + " is \"" + intValue
+ "\" but it should be greater than or equal to " + minimum);
}
return intValue;
}
static long longValue(String stringValue, SMInputCursor cursor, String attributeName, long minimum) throws XMLStreamException {
long longValue;
try {
longValue = Long.valueOf(stringValue);
} catch (NumberFormatException e) {
throw new IllegalStateException(expectedMessage("long value", attributeName, stringValue, cursor.getCursorLocation().getLineNumber()), e);
}
if (longValue < minimum) {
throw new IllegalStateException("Value of attribute \"" + attributeName + "\" at line " + cursor.getCursorLocation().getLineNumber() + " is \"" + longValue
+ "\" but it should be greater than or equal to " + minimum);
}
return longValue;
}
private static String expectedMessage(String expected, String attributeName, String stringValue, int line) {
return "Expected " + expected + " for attribute \"" + attributeName + "\" at line " + line + " but got \"" + stringValue + "\"";
}
public int numberOfMatchedFiles() {
return matchedFileKeys.size();
}
public int numberOfUnknownFiles() {
return numberOfUnknownFiles;
}
public List<String> firstUnknownFiles() {
return firstUnknownFiles;
}
}