Add xml tree renderer
This commit is contained in:
@ -0,0 +1,34 @@
|
||||
/*
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.util.treeexport;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import net.sourceforge.pmd.annotation.Experimental;
|
||||
import net.sourceforge.pmd.lang.ast.Node;
|
||||
|
||||
/**
|
||||
* An object that can export a tree to an external text format.
|
||||
*
|
||||
* @see XmlTreeRenderer
|
||||
*/
|
||||
@Experimental
|
||||
public interface TreeRenderer {
|
||||
|
||||
|
||||
/**
|
||||
* Appends the subtree rooted at the given node on the provided
|
||||
* output writer. The implementation is free to filter out some
|
||||
* nodes from the subtree.
|
||||
*
|
||||
* @param node Node to render
|
||||
* @param out Object onto which the output is appended
|
||||
*
|
||||
* @throws IOException If an IO error occurs while appending to the output
|
||||
*/
|
||||
void renderSubtree(Node node, Appendable out) throws IOException;
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
/*
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.util.treeexport;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.TreeMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import net.sourceforge.pmd.annotation.Experimental;
|
||||
import net.sourceforge.pmd.lang.ast.Node;
|
||||
import net.sourceforge.pmd.lang.ast.xpath.Attribute;
|
||||
|
||||
/**
|
||||
* Renders a tree to XML. The resulting document is as close as possible
|
||||
* to the representation PMD uses to run XPath queries on nodes. This
|
||||
* allows the same XPath queries to match, in theory (it would depend
|
||||
* on the XPath engine used I believe).
|
||||
*/
|
||||
@Experimental
|
||||
public final class XmlTreeRenderer implements TreeRenderer {
|
||||
|
||||
// See https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Name
|
||||
private static final String XML_START_CHAR = "[:A-Z_a-z\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}\\x{10000}-\\x{EFFFF}]";
|
||||
private static final String XML_CHAR = "[" + XML_START_CHAR + ".\\-0-9\\xB7\\x{0300}-\\x{036F}\\x{203F}-\\x{2040}]";
|
||||
private static final Pattern XML_NAME = Pattern.compile(XML_START_CHAR + XML_CHAR + "*");
|
||||
|
||||
|
||||
private final XmlRenderingConfig strategy;
|
||||
private final char attrDelim;
|
||||
|
||||
/*
|
||||
TODO it's unclear to me how the strong typing of XPath 2.0 would
|
||||
impact XPath queries run on the output XML. PMD maps attributes
|
||||
to typed values, the XML only has untyped strings.
|
||||
|
||||
OTOH users should expect differences, and it's even documented
|
||||
on this class.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new XML renderer.
|
||||
*
|
||||
* @param strategy Strategy to parameterize the output of this instance
|
||||
*/
|
||||
public XmlTreeRenderer(XmlRenderingConfig strategy) {
|
||||
this.strategy = strategy;
|
||||
this.attrDelim = strategy.singleQuoteAttributes ? '\'' : '"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new XML renderer with a default configuration.
|
||||
*/
|
||||
public XmlTreeRenderer() {
|
||||
this(new XmlRenderingConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* <p>Each node of the AST has a corresponding XML element, whose
|
||||
* name and attributes are the one the node presents in XPath queries.
|
||||
*
|
||||
* @param node {@inheritDoc}
|
||||
* @param out {@inheritDoc}
|
||||
*
|
||||
* @throws IllegalArgumentException If some node has attributes or
|
||||
* a name that is not a valid XML name
|
||||
*/
|
||||
@Override
|
||||
public void renderSubtree(Node node, Appendable out) throws IOException {
|
||||
if (strategy.renderProlog) {
|
||||
renderProlog(out);
|
||||
}
|
||||
renderSubtree(0, node, out);
|
||||
out.append(strategy.lineSeparator);
|
||||
}
|
||||
|
||||
private void renderProlog(Appendable out) throws IOException {
|
||||
out.append("<?xml version=").append(attrDelim).append("1.0").append(attrDelim)
|
||||
.append(" encoding=").append(attrDelim).append("UTF-8").append(attrDelim)
|
||||
.append(" ?>").append(strategy.lineSeparator);
|
||||
}
|
||||
|
||||
private void renderSubtree(int depth, Node node, Appendable out) throws IOException {
|
||||
|
||||
String eltName = node.getXPathNodeName();
|
||||
|
||||
checkValidName(eltName);
|
||||
|
||||
|
||||
indent(depth, out).append('<').append(eltName);
|
||||
|
||||
Map<String, String> attributes = strategy.getXmlAttributes(node);
|
||||
|
||||
for (String attrName : attributes.keySet()) {
|
||||
appendAttribute(out, attrName, attributes.get(attrName));
|
||||
}
|
||||
|
||||
if (node.jjtGetNumChildren() == 0) {
|
||||
out.append(" />");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
out.append(">");
|
||||
|
||||
for (int i = 0; i < node.jjtGetNumChildren(); i++) {
|
||||
out.append(strategy.lineSeparator);
|
||||
renderSubtree(depth + 1, node.jjtGetChild(i), out);
|
||||
}
|
||||
|
||||
out.append(strategy.lineSeparator);
|
||||
|
||||
indent(depth, out).append("</").append(eltName).append('>');
|
||||
}
|
||||
|
||||
private void appendAttribute(Appendable out, String name, String value) throws IOException {
|
||||
checkValidName(name);
|
||||
|
||||
out.append(' ')
|
||||
.append(name)
|
||||
.append('=')
|
||||
.append(attrDelim)
|
||||
.append(escapeXmlAttribute(value, strategy.singleQuoteAttributes))
|
||||
.append(attrDelim);
|
||||
}
|
||||
|
||||
private void checkValidName(String name) {
|
||||
if (!isValidXmlName(name) || isReservedXmlName(name)) {
|
||||
throw new IllegalArgumentException(name + " is not a valid XML name");
|
||||
}
|
||||
}
|
||||
|
||||
private Appendable indent(int depth, Appendable out) throws IOException {
|
||||
while (depth-- > 0) {
|
||||
out.append(strategy.indentString);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static String escapeXmlText(String xml) {
|
||||
return xml.replaceAll("<", "<")
|
||||
.replaceAll("&", "&");
|
||||
|
||||
}
|
||||
|
||||
private static String escapeXmlAttribute(String xml, boolean isSingleQuoted) {
|
||||
|
||||
return isSingleQuoted ? escapeXmlText(xml).replaceAll("'", "'")
|
||||
: escapeXmlText(xml).replaceAll("\"", """);
|
||||
}
|
||||
|
||||
private static boolean isValidXmlName(String xml) {
|
||||
return XML_NAME.matcher(xml).matches();
|
||||
}
|
||||
|
||||
private static boolean isReservedXmlName(String xml) {
|
||||
return StringUtils.startsWithIgnoreCase(xml, "xml");
|
||||
}
|
||||
|
||||
/**
|
||||
* A strategy to parameterize an {@link XmlTreeRenderer}.
|
||||
*/
|
||||
public static class XmlRenderingConfig {
|
||||
|
||||
private String indentString = " ";
|
||||
private String lineSeparator = System.lineSeparator();
|
||||
private boolean singleQuoteAttributes = true;
|
||||
private boolean renderProlog = true;
|
||||
|
||||
private Map<String, String> getXmlAttributes(Node node) {
|
||||
Map<String, String> attrs = new TreeMap<>();
|
||||
Iterator<Attribute> iter = node.getXPathAttributesIterator();
|
||||
while (iter.hasNext()) {
|
||||
Attribute next = iter.next();
|
||||
if (takeAttribute(node, next)) {
|
||||
attrs.put(next.getName(), next.getStringValue());
|
||||
}
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the attribute should be included in the element
|
||||
* corresponding to the given node. Subclasses can override this
|
||||
* method to filter out some attributes.
|
||||
*
|
||||
* @param node Node owning the attribute
|
||||
* @param attribute Attribute to test
|
||||
*/
|
||||
protected boolean takeAttribute(Node node, Attribute attribute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the string that should be used to separate lines. The
|
||||
* default is the platform-specific line separator.
|
||||
*
|
||||
* @throws NullPointerException If the argument is null
|
||||
*/
|
||||
public XmlRenderingConfig lineSeparator(String lineSeparator) {
|
||||
this.lineSeparator = Objects.requireNonNull(lineSeparator);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the delimiters use for attribute values. The default is
|
||||
* to use single quotes.
|
||||
*
|
||||
* @param useSingleQuote True for single quotes, false for double quotes
|
||||
*/
|
||||
public XmlRenderingConfig singleQuoteAttributes(boolean useSingleQuote) {
|
||||
this.singleQuoteAttributes = useSingleQuote;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to render an XML prolog or not. The default is
|
||||
* true.
|
||||
*/
|
||||
public XmlRenderingConfig renderProlog(boolean renderProlog) {
|
||||
this.renderProlog = renderProlog;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the string that should be used to indent children elements.
|
||||
* The default is four spaces.
|
||||
*
|
||||
* @throws NullPointerException If the argument is null
|
||||
*/
|
||||
public XmlRenderingConfig indentWith(String indentString) {
|
||||
this.indentString = Objects.requireNonNull(indentString);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -4,14 +4,28 @@
|
||||
|
||||
package net.sourceforge.pmd.lang.ast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import net.sourceforge.pmd.lang.ast.xpath.Attribute;
|
||||
|
||||
public class DummyNode extends AbstractNode {
|
||||
private final boolean findBoundary;
|
||||
private final String xpathName;
|
||||
|
||||
private final Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
public DummyNode(int id) {
|
||||
this(id, false);
|
||||
}
|
||||
|
||||
public DummyNode() {
|
||||
this(0);
|
||||
}
|
||||
|
||||
public DummyNode(int id, boolean findBoundary) {
|
||||
this(id, findBoundary, "dummyNode");
|
||||
}
|
||||
@ -22,6 +36,21 @@ public class DummyNode extends AbstractNode {
|
||||
this.xpathName = xpathName;
|
||||
}
|
||||
|
||||
public void setXPathAttribute(String name, String value) {
|
||||
attributes.put(name, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Attribute> getXPathAttributesIterator() {
|
||||
|
||||
List<Attribute> attrs = new ArrayList<>();
|
||||
for (String name : attributes.keySet()) {
|
||||
attrs.add(new Attribute(this, name, attributes.get(name)));
|
||||
}
|
||||
|
||||
return attrs.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return xpathName;
|
||||
@ -31,7 +60,7 @@ public class DummyNode extends AbstractNode {
|
||||
public String getXPathNodeName() {
|
||||
return xpathName;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean isFindBoundary() {
|
||||
return findBoundary;
|
||||
|
@ -0,0 +1,273 @@
|
||||
/*
|
||||
* BSD-style license; for more info see http://pmd.sourceforge.net/license.html
|
||||
*/
|
||||
|
||||
package net.sourceforge.pmd.util.treeexport;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import net.sourceforge.pmd.lang.ast.DummyNode;
|
||||
import net.sourceforge.pmd.lang.ast.Node;
|
||||
import net.sourceforge.pmd.lang.ast.xpath.Attribute;
|
||||
import net.sourceforge.pmd.util.treeexport.XmlTreeRenderer.XmlRenderingConfig;
|
||||
|
||||
/**
|
||||
*/
|
||||
public class XmlTreeRendererTest {
|
||||
|
||||
@Rule
|
||||
public ExpectedException expect = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void testRenderWithAttributes() throws IOException {
|
||||
|
||||
DummyNode dummy = dummyTree1();
|
||||
|
||||
XmlRenderingConfig strat = new XmlRenderingConfig();
|
||||
strat.lineSeparator("\n");
|
||||
XmlTreeRenderer renderer = new XmlTreeRenderer(strat);
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
|
||||
renderer.renderSubtree(dummy, out);
|
||||
|
||||
Assert.assertEquals("<?xml version='1.0' encoding='UTF-8' ?>\n"
|
||||
+ "<dummyNode foo='bar' ohio='4'>\n"
|
||||
+ " <dummyNode o='ha' />\n"
|
||||
+ " <dummyNode />\n"
|
||||
+ "</dummyNode>\n", out.toString());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRenderWithCustomLineSep() throws IOException {
|
||||
|
||||
DummyNode dummy = dummyTree1();
|
||||
|
||||
XmlRenderingConfig strat = new XmlRenderingConfig();
|
||||
strat.lineSeparator("\r\n");
|
||||
XmlTreeRenderer renderer = new XmlTreeRenderer(strat);
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
|
||||
renderer.renderSubtree(dummy, out);
|
||||
|
||||
Assert.assertEquals("<?xml version='1.0' encoding='UTF-8' ?>\r\n"
|
||||
+ "<dummyNode foo='bar' ohio='4'>\r\n"
|
||||
+ " <dummyNode o='ha' />\r\n"
|
||||
+ " <dummyNode />\r\n"
|
||||
+ "</dummyNode>\r\n", out.toString());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRenderWithCustomIndent() throws IOException {
|
||||
|
||||
DummyNode dummy = dummyTree1();
|
||||
|
||||
XmlRenderingConfig strat = new XmlRenderingConfig().lineSeparator("").indentWith("");
|
||||
|
||||
XmlTreeRenderer renderer = new XmlTreeRenderer(strat);
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
|
||||
renderer.renderSubtree(dummy, out);
|
||||
|
||||
Assert.assertEquals("<?xml version='1.0' encoding='UTF-8' ?>"
|
||||
+ "<dummyNode foo='bar' ohio='4'>"
|
||||
+ "<dummyNode o='ha' />"
|
||||
+ "<dummyNode />"
|
||||
+ "</dummyNode>", out.toString());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRenderWithNoAttributes() throws IOException {
|
||||
|
||||
DummyNode dummy = dummyTree1();
|
||||
|
||||
XmlRenderingConfig strat = new XmlRenderingConfig() {
|
||||
@Override
|
||||
public boolean takeAttribute(Node node, Attribute attribute) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
XmlTreeRenderer renderer = new XmlTreeRenderer(strat);
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
|
||||
renderer.renderSubtree(dummy, out);
|
||||
|
||||
Assert.assertEquals("<?xml version='1.0' encoding='UTF-8' ?>\n"
|
||||
+ "<dummyNode>\n"
|
||||
+ " <dummyNode />\n"
|
||||
+ " <dummyNode />\n"
|
||||
+ "</dummyNode>\n", out.toString());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRenderFilterAttributes() throws IOException {
|
||||
|
||||
DummyNode dummy = dummyTree1();
|
||||
|
||||
XmlRenderingConfig strategy = new XmlRenderingConfig() {
|
||||
@Override
|
||||
public boolean takeAttribute(Node node, Attribute attribute) {
|
||||
return attribute.getName().equals("ohio");
|
||||
}
|
||||
};
|
||||
|
||||
XmlTreeRenderer renderer = new XmlTreeRenderer(strategy);
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
renderer.renderSubtree(dummy, out);
|
||||
|
||||
Assert.assertEquals("<?xml version='1.0' encoding='UTF-8' ?>\n"
|
||||
+ "<dummyNode ohio='4'>\n"
|
||||
+ " <dummyNode />\n"
|
||||
+ " <dummyNode />\n"
|
||||
+ "</dummyNode>\n", out.toString());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidAttributeName() throws IOException {
|
||||
|
||||
DummyNode dummy = dummyTree1();
|
||||
|
||||
dummy.setXPathAttribute("¬AName", "foo");
|
||||
|
||||
XmlRenderingConfig config = new XmlRenderingConfig();
|
||||
config.lineSeparator("\n");
|
||||
|
||||
XmlTreeRenderer renderer = new XmlTreeRenderer(config);
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
expect.expect(IllegalArgumentException.class);
|
||||
|
||||
renderer.renderSubtree(dummy, out);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testEscapeAttributes() throws IOException {
|
||||
|
||||
DummyNode dummy = dummyTree1();
|
||||
|
||||
dummy.setXPathAttribute("eh", " 'a &> b\" ");
|
||||
|
||||
XmlRenderingConfig strat = new XmlRenderingConfig().lineSeparator("\n");
|
||||
XmlTreeRenderer renderer = new XmlTreeRenderer(strat);
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
|
||||
renderer.renderSubtree(dummy, out);
|
||||
|
||||
Assert.assertEquals("<?xml version='1.0' encoding='UTF-8' ?>\n"
|
||||
+ "<dummyNode eh=' 'a &> b\" ' foo='bar' ohio='4'>\n"
|
||||
+ " <dummyNode o='ha' />\n"
|
||||
+ " <dummyNode />\n"
|
||||
+ "</dummyNode>\n", out.toString());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEscapeDoubleAttributes() throws IOException {
|
||||
|
||||
DummyNode dummy = dummyTree1();
|
||||
|
||||
dummy.setXPathAttribute("eh", " 'a &> b\" ");
|
||||
|
||||
XmlRenderingConfig strat = new XmlRenderingConfig().lineSeparator("\n").singleQuoteAttributes(false);
|
||||
XmlTreeRenderer renderer = new XmlTreeRenderer(strat);
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
|
||||
renderer.renderSubtree(dummy, out);
|
||||
|
||||
Assert.assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n"
|
||||
+ "<dummyNode eh=\" 'a &> b" \" foo=\"bar\" ohio=\"4\">\n"
|
||||
+ " <dummyNode o=\"ha\" />\n"
|
||||
+ " <dummyNode />\n"
|
||||
+ "</dummyNode>\n", out.toString());
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoProlog() throws IOException {
|
||||
|
||||
DummyNode dummy = dummyTree1();
|
||||
|
||||
|
||||
XmlRenderingConfig strat = new XmlRenderingConfig().lineSeparator("\n").renderProlog(false);
|
||||
XmlTreeRenderer renderer = new XmlTreeRenderer(strat);
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
|
||||
renderer.renderSubtree(dummy, out);
|
||||
|
||||
Assert.assertEquals("<dummyNode foo='bar' ohio='4'>\n"
|
||||
+ " <dummyNode o='ha' />\n"
|
||||
+ " <dummyNode />\n"
|
||||
+ "</dummyNode>\n", out.toString());
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testDefaultLineSep() throws IOException {
|
||||
|
||||
DummyNode dummy = dummyTree1();
|
||||
|
||||
XmlTreeRenderer renderer = new XmlTreeRenderer();
|
||||
|
||||
StringBuilder out = new StringBuilder();
|
||||
|
||||
|
||||
renderer.renderSubtree(dummy, out);
|
||||
|
||||
Assert.assertEquals("<?xml version='1.0' encoding='UTF-8' ?>" + System.lineSeparator()
|
||||
+ "<dummyNode foo='bar' ohio='4'>" + System.lineSeparator()
|
||||
+ " <dummyNode o='ha' />" + System.lineSeparator()
|
||||
+ " <dummyNode />" + System.lineSeparator()
|
||||
+ "</dummyNode>" + System.lineSeparator(), out.toString());
|
||||
|
||||
}
|
||||
|
||||
|
||||
public DummyNode dummyTree1() {
|
||||
DummyNode dummy = new DummyNode();
|
||||
|
||||
dummy.setXPathAttribute("foo", "bar");
|
||||
dummy.setXPathAttribute("ohio", "4");
|
||||
|
||||
DummyNode dummy1 = new DummyNode();
|
||||
|
||||
dummy1.setXPathAttribute("o", "ha");
|
||||
|
||||
DummyNode dummy2 = new DummyNode();
|
||||
|
||||
dummy.jjtAddChild(dummy1, 0);
|
||||
dummy.jjtAddChild(dummy2, 1);
|
||||
return dummy;
|
||||
}
|
||||
|
||||
|
||||
}
|
Reference in New Issue
Block a user