[core] CPD: Add schema for cpd xml report

This commit is contained in:
Andreas Dangel 2024-06-21 20:57:56 +02:00
parent d76a38805b
commit aed90ff62e
No known key found for this signature in database
GPG Key ID: 93450DF2DF9A3FA3
3 changed files with 143 additions and 24 deletions

View File

@ -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;
}
}

View 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>

View File

@ -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 = "&amp;";
@ -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;
}
});
}
}