net.sourceforge.pmd.renderers.XMLRenderer Maven / Gradle / Ivy
Show all versions of pmd-core Show documentation
/**
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd.renderers;
import net.sourceforge.pmd.PMDVersion;
import net.sourceforge.pmd.internal.util.IOUtil;
import net.sourceforge.pmd.properties.PropertyDescriptor;
import net.sourceforge.pmd.properties.PropertyFactory;
import net.sourceforge.pmd.reporting.Report;
import net.sourceforge.pmd.reporting.RuleViolation;
import net.sourceforge.pmd.util.StringUtil;
import org.apache.commons.lang3.StringUtils;
import javax.xml.XMLConstants;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
/**
* Renderer to XML format.
*/
public class XMLRenderer extends AbstractIncrementingRenderer {
public static final String NAME = "xml";
public static final PropertyDescriptor ENCODING =
PropertyFactory.stringProperty("encoding").desc("XML encoding format").defaultValue("UTF-8").build();
private static final String PMD_REPORT_NS_URI = "http://pmd.sourceforge.net/report/2.0.0";
private static final String PMD_REPORT_NS_LOCATION = "https://pmd.github.io/schema/report_2_0_0.xsd";
private static final String XSI_NS_PREFIX = "xsi";
private XMLStreamWriter xmlWriter;
private OutputStream stream;
private byte[] lineSeparator;
public XMLRenderer() {
super(NAME, "XML format.");
definePropertyDescriptor(ENCODING);
}
public XMLRenderer(String encoding) {
this();
setProperty(ENCODING, encoding);
}
public XMLRenderer(String name, String description) {
super(name, description);
definePropertyDescriptor(ENCODING);
}
/* default */ XMLStreamWriter getXmlWriter() {
return xmlWriter;
}
@Override
public String defaultFileExtension() {
return "xml";
}
/* default */ void initLineSeparator() throws UnsupportedEncodingException {
String encoding = getProperty(ENCODING);
String unmarkedEncoding = toUnmarkedEncoding(encoding);
lineSeparator = System.lineSeparator().getBytes(unmarkedEncoding);
}
@Override
public void start() throws IOException {
initLineSeparator();
try {
xmlWriter.writeStartDocument(getProperty(ENCODING), "1.0");
writeNewLine();
xmlWriter.setDefaultNamespace(PMD_REPORT_NS_URI);
xmlWriter.writeStartElement(PMD_REPORT_NS_URI, "pmd");
xmlWriter.writeDefaultNamespace(PMD_REPORT_NS_URI);
xmlWriter.writeNamespace(XSI_NS_PREFIX, XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI);
xmlWriter.writeAttribute(XSI_NS_PREFIX, XMLConstants.W3C_XML_SCHEMA_INSTANCE_NS_URI, "schemaLocation",
PMD_REPORT_NS_URI + " " + PMD_REPORT_NS_LOCATION);
xmlWriter.writeAttribute("version", PMDVersion.VERSION);
xmlWriter.writeAttribute("timestamp", new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS").format(new Date()));
// FIXME: elapsed time not available until the end of the processing
// xmlWriter.writeAttribute("time_elapsed", ...);
} catch (XMLStreamException e) {
throw new IOException(e);
}
}
/**
* Return a encoding, which doesn't write a BOM (byte order mark).
* Only UTF-16 encoders might write a BOM, see {@link Charset}.
*
* This is needed, so that we don't accidentally add BOMs whenever
* we insert a newline.
*
* @return
*/
private static String toUnmarkedEncoding(String encoding) {
if (StandardCharsets.UTF_16.name().equalsIgnoreCase(encoding)) {
return StandardCharsets.UTF_16BE.name();
}
// edge case: UTF-16LE with BOM
if ("UTF-16LE_BOM".equalsIgnoreCase(encoding)) {
return StandardCharsets.UTF_16LE.name();
}
return encoding;
}
/**
* Outputs a platform dependent line separator.
*
* @throws XMLStreamException if XMLStreamWriter couldn't be flushed.
* @throws IOException if an I/O error occurs.
*/
/* default */ void writeNewLine() throws XMLStreamException, IOException {
/*
* Note: we are not using xmlWriter.writeCharacters(PMD.EOL), because some
* XMLStreamWriter implementations might do extra encoding for \r and/or \n.
* Notably IBM's Java 8 will escape "\r" with "
" which will render an
* invalid XML document. IBM's Java 8 would also output a platform dependent
* line separator when writing "\n" which results under Windows, that "\r"
* actually is written twice (once escaped, once raw).
*
* Note2: Before writing the raw bytes to the underlying stream, we need
* to flush XMLStreamWriter. Notably IBM's Java 8 might still need to output
* data.
*
* Note3: Before writing the raw bytes, we issue a empty writeCharacters,
* so that any open tags are closed and we are ready for writing raw bytes.
*/
xmlWriter.writeCharacters("");
xmlWriter.flush();
stream.write(lineSeparator);
}
@Override
public void renderFileViolations(Iterator violations) throws IOException {
String filename = null;
try {
// rule violations
while (violations.hasNext()) {
RuleViolation rv = violations.next();
String nextFilename = determineFileName(rv.getFileId());
if (!nextFilename.equals(filename)) {
// New File
if (filename != null) {
// Not first file ?
xmlWriter.writeEndElement();
}
filename = nextFilename;
writeNewLine();
xmlWriter.writeStartElement("file");
xmlWriter.writeAttribute("name", filename);
writeNewLine();
}
xmlWriter.writeStartElement("violation");
xmlWriter.writeAttribute("beginline", String.valueOf(rv.getBeginLine()));
xmlWriter.writeAttribute("endline", String.valueOf(rv.getEndLine()));
xmlWriter.writeAttribute("begincolumn", String.valueOf(rv.getBeginColumn()));
xmlWriter.writeAttribute("endcolumn", String.valueOf(rv.getEndColumn()));
xmlWriter.writeAttribute("rule", rv.getRule().getName());
xmlWriter.writeAttribute("ruleset", rv.getRule().getRuleSetName());
maybeAdd("package", rv.getAdditionalInfo().get(RuleViolation.PACKAGE_NAME));
maybeAdd("class", rv.getAdditionalInfo().get(RuleViolation.CLASS_NAME));
maybeAdd("method", rv.getAdditionalInfo().get(RuleViolation.METHOD_NAME));
maybeAdd("variable", rv.getAdditionalInfo().get(RuleViolation.VARIABLE_NAME));
// todo other additional info keys are not rendered
maybeAdd("externalInfoUrl", rv.getRule().getExternalInfoUrl());
xmlWriter.writeAttribute("priority", String.valueOf(rv.getRule().getPriority().getPriority()));
writeNewLine();
xmlWriter.writeCharacters(StringUtil.removedInvalidXml10Characters(rv.getDescription()));
writeNewLine();
xmlWriter.writeEndElement();
writeNewLine();
}
if (filename != null) { // Not first file ?
xmlWriter.writeEndElement();
}
} catch (XMLStreamException e) {
throw new IOException(e);
}
}
@Override
public void end() throws IOException {
try {
// errors
for (Report.ProcessingError pe : errors) {
writeNewLine();
xmlWriter.writeStartElement("error");
xmlWriter.writeAttribute("filename", determineFileName(pe.getFileId()));
xmlWriter.writeAttribute("msg", pe.getMsg());
writeNewLine();
// in case the message contains itself some CDATA sections, they need to be handled
// in order to not produce invalid XML...
String detail = pe.getDetail();
// split "]]>" into "]]" and ">" into two cdata sections
detail = detail.replace("]]>", "]]]]>");
xmlWriter.writeCData(detail);
writeNewLine();
xmlWriter.writeEndElement();
}
// suppressed violations
if (showSuppressedViolations) {
for (Report.SuppressedViolation s : suppressed) {
writeNewLine();
xmlWriter.writeStartElement("suppressedviolation");
xmlWriter.writeAttribute("filename", determineFileName(s.getRuleViolation().getFileId()));
xmlWriter.writeAttribute("suppressiontype", s.getSuppressor().getId().toLowerCase(Locale.ROOT));
xmlWriter.writeAttribute("msg", s.getRuleViolation().getDescription());
xmlWriter.writeAttribute("usermsg", s.getUserMessage() == null ? "" : s.getUserMessage());
xmlWriter.writeEndElement();
}
}
// config errors
for (final Report.ConfigurationError ce : configErrors) {
writeNewLine();
xmlWriter.writeEmptyElement("configerror");
xmlWriter.writeAttribute("rule", ce.rule().getName());
xmlWriter.writeAttribute("msg", ce.issue());
}
writeNewLine();
xmlWriter.writeEndElement(); //
writeNewLine();
xmlWriter.writeEndDocument();
xmlWriter.flush();
} catch (XMLStreamException e) {
throw new IOException(e);
}
}
private void maybeAdd(String attr, String value) throws XMLStreamException {
if (value != null && value.length() > 0) {
xmlWriter.writeAttribute(attr, value);
}
}
@Override
public void setReportFile(String reportFilename) {
String encoding = getProperty(ENCODING);
try {
this.stream = StringUtils.isBlank(reportFilename)
? System.out : Files.newOutputStream(new File(reportFilename).toPath());
XMLOutputFactory outputFactory = XMLOutputFactory.newFactory();
this.xmlWriter = outputFactory.createXMLStreamWriter(this.stream, encoding);
// for backwards compatibility, also provide a writer. Note: xmlWriter won't use that.
super.setWriter(new WrappedOutputStreamWriter(xmlWriter, stream, encoding));
} catch (IOException | XMLStreamException e) {
throw new IllegalArgumentException(e);
}
}
@Override
public void setWriter(final Writer writer) {
String encoding = getProperty(ENCODING);
// for backwards compatibility, create a OutputStream that writes to the writer.
this.stream = IOUtil.fromWriter(writer, encoding);
XMLOutputFactory outputFactory = XMLOutputFactory.newFactory();
try {
this.xmlWriter = outputFactory.createXMLStreamWriter(this.stream, encoding);
// for backwards compatibility, also provide a writer.
// Note: both XMLStreamWriter and this writer will write to this.stream
super.setWriter(new WrappedOutputStreamWriter(xmlWriter, stream, encoding));
} catch (XMLStreamException | UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
private static class WrappedOutputStreamWriter extends OutputStreamWriter {
private final XMLStreamWriter xmlWriter;
WrappedOutputStreamWriter(XMLStreamWriter xmlWriter, OutputStream out, String charset) throws UnsupportedEncodingException {
super(out, charset);
this.xmlWriter = xmlWriter;
}
@Override
public void flush() throws IOException {
try {
xmlWriter.flush();
} catch (XMLStreamException e) {
throw new IOException(e);
}
super.flush();
}
@Override
public void close() throws IOException {
try {
xmlWriter.close();
} catch (XMLStreamException e) {
throw new IOException(e);
}
super.close();
}
}
// FIXME: elapsed time not available until the end of the processing
/*
* private String createTimeElapsedAttr(Report rpt) {
* Report.ReadableDuration d = new
* Report.ReadableDuration(rpt.getElapsedTimeInMillis()); return
* " elapsedTime=\"" + d.getTime() + "\""; }
*/
}