[core] CPD: Add schema for cpd xml report
This commit is contained in:
parent
d76a38805b
commit
aed90ff62e
@ -6,7 +6,10 @@ package net.sourceforge.pmd.cpd;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Map;
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
@ -20,6 +23,7 @@ import javax.xml.transform.stream.StreamResult;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import net.sourceforge.pmd.PMDVersion;
|
||||
import net.sourceforge.pmd.lang.document.Chars;
|
||||
import net.sourceforge.pmd.lang.document.FileId;
|
||||
import net.sourceforge.pmd.lang.document.FileLocation;
|
||||
@ -32,6 +36,8 @@ import net.sourceforge.pmd.util.StringUtil;
|
||||
*
|
||||
*/
|
||||
public final class XMLRenderer implements CPDReportRenderer {
|
||||
private static final String NAMESPACE_URI = "https://pmd-code.org/ns/cpd-report/1.0.0";
|
||||
private static final String NAMESPACE_LOCATION = "https://pmd-code.org/ns/cpd-report_1_0_0.xsd";
|
||||
|
||||
private String encoding;
|
||||
|
||||
@ -83,7 +89,7 @@ public final class XMLRenderer implements CPDReportRenderer {
|
||||
transformer.setOutputProperty(OutputKeys.METHOD, "xml");
|
||||
transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
|
||||
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||
transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "codefragment");
|
||||
transformer.setOutputProperty(OutputKeys.CDATA_SECTION_ELEMENTS, "{" + NAMESPACE_URI + "}codefragment");
|
||||
transformer.transform(new DOMSource(doc), new StreamResult(writer));
|
||||
} catch (TransformerException e) {
|
||||
throw new IllegalStateException(e);
|
||||
@ -94,14 +100,18 @@ public final class XMLRenderer implements CPDReportRenderer {
|
||||
@Override
|
||||
public void render(final CPDReport report, final Writer writer) throws IOException {
|
||||
final Document doc = createDocument();
|
||||
final Element root = doc.createElement("pmd-cpd");
|
||||
final Element root = doc.createElementNS(NAMESPACE_URI, "pmd-cpd");
|
||||
root.setAttributeNS(XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "xsi:schemaLocation", NAMESPACE_URI + " " + NAMESPACE_LOCATION);
|
||||
|
||||
root.setAttributeNS(NAMESPACE_URI, "version", PMDVersion.VERSION);
|
||||
root.setAttributeNS(NAMESPACE_URI, "timestamp", OffsetDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME));
|
||||
final Map<FileId, Integer> numberOfTokensPerFile = report.getNumberOfTokensPerFile();
|
||||
doc.appendChild(root);
|
||||
|
||||
for (final Map.Entry<FileId, Integer> pair : numberOfTokensPerFile.entrySet()) {
|
||||
final Element fileElement = doc.createElement("file");
|
||||
fileElement.setAttribute("path", report.getDisplayName(pair.getKey()));
|
||||
fileElement.setAttribute("totalNumberOfTokens", String.valueOf(pair.getValue()));
|
||||
final Element fileElement = doc.createElementNS(NAMESPACE_URI, "file");
|
||||
fileElement.setAttributeNS(NAMESPACE_URI, "path", report.getDisplayName(pair.getKey()));
|
||||
fileElement.setAttributeNS(NAMESPACE_URI, "totalNumberOfTokens", String.valueOf(pair.getValue()));
|
||||
root.appendChild(fileElement);
|
||||
}
|
||||
|
||||
@ -113,9 +123,9 @@ public final class XMLRenderer implements CPDReportRenderer {
|
||||
}
|
||||
|
||||
for (Report.ProcessingError error : report.getProcessingErrors()) {
|
||||
Element errorElt = doc.createElement("error");
|
||||
errorElt.setAttribute("filename", report.getDisplayName(error.getFileId()));
|
||||
errorElt.setAttribute("msg", error.getMsg());
|
||||
Element errorElt = doc.createElementNS(NAMESPACE_URI, "error");
|
||||
errorElt.setAttributeNS(NAMESPACE_URI, "filename", report.getDisplayName(error.getFileId()));
|
||||
errorElt.setAttributeNS(NAMESPACE_URI, "msg", error.getMsg());
|
||||
errorElt.setTextContent(error.getDetail());
|
||||
root.appendChild(errorElt);
|
||||
}
|
||||
@ -126,17 +136,17 @@ public final class XMLRenderer implements CPDReportRenderer {
|
||||
|
||||
private void addFilesToDuplicationElement(Document doc, Element duplication, Match match, CPDReport report) {
|
||||
for (Mark mark : match) {
|
||||
final Element file = doc.createElement("file");
|
||||
final Element file = doc.createElementNS(NAMESPACE_URI, "file");
|
||||
FileLocation loc = mark.getLocation();
|
||||
file.setAttribute("line", String.valueOf(loc.getStartLine()));
|
||||
file.setAttributeNS(NAMESPACE_URI, "line", String.valueOf(loc.getStartLine()));
|
||||
// only remove invalid characters, escaping is done by the DOM impl.
|
||||
String filenameXml10 = StringUtil.removedInvalidXml10Characters(report.getDisplayName(loc.getFileId()));
|
||||
file.setAttribute("path", filenameXml10);
|
||||
file.setAttribute("endline", String.valueOf(loc.getEndLine()));
|
||||
file.setAttribute("column", String.valueOf(loc.getStartColumn()));
|
||||
file.setAttribute("endcolumn", String.valueOf(loc.getEndColumn()));
|
||||
file.setAttribute("begintoken", String.valueOf(mark.getBeginTokenIndex()));
|
||||
file.setAttribute("endtoken", String.valueOf(mark.getEndTokenIndex()));
|
||||
file.setAttributeNS(NAMESPACE_URI, "path", filenameXml10);
|
||||
file.setAttributeNS(NAMESPACE_URI, "endline", String.valueOf(loc.getEndLine()));
|
||||
file.setAttributeNS(NAMESPACE_URI, "column", String.valueOf(loc.getStartColumn()));
|
||||
file.setAttributeNS(NAMESPACE_URI, "endcolumn", String.valueOf(loc.getEndColumn()));
|
||||
file.setAttributeNS(NAMESPACE_URI, "begintoken", String.valueOf(mark.getBeginTokenIndex()));
|
||||
file.setAttributeNS(NAMESPACE_URI, "endtoken", String.valueOf(mark.getEndTokenIndex()));
|
||||
duplication.appendChild(file);
|
||||
}
|
||||
}
|
||||
@ -146,7 +156,7 @@ public final class XMLRenderer implements CPDReportRenderer {
|
||||
if (codeSnippet != null) {
|
||||
// the code snippet has normalized line endings
|
||||
String platformSpecific = codeSnippet.toString().replace("\n", System.lineSeparator());
|
||||
Element codefragment = doc.createElement("codefragment");
|
||||
Element codefragment = doc.createElementNS(NAMESPACE_URI, "codefragment");
|
||||
// only remove invalid characters, escaping is not necessary in CDATA.
|
||||
// if the string contains the end marker of a CDATA section, then the DOM impl will
|
||||
// create two cdata sections automatically.
|
||||
@ -156,9 +166,9 @@ public final class XMLRenderer implements CPDReportRenderer {
|
||||
}
|
||||
|
||||
private Element createDuplicationElement(Document doc, Match match) {
|
||||
Element duplication = doc.createElement("duplication");
|
||||
duplication.setAttribute("lines", String.valueOf(match.getLineCount()));
|
||||
duplication.setAttribute("tokens", String.valueOf(match.getTokenCount()));
|
||||
Element duplication = doc.createElementNS(NAMESPACE_URI, "duplication");
|
||||
duplication.setAttributeNS(NAMESPACE_URI, "lines", String.valueOf(match.getLineCount()));
|
||||
duplication.setAttributeNS(NAMESPACE_URI, "tokens", String.valueOf(match.getTokenCount()));
|
||||
return duplication;
|
||||
}
|
||||
}
|
||||
|
58
pmd-core/src/main/resources/cpd-report_1_0_0.xsd
Normal file
58
pmd-core/src/main/resources/cpd-report_1_0_0.xsd
Normal file
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0"?>
|
||||
<xs:schema
|
||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="https://pmd-code.org/ns/cpd-report/1.0.0"
|
||||
targetNamespace="https://pmd-code.org/ns/cpd-report/1.0.0"
|
||||
elementFormDefault="qualified">
|
||||
|
||||
<xs:element name="pmd-cpd">
|
||||
<xs:complexType>
|
||||
<xs:sequence>
|
||||
<xs:element name="file" type="file" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="duplication" type="duplication" minOccurs="0" maxOccurs="unbounded"/>
|
||||
<xs:element name="error" type="error" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="version" type="xs:string" use="required"/>
|
||||
<xs:attribute name="timestamp" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
</xs:element>
|
||||
|
||||
<xs:complexType name="file">
|
||||
<xs:attribute name="path" type="xs:string" use="required"/>
|
||||
<xs:attribute name="totalNumberOfTokens" type="xs:int" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="duplication">
|
||||
<xs:sequence>
|
||||
<xs:element name="file" type="fileLocation" minOccurs="1" maxOccurs="unbounded"/>
|
||||
<xs:element name="codefragment" type="codefragment" minOccurs="1" maxOccurs="1"/>
|
||||
</xs:sequence>
|
||||
<xs:attribute name="lines" type="xs:positiveInteger" use="required"/>
|
||||
<xs:attribute name="tokens" type="xs:positiveInteger" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="error">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string">
|
||||
<xs:attribute name="filename" type="xs:string" use="required"/>
|
||||
<xs:attribute name="msg" type="xs:string" use="required"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="fileLocation">
|
||||
<xs:attribute name="column" type="xs:positiveInteger" use="required"/>
|
||||
<xs:attribute name="endcolumn" type="xs:positiveInteger" use="required"/>
|
||||
<xs:attribute name="endline" type="xs:positiveInteger" use="required"/>
|
||||
<xs:attribute name="line" type="xs:positiveInteger" use="required"/>
|
||||
<xs:attribute name="begintoken" type="xs:integer" use="required"/>
|
||||
<xs:attribute name="endtoken" type="xs:integer" use="required"/>
|
||||
<xs:attribute name="path" type="xs:string" use="required"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="codefragment">
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:string"/>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
</xs:schema>
|
@ -11,17 +11,27 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Collections;
|
||||
import javax.xml.XMLConstants;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.parsers.SAXParser;
|
||||
import javax.xml.parsers.SAXParserFactory;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
import javax.xml.validation.Schema;
|
||||
import javax.xml.validation.SchemaFactory;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.NamedNodeMap;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
import org.xml.sax.SAXParseException;
|
||||
import org.xml.sax.helpers.DefaultHandler;
|
||||
|
||||
import net.sourceforge.pmd.cpd.CpdTestUtils.CpdReportBuilder;
|
||||
import net.sourceforge.pmd.lang.ast.LexException;
|
||||
@ -44,6 +54,17 @@ class XMLRendererTest {
|
||||
StringWriter sw = new StringWriter();
|
||||
renderer.render(CpdTestUtils.makeReport(Collections.emptyList()), sw);
|
||||
String report = sw.toString();
|
||||
assertReportIsValidSchema(report);
|
||||
|
||||
assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
||||
+ "<pmd-cpd xmlns=\"https://pmd-code.org/ns/cpd-report/1.0.0\"\n"
|
||||
+ " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n"
|
||||
+ " timestamp=\"XXX\"\n"
|
||||
+ " version=\"XXX\"\n"
|
||||
+ " xsi:schemaLocation=\"https://pmd-code.org/ns/cpd-report/1.0.0 https://pmd-code.org/ns/cpd-report_1_0_0.xsd\"/>\n",
|
||||
report.replaceAll(" {4}timestamp=\".+?\"", " timestamp=\"XXX\"")
|
||||
.replaceAll(" {4}version=\".+?\"", " version=\"XXX\""),
|
||||
"namespace is missing");
|
||||
|
||||
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
|
||||
@ -66,6 +87,7 @@ class XMLRendererTest {
|
||||
StringWriter sw = new StringWriter();
|
||||
renderer.render(builder.build(), sw);
|
||||
String report = sw.toString();
|
||||
assertReportIsValidSchema(report);
|
||||
|
||||
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
|
||||
@ -115,6 +137,7 @@ class XMLRendererTest {
|
||||
StringWriter sw = new StringWriter();
|
||||
renderer.render(builder.build(), sw);
|
||||
String report = sw.toString();
|
||||
assertReportIsValidSchema(report);
|
||||
|
||||
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
|
||||
@ -135,6 +158,7 @@ class XMLRendererTest {
|
||||
StringWriter sw = new StringWriter();
|
||||
renderer.render(builder.build(), sw);
|
||||
String report = sw.toString();
|
||||
assertReportIsValidSchema(report);
|
||||
|
||||
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
|
||||
@ -166,7 +190,7 @@ class XMLRendererTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRendererEncodedPath() throws IOException {
|
||||
void testRendererEncodedPath() throws Exception {
|
||||
CPDReportRenderer renderer = new XMLRenderer();
|
||||
CpdReportBuilder builder = new CpdReportBuilder();
|
||||
final String escapeChar = "&";
|
||||
@ -177,6 +201,7 @@ class XMLRendererTest {
|
||||
StringWriter sw = new StringWriter();
|
||||
renderer.render(builder.build(), sw);
|
||||
String report = sw.toString();
|
||||
assertReportIsValidSchema(report);
|
||||
assertThat(report, containsString(escapeChar));
|
||||
}
|
||||
|
||||
@ -196,6 +221,7 @@ class XMLRendererTest {
|
||||
final StringWriter writer = new StringWriter();
|
||||
renderer.render(report, writer);
|
||||
final String xmlOutput = writer.toString();
|
||||
assertReportIsValidSchema(xmlOutput);
|
||||
final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(new ByteArrayInputStream(xmlOutput.getBytes(ENCODING)));
|
||||
final NodeList files = doc.getElementsByTagName("file");
|
||||
@ -221,6 +247,7 @@ class XMLRendererTest {
|
||||
final StringWriter writer = new StringWriter();
|
||||
renderer.render(report, writer);
|
||||
final String xmlOutput = writer.toString();
|
||||
assertReportIsValidSchema(xmlOutput);
|
||||
final Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(new ByteArrayInputStream(xmlOutput.getBytes(ENCODING)));
|
||||
final NodeList files = doc.getElementsByTagName("file");
|
||||
@ -236,7 +263,7 @@ class XMLRendererTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRendererXMLEscaping() throws IOException {
|
||||
void testRendererXMLEscaping() throws Exception {
|
||||
String codefragment = "code fragment" + FORM_FEED
|
||||
+ "\nline2\nline3\nno & escaping necessary in CDATA\nx=\"]]>\";";
|
||||
CPDReportRenderer renderer = new XMLRenderer();
|
||||
@ -253,6 +280,7 @@ class XMLRendererTest {
|
||||
StringWriter sw = new StringWriter();
|
||||
renderer.render(builder.build(), sw);
|
||||
String report = sw.toString();
|
||||
assertReportIsValidSchema(report);
|
||||
assertThat(report, not(containsString(FORM_FEED)));
|
||||
assertThat(report, not(containsString(FORM_FEED_ENTITY)));
|
||||
assertThat(report, containsString("no & escaping necessary in CDATA"));
|
||||
@ -261,7 +289,7 @@ class XMLRendererTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportContainsProcessingError() throws IOException, ParserConfigurationException, SAXException {
|
||||
void reportContainsProcessingError() throws Exception {
|
||||
FileId fileId = FileId.fromPathLikeString("file1.txt");
|
||||
Report.ProcessingError processingError = new Report.ProcessingError(
|
||||
new LexException(2, 1, fileId, "test exception", new RuntimeException("cause exception")),
|
||||
@ -270,7 +298,7 @@ class XMLRendererTest {
|
||||
StringWriter sw = new StringWriter();
|
||||
renderer.render(CpdTestUtils.makeReport(Collections.emptyList(), Collections.emptyMap(), Collections.singletonList(processingError)), sw);
|
||||
String report = sw.toString();
|
||||
System.out.println("report = " + report);
|
||||
assertReportIsValidSchema(report);
|
||||
|
||||
Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder()
|
||||
.parse(new ByteArrayInputStream(report.getBytes(ENCODING)));
|
||||
@ -286,4 +314,27 @@ class XMLRendererTest {
|
||||
String textContent = error.getTextContent();
|
||||
assertEquals(processingError.getDetail(), textContent);
|
||||
}
|
||||
|
||||
private static void assertReportIsValidSchema(String report) throws SAXException, ParserConfigurationException, IOException {
|
||||
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
|
||||
Schema schema = schemaFactory.newSchema(new StreamSource(XMLRenderer.class.getResourceAsStream("/cpd-report_1_0_0.xsd")));
|
||||
|
||||
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
|
||||
saxParserFactory.setNamespaceAware(true);
|
||||
saxParserFactory.setValidating(false);
|
||||
saxParserFactory.setSchema(schema);
|
||||
|
||||
SAXParser saxParser = saxParserFactory.newSAXParser();
|
||||
saxParser.parse(new InputSource(new StringReader(report)), new DefaultHandler() {
|
||||
@Override
|
||||
public void error(SAXParseException e) throws SAXException {
|
||||
throw e;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warning(SAXParseException e) throws SAXException {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user