diff --git a/pmd/pom.xml b/pmd/pom.xml index 12bdf4844c..93221522a7 100644 --- a/pmd/pom.xml +++ b/pmd/pom.xml @@ -634,7 +634,7 @@ xercesImpl 2.9.1 jar - runtime + compile net.java.dev.javacc diff --git a/pmd/src/main/java/net/sourceforge/pmd/lang/xml/ast/XmlParser.java b/pmd/src/main/java/net/sourceforge/pmd/lang/xml/ast/XmlParser.java index 13381244ca..6f6ab3b4f9 100644 --- a/pmd/src/main/java/net/sourceforge/pmd/lang/xml/ast/XmlParser.java +++ b/pmd/src/main/java/net/sourceforge/pmd/lang/xml/ast/XmlParser.java @@ -17,10 +17,12 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Stack; import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; import net.sourceforge.pmd.lang.ast.ParseException; import net.sourceforge.pmd.lang.ast.RootNode; @@ -28,12 +30,27 @@ import net.sourceforge.pmd.lang.ast.xpath.Attribute; import net.sourceforge.pmd.lang.xml.XmlParserOptions; import net.sourceforge.pmd.util.CompoundIterator; +import org.apache.xerces.dom.CoreDocumentImpl; +import org.apache.xerces.dom.EntityImpl; +import org.apache.xerces.jaxp.DocumentBuilderFactoryImpl; +import org.w3c.dom.Attr; +import org.w3c.dom.CDATASection; +import org.w3c.dom.Comment; import org.w3c.dom.Document; +import org.w3c.dom.DocumentType; +import org.w3c.dom.Element; +import org.w3c.dom.Entity; +import org.w3c.dom.EntityReference; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; +import org.w3c.dom.ProcessingInstruction; import org.w3c.dom.Text; +import org.xml.sax.Attributes; import org.xml.sax.InputSource; +import org.xml.sax.Locator; import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.ext.DefaultHandler2; public class XmlParser { protected final XmlParserOptions parserOptions; @@ -46,20 +63,23 @@ public class XmlParser { protected Document parseDocument(Reader reader) throws ParseException { nodeCache.clear(); try { - DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); - documentBuilderFactory.setCoalescing(parserOptions.isCoalescing()); - documentBuilderFactory.setExpandEntityReferences(parserOptions.isExpandEntityReferences()); - documentBuilderFactory.setIgnoringComments(parserOptions.isIgnoringComments()); - documentBuilderFactory.setIgnoringElementContentWhitespace(parserOptions.isIgnoringElementContentWhitespace()); - documentBuilderFactory.setNamespaceAware(parserOptions.isNamespaceAware()); - documentBuilderFactory.setValidating(parserOptions.isValidating()); - documentBuilderFactory.setXIncludeAware(parserOptions.isXincludeAware()); - + SAXParserFactory saxParserFactory = SAXParserFactory.newInstance(); + saxParserFactory.setFeature("http://xml.org/sax/features/external-general-entities", false); + saxParserFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + saxParserFactory.setNamespaceAware(parserOptions.isNamespaceAware()); + saxParserFactory.setValidating(parserOptions.isValidating()); + saxParserFactory.setXIncludeAware(parserOptions.isXincludeAware()); + SAXParser saxParser = saxParserFactory.newSAXParser(); - DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); - documentBuilder.setEntityResolver(parserOptions.getEntityResolver()); - Document document = documentBuilder.parse(new InputSource(reader)); - return document; + LineNumberAwareSaxHandler handler = new LineNumberAwareSaxHandler(parserOptions); + XMLReader xmlReader = saxParser.getXMLReader(); + xmlReader.setContentHandler(handler); + xmlReader.setProperty("http://xml.org/sax/properties/lexical-handler", handler); + xmlReader.setProperty("http://xml.org/sax/properties/declaration-handler", handler); + xmlReader.setEntityResolver(parserOptions.getEntityResolver()); + + xmlReader.parse(new InputSource(reader)); + return handler.getDocument(); } catch (ParserConfigurationException e) { throw new ParseException(e); } catch (SAXException e) { @@ -69,6 +89,224 @@ public class XmlParser { } } + /** + * SAX Handler to build a DOM Document with line numbers. + * @see http://eyalsch.wordpress.com/2010/11/30/xml-dom-2/ + */ + private static class LineNumberAwareSaxHandler extends DefaultHandler2 { + public static final String BEGIN_LINE = "pmd:beginLine"; + public static final String BEGIN_COLUMN = "pmd:beginColumn"; + public static final String END_LINE = "pmd:endLine"; + public static final String END_COLUMN = "pmd:endColumn"; + + private Stack nodeStack = new Stack(); + private StringBuilder text = new StringBuilder(); + private int beginLineText = -1; + private int beginColumnText = -1; + private Locator locator; + private final DocumentBuilder documentBuilder; + private final Document document; + private boolean cdataEnded = false; + + private boolean coalescing; + private boolean expandEntityReferences; + private boolean ignoringComments; + private boolean ignoringElementContentWhitespace; + private boolean namespaceAware; + + public LineNumberAwareSaxHandler(XmlParserOptions options) throws ParserConfigurationException { + // uses xerces directly, so that we can build a DTD / entities section + this.documentBuilder = new DocumentBuilderFactoryImpl().newDocumentBuilder(); + + this.document = this.documentBuilder.newDocument(); + this.coalescing = options.isCoalescing(); + this.expandEntityReferences = options.isExpandEntityReferences(); + this.ignoringComments = options.isIgnoringComments(); + this.ignoringElementContentWhitespace = options.isIgnoringElementContentWhitespace(); + this.namespaceAware = options.isNamespaceAware(); + } + + public Document getDocument() { + return document; + } + + @Override + public void setDocumentLocator(Locator locator) { + this.locator = locator; + } + @Override + public void startElement(String uri, String localName, String qName, Attributes attributes) + throws SAXException { + addTextIfNeeded(false); + + Element element; + if (namespaceAware) { + element = document.createElementNS(uri, qName); + } else { + element = document.createElement(qName); + } + + for (int i = 0; i < attributes.getLength(); i++) { + String attQName = attributes.getQName(i); + String attNamespaceURI = attributes.getURI(i); + String attValue = attributes.getValue(i); + Attr a; + if (namespaceAware) { + a = document.createAttributeNS(attNamespaceURI, attQName); + element.setAttributeNodeNS(a); + } else { + a = document.createAttribute(attQName); + element.setAttributeNode(a); + } + a.setValue(attValue); + } + + element.setUserData(BEGIN_LINE, locator.getLineNumber(), null); + element.setUserData(BEGIN_COLUMN, locator.getColumnNumber(), null); + + nodeStack.push(element); + } + private void addTextIfNeeded(boolean alwaysAdd) { + if (text.length() > 0) { + addTextNode(text.toString(), cdataEnded || alwaysAdd); + text.setLength(0); + cdataEnded = false; + } + } + private void addTextNode(String s, boolean alwaysAdd) { + if (alwaysAdd || !ignoringElementContentWhitespace || s.trim().length() > 0) { + Text textNode = document.createTextNode(s); + textNode.setUserData(BEGIN_LINE, beginLineText, null); + textNode.setUserData(BEGIN_COLUMN, beginColumnText, null); + textNode.setUserData(END_LINE, locator.getLineNumber(), null); + textNode.setUserData(END_COLUMN, locator.getColumnNumber(), null); + appendChild(textNode); + } + } + @Override + public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { + this.characters(ch, start, length); + } + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + if (text.length() == 0) { + beginLineText = locator.getLineNumber(); + beginColumnText = locator.getColumnNumber(); + } + text.append(ch, start, length); + } + @Override + public void endElement(String uri, String localName, String qName) throws SAXException { + addTextIfNeeded(false); + Node element = nodeStack.pop(); + element.setUserData(END_LINE, locator.getLineNumber(), null); + element.setUserData(END_COLUMN, locator.getColumnNumber(), null); + appendChild(element); + } + @Override + public void startDocument() throws SAXException { + document.setUserData(BEGIN_LINE, locator.getLineNumber(), null); + document.setUserData(BEGIN_COLUMN, locator.getColumnNumber(), null); + } + @Override + public void endDocument() throws SAXException { + addTextIfNeeded(false); + document.setUserData(END_LINE, locator.getLineNumber(), null); + document.setUserData(END_COLUMN, locator.getColumnNumber(), null); + } + @Override + public void startCDATA() throws SAXException { + if (!coalescing) { + addTextIfNeeded(true); + } + } + @Override + public void endCDATA() throws SAXException { + if (!coalescing) { + CDATASection cdataSection = document.createCDATASection(text.toString()); + cdataSection.setUserData(BEGIN_LINE, beginLineText, null); + cdataSection.setUserData(BEGIN_COLUMN, beginColumnText, null); + cdataSection.setUserData(END_LINE, locator.getLineNumber(), null); + cdataSection.setUserData(END_COLUMN, locator.getColumnNumber(), null); + appendChild(cdataSection); + text.setLength(0); + cdataEnded = true; + } + } + @Override + public void comment(char[] ch, int start, int length) throws SAXException { + if (!ignoringComments) { + addTextIfNeeded(false); + Comment comment = document.createComment(new String(ch, start, length)); + comment.setUserData(BEGIN_LINE, locator.getLineNumber(), null); + comment.setUserData(BEGIN_COLUMN, locator.getColumnNumber(), null); + comment.setUserData(END_LINE, locator.getLineNumber(), null); + comment.setUserData(END_COLUMN, locator.getColumnNumber(), null); + appendChild(comment); + } + } + @Override + public void startDTD(String name, String publicId, String systemId) throws SAXException { + DocumentType docType = documentBuilder + .getDOMImplementation() + .createDocumentType(name, publicId, systemId); + docType.setUserData(BEGIN_LINE, locator.getLineNumber(), null); + docType.setUserData(BEGIN_COLUMN, locator.getColumnNumber(), null); + document.appendChild(docType); + } + @Override + public void startEntity(String name) throws SAXException { + if (!expandEntityReferences) { + addTextIfNeeded(false); + } + } + @Override + public void endEntity(String name) throws SAXException { + if (!expandEntityReferences) { + EntityReference entity = document.createEntityReference(name); + entity.setUserData(BEGIN_LINE, beginLineText, null); + entity.setUserData(BEGIN_COLUMN, beginColumnText, null); + entity.setUserData(END_LINE, locator.getLineNumber(), null); + entity.setUserData(END_COLUMN, locator.getColumnNumber(), null); + appendChild(entity); + text.setLength(0); // throw the expanded entity text away + } + } + @Override + public void endDTD() throws SAXException { + DocumentType doctype = document.getDoctype(); + doctype.setUserData(END_LINE, locator.getLineNumber(), null); + doctype.setUserData(END_COLUMN, locator.getColumnNumber(), null); + } + @Override + public void internalEntityDecl(String name, String value) throws SAXException { + Entity entity = new ChangeableEntity(document, name); + entity.appendChild(document.createTextNode(value)); + + NamedNodeMap entities = document.getDoctype().getEntities(); + entities.setNamedItem(entity); + } + @Override + public void processingInstruction(String target, String data) throws SAXException { + ProcessingInstruction pi = document.createProcessingInstruction(target, data); + appendChild(pi); + } + private void appendChild(Node node) { + if (nodeStack.isEmpty()) { + document.appendChild(node); + } else { + nodeStack.peek().appendChild(node); + } + } + private static class ChangeableEntity extends EntityImpl { + public ChangeableEntity(Document document, String name) { + super((CoreDocumentImpl)document, name); + flags = (short) (flags & ~READONLY); // make it changeable again, so that we can add a text node as child + } + } + } + + public XmlNode parse(Reader reader) { Document document = parseDocument(reader); return createProxy(document); @@ -165,13 +403,13 @@ public class XmlParser { return new CompoundIterator(iterators.toArray(new Iterator[iterators.size()])); } else if ("getBeginLine".equals(method.getName())) { - return Integer.valueOf(-1); + return getUserData(LineNumberAwareSaxHandler.BEGIN_LINE); } else if ("getBeginColumn".equals(method.getName())) { - return Integer.valueOf(-1); + return getUserData(LineNumberAwareSaxHandler.BEGIN_COLUMN); } else if ("getEndLine".equals(method.getName())) { - return Integer.valueOf(-1); + return getUserData(LineNumberAwareSaxHandler.END_LINE); } else if ("getEndColumn".equals(method.getName())) { - return Integer.valueOf(-1); + return getUserData(LineNumberAwareSaxHandler.END_COLUMN); } else if ("getNode".equals(method.getName())) { return node; } else if ("getUserData".equals(method.getName())) { @@ -179,6 +417,8 @@ public class XmlParser { } else if ("setUserData".equals(method.getName())) { userData = args[0]; return null; + } else if ("isFindBoundary".equals(method.getName())) { + return false; } throw new UnsupportedOperationException("Method not supported for XmlNode: " + method); } @@ -193,5 +433,12 @@ public class XmlParser { return result; } } + + private Integer getUserData(String key) { + if (node.getUserData(key) != null) { + return (Integer)node.getUserData(key); + } + return Integer.valueOf(-1); + } } } diff --git a/pmd/src/site/markdown/changelog.md b/pmd/src/site/markdown/changelog.md index 082aba77ed..06f039699d 100644 --- a/pmd/src/site/markdown/changelog.md +++ b/pmd/src/site/markdown/changelog.md @@ -42,6 +42,8 @@ * Fixed [bug 881]: private final without setter is flagged * Fixed [bug 1059]: Change rule name "Use Singleton" should be "Use Utility class" * Fixed [bug 1106]: PMD 5.0.4 fails with NPE on parsing java enum with inner class instance creation +* Fixed [bug 1045]: //NOPMD not working (or not implemented) with ECMAscript +* Fixed [bug 1054]: XML Rules ever report a line -1 and not the line/column where the error occurs * Fixed [bug 1115]: commentRequiredRule in pmd 5.1 is not working properly * Fixed [bug 1120]: equalsnull false positive * Fixed [bug 1121]: NullPointerException when invoking XPathCLI @@ -58,7 +60,6 @@ * Fixed [bug 1141]: ECMAScript: getFinallyBlock() is buggy. * Fixed [bug 1142]: ECMAScript: getCatchClause() is buggy. * Fixed [bug 1144]: CPD encoding argument has no effect -* Fixed [bug 1045]: //NOPMD not working (or not implemented) with ECMAscript * Fixed [bug 1146]: UseArrayListInsteadOfVector false positive when using own Vector class * Fixed [bug 1147]: EmptyMethodInAbstractClassShouldBeAbstract false positives * Fixed [bug 1150]: "EmptyExpression" for valid statements! @@ -69,6 +70,8 @@ [bug 881]: https://sourceforge.net/p/pmd/bugs/881 [bug 1059]: https://sourceforge.net/p/pmd/bugs/1059 +[bug 1045]: https://sourceforge.net/p/pmd/bugs/1045 +[bug 1054]: https://sourceforge.net/p/pmd/bugs/1054 [bug 1106]: https://sourceforge.net/p/pmd/bugs/1106 [bug 1115]: https://sourceforge.net/p/pmd/bugs/1115 [bug 1120]: https://sourceforge.net/p/pmd/bugs/1120 @@ -86,7 +89,6 @@ [bug 1141]: https://sourceforge.net/p/pmd/bugs/1141 [bug 1142]: https://sourceforge.net/p/pmd/bugs/1142 [bug 1144]: https://sourceforge.net/p/pmd/bugs/1144 -[bug 1045]: https://sourceforge.net/p/pmd/bugs/1045 [bug 1146]: https://sourceforge.net/p/pmd/bugs/1146 [bug 1147]: https://sourceforge.net/p/pmd/bugs/1147 [bug 1150]: https://sourceforge.net/p/pmd/bugs/1150 diff --git a/pmd/src/test/java/net/sourceforge/pmd/lang/xml/XmlParserTest.java b/pmd/src/test/java/net/sourceforge/pmd/lang/xml/XmlParserTest.java new file mode 100644 index 0000000000..b15e845127 --- /dev/null +++ b/pmd/src/test/java/net/sourceforge/pmd/lang/xml/XmlParserTest.java @@ -0,0 +1,423 @@ +/** + * BSD-style license; for more info see http://pmd.sourceforge.net/license.html + */ +package net.sourceforge.pmd.lang.xml; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.util.Iterator; + +import net.sourceforge.pmd.lang.Language; +import net.sourceforge.pmd.lang.LanguageVersionHandler; +import net.sourceforge.pmd.lang.Parser; +import net.sourceforge.pmd.lang.ast.Node; +import net.sourceforge.pmd.lang.ast.xpath.Attribute; +import net.sourceforge.pmd.lang.xml.ast.XmlNode; +import net.sourceforge.pmd.lang.xml.ast.XmlParser; +import net.sourceforge.pmd.util.StringUtil; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit test for the {@link XmlParser}. + */ +public class XmlParserTest { + + private static final String XML_TEST = + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "]\n" + + ">\n" + + "\n" + + " \n" + + " entity: &pmd;\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + private static final String XML_NAMESPACE_TEST = + "\n" + + "\n" + + " \n" + + " entity: &\n" + + " \n" + + " \n" + + " \n" + + " \n" + + ""; + + private static final String XML_INVALID_WITH_DTD = + "\n" + + "\n" + + "\n" + + "]\n" + + ">\n" + + "\n" + + " \n" + + ""; + + /** + * See bug #1054: + * XML Rules ever report a line -1 and not the line/column where the error occurs + * @throws Exception any error + */ + @Test + public void testLineNumbers() throws Exception { + LanguageVersionHandler xmlVersionHandler = Language.XML.getDefaultVersion().getLanguageVersionHandler(); + Parser parser = xmlVersionHandler.getParser(xmlVersionHandler.getDefaultParserOptions()); + Node document = parser.parse(null, new StringReader(XML_TEST)); + + assertNode(document, "document", 2); + assertLineNumbers(document, 1, 1, 19, 15); + Node dtdElement = document.jjtGetChild(0); + assertNode(dtdElement, "rootElement", 0); + assertLineNumbers(dtdElement, 3, 1, 10, 1); + Node rootElement = document.jjtGetChild(1); + assertNode(rootElement, "rootElement", 7); + assertLineNumbers(rootElement, 12, 14, 19, 15); + assertTextNode(rootElement.jjtGetChild(0), "\\n "); + assertLineNumbers(rootElement.jjtGetChild(0), 13, 5, 13, 30); + assertNode(rootElement.jjtGetChild(1), "comment", 0); + assertLineNumbers(rootElement.jjtGetChild(1), 13, 30, 13, 30); + assertTextNode(rootElement.jjtGetChild(2), "\\n "); + assertLineNumbers(rootElement.jjtGetChild(2), 14, 5, 14, 22); + Node child1 = rootElement.jjtGetChild(3); + assertNode(child1, "child1", 1, "test", "1"); + assertLineNumbers(child1, 14, 22, 15, 14); + assertTextNode(child1.jjtGetChild(0), "entity: Copyright: PMD\\n "); + assertLineNumbers(child1.jjtGetChild(0), 14, 30, 15, 14); + assertTextNode(rootElement.jjtGetChild(4), "\\n "); + assertLineNumbers(rootElement.jjtGetChild(4), 16, 5, 16, 13); + Node child2 = rootElement.jjtGetChild(5); + assertNode(child2, "child2", 3); + assertLineNumbers(child2, 16, 13, 18, 14); + assertTextNode(child2.jjtGetChild(0), "\\n "); + assertLineNumbers(child2.jjtGetChild(0), 17, 7, 17, 16); + assertTextNode(child2.jjtGetChild(1), " cdata section ", "cdata-section"); + assertLineNumbers(child2.jjtGetChild(1), 17, 33, 17, 34); + assertTextNode(child2.jjtGetChild(2), "\\n "); + assertLineNumbers(child2.jjtGetChild(2), 18, 5, 18, 14); + assertTextNode(rootElement.jjtGetChild(6), "\\n"); + assertLineNumbers(rootElement.jjtGetChild(6), 19, 1, 19, 15); + } + + /** + * Verifies the default parsing behavior of the XML parser. + */ + @Test + public void testDefaultParsing() { + LanguageVersionHandler xmlVersionHandler = Language.XML.getDefaultVersion().getLanguageVersionHandler(); + Parser parser = xmlVersionHandler.getParser(xmlVersionHandler.getDefaultParserOptions()); + Node document = parser.parse(null, new StringReader(XML_TEST)); + + assertNode(document, "document", 2); + Node dtdElement = document.jjtGetChild(0); + assertNode(dtdElement, "rootElement", 0); + Node rootElement = document.jjtGetChild(1); + assertNode(rootElement, "rootElement", 7); + assertTextNode(rootElement.jjtGetChild(0), "\\n "); + assertNode(rootElement.jjtGetChild(1), "comment", 0); + assertTextNode(rootElement.jjtGetChild(2), "\\n "); + Node child1 = rootElement.jjtGetChild(3); + assertNode(child1, "child1", 1, "test", "1"); + assertTextNode(child1.jjtGetChild(0), "entity: Copyright: PMD\\n "); + assertTextNode(rootElement.jjtGetChild(4), "\\n "); + Node child2 = rootElement.jjtGetChild(5); + assertNode(child2, "child2", 3); + assertTextNode(child2.jjtGetChild(0), "\\n "); + assertTextNode(child2.jjtGetChild(1), " cdata section ", "cdata-section"); + assertTextNode(child2.jjtGetChild(2), "\\n "); + assertTextNode(rootElement.jjtGetChild(6), "\\n"); + } + + /** + * Verifies the parsing behavior of the XML parser with coalescing enabled. + */ + @Test + public void testParsingCoalescingEnabled() { + LanguageVersionHandler xmlVersionHandler = Language.XML.getDefaultVersion().getLanguageVersionHandler(); + XmlParserOptions parserOptions = new XmlParserOptions(); + parserOptions.setCoalescing(true); + Parser parser = xmlVersionHandler.getParser(parserOptions); + Node document = parser.parse(null, new StringReader(XML_TEST)); + + assertNode(document, "document", 2); + Node dtdElement = document.jjtGetChild(0); + assertNode(dtdElement, "rootElement", 0); + Node rootElement = document.jjtGetChild(1); + assertNode(rootElement, "rootElement", 7); + assertTextNode(rootElement.jjtGetChild(0), "\\n "); + assertNode(rootElement.jjtGetChild(1), "comment", 0); + assertTextNode(rootElement.jjtGetChild(2), "\\n "); + Node child1 = rootElement.jjtGetChild(3); + assertNode(child1, "child1", 1, "test", "1"); + assertTextNode(child1.jjtGetChild(0), "entity: Copyright: PMD\\n "); + assertTextNode(rootElement.jjtGetChild(4), "\\n "); + Node child2 = rootElement.jjtGetChild(5); + assertNode(child2, "child2", 1); + assertTextNode(child2.jjtGetChild(0), "\\n cdata section \\n "); + assertTextNode(rootElement.jjtGetChild(6), "\\n"); + } + + /** + * Verifies the parsing behavior of the XML parser if entities are not expanded. + */ + @Test + public void testParsingDoNotExpandEntities() { + LanguageVersionHandler xmlVersionHandler = Language.XML.getDefaultVersion().getLanguageVersionHandler(); + XmlParserOptions parserOptions = new XmlParserOptions(); + parserOptions.setExpandEntityReferences(false); + Parser parser = xmlVersionHandler.getParser(parserOptions); + Node document = parser.parse(null, new StringReader(XML_TEST)); + + assertNode(document, "document", 2); + Node dtdElement = document.jjtGetChild(0); + assertNode(dtdElement, "rootElement", 0); + Node rootElement = document.jjtGetChild(1); + assertNode(rootElement, "rootElement", 7); + assertTextNode(rootElement.jjtGetChild(0), "\\n "); + assertNode(rootElement.jjtGetChild(1), "comment", 0); + assertTextNode(rootElement.jjtGetChild(2), "\\n "); + Node child1 = rootElement.jjtGetChild(3); + assertNode(child1, "child1", 3, "test", "1"); + assertTextNode(child1.jjtGetChild(0), "entity: "); + assertNode(child1.jjtGetChild(1), "pmd", 1); + assertTextNode(child1.jjtGetChild(1).jjtGetChild(0), "Copyright: PMD"); + assertTextNode(child1.jjtGetChild(2), "\\n "); + assertTextNode(rootElement.jjtGetChild(4), "\\n "); + Node child2 = rootElement.jjtGetChild(5); + assertNode(child2, "child2", 3); + assertTextNode(child2.jjtGetChild(0), "\\n "); + assertTextNode(child2.jjtGetChild(1), " cdata section ", "cdata-section"); + assertTextNode(child2.jjtGetChild(2), "\\n "); + assertTextNode(rootElement.jjtGetChild(6), "\\n"); + } + + /** + * Verifies the parsing behavior of the XML parser if ignoring comments. + */ + @Test + public void testParsingIgnoreComments() { + LanguageVersionHandler xmlVersionHandler = Language.XML.getDefaultVersion().getLanguageVersionHandler(); + XmlParserOptions parserOptions = new XmlParserOptions(); + parserOptions.setIgnoringComments(true); + Parser parser = xmlVersionHandler.getParser(parserOptions); + Node document = parser.parse(null, new StringReader(XML_TEST)); + + assertNode(document, "document", 2); + Node dtdElement = document.jjtGetChild(0); + assertNode(dtdElement, "rootElement", 0); + Node rootElement = document.jjtGetChild(1); + assertNode(rootElement, "rootElement", 5); + assertTextNode(rootElement.jjtGetChild(0), "\\n \\n "); + Node child1 = rootElement.jjtGetChild(1); + assertNode(child1, "child1", 1, "test", "1"); + assertTextNode(child1.jjtGetChild(0), "entity: Copyright: PMD\\n "); + assertTextNode(rootElement.jjtGetChild(2), "\\n "); + Node child2 = rootElement.jjtGetChild(3); + assertNode(child2, "child2", 3); + assertTextNode(child2.jjtGetChild(0), "\\n "); + assertTextNode(child2.jjtGetChild(1), " cdata section ", "cdata-section"); + assertTextNode(child2.jjtGetChild(2), "\\n "); + assertTextNode(rootElement.jjtGetChild(4), "\\n"); + } + + /** + * Verifies the parsing behavior of the XML parser if ignoring whitespaces in elements. + */ + @Test + public void testParsingIgnoreElementContentWhitespace() { + LanguageVersionHandler xmlVersionHandler = Language.XML.getDefaultVersion().getLanguageVersionHandler(); + XmlParserOptions parserOptions = new XmlParserOptions(); + parserOptions.setIgnoringElementContentWhitespace(true); + Parser parser = xmlVersionHandler.getParser(parserOptions); + Node document = parser.parse(null, new StringReader(XML_TEST)); + + assertNode(document, "document", 2); + Node dtdElement = document.jjtGetChild(0); + assertNode(dtdElement, "rootElement", 0); + Node rootElement = document.jjtGetChild(1); + assertNode(rootElement, "rootElement", 3); + assertNode(rootElement.jjtGetChild(0), "comment", 0); + Node child1 = rootElement.jjtGetChild(1); + assertNode(child1, "child1", 1, "test", "1"); + assertTextNode(child1.jjtGetChild(0), "entity: Copyright: PMD\\n "); + Node child2 = rootElement.jjtGetChild(2); + assertNode(child2, "child2", 3); + assertTextNode(child2.jjtGetChild(0), "\\n "); + assertTextNode(child2.jjtGetChild(1), " cdata section ", "cdata-section"); + assertTextNode(child2.jjtGetChild(2), "\\n "); + } + + /** + * Verifies the default parsing behavior of the XML parser with namespaces. + */ + @Test + public void testDefaultParsingNamespaces() { + LanguageVersionHandler xmlVersionHandler = Language.XML.getDefaultVersion().getLanguageVersionHandler(); + Parser parser = xmlVersionHandler.getParser(xmlVersionHandler.getDefaultParserOptions()); + Node document = parser.parse(null, new StringReader(XML_NAMESPACE_TEST)); + + assertNode(document, "document", 1); + Node rootElement = document.jjtGetChild(0); + assertNode(rootElement, "pmd:rootElement", 7); + Assert.assertEquals("http://pmd.sf.net", ((XmlNode)rootElement).getNode().getNamespaceURI()); + Assert.assertEquals("pmd", ((XmlNode)rootElement).getNode().getPrefix()); + Assert.assertEquals("rootElement", ((XmlNode)rootElement).getNode().getLocalName()); + Assert.assertEquals("pmd:rootElement", ((XmlNode)rootElement).getNode().getNodeName()); + assertTextNode(rootElement.jjtGetChild(0), "\\n "); + assertNode(rootElement.jjtGetChild(1), "comment", 0); + assertTextNode(rootElement.jjtGetChild(2), "\\n "); + Node child1 = rootElement.jjtGetChild(3); + assertNode(child1, "pmd:child1", 1, "test", "1"); + assertTextNode(child1.jjtGetChild(0), "entity: &\\n "); + assertTextNode(rootElement.jjtGetChild(4), "\\n "); + Node child2 = rootElement.jjtGetChild(5); + assertNode(child2, "pmd:child2", 3); + assertTextNode(child2.jjtGetChild(0), "\\n "); + assertTextNode(child2.jjtGetChild(1), " cdata section ", "cdata-section"); + assertTextNode(child2.jjtGetChild(2), "\\n "); + assertTextNode(rootElement.jjtGetChild(6), "\\n"); + } + + /** + * Verifies the default parsing behavior of the XML parser with namespaces but not namespace aware. + */ + @Test + public void testParsingNotNamespaceAware() { + LanguageVersionHandler xmlVersionHandler = Language.XML.getDefaultVersion().getLanguageVersionHandler(); + XmlParserOptions parserOptions = new XmlParserOptions(); + parserOptions.setNamespaceAware(false); + Parser parser = xmlVersionHandler.getParser(parserOptions); + Node document = parser.parse(null, new StringReader(XML_NAMESPACE_TEST)); + + assertNode(document, "document", 1); + Node rootElement = document.jjtGetChild(0); + assertNode(rootElement, "pmd:rootElement", 7, "xmlns:pmd", "http://pmd.sf.net"); + Assert.assertNull(((XmlNode)rootElement).getNode().getNamespaceURI()); + Assert.assertNull(((XmlNode)rootElement).getNode().getPrefix()); + Assert.assertNull(((XmlNode)rootElement).getNode().getLocalName()); + Assert.assertEquals("pmd:rootElement", ((XmlNode)rootElement).getNode().getNodeName()); + assertTextNode(rootElement.jjtGetChild(0), "\\n "); + assertNode(rootElement.jjtGetChild(1), "comment", 0); + assertTextNode(rootElement.jjtGetChild(2), "\\n "); + Node child1 = rootElement.jjtGetChild(3); + assertNode(child1, "pmd:child1", 1, "test", "1"); + assertTextNode(child1.jjtGetChild(0), "entity: &\\n "); + assertTextNode(rootElement.jjtGetChild(4), "\\n "); + Node child2 = rootElement.jjtGetChild(5); + assertNode(child2, "pmd:child2", 3); + assertTextNode(child2.jjtGetChild(0), "\\n "); + assertTextNode(child2.jjtGetChild(1), " cdata section ", "cdata-section"); + assertTextNode(child2.jjtGetChild(2), "\\n "); + assertTextNode(rootElement.jjtGetChild(6), "\\n"); + } + + /** + * Verifies the parsing behavior of the XML parser with validation on. + * @throws UnsupportedEncodingException error + */ + @Test + public void testParsingWithValidation() throws UnsupportedEncodingException { + LanguageVersionHandler xmlVersionHandler = Language.XML.getDefaultVersion().getLanguageVersionHandler(); + XmlParserOptions parserOptions = new XmlParserOptions(); + parserOptions.setValidating(true); + Parser parser = xmlVersionHandler.getParser(parserOptions); + PrintStream oldErr = System.err; + try { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + System.setErr(new PrintStream(bos)); + Node document = parser.parse(null, new StringReader(XML_INVALID_WITH_DTD)); + Assert.assertNotNull(document); + String output = bos.toString("UTF-8"); + Assert.assertTrue(output.contains("Element type \"invalidChild\" must be declared.")); + Assert.assertTrue(output.contains("The content of element type \"rootElement\" must match \"(child)\".")); + Assert.assertEquals(2, document.jjtGetNumChildren()); + Assert.assertEquals("invalidChild", String.valueOf(document.jjtGetChild(1).jjtGetChild(1))); + } finally { + System.setErr(oldErr); + } + } + + /** + * Asserts a single node inclusive attributes. + * @param node the node + * @param toString the to String representation to expect + * @param childs number of childs + * @param atts attributes - each object pair forms one attribute: first name, then value. + */ + private void assertNode(Node node, String toString, int childs, Object ... atts) { + Assert.assertEquals(toString, String.valueOf(node)); + Assert.assertEquals(childs, node.jjtGetNumChildren()); + Iterator attributeIterator = ((XmlNode)node).getAttributeIterator(); + if (atts != null) { + for (int i = 0; i < atts.length; i += 2) { + Assert.assertTrue(attributeIterator.hasNext()); + String name = String.valueOf(atts[i]); + Object value = atts[i + 1]; + Attribute attribute = attributeIterator.next(); + Assert.assertEquals(name, attribute.getName()); + Assert.assertEquals(value, attribute.getValue()); + } + } + Assert.assertFalse(attributeIterator.hasNext()); + } + + /** + * Assert a single text node. + * @param node the node to check + * @param text the text to expect + */ + private void assertTextNode(Node node, String text) { + assertTextNode(node, text, "text"); + } + + /** + * Assert a single text node. + * + * @param node the node to check + * @param text the text to expect + * @param toString the to string representation + */ + private void assertTextNode(Node node, String text, String toString) { + Assert.assertEquals(toString, String.valueOf(node)); + Assert.assertEquals(0, node.jjtGetNumChildren()); + Assert.assertEquals(text, StringUtil.escapeWhitespace(node.getImage())); + Iterator attributeIterator = ((XmlNode)node).getAttributeIterator(); + Assert.assertTrue(attributeIterator.hasNext()); + Attribute attribute = attributeIterator.next(); + Assert.assertEquals("Image", attribute.getName()); + Assert.assertEquals(text, StringUtil.escapeWhitespace(attribute.getValue())); + Assert.assertFalse(attributeIterator.hasNext()); + } + + /** + * Assert the line numbers of a node. + * + * @param node the node + * @param beginLine the begin line + * @param beginColumn the begin column + * @param endLine the end line + * @param endColumn the end column + */ + private void assertLineNumbers(Node node, int beginLine, int beginColumn, int endLine, int endColumn) { + Assert.assertEquals("begin line wrong", beginLine, node.getBeginLine()); + Assert.assertEquals("begin column wrong", beginColumn, node.getBeginColumn()); + Assert.assertEquals("end line wrong", endLine, node.getEndLine()); + Assert.assertEquals("end column wrong", endColumn, node.getEndColumn()); + } +} diff --git a/pmd/src/test/java/net/sourceforge/pmd/lang/xml/rule/AbstractDomXmlRuleTest.java b/pmd/src/test/java/net/sourceforge/pmd/lang/xml/rule/AbstractDomXmlRuleTest.java index 2931e05466..537292c8a0 100644 --- a/pmd/src/test/java/net/sourceforge/pmd/lang/xml/rule/AbstractDomXmlRuleTest.java +++ b/pmd/src/test/java/net/sourceforge/pmd/lang/xml/rule/AbstractDomXmlRuleTest.java @@ -76,8 +76,10 @@ public class AbstractDomXmlRuleTest { // assertEquals(0, visited.size()); visited = rule.visitedNodes.get("EntityReference"); - assertEquals(1, visited.size()); - assertEquals("entity", ((EntityReference) visited.get(0)).getNodeName()); + assertEquals(3, visited.size()); + assertEquals("gt", ((EntityReference) visited.get(0)).getNodeName()); + assertEquals("entity", ((EntityReference) visited.get(1)).getNodeName()); + assertEquals("lt", ((EntityReference) visited.get(2)).getNodeName()); // TODO Figure out how to trigger this. // visited = rule.visitedNodes.get("Notation"); @@ -89,11 +91,9 @@ public class AbstractDomXmlRuleTest { ((ProcessingInstruction) visited.get(0)).getTarget()); visited = rule.visitedNodes.get("Text"); - assertEquals(4, visited.size()); + assertEquals(2, visited.size()); assertEquals("TEXT", ((Text) visited.get(0)).getData()); - assertEquals(">", ((Text) visited.get(1)).getData()); - assertEquals("e", ((Text) visited.get(2)).getData()); - assertEquals("<", ((Text) visited.get(3)).getData()); + assertEquals("e", ((Text) visited.get(1)).getData()); } @Test