From 05fb36d4cf6fe1d22d01e3deda1b28caea75c1fd Mon Sep 17 00:00:00 2001 From: James Murty Date: Mon, 19 May 2014 16:35:44 +0100 Subject: [PATCH 01/43] Test #asString returns full doc even if moderate size, re #1 Ensure #asString method returns a whole XML document even when the doc gets moderately sized -- over 0.5 MB in this test case. --- .../com/jamesmurty/utils/TestXmlBuilder.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java index ce5eea6..fd7850e 100644 --- a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java +++ b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java @@ -1,5 +1,7 @@ package com.jamesmurty.utils; +import java.io.File; +import java.io.FileWriter; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; @@ -549,4 +551,40 @@ public void testSetStandaloneToYes() throws Exception { writer.toString()); } + /** + * Test the {@link XMLBuilder#asString(Properties)} method output a document + * of the correct size when the document is moderately large, re issue + * #1 (on GitHub). + * + * @throws Exception + */ + public void testModerateDocumentSizeAsString() throws Exception { + // Create a moderate document around 0.5 MB + long expectedByteSize = 505021; + XMLBuilder builder = XMLBuilder.create("RootNode"); + for (int i = 0; i < 5000; i++) { + builder + .e("TreeRoot") + .e("TreeTrunk") + .e("TreeBranch") + .e("TreeLeaf") + .t("Some Aphids"); + + } + // Omit XML declaration, which will otherwise be included in file + // via #toWriter but not in string via #asString + Properties outputProperties = new Properties(); + outputProperties.put( + javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); + // Ensure XML as string has expected length... + String xmlString = builder.asString(outputProperties); + assertEquals(expectedByteSize, xmlString.length()); + // ...and matches size of XML written to file + File f = File.createTempFile( + "java-xmlbuilder-testmoderatedocumentsizeasstring", ".xml"); + builder.toWriter(new FileWriter(f), outputProperties); + assertEquals(expectedByteSize, f.length()); + f.delete(); + } + } From b2965f89c36bf31ddd49341bf00ef34e6dc5019e Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 19 Jun 2014 22:34:31 +0100 Subject: [PATCH 02/43] Tests now use system line-separator for newlines, fixes #2 Use System.getProperty("line.separator") instead of hard-coded "\n" for newlines to improve chances of tests passing on non-Unixy systems (i.e. Windows) --- src/test/java/com/jamesmurty/utils/TestXmlBuilder.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java index fd7850e..6a4de46 100644 --- a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java +++ b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java @@ -26,6 +26,8 @@ public class TestXmlBuilder extends TestCase { + public static final String NEWLINE = System.getProperty("line.separator"); + public static final String EXAMPLE_XML_DOC_START = "" + "" + @@ -217,12 +219,12 @@ public void testStripWhitespaceNodesFromDocument() // Re-parse document that now has whitespace text nodes XMLBuilder builder = XMLBuilder.parse(xmlWithWhitespaceNodes); - assertTrue(builder.asString().contains("\n")); + assertTrue(builder.asString().contains(NEWLINE)); assertTrue(builder.asString().contains(" ")); // Strip whitespace nodes builder.stripWhitespaceOnlyTextNodes(); - assertFalse(builder.asString().contains("\n")); + assertFalse(builder.asString().contains(NEWLINE)); assertFalse(builder.asString().contains(" ")); } @@ -511,13 +513,13 @@ public void testProcessingInstructionNodes() throws Exception { // Insert instruction as first node of the root document builder = XMLBuilder.create("TestDocument3").insertInstruction("test", "data"); - assertEquals("\n", builder.asString()); + assertEquals("" + NEWLINE + "", builder.asString()); // Insert instruction as first node of the root document, second example builder = XMLBuilder.create("TestDocument4").elem("ChildElem") .root().insertInstruction("test", "data"); assertEquals( - "\n", + "" + NEWLINE + "", builder.asString()); } From 0db0cfe5e58ee8bc90ed2d9135570c9f0122970e Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 19 Jun 2014 22:41:53 +0100 Subject: [PATCH 03/43] Only GPG sign artifacts during "deploy" phase, fixes #3 This POM tweak should make it easier for others to install the jar without needing any GPG supporting infrastructure, while still allowing me to sign and deploy artifacts to Maven Central. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6d38f34..bd3cf7f 100644 --- a/pom.xml +++ b/pom.xml @@ -105,7 +105,7 @@ sign-artifacts - verify + deploy sign From 3d2f10db9c8019817c18f67beeff5591804f3003 Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 26 Jun 2014 21:39:21 +0100 Subject: [PATCH 04/43] Revert "Tests now use system line-separator for newlines, fixes #2" This reverts commit b2965f89c36bf31ddd49341bf00ef34e6dc5019e. --- src/test/java/com/jamesmurty/utils/TestXmlBuilder.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java index 6a4de46..fd7850e 100644 --- a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java +++ b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java @@ -26,8 +26,6 @@ public class TestXmlBuilder extends TestCase { - public static final String NEWLINE = System.getProperty("line.separator"); - public static final String EXAMPLE_XML_DOC_START = "" + "" + @@ -219,12 +217,12 @@ public void testStripWhitespaceNodesFromDocument() // Re-parse document that now has whitespace text nodes XMLBuilder builder = XMLBuilder.parse(xmlWithWhitespaceNodes); - assertTrue(builder.asString().contains(NEWLINE)); + assertTrue(builder.asString().contains("\n")); assertTrue(builder.asString().contains(" ")); // Strip whitespace nodes builder.stripWhitespaceOnlyTextNodes(); - assertFalse(builder.asString().contains(NEWLINE)); + assertFalse(builder.asString().contains("\n")); assertFalse(builder.asString().contains(" ")); } @@ -513,13 +511,13 @@ public void testProcessingInstructionNodes() throws Exception { // Insert instruction as first node of the root document builder = XMLBuilder.create("TestDocument3").insertInstruction("test", "data"); - assertEquals("" + NEWLINE + "", builder.asString()); + assertEquals("\n", builder.asString()); // Insert instruction as first node of the root document, second example builder = XMLBuilder.create("TestDocument4").elem("ChildElem") .root().insertInstruction("test", "data"); assertEquals( - "" + NEWLINE + "", + "\n", builder.asString()); } From 2f97e605315cbb9227e563380f09f8b9e48e15e0 Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 26 Jun 2014 21:47:10 +0100 Subject: [PATCH 05/43] Ignore cross-platform newline differences in ProcessingInstruction tests, fixes #2 --- src/test/java/com/jamesmurty/utils/TestXmlBuilder.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java index fd7850e..23c8804 100644 --- a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java +++ b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java @@ -511,14 +511,18 @@ public void testProcessingInstructionNodes() throws Exception { // Insert instruction as first node of the root document builder = XMLBuilder.create("TestDocument3").insertInstruction("test", "data"); - assertEquals("\n", builder.asString()); + assertEquals( + "", + // Remove newlines from XML as this differs across platforms + builder.asString().replace("\n", "")); // Insert instruction as first node of the root document, second example builder = XMLBuilder.create("TestDocument4").elem("ChildElem") .root().insertInstruction("test", "data"); assertEquals( - "\n", - builder.asString()); + "", + // Remove newlines from XML as this differs across platforms + builder.asString().replace("\n", "")); } /** From e0dc7cc684dd924827ff8e449ac63a704b88e703 Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 17 Jul 2014 10:40:05 +0100 Subject: [PATCH 06/43] Update pointer to GitHub code repo in source code comment --- src/main/java/com/jamesmurty/utils/XMLBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder.java b/src/main/java/com/jamesmurty/utils/XMLBuilder.java index 6e255bd..773827e 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder.java @@ -14,8 +14,8 @@ * limitations under the License. * * - * This code is available from the Google Code repository at: - * http://code.google.com/p/java-xmlbuilder + * This code is available from the GitHub code repository at: + * https://github.com/jmurty/java-xmlbuilder */ package com.jamesmurty.utils; From 7dbf6d33a78b426af654419b561ba3b61d75714e Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 17 Jul 2014 11:02:42 +0100 Subject: [PATCH 07/43] Remove unused import --- src/main/java/com/jamesmurty/utils/XMLBuilder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder.java b/src/main/java/com/jamesmurty/utils/XMLBuilder.java index 773827e..ef391ce 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder.java @@ -52,7 +52,6 @@ import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import org.w3c.dom.Text; import org.xml.sax.InputSource; import org.xml.sax.SAXException; From 57c617f1808de66b3090b3e2385ade969082a05e Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 17 Jul 2014 12:18:31 +0100 Subject: [PATCH 08/43] Minor cleanup to remove warnings --- .../java/com/jamesmurty/utils/NamespaceContextImpl.java | 7 +++++-- src/test/java/com/jamesmurty/utils/TestXmlBuilder.java | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/jamesmurty/utils/NamespaceContextImpl.java b/src/main/java/com/jamesmurty/utils/NamespaceContextImpl.java index e64e7fc..9b20dcb 100644 --- a/src/main/java/com/jamesmurty/utils/NamespaceContextImpl.java +++ b/src/main/java/com/jamesmurty/utils/NamespaceContextImpl.java @@ -31,6 +31,9 @@ public NamespaceContextImpl() { /** * Create a namespace context that will lookup namespace * information in the given element. + * + * @param element + * Element in which to look up namespace information. */ public NamespaceContextImpl(Element element) { this.element = element; @@ -77,8 +80,8 @@ public String getPrefix(String namespaceURI) { return null; } - // No implemented - @SuppressWarnings("unchecked") + // Not implemented + @SuppressWarnings({ "rawtypes" }) public Iterator getPrefixes(String namespaceURI) { return Collections.EMPTY_LIST.iterator(); } diff --git a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java index 23c8804..bc44251 100644 --- a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java +++ b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java @@ -18,7 +18,6 @@ import junit.framework.TestCase; import net.iharder.Base64; -import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; From 7bb5795f633370865f35db6d28393931794e6ce7 Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 17 Jul 2014 12:36:30 +0100 Subject: [PATCH 09/43] Initial refactor of builder into Base and implementation, re #4 Refactored the bulk of XMLBuilder functionality into a new abstract base class BaseXMLBuilder, in preparation for creating a new XMLBuilder implementation that throws only runtime exceptions not checked ones. --- .../com/jamesmurty/utils/BaseXMLBuilder.java | 787 ++++++++++++++++++ .../java/com/jamesmurty/utils/XMLBuilder.java | 492 +---------- 2 files changed, 823 insertions(+), 456 deletions(-) create mode 100644 src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java diff --git a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java new file mode 100644 index 0000000..f46d3e3 --- /dev/null +++ b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java @@ -0,0 +1,787 @@ +/* + * Copyright 2008-2014 James Murty (www.jamesmurty.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * This code is available from the GitHub code repository at: + * https://github.com/jmurty/java-xmlbuilder + */ +package com.jamesmurty.utils; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Map.Entry; +import java.util.Properties; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpression; +import javax.xml.xpath.XPathExpressionException; +import javax.xml.xpath.XPathFactory; + +import net.iharder.Base64; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * Base abstract class for all XML Builder implementations. + * Most of the work is done here. + * + * @author jmurty + */ +public abstract class BaseXMLBuilder { + + /** + * A DOM Document that stores the underlying XML document operated on by + * BaseXMLBuilder instances. This document object belongs to the root node + * of a document, and is shared by this node with all other BaseXMLBuilder + * instances via the {@link #getDocument()} method. + * This instance variable must only be created once, by the root node for + * any given document. + */ + private Document xmlDocument = null; + + /** + * The underlying node represented by this builder node. + */ + private Node xmlNode = null; + + private static boolean isNamespaceAware = true; + + /** + * Construct a new builder object that wraps the given XML document. + * This constructor is for internal use only. + * + * @param xmlDocument + * an XML document that the builder will manage and manipulate. + */ + protected BaseXMLBuilder(Document xmlDocument) { + this.xmlDocument = xmlDocument; + this.xmlNode = xmlDocument.getDocumentElement(); + } + + /** + * Construct a new builder object that wraps the given XML document and node. + * This constructor is for internal use only. + * + * @param myNode + * the XML node that this builder node will wrap. This node may + * be part of the XML document, or it may be a new element that is to be + * added to the document. + * @param parentNode + * If not null, the given myElement will be appended as child node of the + * parentNode node. + */ + protected BaseXMLBuilder(Node myNode, Node parentNode) { + this.xmlNode = myNode; + if (myNode instanceof Document) { + this.xmlDocument = (Document) myNode; + } else { + this.xmlDocument = myNode.getOwnerDocument(); + } + if (parentNode != null) { + parentNode.appendChild(myNode); + } + } + + /** + * Construct an XML Document with a default namespace with the given + * root element. + * + * @param name + * the name of the document's root element. + * @param namespaceURI + * default namespace URI for document, ignored if null or empty. + * @return + * an XML Document. + * + * @throws FactoryConfigurationError + * @throws ParserConfigurationException + */ + protected static Document createDocumentImpl(String name, String namespaceURI) + throws ParserConfigurationException, FactoryConfigurationError + { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(isNamespaceAware); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.newDocument(); + Element rootElement = null; + if (namespaceURI != null && namespaceURI.length() > 0) { + rootElement = document.createElementNS(namespaceURI, name); + } else { + rootElement = document.createElement(name); + } + document.appendChild(rootElement); + return document; + } + + /** + * Return an XML Document parsed from the given input source. + * + * @param inputSource + * an XML document input source that will be parsed into a DOM. + * @return + * a builder node that can be used to add more nodes to the XML document. + * @throws ParserConfigurationException + * + * @throws FactoryConfigurationError + * @throws ParserConfigurationException + * @throws IOException + * @throws SAXException + */ + protected static Document parseDocumentImpl(InputSource inputSource) + throws ParserConfigurationException, SAXException, IOException + { + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(isNamespaceAware); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(inputSource); + return document; + } + + /** + * Find and delete from the underlying Document any text nodes that + * contain nothing but whitespace, such as newlines and tab or space + * characters used to indent or pretty-print an XML document. + * + * Uses approach I documented on StackOverflow: + * http://stackoverflow.com/a/979606/4970 + * + * @throws XPathExpressionException + */ + protected void stripWhitespaceOnlyTextNodesImpl() + throws XPathExpressionException + { + XPathFactory xpathFactory = XPathFactory.newInstance(); + // XPath to find empty text nodes. + XPathExpression xpathExp = xpathFactory.newXPath().compile( + "//text()[normalize-space(.) = '']"); + NodeList emptyTextNodes = (NodeList) xpathExp.evaluate( + this.getDocument(), XPathConstants.NODESET); + + // Remove each empty text node from document. + for (int i = 0; i < emptyTextNodes.getLength(); i++) { + Node emptyTextNode = emptyTextNodes.item(i); + emptyTextNode.getParentNode().removeChild(emptyTextNode); + } + } + + /** + * Imports another BaseXMLBuilder document into this document at the + * current position. The entire document provided is imported. + * + * @param builder + * the BaseXMLBuilder document to be imported. + */ + protected void importXMLBuilderImpl(BaseXMLBuilder builder) { + assertElementContainsNoOrWhitespaceOnlyTextNodes(this.xmlNode); + Node importedNode = getDocument().importNode( + builder.getDocument().getDocumentElement(), true); + this.xmlNode.appendChild(importedNode); + } + + /** + * @return + * true if the XML Document and Element objects wrapped by this + * builder are equal to the other's wrapped objects. + */ + @Override + public boolean equals(Object obj) { + if (obj != null && obj instanceof BaseXMLBuilder) { + BaseXMLBuilder other = (BaseXMLBuilder) obj; + return + this.xmlDocument.equals(other.getDocument()) + && this.xmlNode.equals(other.getElement()); + } + return false; + } + + /** + * @return + * the XML element wrapped by this builder node, or null if the builder node wraps the + * root Document node. + */ + public Element getElement() { + if (this.xmlNode instanceof Element) { + return (Element) this.xmlNode; + } else { + return null; + } + } + + /** + * @return + * the XML document constructed by all builder nodes. + */ + public Document getDocument() { + return this.xmlDocument; + } + + /** + * Return the result of evaluating an XPath query on the builder's DOM + * using the given namespace. Returns null if the query finds nothing, + * or finds a node that does not match the type specified by returnType. + * + * @param xpath + * an XPath expression + * @param type + * the type the XPath is expected to resolve to, e.g: + * {@link XPathConstants#NODE}, {@link XPathConstants#NODESET}, + * {@link XPathConstants#STRING}. + * @param nsContext + * a mapping of prefixes to namespace URIs that allows the XPath expression + * to use namespaces, or null for a non-namespaced document. + * + * @return + * a builder node representing the first Element that matches the + * XPath expression. + * + * @throws XPathExpressionException + * If the XPath is invalid, or if does not resolve to at least one + * {@link Node#ELEMENT_NODE}. + */ + public Object xpathQuery(String xpath, QName type, NamespaceContext nsContext) + throws XPathExpressionException { + XPathFactory xpathFactory = XPathFactory.newInstance(); + XPath xPath = xpathFactory.newXPath(); + if (nsContext != null) { + xPath.setNamespaceContext(nsContext); + } + XPathExpression xpathExp = xPath.compile(xpath); + try { + return xpathExp.evaluate(this.xmlNode, type); + } catch (IllegalArgumentException e) { + // Thrown if item found does not match expected type + return null; + } + } + + /** + * Return the result of evaluating an XPath query on the builder's DOM. + * Returns null if the query finds nothing, + * or finds a node that does not match the type specified by returnType. + * + * @param xpath + * an XPath expression + * @param type + * the type the XPath is expected to resolve to, e.g: + * {@link XPathConstants#NODE}, {@link XPathConstants#NODESET}, + * {@link XPathConstants#STRING} + * + * @return + * a builder node representing the first Element that matches the + * XPath expression. + * + * @throws XPathExpressionException + * If the XPath is invalid, or if does not resolve to at least one + * {@link Node#ELEMENT_NODE}. + */ + public Object xpathQuery(String xpath, QName type) + throws XPathExpressionException { + return xpathQuery(xpath, type, null); + } + + /** + * Find the first element in the builder's DOM matching the given + * XPath expression, where the expression may include namespaces if + * a {@link NamespaceContext} is provided. + * + * @param xpath + * An XPath expression that *must* resolve to an existing Element within + * the document object model. + * @param nsContext + * a mapping of prefixes to namespace URIs that allows the XPath expression + * to use namespaces. + * + * @return + * the first Element that matches the XPath expression. + * + * @throws XPathExpressionException + * If the XPath is invalid, or if does not resolve to at least one + * {@link Node#ELEMENT_NODE}. + */ + protected Node xpathFindImpl(String xpath, NamespaceContext nsContext) + throws XPathExpressionException + { + Node foundNode = (Node) this.xpathQuery(xpath, XPathConstants.NODE, nsContext); + if (foundNode == null || foundNode.getNodeType() != Node.ELEMENT_NODE) { + throw new XPathExpressionException("XPath expression \"" + + xpath + "\" does not resolve to an Element in context " + + this.xmlNode + ": " + foundNode); + } + return foundNode; + } + + /** + * Look up the namespace matching the current builder node's qualified + * name prefix (if any) or the document's default namespace. + * + * @param name + * the name of the XML element. + * + * @return + * The namespace URI, or null if none applies. + */ + protected String lookupNamespaceURIImpl(String name) { + String prefix = getPrefixFromQualifiedName(name); + String namespaceURI = this.xmlNode.lookupNamespaceURI(prefix); + return namespaceURI; + } + + /** + * Add a named and namespaced XML element to the document as a child of + * this builder's node. + * + * @param name + * the name of the XML element. + * @param namespaceURI + * a namespace URI + * @return + * + * @throws IllegalStateException + * if you attempt to add a child element to an XML node that already + * contains a text node value. + */ + protected Element elementImpl(String name, String namespaceURI) { + assertElementContainsNoOrWhitespaceOnlyTextNodes(this.xmlNode); + if (namespaceURI == null) { + return getDocument().createElement(name); + } else { + return getDocument().createElementNS(namespaceURI, name); + } + } + + /** + * Add a named XML element to the document as a sibling element + * that precedes the position of this builder node. + * + * When adding an element to a namespaced document, the new node will be + * assigned a namespace matching it's qualified name prefix (if any) or + * the document's default namespace. NOTE: If the element has a prefix that + * does not match any known namespaces, the element will be created + * without any namespace. + * + * @param name + * the name of the XML element. + * + * @throws IllegalStateException + * if you attempt to add a sibling element to a node where there are already + * one or more siblings that are text nodes. + */ + protected Element elementBeforeImpl(String name) { + String prefix = getPrefixFromQualifiedName(name); + String namespaceURI = this.xmlNode.lookupNamespaceURI(prefix); + return elementBeforeImpl(name, namespaceURI); + } + + /** + * Add a named and namespaced XML element to the document as a sibling element + * that precedes the position of this builder node. + * + * @param name + * the name of the XML element. + * @param namespaceURI + * a namespace URI + * + * @throws IllegalStateException + * if you attempt to add a sibling element to a node where there are already + * one or more siblings that are text nodes. + */ + protected Element elementBeforeImpl(String name, String namespaceURI) { + Node parentNode = this.xmlNode.getParentNode(); + assertElementContainsNoOrWhitespaceOnlyTextNodes(parentNode); + + Element newElement = (namespaceURI == null + ? getDocument().createElement(name) + : getDocument().createElementNS(namespaceURI, name)); + + // Insert new element before the current element + parentNode.insertBefore(newElement, this.xmlNode); + // Return a new builder node pointing at the new element + return newElement; + } + + /** + * Add a named attribute value to the element for this builder node. + * + * @param name + * the attribute's name. + * @param value + * the attribute's value. + */ + protected void attributeImpl(String name, String value) { + if (! (this.xmlNode instanceof Element)) { + throw new RuntimeException( + "Cannot add an attribute to non-Element underlying node: " + + this.xmlNode); + } + ((Element) xmlNode).setAttribute(name, value); + } + + /** + * Add or replace the text value of an element for this builder node. + * + * @param value + * the text value to set or add to the element. + * @param replaceText + * if True any existing text content of the node is replaced with the + * given text value, if the given value is appended to any existing text. + */ + protected void textImpl(String value, boolean replaceText) { + // Issue 10: null text values cause exceptions on subsequent call to + // Transformer to render document, so we fail-fast here on bad data. + if (value == null) { + throw new IllegalArgumentException("Illegal null text value"); + } + + if (replaceText) { + xmlNode.setTextContent(value); + } else { + xmlNode.appendChild(getDocument().createTextNode(value)); + } + } + + + /** + * Add a CDATA node with String content to the element for this builder node. + * + * @param data + * the String value that will be added to a CDATA element. + */ + protected void cdataImpl(String data) { + xmlNode.appendChild( + getDocument().createCDATASection(data)); + } + + /** + * Add a CDATA node with Base64-encoded byte data content to the element + * for this builder node. + * + * @param data + * the data value that will be Base64-encoded and added to a CDATA element. + */ + protected void cdataImpl(byte[] data) { + xmlNode.appendChild( + getDocument().createCDATASection( + Base64.encodeBytes(data))); + } + + /** + * Add a comment to the element represented by this builder node. + * + * @param comment + * the comment to add to the element. + */ + protected void commentImpl(String comment) { + xmlNode.appendChild(getDocument().createComment(comment)); + } + + /** + * Add an instruction to the element represented by this builder node. + * + * @param target + * the target value for the instruction. + * @param data + * the data value for the instruction + */ + protected void instructionImpl(String target, String data) { + xmlNode.appendChild(getDocument().createProcessingInstruction(target, data)); + } + + /** + * Insert an instruction before the element represented by this builder node. + * + * @param target + * the target value for the instruction. + * @param data + * the data value for the instruction + */ + protected void insertInstructionImpl(String target, String data) { + getDocument().insertBefore( + getDocument().createProcessingInstruction(target, data), + xmlNode); + } + + /** + * Add a reference to the element represented by this builder node. + * + * @param name + * the name value for the reference. + */ + protected void referenceImpl(String name) { + xmlNode.appendChild(getDocument().createEntityReference(name)); + } + + /** + * Add an XML namespace attribute to this builder's element node. + * + * @param prefix + * a prefix for the namespace URI within the document, may be null + * or empty in which case a default "xmlns" attribute is created. + * @param namespaceURI + * a namespace uri + */ + protected void namespaceImpl(String prefix, String namespaceURI) { + if (! (this.xmlNode instanceof Element)) { + throw new RuntimeException( + "Cannot add an attribute to non-Element underlying node: " + + this.xmlNode); + } + if (prefix != null && prefix.length() > 0) { + ((Element) xmlNode).setAttributeNS("http://www.w3.org/2000/xmlns/", + "xmlns:" + prefix, namespaceURI); + } else { + ((Element) xmlNode).setAttributeNS("http://www.w3.org/2000/xmlns/", + "xmlns", namespaceURI); + } + } + + /** + * Add an XML namespace attribute to this builder's element node + * without a prefix. + * + * @param namespaceURI + * a namespace uri + */ + protected void namespaceImpl(String namespaceURI) { + namespaceImpl(null, namespaceURI); + } + + /** + * Return the Document node representing the nth ancestor element + * of this node, or the root node if n exceeds the document's depth. + * + * @param steps + * the number of parent elements to step over while navigating up the chain + * of node ancestors. A steps value of 1 will find a node's parent, 2 will + * find its grandparent etc. + * + * @return + * the nth ancestor of this node, or the root node if this is + * reached before the nth parent is found. + */ + protected Node upImpl(int steps) { + Node currNode = this.xmlNode; + int stepCount = 0; + while (currNode.getParentNode() != null && stepCount < steps) { + currNode = currNode.getParentNode(); + stepCount++; + } + return currNode; + } + + /** + * @throws IllegalStateException + * if the current element contains any child text nodes that aren't pure whitespace. + * We allow whitespace so parsed XML documents containing indenting or pretty-printing + * can still be amended, per issue #17. + */ + protected void assertElementContainsNoOrWhitespaceOnlyTextNodes( + Node anXmlElement) { + Node textNodeWithNonWhitespace = null; + NodeList childNodes = anXmlElement.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + if (Element.TEXT_NODE == childNodes.item(i).getNodeType()) { + Node textNode = childNodes.item(i); + String textWithoutWhitespace = + textNode.getTextContent().replaceAll("\\s", ""); + if (textWithoutWhitespace.length() > 0) { + textNodeWithNonWhitespace = textNode; + break; + } + } + } + if (textNodeWithNonWhitespace != null) { + throw new IllegalStateException( + "Cannot add sub-element to element <" + anXmlElement.getNodeName() + + "> that contains a Text node that isn't purely whitespace: " + + textNodeWithNonWhitespace); + } + } + + /** + * Serialize either the specific Element wrapped by this BaseXMLBuilder, + * or its entire XML document, to the given writer using the default + * {@link TransformerFactory} and {@link Transformer} classes. + * If output options are provided, these options are provided to the + * {@link Transformer} serializer. + * + * @param wholeDocument + * if true the whole XML document (i.e. the document root) is serialized, + * if false just the current Element and its descendants are serialized. + * @param writer + * a writer to which the serialized document is written. + * @param outputProperties + * settings for the {@link Transformer} serializer. This parameter may be + * null or an empty Properties object, in which case the default output + * properties will be applied. + * + * @throws TransformerException + */ + public void toWriter(boolean wholeDocument, Writer writer, Properties outputProperties) + throws TransformerException { + StreamResult streamResult = new StreamResult(writer); + + DOMSource domSource = null; + if (wholeDocument) { + domSource = new DOMSource(getDocument()); + } else { + domSource = new DOMSource(getElement()); + } + + TransformerFactory tf = TransformerFactory.newInstance(); + Transformer serializer = tf.newTransformer(); + + if (outputProperties != null) { + for (Entry entry: outputProperties.entrySet()) { + serializer.setOutputProperty( + (String) entry.getKey(), + (String) entry.getValue()); + } + } + serializer.transform(domSource, streamResult); + } + + /** + * Serialize the XML document to the given writer using the default + * {@link TransformerFactory} and {@link Transformer} classes. If output + * options are provided, these options are provided to the + * {@link Transformer} serializer. + * + * @param writer + * a writer to which the serialized document is written. + * @param outputProperties + * settings for the {@link Transformer} serializer. This parameter may be + * null or an empty Properties object, in which case the default output + * properties will be applied. + * + * @throws TransformerException + */ + public void toWriter(Writer writer, Properties outputProperties) + throws TransformerException { + this.toWriter(true, writer, outputProperties); + } + + /** + * Serialize the XML document to a string by delegating to the + * {@link #toWriter(Writer, Properties)} method. If output options are + * provided, these options are provided to the {@link Transformer} + * serializer. + * + * @param outputProperties + * settings for the {@link Transformer} serializer. This parameter may be + * null or an empty Properties object, in which case the default output + * properties will be applied. + * + * @return + * the XML document as a string + * + * @throws TransformerException + */ + public String asString(Properties outputProperties) throws TransformerException { + StringWriter writer = new StringWriter(); + toWriter(writer, outputProperties); + return writer.toString(); + } + + /** + * Serialize the current XML Element and its descendants to a string by + * delegating to the {@link #toWriter(Writer, Properties)} method. + * If output options are provided, these options are provided to the + * {@link Transformer} serializer. + * + * @param outputProperties + * settings for the {@link Transformer} serializer. This parameter may be + * null or an empty Properties object, in which case the default output + * properties will be applied. + * + * @return + * the XML document as a string + * + * @throws TransformerException + */ + public String elementAsString(Properties outputProperties) throws TransformerException { + StringWriter writer = new StringWriter(); + toWriter(false, writer, outputProperties); + return writer.toString(); + } + + /** + * Serialize the XML document to a string excluding the XML declaration. + * + * @return + * the XML document as a string without the XML declaration at the + * beginning of the output. + * + * @throws TransformerException + */ + public String asString() throws TransformerException { + Properties outputProperties = new Properties(); + outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); + return asString(outputProperties); + } + + /** + * Serialize the current XML Element and its descendants to a string + * excluding the XML declaration. + * + * @return + * the XML document as a string without the XML declaration at the + * beginning of the output. + * + * @throws TransformerException + */ + public String elementAsString() throws TransformerException { + Properties outputProperties = new Properties(); + outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); + return elementAsString(outputProperties); + } + + /** + * @return + * a namespace context containing the prefixes and namespace URI's used + * within this builder's document, to assist in running namespace-aware + * XPath queries against the document. + */ + public NamespaceContextImpl buildDocumentNamespaceContext() { + return new NamespaceContextImpl(xmlDocument.getDocumentElement()); + } + + protected String getPrefixFromQualifiedName(String qualifiedName) { + int colonPos = qualifiedName.indexOf(':'); + if (colonPos > 0) { + return qualifiedName.substring(0, colonPos); + } else { + return null; + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder.java b/src/main/java/com/jamesmurty/utils/XMLBuilder.java index ef391ce..c44e870 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder.java @@ -23,35 +23,17 @@ import java.io.FileReader; import java.io.IOException; import java.io.StringReader; -import java.io.StringWriter; -import java.io.Writer; -import java.util.Iterator; -import java.util.Properties; -import java.util.Map.Entry; import javax.xml.namespace.NamespaceContext; -import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerException; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import javax.xml.xpath.XPath; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; -import javax.xml.xpath.XPathFactory; - -import net.iharder.Base64; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; -import org.w3c.dom.NodeList; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -78,23 +60,7 @@ * * @author James Murty */ -public class XMLBuilder { - /** - * A DOM Document that stores the underlying XML document operated on by - * XMLBuilder instances. This document object belongs to the root node - * of a document, and is shared by this node with all other XMLBuilder - * instances via the {@link #getDocument()} method. - * This instance variable must only be created once, by the root node for - * any given document. - */ - private Document xmlDocument = null; - - /** - * The underlying node represented by this builder node. - */ - private Node xmlNode = null; - - private static boolean isNamespaceAware = true; // TODO: Make this configurable? +public class XMLBuilder extends BaseXMLBuilder { /** * Construct a new builder object that wraps the given XML document. @@ -104,8 +70,7 @@ public class XMLBuilder { * an XML document that the builder will manage and manipulate. */ protected XMLBuilder(Document xmlDocument) { - this.xmlDocument = xmlDocument; - this.xmlNode = xmlDocument.getDocumentElement(); + super(xmlDocument); } /** @@ -121,15 +86,7 @@ protected XMLBuilder(Document xmlDocument) { * parentNode node. */ protected XMLBuilder(Node myNode, Node parentNode) { - this.xmlNode = myNode; - if (myNode instanceof Document) { - this.xmlDocument = (Document) myNode; - } else { - this.xmlDocument = myNode.getOwnerDocument(); - } - if (parentNode != null) { - parentNode.appendChild(myNode); - } + super(myNode, parentNode); } /** @@ -151,19 +108,7 @@ protected XMLBuilder(Node myNode, Node parentNode) { public static XMLBuilder create(String name, String namespaceURI) throws ParserConfigurationException, FactoryConfigurationError { - // Init DOM builder and Document. - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(isNamespaceAware); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.newDocument(); - Element rootElement = null; - if (namespaceURI != null && namespaceURI.length() > 0) { - rootElement = document.createElementNS(namespaceURI, name); - } else { - rootElement = document.createElement(name); - } - document.appendChild(rootElement); - return new XMLBuilder(document); + return new XMLBuilder(createDocumentImpl(name, namespaceURI)); } /** @@ -202,13 +147,9 @@ public static XMLBuilder create(String name) * @throws SAXException */ public static XMLBuilder parse(InputSource inputSource) - throws ParserConfigurationException, SAXException, IOException + throws ParserConfigurationException, SAXException, IOException { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - factory.setNamespaceAware(isNamespaceAware); - DocumentBuilder builder = factory.newDocumentBuilder(); - Document document = builder.parse(inputSource); - return new XMLBuilder(document); + return new XMLBuilder(parseDocumentImpl(inputSource)); } /** @@ -267,21 +208,10 @@ public static XMLBuilder parse(File xmlFile) * a builder node at the same location as before the operation. * @throws XPathExpressionException */ - public XMLBuilder stripWhitespaceOnlyTextNodes() + public BaseXMLBuilder stripWhitespaceOnlyTextNodes() throws XPathExpressionException { - XPathFactory xpathFactory = XPathFactory.newInstance(); - // XPath to find empty text nodes. - XPathExpression xpathExp = xpathFactory.newXPath().compile( - "//text()[normalize-space(.) = '']"); - NodeList emptyTextNodes = (NodeList) xpathExp.evaluate( - this.getDocument(), XPathConstants.NODESET); - - // Remove each empty text node from document. - for (int i = 0; i < emptyTextNodes.getLength(); i++) { - Node emptyTextNode = emptyTextNodes.item(i); - emptyTextNode.getParentNode().removeChild(emptyTextNode); - } + super.stripWhitespaceOnlyTextNodesImpl(); return this; } @@ -297,54 +227,10 @@ public XMLBuilder stripWhitespaceOnlyTextNodes() * now containing the entire document tree provided. */ public XMLBuilder importXMLBuilder(XMLBuilder builder) { - assertElementContainsNoOrWhitespaceOnlyTextNodes(this.xmlNode); - Node importedNode = getDocument().importNode( - builder.root().getElement(), true); - this.xmlNode.appendChild(importedNode); + super.importXMLBuilderImpl(builder); return this; } - /** - * @return - * true if the XML Document and Element objects wrapped by this - * builder are equal to the other's wrapped objects. - */ - @Override - public boolean equals(Object obj) { - if (obj != null && obj instanceof XMLBuilder) { - XMLBuilder other = (XMLBuilder) obj; - return - this.xmlDocument.equals(other.getDocument()) - && this.xmlNode.equals(other.getElement()); - } - return false; - } - - /** - * @return - * the XML element wrapped by this builder node, or null if the builder node wraps the - * root Document node. - */ - public Element getElement() { - if (this.xmlNode instanceof Element) { - return (Element) this.xmlNode; - } else { - return null; - } - } - - /** - * BEWARE: The builder returned by this method respresents a Document node, not - * an Element node as is usually the case, so attempts to use the attribute or - * namespace methods on this builder will likely fail. - * - * @return - * the builder node representing the root XML document. - */ - public XMLBuilder document() { - return new XMLBuilder(getDocument(), null); - } - /** * @return * the builder node representing the root element of the XML document. @@ -355,80 +241,6 @@ public XMLBuilder root() { return new XMLBuilder(getDocument()); } - /** - * @return - * the XML document constructed by all builder nodes. - */ - public Document getDocument() { - return this.xmlDocument; - } - - /** - * Return the result of evaluating an XPath query on the builder's DOM - * using the given namespace. Returns null if the query finds nothing, - * or finds a node that does not match the type specified by returnType. - * - * @param xpath - * an XPath expression - * @param type - * the type the XPath is expected to resolve to, e.g: - * {@link XPathConstants#NODE}, {@link XPathConstants#NODESET}, - * {@link XPathConstants#STRING}. - * @param nsContext - * a mapping of prefixes to namespace URIs that allows the XPath expression - * to use namespaces, or null for a non-namespaced document. - * - * @return - * a builder node representing the first Element that matches the - * XPath expression. - * - * @throws XPathExpressionException - * If the XPath is invalid, or if does not resolve to at least one - * {@link Node#ELEMENT_NODE}. - */ - public Object xpathQuery(String xpath, QName type, NamespaceContext nsContext) - throws XPathExpressionException - { - XPathFactory xpathFactory = XPathFactory.newInstance(); - XPath xPath = xpathFactory.newXPath(); - if (nsContext != null) { - xPath.setNamespaceContext(nsContext); - } - XPathExpression xpathExp = xPath.compile(xpath); - try { - return xpathExp.evaluate(this.xmlNode, type); - } catch (IllegalArgumentException e) { - // Thrown if item found does not match expected type - return null; - } - } - - /** - * Return the result of evaluating an XPath query on the builder's DOM. - * Returns null if the query finds nothing, - * or finds a node that does not match the type specified by returnType. - * - * @param xpath - * an XPath expression - * @param type - * the type the XPath is expected to resolve to, e.g: - * {@link XPathConstants#NODE}, {@link XPathConstants#NODESET}, - * {@link XPathConstants#STRING} - * - * @return - * a builder node representing the first Element that matches the - * XPath expression. - * - * @throws XPathExpressionException - * If the XPath is invalid, or if does not resolve to at least one - * {@link Node#ELEMENT_NODE}. - */ - public Object xpathQuery(String xpath, QName type) - throws XPathExpressionException - { - return xpathQuery(xpath, type, null); - } - /** * Find the first element in the builder's DOM matching the given * XPath expression, where the expression may include namespaces if @@ -452,13 +264,8 @@ public Object xpathQuery(String xpath, QName type) public XMLBuilder xpathFind(String xpath, NamespaceContext nsContext) throws XPathExpressionException { - Node foundNode = (Node) this.xpathQuery(xpath, XPathConstants.NODE, nsContext); - if (foundNode == null || foundNode.getNodeType() != Node.ELEMENT_NODE) { - throw new XPathExpressionException("XPath expression \"" - + xpath + "\" does not resolve to an Element in context " - + this.xmlNode + ": " + foundNode); - } - return new XMLBuilder(foundNode, null); + Node foundNode = super.xpathFindImpl(xpath, nsContext); + return new XMLBuilder(foundNode, null); } /** @@ -502,8 +309,7 @@ public XMLBuilder xpathFind(String xpath) throws XPathExpressionException { * contains a text node value. */ public XMLBuilder element(String name) { - String prefix = getPrefixFromQualifiedName(name); - String namespaceURI = this.xmlNode.lookupNamespaceURI(prefix); + String namespaceURI = super.lookupNamespaceURIImpl(name); return element(name, namespaceURI); } @@ -558,12 +364,8 @@ public XMLBuilder e(String name) { * contains a text node value. */ public XMLBuilder element(String name, String namespaceURI) { - assertElementContainsNoOrWhitespaceOnlyTextNodes(this.xmlNode); - return new XMLBuilder( - (namespaceURI == null - ? getDocument().createElement(name) - : getDocument().createElementNS(namespaceURI, name)), - this.xmlNode); + Element elem = super.elementImpl(name, namespaceURI); + return new XMLBuilder(elem, this.getElement()); } /** @@ -588,9 +390,8 @@ public XMLBuilder element(String name, String namespaceURI) { * one or more siblings that are text nodes. */ public XMLBuilder elementBefore(String name) { - String prefix = getPrefixFromQualifiedName(name); - String namespaceURI = this.xmlNode.lookupNamespaceURI(prefix); - return elementBefore(name, namespaceURI); + Element newElement = super.elementBeforeImpl(name); + return new XMLBuilder(newElement, null); } /** @@ -611,16 +412,7 @@ public XMLBuilder elementBefore(String name) { * one or more siblings that are text nodes. */ public XMLBuilder elementBefore(String name, String namespaceURI) { - Node parentNode = this.xmlNode.getParentNode(); - assertElementContainsNoOrWhitespaceOnlyTextNodes(parentNode); - - Element newElement = (namespaceURI == null - ? getDocument().createElement(name) - : getDocument().createElementNS(namespaceURI, name)); - - // Insert new element before the current element - parentNode.insertBefore(newElement, this.xmlNode); - // Return a new builder node pointing at the new element + Element newElement = super.elementBeforeImpl(name, namespaceURI); return new XMLBuilder(newElement, null); } @@ -639,12 +431,7 @@ public XMLBuilder elementBefore(String name, String namespaceURI) { * added. */ public XMLBuilder attribute(String name, String value) { - if (! (this.xmlNode instanceof Element)) { - throw new RuntimeException( - "Cannot add an attribute to non-Element underlying node: " - + this.xmlNode); - } - ((Element) xmlNode).setAttribute(name, value); + super.attributeImpl(name, value); return this; } @@ -680,6 +467,7 @@ public XMLBuilder a(String name, String value) { return attribute(name, value); } + /** * Add or replace the text value of an element represented by this builder * node, and return the node representing the element to which the text @@ -695,17 +483,7 @@ public XMLBuilder a(String name, String value) { * the builder node representing the element to which the text was added. */ public XMLBuilder text(String value, boolean replaceText) { - // Issue 10: null text values cause exceptions on subsequent call to - // Transformer to render document, so we fail-fast here on bad data. - if (value == null) { - throw new IllegalArgumentException("Illegal null text value"); - } - - if (replaceText) { - xmlNode.setTextContent(value); - } else { - xmlNode.appendChild(getDocument().createTextNode(value)); - } + super.textImpl(value, replaceText); return this; } @@ -749,8 +527,7 @@ public XMLBuilder t(String value) { * the builder node representing the element to which the data was added. */ public XMLBuilder cdata(String data) { - xmlNode.appendChild( - getDocument().createCDATASection(data)); + super.cdataImpl(data); return this; } @@ -792,9 +569,7 @@ public XMLBuilder d(String data) { * the builder node representing the element to which the data was added. */ public XMLBuilder cdata(byte[] data) { - xmlNode.appendChild( - getDocument().createCDATASection( - Base64.encodeBytes(data))); + super.cdataImpl(data); return this; } @@ -836,7 +611,7 @@ public XMLBuilder d(byte[] data) { * the builder node representing the element to which the comment was added. */ public XMLBuilder comment(String comment) { - xmlNode.appendChild(getDocument().createComment(comment)); + super.commentImpl(comment); return this; } @@ -881,7 +656,7 @@ public XMLBuilder c(String comment) { * added. */ public XMLBuilder instruction(String target, String data) { - xmlNode.appendChild(getDocument().createProcessingInstruction(target, data)); + super.instructionImpl(target, data); return this; } @@ -918,8 +693,8 @@ public XMLBuilder i(String target, String data) { } /** - * Insert an instruction before the element represented by this builder node, and - * return the node representing that same element + * Insert an instruction before the element represented by this builder node, + * and return the node representing that same element * (not the new instruction node). * * @param target @@ -931,7 +706,7 @@ public XMLBuilder i(String target, String data) { * the builder node representing the element before which the instruction was inserted. */ public XMLBuilder insertInstruction(String target, String data) { - getDocument().insertBefore(getDocument().createProcessingInstruction(target, data), xmlNode); + super.insertInstructionImpl(target, data); return this; } @@ -948,7 +723,7 @@ public XMLBuilder insertInstruction(String target, String data) { * added. */ public XMLBuilder reference(String name) { - xmlNode.appendChild(getDocument().createEntityReference(name)); + super.referenceImpl(name); return this; } @@ -993,18 +768,7 @@ public XMLBuilder r(String name) { * the builder node representing the element to which the attribute was added. */ public XMLBuilder namespace(String prefix, String namespaceURI) { - if (! (this.xmlNode instanceof Element)) { - throw new RuntimeException( - "Cannot add an attribute to non-Element underlying node: " - + this.xmlNode); - } - if (prefix != null && prefix.length() > 0) { - ((Element) xmlNode).setAttributeNS("http://www.w3.org/2000/xmlns/", - "xmlns:" + prefix, namespaceURI); - } else { - ((Element) xmlNode).setAttributeNS("http://www.w3.org/2000/xmlns/", - "xmlns", namespaceURI); - } + super.namespaceImpl(prefix, namespaceURI); return this; } @@ -1066,12 +830,7 @@ public XMLBuilder ns(String namespaceURI) { * reached before the nth parent is found. */ public XMLBuilder up(int steps) { - Node currNode = this.xmlNode; - int stepCount = 0; - while (currNode.getParentNode() != null && stepCount < steps) { - currNode = currNode.getParentNode(); - stepCount++; - } + Node currNode = super.upImpl(steps); if (currNode instanceof Document) { return new XMLBuilder((Document) currNode); } else { @@ -1091,194 +850,15 @@ public XMLBuilder up() { } /** - * @throws IllegalStateException - * if the current element contains any child text nodes that aren't pure whitespace. - * We allow whitespace so parsed XML documents containing indenting or pretty-printing - * can still be amended, per issue #17. - */ - protected void assertElementContainsNoOrWhitespaceOnlyTextNodes(Node anXmlElement) { - Node textNodeWithNonWhitespace = null; - NodeList childNodes = anXmlElement.getChildNodes(); - for (int i = 0; i < childNodes.getLength(); i++) { - if (Element.TEXT_NODE == childNodes.item(i).getNodeType()) { - Node textNode = childNodes.item(i); - String textWithoutWhitespace = - textNode.getTextContent().replaceAll("\\s", ""); - if (textWithoutWhitespace.length() > 0) { - textNodeWithNonWhitespace = textNode; - break; - } - } - } - if (textNodeWithNonWhitespace != null) { - throw new IllegalStateException( - "Cannot add sub-element to element <" + anXmlElement.getNodeName() - + "> that contains a Text node that isn't purely whitespace: " - + textNodeWithNonWhitespace); - } - } - - /** - * Serialize either the specific Element wrapped by this XMLBuilder, or its entire - * XML document, to the given writer using the default {@link TransformerFactory} - * and {@link Transformer} classes. - * If output options are provided, these options are provided to the - * {@link Transformer} serializer. - * - * @param wholeDocument - * if true the whole XML document (i.e. the document root) is serialized, - * if false just the current Element and its descendants are serialized. - * @param writer - * a writer to which the serialized document is written. - * @param outputProperties - * settings for the {@link Transformer} serializer. This parameter may be - * null or an empty Properties object, in which case the default output - * properties will be applied. - * - * @throws TransformerException - */ - public void toWriter(boolean wholeDocument, Writer writer, Properties outputProperties) - throws TransformerException - { - StreamResult streamResult = new StreamResult(writer); - - DOMSource domSource = null; - if (wholeDocument) { - domSource = new DOMSource(getDocument()); - } else { - domSource = new DOMSource(getElement()); - } - - TransformerFactory tf = TransformerFactory.newInstance(); - Transformer serializer = tf.newTransformer(); - - if (outputProperties != null) { - Iterator iter = outputProperties.entrySet().iterator(); - while (iter.hasNext()) { - Entry entry = (Entry) iter.next(); - serializer.setOutputProperty((String) entry.getKey(), (String) entry.getValue()); - } - } - serializer.transform(domSource, streamResult); - } - - /** - * Serialize the XML document to the given writer using the default - * {@link TransformerFactory} and {@link Transformer} classes. If output - * options are provided, these options are provided to the - * {@link Transformer} serializer. - * - * @param writer - * a writer to which the serialized document is written. - * @param outputProperties - * settings for the {@link Transformer} serializer. This parameter may be - * null or an empty Properties object, in which case the default output - * properties will be applied. - * - * @throws TransformerException - */ - public void toWriter(Writer writer, Properties outputProperties) - throws TransformerException - { - this.toWriter(true, writer, outputProperties); - } - - /** - * Serialize the XML document to a string by delegating to the - * {@link #toWriter(Writer, Properties)} method. If output options are - * provided, these options are provided to the {@link Transformer} - * serializer. - * - * @param outputProperties - * settings for the {@link Transformer} serializer. This parameter may be - * null or an empty Properties object, in which case the default output - * properties will be applied. - * - * @return - * the XML document as a string - * - * @throws TransformerException - */ - public String asString(Properties outputProperties) - throws TransformerException - { - StringWriter writer = new StringWriter(); - toWriter(writer, outputProperties); - return writer.toString(); - } - - /** - * Serialize the current XML Element and its descendants to a string by - * delegating to the {@link #toWriter(Writer, Properties)} method. - * If output options are provided, these options are provided to the - * {@link Transformer} serializer. - * - * @param outputProperties - * settings for the {@link Transformer} serializer. This parameter may be - * null or an empty Properties object, in which case the default output - * properties will be applied. - * - * @return - * the XML document as a string - * - * @throws TransformerException - */ - public String elementAsString(Properties outputProperties) - throws TransformerException - { - StringWriter writer = new StringWriter(); - toWriter(false, writer, outputProperties); - return writer.toString(); - } - - /** - * Serialize the XML document to a string excluding the XML declaration. - * - * @return - * the XML document as a string without the XML declaration at the - * beginning of the output. - * - * @throws TransformerException - */ - public String asString() throws TransformerException { - Properties outputProperties = new Properties(); - outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); - return asString(outputProperties); - } - - /** - * Serialize the current XML Element and its descendants to a string - * excluding the XML declaration. + * BEWARE: The builder returned by this method represents a Document node, not + * an Element node as is usually the case, so attempts to use the attribute or + * namespace methods on this builder will likely fail. * * @return - * the XML document as a string without the XML declaration at the - * beginning of the output. - * - * @throws TransformerException - */ - public String elementAsString() throws TransformerException { - Properties outputProperties = new Properties(); - outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); - return elementAsString(outputProperties); - } - - /** - * @return - * a namespace context containing the prefixes and namespace URI's used - * within this builder's document, to assist in running namespace-aware - * XPath queries against the document. + * the builder node representing the root XML document. */ - public NamespaceContextImpl buildDocumentNamespaceContext() { - return new NamespaceContextImpl(this.root().getElement()); - } - - protected String getPrefixFromQualifiedName(String qualifiedName) { - int colonPos = qualifiedName.indexOf(':'); - if (colonPos > 0) { - return qualifiedName.substring(0, colonPos); - } else { - return null; - } + public XMLBuilder document() { + return new XMLBuilder(getDocument(), null); } } From dee5e33dac6448f33975a4d7172daca8166c797c Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 17 Jul 2014 13:23:18 +0100 Subject: [PATCH 10/43] Initial implementation of XMLBuilder2 with no checked exceptions, re #4 --- .../com/jamesmurty/utils/BaseXMLBuilder.java | 2 +- .../java/com/jamesmurty/utils/XMLBuilder.java | 2 +- .../com/jamesmurty/utils/XMLBuilder2.java | 926 ++++++++++++++++++ .../utils/XMLBuilderRuntimeException.java | 16 + 4 files changed, 944 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/jamesmurty/utils/XMLBuilder2.java create mode 100644 src/main/java/com/jamesmurty/utils/XMLBuilderRuntimeException.java diff --git a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java index f46d3e3..e7b5e04 100644 --- a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java @@ -771,7 +771,7 @@ public String elementAsString() throws TransformerException { * within this builder's document, to assist in running namespace-aware * XPath queries against the document. */ - public NamespaceContextImpl buildDocumentNamespaceContext() { + protected NamespaceContextImpl buildDocumentNamespaceContext() { return new NamespaceContextImpl(xmlDocument.getDocumentElement()); } diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder.java b/src/main/java/com/jamesmurty/utils/XMLBuilder.java index c44e870..60b1fd5 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder.java @@ -208,7 +208,7 @@ public static XMLBuilder parse(File xmlFile) * a builder node at the same location as before the operation. * @throws XPathExpressionException */ - public BaseXMLBuilder stripWhitespaceOnlyTextNodes() + public XMLBuilder stripWhitespaceOnlyTextNodes() throws XPathExpressionException { super.stripWhitespaceOnlyTextNodesImpl(); diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java new file mode 100644 index 0000000..03dd4d8 --- /dev/null +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java @@ -0,0 +1,926 @@ +/* + * Copyright 2008-2014 James Murty (www.jamesmurty.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * + * This code is available from the GitHub code repository at: + * https://github.com/jmurty/java-xmlbuilder + */ +package com.jamesmurty.utils; + +import java.io.File; +import java.io.FileReader; +import java.io.StringReader; +import java.io.Writer; +import java.util.Properties; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.namespace.QName; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.TransformerException; +import javax.xml.xpath.XPathExpressionException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; + +/** + * XML Builder is a utility that creates simple XML documents using relatively + * sparse Java code. It is intended to allow for quick and painless creation of + * XML documents where you might otherwise be tempted to use concatenated + * strings, rather than face the tedium and verbosity of coding with + * JAXP (http://jaxp.dev.java.net/). + *

+ * Internally, XML Builder uses JAXP to build a standard W3C + * {@link org.w3c.dom.Document} model (DOM) that you can easily export as a + * string, or access and manipulate further if you have special requirements. + *

+ *

+ * The XMLBuilder2 class serves as a wrapper of {@link org.w3c.dom.Element} nodes, + * and provides a number of utility methods that make it simple to + * manipulate the underlying element and the document to which it belongs. + * In essence, this class performs dual roles: it represents a specific XML + * node, and also allows manipulation of the entire underlying XML document. + * The platform's default {@link DocumentBuilderFactory} and + * {@link DocumentBuilder} classes are used to build the document. + *

+ *

+ * XMLBuilder2 has an feature set to the original XMLBuilder, but only ever + * throws runtime exceptions (as opposed to checked exceptions). Any internal + * checked exceptions are caught and wrapped in an + * {@link XMLBuilderRuntimeException} object. + *

+ * + * @author James Murty + */ +public class XMLBuilder2 extends BaseXMLBuilder { + + /** + * Construct a new builder object that wraps the given XML document. + * This constructor is for internal use only. + * + * @param xmlDocument + * an XML document that the builder will manage and manipulate. + */ + protected XMLBuilder2(Document xmlDocument) { + super(xmlDocument); + } + + /** + * Construct a new builder object that wraps the given XML document and node. + * This constructor is for internal use only. + * + * @param myNode + * the XML node that this builder node will wrap. This node may + * be part of the XML document, or it may be a new element that is to be + * added to the document. + * @param parentNode + * If not null, the given myElement will be appended as child node of the + * parentNode node. + */ + protected XMLBuilder2(Node myNode, Node parentNode) { + super(myNode, parentNode); + } + + /** + * Construct a builder for new XML document with a default namespace. + * The document will be created with the given root element, and the builder + * returned by this method will serve as the starting-point for any further + * document additions. + * + * @param name + * the name of the document's root element. + * @param namespaceURI + * default namespace URI for document, ignored if null or empty. + * @return + * a builder node that can be used to add more nodes to the XML document. + */ + public static XMLBuilder2 create(String name, String namespaceURI) + { + try { + return new XMLBuilder2(createDocumentImpl(name, namespaceURI)); + } catch (Exception ex) { + throw new XMLBuilderRuntimeException(ex); + } + } + + /** + * Construct a builder for new XML document. The document will be created + * with the given root element, and the builder returned by this method + * will serve as the starting-point for any further document additions. + * + * @param name + * the name of the document's root element. + * @return + * a builder node that can be used to add more nodes to the XML document. + */ + public static XMLBuilder2 create(String name) + { + return create(name, null); + } + + /** + * Construct a builder from an existing XML document. The provided XML + * document will be parsed and an XMLBuilder2 object referencing the + * document's root element will be returned. + * + * @param inputSource + * an XML document input source that will be parsed into a DOM. + * @return + * a builder node that can be used to add more nodes to the XML document. + */ + public static XMLBuilder2 parse(InputSource inputSource) + { + try { + return new XMLBuilder2(parseDocumentImpl(inputSource)); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + /** + * Construct a builder from an existing XML document string. + * The provided XML document will be parsed and an XMLBuilder2 + * object referencing the document's root element will be returned. + * + * @param xmlString + * an XML document string that will be parsed into a DOM. + * @return + * a builder node that can be used to add more nodes to the XML document. + */ + public static XMLBuilder2 parse(String xmlString) + { + return XMLBuilder2.parse(new InputSource(new StringReader(xmlString))); + } + + /** + * Construct a builder from an existing XML document file. + * The provided XML document will be parsed and an XMLBuilder2 + * object referencing the document's root element will be returned. + * + * @param xmlFile + * an XML document file that will be parsed into a DOM. + * @return + * a builder node that can be used to add more nodes to the XML document. + */ + public static XMLBuilder2 parse(File xmlFile) + { + try { + return XMLBuilder2.parse(new InputSource(new FileReader(xmlFile))); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + /** + * Find and delete from the underlying Document any text nodes that + * contain nothing but whitespace, such as newlines and tab or space + * characters used to indent or pretty-print an XML document. + * + * Uses approach I documented on StackOverflow: + * http://stackoverflow.com/a/979606/4970 + * + * @return + * a builder node at the same location as before the operation. + */ + public XMLBuilder2 stripWhitespaceOnlyTextNodes() + { + try { + super.stripWhitespaceOnlyTextNodesImpl(); + return this; + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + /** + * Imports another XMLBuilder2 document into this document at the + * current position. The entire document provided is imported. + * + * @param builder + * the XMLBuilder2 document to be imported. + * + * @return + * a builder node at the same location as before the import, but + * now containing the entire document tree provided. + */ + public XMLBuilder2 importXMLBuilder(XMLBuilder2 builder) { + super.importXMLBuilderImpl(builder); + return this; + } + + /** + * @return + * the builder node representing the root element of the XML document. + * In other words, the same builder node returned by the initial + * {@link #create(String)} or {@link #parse(InputSource)} method. + */ + public XMLBuilder2 root() { + return new XMLBuilder2(getDocument()); + } + + /** + * Find the first element in the builder's DOM matching the given + * XPath expression, where the expression may include namespaces if + * a {@link NamespaceContext} is provided. + * + * @param xpath + * An XPath expression that *must* resolve to an existing Element within + * the document object model. + * @param nsContext + * a mapping of prefixes to namespace URIs that allows the XPath expression + * to use namespaces. + * + * @return + * a builder node representing the first Element that matches the + * XPath expression. + */ + public XMLBuilder2 xpathFind(String xpath, NamespaceContext nsContext) + { + try { + Node foundNode = super.xpathFindImpl(xpath, nsContext); + return new XMLBuilder2(foundNode, null); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + /** + * Find the first element in the builder's DOM matching the given + * XPath expression. + * + * @param xpath + * An XPath expression that *must* resolve to an existing Element within + * the document object model. + * + * @return + * a builder node representing the first Element that matches the + * XPath expression. + */ + public XMLBuilder2 xpathFind(String xpath) { + return xpathFind(xpath, null); + } + + /** + * Add a named XML element to the document as a child of this builder node, + * and return the builder node representing the new child. + * + * When adding an element to a namespaced document, the new node will be + * assigned a namespace matching it's qualified name prefix (if any) or + * the document's default namespace. NOTE: If the element has a prefix that + * does not match any known namespaces, the element will be created + * without any namespace. + * + * @param name + * the name of the XML element. + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a child element to an XML node that already + * contains a text node value. + */ + public XMLBuilder2 element(String name) { + String namespaceURI = super.lookupNamespaceURIImpl(name); + return element(name, namespaceURI); + } + + /** + * Synonym for {@link #element(String)}. + * + * @param name + * the name of the XML element. + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a child element to an XML node that already + * contains a text node value. + */ + public XMLBuilder2 elem(String name) { + return element(name); + } + + /** + * Synonym for {@link #element(String)}. + * + * @param name + * the name of the XML element. + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a child element to an XML node that already + * contains a text node value. + */ + public XMLBuilder2 e(String name) { + return element(name); + } + + /** + * Add a named and namespaced XML element to the document as a child of + * this builder node, and return the builder node representing the new child. + * + * @param name + * the name of the XML element. + * @param namespaceURI + * a namespace URI + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a child element to an XML node that already + * contains a text node value. + */ + public XMLBuilder2 element(String name, String namespaceURI) { + Element elem = super.elementImpl(name, namespaceURI); + return new XMLBuilder2(elem, this.getElement()); + } + + /** + * Add a named XML element to the document as a sibling element + * that precedes the position of this builder node, and return the builder node + * representing the new child. + * + * When adding an element to a namespaced document, the new node will be + * assigned a namespace matching it's qualified name prefix (if any) or + * the document's default namespace. NOTE: If the element has a prefix that + * does not match any known namespaces, the element will be created + * without any namespace. + * + * @param name + * the name of the XML element. + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a sibling element to a node where there are already + * one or more siblings that are text nodes. + */ + public XMLBuilder2 elementBefore(String name) { + Element newElement = super.elementBeforeImpl(name); + return new XMLBuilder2(newElement, null); + } + + /** + * Add a named and namespaced XML element to the document as a sibling element + * that precedes the position of this builder node, and return the builder node + * representing the new child. + * + * @param name + * the name of the XML element. + * @param namespaceURI + * a namespace URI + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a sibling element to a node where there are already + * one or more siblings that are text nodes. + */ + public XMLBuilder2 elementBefore(String name, String namespaceURI) { + Element newElement = super.elementBeforeImpl(name, namespaceURI); + return new XMLBuilder2(newElement, null); + } + + /** + * Add a named attribute value to the element represented by this builder + * node, and return the node representing the element to which the + * attribute was added (not the new attribute node). + * + * @param name + * the attribute's name. + * @param value + * the attribute's value. + * + * @return + * the builder node representing the element to which the attribute was + * added. + */ + public XMLBuilder2 attribute(String name, String value) { + super.attributeImpl(name, value); + return this; + } + + /** + * Synonym for {@link #attribute(String, String)}. + * + * @param name + * the attribute's name. + * @param value + * the attribute's value. + * + * @return + * the builder node representing the element to which the attribute was + * added. + */ + public XMLBuilder2 attr(String name, String value) { + return attribute(name, value); + } + + /** + * Synonym for {@link #attribute(String, String)}. + * + * @param name + * the attribute's name. + * @param value + * the attribute's value. + * + * @return + * the builder node representing the element to which the attribute was + * added. + */ + public XMLBuilder2 a(String name, String value) { + return attribute(name, value); + } + + + /** + * Add or replace the text value of an element represented by this builder + * node, and return the node representing the element to which the text + * was added (not the new text node). + * + * @param value + * the text value to set or add to the element. + * @param replaceText + * if True any existing text content of the node is replaced with the + * given text value, if the given value is appended to any existing text. + * + * @return + * the builder node representing the element to which the text was added. + */ + public XMLBuilder2 text(String value, boolean replaceText) { + super.textImpl(value, replaceText); + return this; + } + + /** + * Add a text value to the element represented by this builder node, and + * return the node representing the element to which the text + * was added (not the new text node). + * + * @param value + * the text value to add to the element. + * + * @return + * the builder node representing the element to which the text was added. + */ + public XMLBuilder2 text(String value) { + return this.text(value, false); + } + + /** + * Synonym for {@link #text(String)}. + * + * @param value + * the text value to add to the element. + * + * @return + * the builder node representing the element to which the text was added. + */ + public XMLBuilder2 t(String value) { + return text(value); + } + + /** + * Add a CDATA node with String content to the element represented by this + * builder node, and return the node representing the element to which the + * data was added (not the new CDATA node). + * + * @param data + * the String value that will be added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public XMLBuilder2 cdata(String data) { + super.cdataImpl(data); + return this; + } + + /** + * Synonym for {@link #cdata(String)}. + * + * @param data + * the String value that will be added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public XMLBuilder2 data(String data) { + return cdata(data); + } + + /** + * Synonym for {@link #cdata(String)}. + * + * @param data + * the String value that will be added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public XMLBuilder2 d(String data) { + return cdata(data); + } + + /** + * Add a CDATA node with Base64-encoded byte data content to the element represented + * by this builder node, and return the node representing the element to which the + * data was added (not the new CDATA node). + * + * @param data + * the data value that will be Base64-encoded and added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public XMLBuilder2 cdata(byte[] data) { + super.cdataImpl(data); + return this; + } + + /** + * Synonym for {@link #cdata(byte[])}. + * + * @param data + * the data value that will be Base64-encoded and added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public XMLBuilder2 data(byte[] data) { + return cdata(data); + } + + /** + * Synonym for {@link #cdata(byte[])}. + * + * @param data + * the data value that will be Base64-encoded and added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public XMLBuilder2 d(byte[] data) { + return cdata(data); + } + + /** + * Add a comment to the element represented by this builder node, and + * return the node representing the element to which the comment + * was added (not the new comment node). + * + * @param comment + * the comment to add to the element. + * + * @return + * the builder node representing the element to which the comment was added. + */ + public XMLBuilder2 comment(String comment) { + super.commentImpl(comment); + return this; + } + + /** + * Synonym for {@link #comment(String)}. + * + * @param comment + * the comment to add to the element. + * + * @return + * the builder node representing the element to which the comment was added. + */ + public XMLBuilder2 cmnt(String comment) { + return comment(comment); + } + + /** + * Synonym for {@link #comment(String)}. + * + * @param comment + * the comment to add to the element. + * + * @return + * the builder node representing the element to which the comment was added. + */ + public XMLBuilder2 c(String comment) { + return comment(comment); + } + + /** + * Add an instruction to the element represented by this builder node, and + * return the node representing the element to which the instruction + * was added (not the new instruction node). + * + * @param target + * the target value for the instruction. + * @param data + * the data value for the instruction + * + * @return + * the builder node representing the element to which the instruction was + * added. + */ + public XMLBuilder2 instruction(String target, String data) { + super.instructionImpl(target, data); + return this; + } + + /** + * Synonym for {@link #instruction(String, String)}. + * + * @param target + * the target value for the instruction. + * @param data + * the data value for the instruction + * + * @return + * the builder node representing the element to which the instruction was + * added. + */ + public XMLBuilder2 inst(String target, String data) { + return instruction(target, data); + } + + /** + * Synonym for {@link #instruction(String, String)}. + * + * @param target + * the target value for the instruction. + * @param data + * the data value for the instruction + * + * @return + * the builder node representing the element to which the instruction was + * added. + */ + public XMLBuilder2 i(String target, String data) { + return instruction(target, data); + } + + /** + * Insert an instruction before the element represented by this builder node, + * and return the node representing that same element + * (not the new instruction node). + * + * @param target + * the target value for the instruction. + * @param data + * the data value for the instruction + * + * @return + * the builder node representing the element before which the instruction was inserted. + */ + public XMLBuilder2 insertInstruction(String target, String data) { + super.insertInstructionImpl(target, data); + return this; + } + + /** + * Add a reference to the element represented by this builder node, and + * return the node representing the element to which the reference + * was added (not the new reference node). + * + * @param name + * the name value for the reference. + * + * @return + * the builder node representing the element to which the reference was + * added. + */ + public XMLBuilder2 reference(String name) { + super.referenceImpl(name); + return this; + } + + /** + * Synonym for {@link #reference(String)}. + * + * @param name + * the name value for the reference. + * + * @return + * the builder node representing the element to which the reference was + * added. + */ + public XMLBuilder2 ref(String name) { + return reference(name); + } + + /** + * Synonym for {@link #reference(String)}. + * + * @param name + * the name value for the reference. + * + * @return + * the builder node representing the element to which the reference was + * added. + */ + public XMLBuilder2 r(String name) { + return reference(name); + } + + /** + * Add an XML namespace attribute to this builder's element node. + * + * @param prefix + * a prefix for the namespace URI within the document, may be null + * or empty in which case a default "xmlns" attribute is created. + * @param namespaceURI + * a namespace uri + * + * @return + * the builder node representing the element to which the attribute was added. + */ + public XMLBuilder2 namespace(String prefix, String namespaceURI) { + super.namespaceImpl(prefix, namespaceURI); + return this; + } + + /** + * Synonym for {@link #namespace(String, String)}. + * + * @param prefix + * a prefix for the namespace URI within the document, may be null + * or empty in which case a default xmlns attribute is created. + * @param namespaceURI + * a namespace uri + * + * @return + * the builder node representing the element to which the attribute was added. + */ + public XMLBuilder2 ns(String prefix, String namespaceURI) { + return attribute(prefix, namespaceURI); + } + + /** + * Add an XML namespace attribute to this builder's element node + * without a prefix. + * + * @param namespaceURI + * a namespace uri + * + * @return + * the builder node representing the element to which the attribute was added. + */ + public XMLBuilder2 namespace(String namespaceURI) { + this.namespace(null, namespaceURI); + return this; + } + + /** + * Synonym for {@link #namespace(String)}. + * + * @param namespaceURI + * a namespace uri + * + * @return + * the builder node representing the element to which the attribute was added. + */ + public XMLBuilder2 ns(String namespaceURI) { + return namespace(namespaceURI); + } + + /** + * Return the builder node representing the nth ancestor element + * of this node, or the root node if n exceeds the document's depth. + * + * @param steps + * the number of parent elements to step over while navigating up the chain + * of node ancestors. A steps value of 1 will find a node's parent, 2 will + * find its grandparent etc. + * + * @return + * the nth ancestor of this node, or the root node if this is + * reached before the nth parent is found. + */ + public XMLBuilder2 up(int steps) { + Node currNode = super.upImpl(steps); + if (currNode instanceof Document) { + return new XMLBuilder2((Document) currNode); + } else { + return new XMLBuilder2(currNode, null); + } + } + + /** + * Return the builder node representing the parent of the current node. + * + * @return + * the parent of this node, or the root node if this method is called on the + * root node. + */ + public XMLBuilder2 up() { + return up(1); + } + + /** + * BEWARE: The builder returned by this method represents a Document node, not + * an Element node as is usually the case, so attempts to use the attribute or + * namespace methods on this builder will likely fail. + * + * @return + * the builder node representing the root XML document. + */ + public XMLBuilder2 document() { + return new XMLBuilder2(getDocument(), null); + } + + @Override + public String asString() { + try { + return super.asString(); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + @Override + public String asString(Properties properties) { + try { + return super.asString(properties); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + @Override + public String elementAsString() { + try { + return super.elementAsString(); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + @Override + public String elementAsString(Properties outputProperties) { + try { + return super.elementAsString(outputProperties); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + @Override + public void toWriter(boolean wholeDocument, Writer writer, Properties outputProperties) + { + try { + super.toWriter(wholeDocument, writer, outputProperties); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + @Override + public void toWriter(Writer writer, Properties outputProperties) + { + try { + super.toWriter(writer, outputProperties); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + @Override + public Object xpathQuery(String xpath, QName type, NamespaceContext nsContext) + { + try { + return super.xpathQuery(xpath, type, nsContext); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + + @Override + public Object xpathQuery(String xpath, QName type) + { + try { + return super.xpathQuery(xpath, type); + } catch (Exception e) { + throw new XMLBuilderRuntimeException(e); + } + } + +} diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilderRuntimeException.java b/src/main/java/com/jamesmurty/utils/XMLBuilderRuntimeException.java new file mode 100644 index 0000000..fd4df22 --- /dev/null +++ b/src/main/java/com/jamesmurty/utils/XMLBuilderRuntimeException.java @@ -0,0 +1,16 @@ +package com.jamesmurty.utils; + +/** + * A runtime exception class used in {@link XMLBuilder2} to wrap any exceptions + * that would otherwise lead to checked exceptions in the interface. + * + * @author jmurty + * + */ +public class XMLBuilderRuntimeException extends RuntimeException { + + public XMLBuilderRuntimeException(Exception exception) { + super(exception); + } + +} From bba981b7729a980751f9d9f6916eed67819bc342 Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 17 Jul 2014 14:30:39 +0100 Subject: [PATCH 11/43] Refactored builder implementations to move interface to base, re #4 The abstract base class now has the full API interface, which the implementations adapt or override large portions of to enforce the use of specific XMLBuilder/XMLBuilder2 classes. This is necessary to make test cases reusable over the two implementations. --- .../com/jamesmurty/utils/BaseXMLBuilder.java | 599 +++++++++++++++++- .../java/com/jamesmurty/utils/XMLBuilder.java | 520 ++------------- .../com/jamesmurty/utils/XMLBuilder2.java | 549 ++-------------- 3 files changed, 684 insertions(+), 984 deletions(-) diff --git a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java index e7b5e04..da71b9f 100644 --- a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java @@ -194,6 +194,21 @@ protected void stripWhitespaceOnlyTextNodesImpl() } } + /** + * Find and delete from the underlying Document any text nodes that + * contain nothing but whitespace, such as newlines and tab or space + * characters used to indent or pretty-print an XML document. + * + * Uses approach I documented on StackOverflow: + * http://stackoverflow.com/a/979606/4970 + * + * @return + * a builder node at the same location as before the operation. + * @throws XPathExpressionException + */ + public abstract BaseXMLBuilder stripWhitespaceOnlyTextNodes() + throws XPathExpressionException; + /** * Imports another BaseXMLBuilder document into this document at the * current position. The entire document provided is imported. @@ -245,6 +260,25 @@ public Document getDocument() { return this.xmlDocument; } + /** + * Imports another XMLBuilder document into this document at the + * current position. The entire document provided is imported. + * + * @param builder + * the XMLBuilder document to be imported. + * + * @return + * a builder node at the same location as before the import, but + * now containing the entire document tree provided. + */ + public abstract BaseXMLBuilder importXMLBuilder(BaseXMLBuilder builder); + + /** + * @return + * the builder node representing the root element of the XML document. + */ + public abstract BaseXMLBuilder root(); + /** * Return the result of evaluating an XPath query on the builder's DOM * using the given namespace. Returns null if the query finds nothing, @@ -309,6 +343,48 @@ public Object xpathQuery(String xpath, QName type) return xpathQuery(xpath, type, null); } + /** + * Find the first element in the builder's DOM matching the given + * XPath expression, where the expression may include namespaces if + * a {@link NamespaceContext} is provided. + * + * @param xpath + * An XPath expression that *must* resolve to an existing Element within + * the document object model. + * @param nsContext + * a mapping of prefixes to namespace URIs that allows the XPath expression + * to use namespaces. + * + * @return + * a builder node representing the first Element that matches the + * XPath expression. + * + * @throws XPathExpressionException + * If the XPath is invalid, or if does not resolve to at least one + * {@link Node#ELEMENT_NODE}. + */ + public abstract BaseXMLBuilder xpathFind(String xpath, NamespaceContext nsContext) + throws XPathExpressionException; + + /** + * Find the first element in the builder's DOM matching the given + * XPath expression. + * + * @param xpath + * An XPath expression that *must* resolve to an existing Element within + * the document object model. + * + * @return + * a builder node representing the first Element that matches the + * XPath expression. + * + * @throws XPathExpressionException + * If the XPath is invalid, or if does not resolve to at least one + * {@link Node#ELEMENT_NODE}. + */ + public abstract BaseXMLBuilder xpathFind(String xpath) + throws XPathExpressionException; + /** * Find the first element in the builder's DOM matching the given * XPath expression, where the expression may include namespaces if @@ -379,6 +455,203 @@ protected Element elementImpl(String name, String namespaceURI) { } } + /** + * Add a named XML element to the document as a child of this builder node, + * and return the builder node representing the new child. + * + * When adding an element to a namespaced document, the new node will be + * assigned a namespace matching it's qualified name prefix (if any) or + * the document's default namespace. NOTE: If the element has a prefix that + * does not match any known namespaces, the element will be created + * without any namespace. + * + * @param name + * the name of the XML element. + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a child element to an XML node that already + * contains a text node value. + */ + public abstract BaseXMLBuilder element(String name); + + /** + * Synonym for {@link #element(String)}. + * + * @param name + * the name of the XML element. + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a child element to an XML node that already + * contains a text node value. + */ + public abstract BaseXMLBuilder elem(String name); + + /** + * Synonym for {@link #element(String)}. + * + * @param name + * the name of the XML element. + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a child element to an XML node that already + * contains a text node value. + */ + public abstract BaseXMLBuilder e(String name); + + /** + * Add a named and namespaced XML element to the document as a child of + * this builder node, and return the builder node representing the new child. + * + * @param name + * the name of the XML element. + * @param namespaceURI + * a namespace URI + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a child element to an XML node that already + * contains a text node value. + */ + public abstract BaseXMLBuilder element(String name, String namespaceURI); + + /** + * Add a named XML element to the document as a sibling element + * that precedes the position of this builder node, and return the builder node + * representing the new child. + * + * When adding an element to a namespaced document, the new node will be + * assigned a namespace matching it's qualified name prefix (if any) or + * the document's default namespace. NOTE: If the element has a prefix that + * does not match any known namespaces, the element will be created + * without any namespace. + * + * @param name + * the name of the XML element. + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a sibling element to a node where there are already + * one or more siblings that are text nodes. + */ + public abstract BaseXMLBuilder elementBefore(String name); + + /** + * Add a named and namespaced XML element to the document as a sibling element + * that precedes the position of this builder node, and return the builder node + * representing the new child. + * + * @param name + * the name of the XML element. + * @param namespaceURI + * a namespace URI + * + * @return + * a builder node representing the new child. + * + * @throws IllegalStateException + * if you attempt to add a sibling element to a node where there are already + * one or more siblings that are text nodes. + */ + public abstract BaseXMLBuilder elementBefore(String name, String namespaceURI); + + /** + * Add a named attribute value to the element represented by this builder + * node, and return the node representing the element to which the + * attribute was added (not the new attribute node). + * + * @param name + * the attribute's name. + * @param value + * the attribute's value. + * + * @return + * the builder node representing the element to which the attribute was + * added. + */ + public abstract BaseXMLBuilder attribute(String name, String value); + + /** + * Synonym for {@link #attribute(String, String)}. + * + * @param name + * the attribute's name. + * @param value + * the attribute's value. + * + * @return + * the builder node representing the element to which the attribute was + * added. + */ + public abstract BaseXMLBuilder attr(String name, String value); + + /** + * Synonym for {@link #attribute(String, String)}. + * + * @param name + * the attribute's name. + * @param value + * the attribute's value. + * + * @return + * the builder node representing the element to which the attribute was + * added. + */ + public abstract BaseXMLBuilder a(String name, String value); + + + /** + * Add or replace the text value of an element represented by this builder + * node, and return the node representing the element to which the text + * was added (not the new text node). + * + * @param value + * the text value to set or add to the element. + * @param replaceText + * if True any existing text content of the node is replaced with the + * given text value, if the given value is appended to any existing text. + * + * @return + * the builder node representing the element to which the text was added. + */ + public abstract BaseXMLBuilder text(String value, boolean replaceText); + + /** + * Add a text value to the element represented by this builder node, and + * return the node representing the element to which the text + * was added (not the new text node). + * + * @param value + * the text value to add to the element. + * + * @return + * the builder node representing the element to which the text was added. + */ + public abstract BaseXMLBuilder text(String value); + + /** + * Synonym for {@link #text(String)}. + * + * @param value + * the text value to add to the element. + * + * @return + * the builder node representing the element to which the text was added. + */ + public abstract BaseXMLBuilder t(String value); + /** * Add a named XML element to the document as a sibling element * that precedes the position of this builder node. @@ -575,6 +848,294 @@ protected void namespaceImpl(String namespaceURI) { namespaceImpl(null, namespaceURI); } + /** + * Add a CDATA node with String content to the element represented by this + * builder node, and return the node representing the element to which the + * data was added (not the new CDATA node). + * + * @param data + * the String value that will be added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public abstract BaseXMLBuilder cdata(String data); + + /** + * Synonym for {@link #cdata(String)}. + * + * @param data + * the String value that will be added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public abstract BaseXMLBuilder data(String data); + + /** + * Synonym for {@link #cdata(String)}. + * + * @param data + * the String value that will be added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public abstract BaseXMLBuilder d(String data); + + /** + * Add a CDATA node with Base64-encoded byte data content to the element represented + * by this builder node, and return the node representing the element to which the + * data was added (not the new CDATA node). + * + * @param data + * the data value that will be Base64-encoded and added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public abstract BaseXMLBuilder cdata(byte[] data); + + /** + * Synonym for {@link #cdata(byte[])}. + * + * @param data + * the data value that will be Base64-encoded and added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public abstract BaseXMLBuilder data(byte[] data); + + /** + * Synonym for {@link #cdata(byte[])}. + * + * @param data + * the data value that will be Base64-encoded and added to a CDATA element. + * + * @return + * the builder node representing the element to which the data was added. + */ + public abstract BaseXMLBuilder d(byte[] data); + + /** + * Add a comment to the element represented by this builder node, and + * return the node representing the element to which the comment + * was added (not the new comment node). + * + * @param comment + * the comment to add to the element. + * + * @return + * the builder node representing the element to which the comment was added. + */ + public abstract BaseXMLBuilder comment(String comment); + + /** + * Synonym for {@link #comment(String)}. + * + * @param comment + * the comment to add to the element. + * + * @return + * the builder node representing the element to which the comment was added. + */ + public abstract BaseXMLBuilder cmnt(String comment); + + /** + * Synonym for {@link #comment(String)}. + * + * @param comment + * the comment to add to the element. + * + * @return + * the builder node representing the element to which the comment was added. + */ + public abstract BaseXMLBuilder c(String comment); + + /** + * Add an instruction to the element represented by this builder node, and + * return the node representing the element to which the instruction + * was added (not the new instruction node). + * + * @param target + * the target value for the instruction. + * @param data + * the data value for the instruction + * + * @return + * the builder node representing the element to which the instruction was + * added. + */ + public abstract BaseXMLBuilder instruction(String target, String data); + + /** + * Synonym for {@link #instruction(String, String)}. + * + * @param target + * the target value for the instruction. + * @param data + * the data value for the instruction + * + * @return + * the builder node representing the element to which the instruction was + * added. + */ + public abstract BaseXMLBuilder inst(String target, String data); + + /** + * Synonym for {@link #instruction(String, String)}. + * + * @param target + * the target value for the instruction. + * @param data + * the data value for the instruction + * + * @return + * the builder node representing the element to which the instruction was + * added. + */ + public abstract BaseXMLBuilder i(String target, String data); + + /** + * Insert an instruction before the element represented by this builder node, + * and return the node representing that same element + * (not the new instruction node). + * + * @param target + * the target value for the instruction. + * @param data + * the data value for the instruction + * + * @return + * the builder node representing the element before which the instruction was inserted. + */ + public abstract BaseXMLBuilder insertInstruction(String target, String data); + + /** + * Add a reference to the element represented by this builder node, and + * return the node representing the element to which the reference + * was added (not the new reference node). + * + * @param name + * the name value for the reference. + * + * @return + * the builder node representing the element to which the reference was + * added. + */ + public abstract BaseXMLBuilder reference(String name); + + /** + * Synonym for {@link #reference(String)}. + * + * @param name + * the name value for the reference. + * + * @return + * the builder node representing the element to which the reference was + * added. + */ + public abstract BaseXMLBuilder ref(String name); + + /** + * Synonym for {@link #reference(String)}. + * + * @param name + * the name value for the reference. + * + * @return + * the builder node representing the element to which the reference was + * added. + */ + public abstract BaseXMLBuilder r(String name); + + /** + * Add an XML namespace attribute to this builder's element node. + * + * @param prefix + * a prefix for the namespace URI within the document, may be null + * or empty in which case a default "xmlns" attribute is created. + * @param namespaceURI + * a namespace uri + * + * @return + * the builder node representing the element to which the attribute was added. + */ + public abstract BaseXMLBuilder namespace(String prefix, String namespaceURI); + + /** + * Synonym for {@link #namespace(String, String)}. + * + * @param prefix + * a prefix for the namespace URI within the document, may be null + * or empty in which case a default xmlns attribute is created. + * @param namespaceURI + * a namespace uri + * + * @return + * the builder node representing the element to which the attribute was added. + */ + public abstract BaseXMLBuilder ns(String prefix, String namespaceURI); + + /** + * Add an XML namespace attribute to this builder's element node + * without a prefix. + * + * @param namespaceURI + * a namespace uri + * + * @return + * the builder node representing the element to which the attribute was added. + */ + public abstract BaseXMLBuilder namespace(String namespaceURI); + + /** + * Synonym for {@link #namespace(String)}. + * + * @param namespaceURI + * a namespace uri + * + * @return + * the builder node representing the element to which the attribute was added. + */ + public abstract BaseXMLBuilder ns(String namespaceURI); + + + /** + * Return the builder node representing the nth ancestor element + * of this node, or the root node if n exceeds the document's depth. + * + * @param steps + * the number of parent elements to step over while navigating up the chain + * of node ancestors. A steps value of 1 will find a node's parent, 2 will + * find its grandparent etc. + * + * @return + * the nth ancestor of this node, or the root node if this is + * reached before the nth parent is found. + */ + public abstract BaseXMLBuilder up(int steps); + + /** + * Return the builder node representing the parent of the current node. + * + * @return + * the parent of this node, or the root node if this method is called on the + * root node. + */ + public abstract BaseXMLBuilder up(); + + /** + * BEWARE: The builder returned by this method represents a Document node, not + * an Element node as is usually the case, so attempts to use the attribute or + * namespace methods on this builder will likely fail. + * + * @return + * the builder node representing the root XML document. + */ + public abstract BaseXMLBuilder document(); + /** * Return the Document node representing the nth ancestor element * of this node, or the root node if n exceeds the document's depth. @@ -604,28 +1165,28 @@ protected Node upImpl(int steps) { * We allow whitespace so parsed XML documents containing indenting or pretty-printing * can still be amended, per issue #17. */ - protected void assertElementContainsNoOrWhitespaceOnlyTextNodes( - Node anXmlElement) { - Node textNodeWithNonWhitespace = null; - NodeList childNodes = anXmlElement.getChildNodes(); - for (int i = 0; i < childNodes.getLength(); i++) { - if (Element.TEXT_NODE == childNodes.item(i).getNodeType()) { - Node textNode = childNodes.item(i); - String textWithoutWhitespace = - textNode.getTextContent().replaceAll("\\s", ""); - if (textWithoutWhitespace.length() > 0) { - textNodeWithNonWhitespace = textNode; - break; - } + protected void assertElementContainsNoOrWhitespaceOnlyTextNodes(Node anXmlElement) + { + Node textNodeWithNonWhitespace = null; + NodeList childNodes = anXmlElement.getChildNodes(); + for (int i = 0; i < childNodes.getLength(); i++) { + if (Element.TEXT_NODE == childNodes.item(i).getNodeType()) { + Node textNode = childNodes.item(i); + String textWithoutWhitespace = + textNode.getTextContent().replaceAll("\\s", ""); + if (textWithoutWhitespace.length() > 0) { + textNodeWithNonWhitespace = textNode; + break; } } - if (textNodeWithNonWhitespace != null) { - throw new IllegalStateException( - "Cannot add sub-element to element <" + anXmlElement.getNodeName() - + "> that contains a Text node that isn't purely whitespace: " - + textNodeWithNonWhitespace); - } } + if (textNodeWithNonWhitespace != null) { + throw new IllegalStateException( + "Cannot add sub-element to element <" + anXmlElement.getNodeName() + + "> that contains a Text node that isn't purely whitespace: " + + textNodeWithNonWhitespace); + } + } /** * Serialize either the specific Element wrapped by this BaseXMLBuilder, diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder.java b/src/main/java/com/jamesmurty/utils/XMLBuilder.java index 60b1fd5..0339e4d 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder.java @@ -196,18 +196,7 @@ public static XMLBuilder parse(File xmlFile) return XMLBuilder.parse(new InputSource(new FileReader(xmlFile))); } - /** - * Find and delete from the underlying Document any text nodes that - * contain nothing but whitespace, such as newlines and tab or space - * characters used to indent or pretty-print an XML document. - * - * Uses approach I documented on StackOverflow: - * http://stackoverflow.com/a/979606/4970 - * - * @return - * a builder node at the same location as before the operation. - * @throws XPathExpressionException - */ + @Override public XMLBuilder stripWhitespaceOnlyTextNodes() throws XPathExpressionException { @@ -215,52 +204,18 @@ public XMLBuilder stripWhitespaceOnlyTextNodes() return this; } - /** - * Imports another XMLBuilder document into this document at the - * current position. The entire document provided is imported. - * - * @param builder - * the XMLBuilder document to be imported. - * - * @return - * a builder node at the same location as before the import, but - * now containing the entire document tree provided. - */ - public XMLBuilder importXMLBuilder(XMLBuilder builder) { + @Override + public XMLBuilder importXMLBuilder(BaseXMLBuilder builder) { super.importXMLBuilderImpl(builder); return this; } - /** - * @return - * the builder node representing the root element of the XML document. - * In other words, the same builder node returned by the initial - * {@link #create(String)} or {@link #parse(InputSource)} method. - */ + @Override public XMLBuilder root() { return new XMLBuilder(getDocument()); } - /** - * Find the first element in the builder's DOM matching the given - * XPath expression, where the expression may include namespaces if - * a {@link NamespaceContext} is provided. - * - * @param xpath - * An XPath expression that *must* resolve to an existing Element within - * the document object model. - * @param nsContext - * a mapping of prefixes to namespace URIs that allows the XPath expression - * to use namespaces. - * - * @return - * a builder node representing the first Element that matches the - * XPath expression. - * - * @throws XPathExpressionException - * If the XPath is invalid, or if does not resolve to at least one - * {@link Node#ELEMENT_NODE}. - */ + @Override public XMLBuilder xpathFind(String xpath, NamespaceContext nsContext) throws XPathExpressionException { @@ -268,567 +223,187 @@ public XMLBuilder xpathFind(String xpath, NamespaceContext nsContext) return new XMLBuilder(foundNode, null); } - /** - * Find the first element in the builder's DOM matching the given - * XPath expression. - * - * @param xpath - * An XPath expression that *must* resolve to an existing Element within - * the document object model. - * - * @return - * a builder node representing the first Element that matches the - * XPath expression. - * - * @throws XPathExpressionException - * If the XPath is invalid, or if does not resolve to at least one - * {@link Node#ELEMENT_NODE}. - */ + @Override public XMLBuilder xpathFind(String xpath) throws XPathExpressionException { return xpathFind(xpath, null); } - /** - * Add a named XML element to the document as a child of this builder node, - * and return the builder node representing the new child. - * - * When adding an element to a namespaced document, the new node will be - * assigned a namespace matching it's qualified name prefix (if any) or - * the document's default namespace. NOTE: If the element has a prefix that - * does not match any known namespaces, the element will be created - * without any namespace. - * - * @param name - * the name of the XML element. - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a child element to an XML node that already - * contains a text node value. - */ + @Override public XMLBuilder element(String name) { String namespaceURI = super.lookupNamespaceURIImpl(name); return element(name, namespaceURI); } - /** - * Synonym for {@link #element(String)}. - * - * @param name - * the name of the XML element. - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a child element to an XML node that already - * contains a text node value. - */ + @Override public XMLBuilder elem(String name) { return element(name); } - /** - * Synonym for {@link #element(String)}. - * - * @param name - * the name of the XML element. - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a child element to an XML node that already - * contains a text node value. - */ + @Override public XMLBuilder e(String name) { return element(name); } - /** - * Add a named and namespaced XML element to the document as a child of - * this builder node, and return the builder node representing the new child. - * - * @param name - * the name of the XML element. - * @param namespaceURI - * a namespace URI - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a child element to an XML node that already - * contains a text node value. - */ + @Override public XMLBuilder element(String name, String namespaceURI) { Element elem = super.elementImpl(name, namespaceURI); return new XMLBuilder(elem, this.getElement()); } - /** - * Add a named XML element to the document as a sibling element - * that precedes the position of this builder node, and return the builder node - * representing the new child. - * - * When adding an element to a namespaced document, the new node will be - * assigned a namespace matching it's qualified name prefix (if any) or - * the document's default namespace. NOTE: If the element has a prefix that - * does not match any known namespaces, the element will be created - * without any namespace. - * - * @param name - * the name of the XML element. - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a sibling element to a node where there are already - * one or more siblings that are text nodes. - */ + @Override public XMLBuilder elementBefore(String name) { Element newElement = super.elementBeforeImpl(name); return new XMLBuilder(newElement, null); } - /** - * Add a named and namespaced XML element to the document as a sibling element - * that precedes the position of this builder node, and return the builder node - * representing the new child. - * - * @param name - * the name of the XML element. - * @param namespaceURI - * a namespace URI - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a sibling element to a node where there are already - * one or more siblings that are text nodes. - */ + @Override public XMLBuilder elementBefore(String name, String namespaceURI) { Element newElement = super.elementBeforeImpl(name, namespaceURI); return new XMLBuilder(newElement, null); } - /** - * Add a named attribute value to the element represented by this builder - * node, and return the node representing the element to which the - * attribute was added (not the new attribute node). - * - * @param name - * the attribute's name. - * @param value - * the attribute's value. - * - * @return - * the builder node representing the element to which the attribute was - * added. - */ + @Override public XMLBuilder attribute(String name, String value) { super.attributeImpl(name, value); return this; } - /** - * Synonym for {@link #attribute(String, String)}. - * - * @param name - * the attribute's name. - * @param value - * the attribute's value. - * - * @return - * the builder node representing the element to which the attribute was - * added. - */ + @Override public XMLBuilder attr(String name, String value) { return attribute(name, value); } - /** - * Synonym for {@link #attribute(String, String)}. - * - * @param name - * the attribute's name. - * @param value - * the attribute's value. - * - * @return - * the builder node representing the element to which the attribute was - * added. - */ + @Override public XMLBuilder a(String name, String value) { return attribute(name, value); } - /** - * Add or replace the text value of an element represented by this builder - * node, and return the node representing the element to which the text - * was added (not the new text node). - * - * @param value - * the text value to set or add to the element. - * @param replaceText - * if True any existing text content of the node is replaced with the - * given text value, if the given value is appended to any existing text. - * - * @return - * the builder node representing the element to which the text was added. - */ + @Override public XMLBuilder text(String value, boolean replaceText) { super.textImpl(value, replaceText); return this; } - /** - * Add a text value to the element represented by this builder node, and - * return the node representing the element to which the text - * was added (not the new text node). - * - * @param value - * the text value to add to the element. - * - * @return - * the builder node representing the element to which the text was added. - */ + @Override public XMLBuilder text(String value) { return this.text(value, false); } - /** - * Synonym for {@link #text(String)}. - * - * @param value - * the text value to add to the element. - * - * @return - * the builder node representing the element to which the text was added. - */ + @Override public XMLBuilder t(String value) { return text(value); } - /** - * Add a CDATA node with String content to the element represented by this - * builder node, and return the node representing the element to which the - * data was added (not the new CDATA node). - * - * @param data - * the String value that will be added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder cdata(String data) { super.cdataImpl(data); return this; } - /** - * Synonym for {@link #cdata(String)}. - * - * @param data - * the String value that will be added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder data(String data) { return cdata(data); } - /** - * Synonym for {@link #cdata(String)}. - * - * @param data - * the String value that will be added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder d(String data) { return cdata(data); } - /** - * Add a CDATA node with Base64-encoded byte data content to the element represented - * by this builder node, and return the node representing the element to which the - * data was added (not the new CDATA node). - * - * @param data - * the data value that will be Base64-encoded and added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder cdata(byte[] data) { super.cdataImpl(data); return this; } - /** - * Synonym for {@link #cdata(byte[])}. - * - * @param data - * the data value that will be Base64-encoded and added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder data(byte[] data) { return cdata(data); } - /** - * Synonym for {@link #cdata(byte[])}. - * - * @param data - * the data value that will be Base64-encoded and added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder d(byte[] data) { return cdata(data); } - /** - * Add a comment to the element represented by this builder node, and - * return the node representing the element to which the comment - * was added (not the new comment node). - * - * @param comment - * the comment to add to the element. - * - * @return - * the builder node representing the element to which the comment was added. - */ + @Override public XMLBuilder comment(String comment) { super.commentImpl(comment); return this; } - /** - * Synonym for {@link #comment(String)}. - * - * @param comment - * the comment to add to the element. - * - * @return - * the builder node representing the element to which the comment was added. - */ + @Override public XMLBuilder cmnt(String comment) { return comment(comment); } - /** - * Synonym for {@link #comment(String)}. - * - * @param comment - * the comment to add to the element. - * - * @return - * the builder node representing the element to which the comment was added. - */ + @Override public XMLBuilder c(String comment) { return comment(comment); } - /** - * Add an instruction to the element represented by this builder node, and - * return the node representing the element to which the instruction - * was added (not the new instruction node). - * - * @param target - * the target value for the instruction. - * @param data - * the data value for the instruction - * - * @return - * the builder node representing the element to which the instruction was - * added. - */ + @Override public XMLBuilder instruction(String target, String data) { super.instructionImpl(target, data); return this; } - /** - * Synonym for {@link #instruction(String, String)}. - * - * @param target - * the target value for the instruction. - * @param data - * the data value for the instruction - * - * @return - * the builder node representing the element to which the instruction was - * added. - */ + @Override public XMLBuilder inst(String target, String data) { return instruction(target, data); } - /** - * Synonym for {@link #instruction(String, String)}. - * - * @param target - * the target value for the instruction. - * @param data - * the data value for the instruction - * - * @return - * the builder node representing the element to which the instruction was - * added. - */ + @Override public XMLBuilder i(String target, String data) { return instruction(target, data); } - /** - * Insert an instruction before the element represented by this builder node, - * and return the node representing that same element - * (not the new instruction node). - * - * @param target - * the target value for the instruction. - * @param data - * the data value for the instruction - * - * @return - * the builder node representing the element before which the instruction was inserted. - */ + @Override public XMLBuilder insertInstruction(String target, String data) { super.insertInstructionImpl(target, data); return this; } - /** - * Add a reference to the element represented by this builder node, and - * return the node representing the element to which the reference - * was added (not the new reference node). - * - * @param name - * the name value for the reference. - * - * @return - * the builder node representing the element to which the reference was - * added. - */ + @Override public XMLBuilder reference(String name) { super.referenceImpl(name); return this; } - /** - * Synonym for {@link #reference(String)}. - * - * @param name - * the name value for the reference. - * - * @return - * the builder node representing the element to which the reference was - * added. - */ + @Override public XMLBuilder ref(String name) { return reference(name); } - /** - * Synonym for {@link #reference(String)}. - * - * @param name - * the name value for the reference. - * - * @return - * the builder node representing the element to which the reference was - * added. - */ + @Override public XMLBuilder r(String name) { return reference(name); } - /** - * Add an XML namespace attribute to this builder's element node. - * - * @param prefix - * a prefix for the namespace URI within the document, may be null - * or empty in which case a default "xmlns" attribute is created. - * @param namespaceURI - * a namespace uri - * - * @return - * the builder node representing the element to which the attribute was added. - */ + @Override public XMLBuilder namespace(String prefix, String namespaceURI) { super.namespaceImpl(prefix, namespaceURI); return this; } - /** - * Synonym for {@link #namespace(String, String)}. - * - * @param prefix - * a prefix for the namespace URI within the document, may be null - * or empty in which case a default xmlns attribute is created. - * @param namespaceURI - * a namespace uri - * - * @return - * the builder node representing the element to which the attribute was added. - */ + @Override public XMLBuilder ns(String prefix, String namespaceURI) { return attribute(prefix, namespaceURI); } - /** - * Add an XML namespace attribute to this builder's element node - * without a prefix. - * - * @param namespaceURI - * a namespace uri - * - * @return - * the builder node representing the element to which the attribute was added. - */ + @Override public XMLBuilder namespace(String namespaceURI) { this.namespace(null, namespaceURI); return this; } - /** - * Synonym for {@link #namespace(String)}. - * - * @param namespaceURI - * a namespace uri - * - * @return - * the builder node representing the element to which the attribute was added. - */ + @Override public XMLBuilder ns(String namespaceURI) { return namespace(namespaceURI); } - /** - * Return the builder node representing the nth ancestor element - * of this node, or the root node if n exceeds the document's depth. - * - * @param steps - * the number of parent elements to step over while navigating up the chain - * of node ancestors. A steps value of 1 will find a node's parent, 2 will - * find its grandparent etc. - * - * @return - * the nth ancestor of this node, or the root node if this is - * reached before the nth parent is found. - */ + @Override public XMLBuilder up(int steps) { Node currNode = super.upImpl(steps); if (currNode instanceof Document) { @@ -838,25 +413,12 @@ public XMLBuilder up(int steps) { } } - /** - * Return the builder node representing the parent of the current node. - * - * @return - * the parent of this node, or the root node if this method is called on the - * root node. - */ + @Override public XMLBuilder up() { return up(1); } - /** - * BEWARE: The builder returned by this method represents a Document node, not - * an Element node as is usually the case, so attempts to use the attribute or - * namespace methods on this builder will likely fail. - * - * @return - * the builder node representing the root XML document. - */ + @Override public XMLBuilder document() { return new XMLBuilder(getDocument(), null); } diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java index 03dd4d8..2a77171 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java @@ -29,8 +29,6 @@ import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.transform.TransformerException; -import javax.xml.xpath.XPathExpressionException; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -95,6 +93,14 @@ protected XMLBuilder2(Node myNode, Node parentNode) { super(myNode, parentNode); } + private static RuntimeException wrapExceptionAsRuntimeException(Exception e) { + if (e instanceof RuntimeException) { + return (RuntimeException) e; + } else { + return new XMLBuilderRuntimeException(e); + } + } + /** * Construct a builder for new XML document with a default namespace. * The document will be created with the given root element, and the builder @@ -112,8 +118,8 @@ public static XMLBuilder2 create(String name, String namespaceURI) { try { return new XMLBuilder2(createDocumentImpl(name, namespaceURI)); - } catch (Exception ex) { - throw new XMLBuilderRuntimeException(ex); + } catch (Exception e) { + throw wrapExceptionAsRuntimeException(e); } } @@ -147,7 +153,7 @@ public static XMLBuilder2 parse(InputSource inputSource) try { return new XMLBuilder2(parseDocumentImpl(inputSource)); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } @@ -181,640 +187,224 @@ public static XMLBuilder2 parse(File xmlFile) try { return XMLBuilder2.parse(new InputSource(new FileReader(xmlFile))); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } - /** - * Find and delete from the underlying Document any text nodes that - * contain nothing but whitespace, such as newlines and tab or space - * characters used to indent or pretty-print an XML document. - * - * Uses approach I documented on StackOverflow: - * http://stackoverflow.com/a/979606/4970 - * - * @return - * a builder node at the same location as before the operation. - */ + @Override public XMLBuilder2 stripWhitespaceOnlyTextNodes() { try { super.stripWhitespaceOnlyTextNodesImpl(); return this; } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } - /** - * Imports another XMLBuilder2 document into this document at the - * current position. The entire document provided is imported. - * - * @param builder - * the XMLBuilder2 document to be imported. - * - * @return - * a builder node at the same location as before the import, but - * now containing the entire document tree provided. - */ - public XMLBuilder2 importXMLBuilder(XMLBuilder2 builder) { + @Override + public XMLBuilder2 importXMLBuilder(BaseXMLBuilder builder) { super.importXMLBuilderImpl(builder); return this; } - /** - * @return - * the builder node representing the root element of the XML document. - * In other words, the same builder node returned by the initial - * {@link #create(String)} or {@link #parse(InputSource)} method. - */ + @Override public XMLBuilder2 root() { return new XMLBuilder2(getDocument()); } - /** - * Find the first element in the builder's DOM matching the given - * XPath expression, where the expression may include namespaces if - * a {@link NamespaceContext} is provided. - * - * @param xpath - * An XPath expression that *must* resolve to an existing Element within - * the document object model. - * @param nsContext - * a mapping of prefixes to namespace URIs that allows the XPath expression - * to use namespaces. - * - * @return - * a builder node representing the first Element that matches the - * XPath expression. - */ + @Override public XMLBuilder2 xpathFind(String xpath, NamespaceContext nsContext) { try { Node foundNode = super.xpathFindImpl(xpath, nsContext); return new XMLBuilder2(foundNode, null); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } - /** - * Find the first element in the builder's DOM matching the given - * XPath expression. - * - * @param xpath - * An XPath expression that *must* resolve to an existing Element within - * the document object model. - * - * @return - * a builder node representing the first Element that matches the - * XPath expression. - */ + @Override public XMLBuilder2 xpathFind(String xpath) { return xpathFind(xpath, null); } - /** - * Add a named XML element to the document as a child of this builder node, - * and return the builder node representing the new child. - * - * When adding an element to a namespaced document, the new node will be - * assigned a namespace matching it's qualified name prefix (if any) or - * the document's default namespace. NOTE: If the element has a prefix that - * does not match any known namespaces, the element will be created - * without any namespace. - * - * @param name - * the name of the XML element. - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a child element to an XML node that already - * contains a text node value. - */ + @Override public XMLBuilder2 element(String name) { String namespaceURI = super.lookupNamespaceURIImpl(name); return element(name, namespaceURI); } - /** - * Synonym for {@link #element(String)}. - * - * @param name - * the name of the XML element. - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a child element to an XML node that already - * contains a text node value. - */ + @Override public XMLBuilder2 elem(String name) { return element(name); } - /** - * Synonym for {@link #element(String)}. - * - * @param name - * the name of the XML element. - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a child element to an XML node that already - * contains a text node value. - */ + @Override public XMLBuilder2 e(String name) { return element(name); } - /** - * Add a named and namespaced XML element to the document as a child of - * this builder node, and return the builder node representing the new child. - * - * @param name - * the name of the XML element. - * @param namespaceURI - * a namespace URI - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a child element to an XML node that already - * contains a text node value. - */ + @Override public XMLBuilder2 element(String name, String namespaceURI) { Element elem = super.elementImpl(name, namespaceURI); return new XMLBuilder2(elem, this.getElement()); } - /** - * Add a named XML element to the document as a sibling element - * that precedes the position of this builder node, and return the builder node - * representing the new child. - * - * When adding an element to a namespaced document, the new node will be - * assigned a namespace matching it's qualified name prefix (if any) or - * the document's default namespace. NOTE: If the element has a prefix that - * does not match any known namespaces, the element will be created - * without any namespace. - * - * @param name - * the name of the XML element. - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a sibling element to a node where there are already - * one or more siblings that are text nodes. - */ + @Override public XMLBuilder2 elementBefore(String name) { Element newElement = super.elementBeforeImpl(name); return new XMLBuilder2(newElement, null); } - /** - * Add a named and namespaced XML element to the document as a sibling element - * that precedes the position of this builder node, and return the builder node - * representing the new child. - * - * @param name - * the name of the XML element. - * @param namespaceURI - * a namespace URI - * - * @return - * a builder node representing the new child. - * - * @throws IllegalStateException - * if you attempt to add a sibling element to a node where there are already - * one or more siblings that are text nodes. - */ + @Override public XMLBuilder2 elementBefore(String name, String namespaceURI) { Element newElement = super.elementBeforeImpl(name, namespaceURI); return new XMLBuilder2(newElement, null); } - /** - * Add a named attribute value to the element represented by this builder - * node, and return the node representing the element to which the - * attribute was added (not the new attribute node). - * - * @param name - * the attribute's name. - * @param value - * the attribute's value. - * - * @return - * the builder node representing the element to which the attribute was - * added. - */ + @Override public XMLBuilder2 attribute(String name, String value) { super.attributeImpl(name, value); return this; } - /** - * Synonym for {@link #attribute(String, String)}. - * - * @param name - * the attribute's name. - * @param value - * the attribute's value. - * - * @return - * the builder node representing the element to which the attribute was - * added. - */ + @Override public XMLBuilder2 attr(String name, String value) { return attribute(name, value); } - /** - * Synonym for {@link #attribute(String, String)}. - * - * @param name - * the attribute's name. - * @param value - * the attribute's value. - * - * @return - * the builder node representing the element to which the attribute was - * added. - */ + @Override public XMLBuilder2 a(String name, String value) { return attribute(name, value); } - /** - * Add or replace the text value of an element represented by this builder - * node, and return the node representing the element to which the text - * was added (not the new text node). - * - * @param value - * the text value to set or add to the element. - * @param replaceText - * if True any existing text content of the node is replaced with the - * given text value, if the given value is appended to any existing text. - * - * @return - * the builder node representing the element to which the text was added. - */ + @Override public XMLBuilder2 text(String value, boolean replaceText) { super.textImpl(value, replaceText); return this; } - /** - * Add a text value to the element represented by this builder node, and - * return the node representing the element to which the text - * was added (not the new text node). - * - * @param value - * the text value to add to the element. - * - * @return - * the builder node representing the element to which the text was added. - */ + @Override public XMLBuilder2 text(String value) { return this.text(value, false); } - /** - * Synonym for {@link #text(String)}. - * - * @param value - * the text value to add to the element. - * - * @return - * the builder node representing the element to which the text was added. - */ + @Override public XMLBuilder2 t(String value) { return text(value); } - /** - * Add a CDATA node with String content to the element represented by this - * builder node, and return the node representing the element to which the - * data was added (not the new CDATA node). - * - * @param data - * the String value that will be added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder2 cdata(String data) { super.cdataImpl(data); return this; } - /** - * Synonym for {@link #cdata(String)}. - * - * @param data - * the String value that will be added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder2 data(String data) { return cdata(data); } - /** - * Synonym for {@link #cdata(String)}. - * - * @param data - * the String value that will be added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder2 d(String data) { return cdata(data); } - /** - * Add a CDATA node with Base64-encoded byte data content to the element represented - * by this builder node, and return the node representing the element to which the - * data was added (not the new CDATA node). - * - * @param data - * the data value that will be Base64-encoded and added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder2 cdata(byte[] data) { super.cdataImpl(data); return this; } - /** - * Synonym for {@link #cdata(byte[])}. - * - * @param data - * the data value that will be Base64-encoded and added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder2 data(byte[] data) { return cdata(data); } - /** - * Synonym for {@link #cdata(byte[])}. - * - * @param data - * the data value that will be Base64-encoded and added to a CDATA element. - * - * @return - * the builder node representing the element to which the data was added. - */ + @Override public XMLBuilder2 d(byte[] data) { return cdata(data); } - /** - * Add a comment to the element represented by this builder node, and - * return the node representing the element to which the comment - * was added (not the new comment node). - * - * @param comment - * the comment to add to the element. - * - * @return - * the builder node representing the element to which the comment was added. - */ + @Override public XMLBuilder2 comment(String comment) { super.commentImpl(comment); return this; } - /** - * Synonym for {@link #comment(String)}. - * - * @param comment - * the comment to add to the element. - * - * @return - * the builder node representing the element to which the comment was added. - */ + @Override public XMLBuilder2 cmnt(String comment) { return comment(comment); } - /** - * Synonym for {@link #comment(String)}. - * - * @param comment - * the comment to add to the element. - * - * @return - * the builder node representing the element to which the comment was added. - */ + @Override public XMLBuilder2 c(String comment) { return comment(comment); } - /** - * Add an instruction to the element represented by this builder node, and - * return the node representing the element to which the instruction - * was added (not the new instruction node). - * - * @param target - * the target value for the instruction. - * @param data - * the data value for the instruction - * - * @return - * the builder node representing the element to which the instruction was - * added. - */ + @Override public XMLBuilder2 instruction(String target, String data) { super.instructionImpl(target, data); return this; } - /** - * Synonym for {@link #instruction(String, String)}. - * - * @param target - * the target value for the instruction. - * @param data - * the data value for the instruction - * - * @return - * the builder node representing the element to which the instruction was - * added. - */ + @Override public XMLBuilder2 inst(String target, String data) { return instruction(target, data); } - /** - * Synonym for {@link #instruction(String, String)}. - * - * @param target - * the target value for the instruction. - * @param data - * the data value for the instruction - * - * @return - * the builder node representing the element to which the instruction was - * added. - */ + @Override public XMLBuilder2 i(String target, String data) { return instruction(target, data); } - /** - * Insert an instruction before the element represented by this builder node, - * and return the node representing that same element - * (not the new instruction node). - * - * @param target - * the target value for the instruction. - * @param data - * the data value for the instruction - * - * @return - * the builder node representing the element before which the instruction was inserted. - */ + @Override public XMLBuilder2 insertInstruction(String target, String data) { super.insertInstructionImpl(target, data); return this; } - /** - * Add a reference to the element represented by this builder node, and - * return the node representing the element to which the reference - * was added (not the new reference node). - * - * @param name - * the name value for the reference. - * - * @return - * the builder node representing the element to which the reference was - * added. - */ + @Override public XMLBuilder2 reference(String name) { super.referenceImpl(name); return this; } - /** - * Synonym for {@link #reference(String)}. - * - * @param name - * the name value for the reference. - * - * @return - * the builder node representing the element to which the reference was - * added. - */ + @Override public XMLBuilder2 ref(String name) { return reference(name); } - /** - * Synonym for {@link #reference(String)}. - * - * @param name - * the name value for the reference. - * - * @return - * the builder node representing the element to which the reference was - * added. - */ + @Override public XMLBuilder2 r(String name) { return reference(name); } - /** - * Add an XML namespace attribute to this builder's element node. - * - * @param prefix - * a prefix for the namespace URI within the document, may be null - * or empty in which case a default "xmlns" attribute is created. - * @param namespaceURI - * a namespace uri - * - * @return - * the builder node representing the element to which the attribute was added. - */ + @Override public XMLBuilder2 namespace(String prefix, String namespaceURI) { super.namespaceImpl(prefix, namespaceURI); return this; } - /** - * Synonym for {@link #namespace(String, String)}. - * - * @param prefix - * a prefix for the namespace URI within the document, may be null - * or empty in which case a default xmlns attribute is created. - * @param namespaceURI - * a namespace uri - * - * @return - * the builder node representing the element to which the attribute was added. - */ + @Override public XMLBuilder2 ns(String prefix, String namespaceURI) { return attribute(prefix, namespaceURI); } - /** - * Add an XML namespace attribute to this builder's element node - * without a prefix. - * - * @param namespaceURI - * a namespace uri - * - * @return - * the builder node representing the element to which the attribute was added. - */ + @Override public XMLBuilder2 namespace(String namespaceURI) { this.namespace(null, namespaceURI); return this; } - /** - * Synonym for {@link #namespace(String)}. - * - * @param namespaceURI - * a namespace uri - * - * @return - * the builder node representing the element to which the attribute was added. - */ + @Override public XMLBuilder2 ns(String namespaceURI) { return namespace(namespaceURI); } - /** - * Return the builder node representing the nth ancestor element - * of this node, or the root node if n exceeds the document's depth. - * - * @param steps - * the number of parent elements to step over while navigating up the chain - * of node ancestors. A steps value of 1 will find a node's parent, 2 will - * find its grandparent etc. - * - * @return - * the nth ancestor of this node, or the root node if this is - * reached before the nth parent is found. - */ + @Override public XMLBuilder2 up(int steps) { Node currNode = super.upImpl(steps); if (currNode instanceof Document) { @@ -824,25 +414,12 @@ public XMLBuilder2 up(int steps) { } } - /** - * Return the builder node representing the parent of the current node. - * - * @return - * the parent of this node, or the root node if this method is called on the - * root node. - */ + @Override public XMLBuilder2 up() { return up(1); } - /** - * BEWARE: The builder returned by this method represents a Document node, not - * an Element node as is usually the case, so attempts to use the attribute or - * namespace methods on this builder will likely fail. - * - * @return - * the builder node representing the root XML document. - */ + @Override public XMLBuilder2 document() { return new XMLBuilder2(getDocument(), null); } @@ -852,7 +429,7 @@ public String asString() { try { return super.asString(); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } @@ -861,7 +438,7 @@ public String asString(Properties properties) { try { return super.asString(properties); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } @@ -870,7 +447,7 @@ public String elementAsString() { try { return super.elementAsString(); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } @@ -879,7 +456,7 @@ public String elementAsString(Properties outputProperties) { try { return super.elementAsString(outputProperties); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } @@ -889,7 +466,7 @@ public void toWriter(boolean wholeDocument, Writer writer, Properties outputProp try { super.toWriter(wholeDocument, writer, outputProperties); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } @@ -899,7 +476,7 @@ public void toWriter(Writer writer, Properties outputProperties) try { super.toWriter(writer, outputProperties); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } @@ -909,7 +486,7 @@ public Object xpathQuery(String xpath, QName type, NamespaceContext nsContext) try { return super.xpathQuery(xpath, type, nsContext); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } @@ -919,7 +496,7 @@ public Object xpathQuery(String xpath, QName type) try { return super.xpathQuery(xpath, type); } catch (Exception e) { - throw new XMLBuilderRuntimeException(e); + throw wrapExceptionAsRuntimeException(e); } } From 485d78be7f3a746a580b1a982614e514e1949526 Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 17 Jul 2014 14:31:28 +0100 Subject: [PATCH 12/43] Refactored unit tests to apply to both implementations, re #4 Both XMLBuilder and XMLBuilder2 are now tested by a single base test case class, with class-specific test stubs overridding only what is necessary. --- .../jamesmurty/utils/BaseXMLBuilderTests.java | 622 ++++++++++++++++++ .../com/jamesmurty/utils/TestXMLBuilder2.java | 15 + .../com/jamesmurty/utils/TestXmlBuilder.java | 592 +---------------- 3 files changed, 644 insertions(+), 585 deletions(-) create mode 100644 src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java create mode 100644 src/test/java/com/jamesmurty/utils/TestXMLBuilder2.java diff --git a/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java b/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java new file mode 100644 index 0000000..e488307 --- /dev/null +++ b/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java @@ -0,0 +1,622 @@ +package com.jamesmurty.utils; + +import java.io.File; +import java.io.FileWriter; +import java.io.StringReader; +import java.io.StringWriter; +import java.util.Properties; + +import javax.xml.transform.OutputKeys; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; + +import junit.framework.TestCase; +import net.iharder.Base64; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.InputSource; + +public abstract class BaseXMLBuilderTests extends TestCase { + + public static final String EXAMPLE_XML_DOC_START = + "" + + "" + + "http://code.google.com/p/java-xmlbuilder/" + + "" + + "" + + "http://jets3t.s3.amazonaws.com/index.html"; + + public static final String EXAMPLE_XML_DOC_END = + "" + + ""; + + public static final String EXAMPLE_XML_DOC = EXAMPLE_XML_DOC_START + EXAMPLE_XML_DOC_END; + + protected abstract Class XMLBuilderToTest() throws Exception; + + protected abstract boolean isRuntimeExceptionsOnly(); + + protected BaseXMLBuilder XMLBuilder_create(String name) throws Exception { + return (BaseXMLBuilder) XMLBuilderToTest().getMethod( + "create", String.class).invoke(null, name); + } + + protected BaseXMLBuilder XMLBuilder_create(String name, String nsURI) throws Exception { + return (BaseXMLBuilder) XMLBuilderToTest().getMethod( + "create", String.class, String.class).invoke(null, name, nsURI); + } + + protected BaseXMLBuilder XMLBuilder_parse(InputSource source) throws Exception { + return (BaseXMLBuilder) XMLBuilderToTest().getMethod( + "parse", InputSource.class).invoke(null, source); + } + + protected BaseXMLBuilder XMLBuilder_parse(String documentString) throws Exception { + return (BaseXMLBuilder) XMLBuilderToTest().getMethod( + "parse", String.class).invoke(null, documentString); + } + + public void testXmlDocumentCreation() throws Exception { + /* Build XML document in-place */ + BaseXMLBuilder builder = XMLBuilder_create("Projects") + .e("java-xmlbuilder") + .a("language", "Java") + .a("scm","SVN") + .e("Location") + .a("type", "URL") + .t("http://code.google.com/p/java-xmlbuilder/") + .up() + .up() + .e("JetS3t") + .a("language", "Java") + .a("scm","CVS") + .e("Location") + .a("type", "URL") + .t("http://jets3t.s3.amazonaws.com/index.html"); + + /* Set output properties */ + Properties outputProperties = new Properties(); + // Explicitly identify the output as an XML document + outputProperties.put(javax.xml.transform.OutputKeys.METHOD, "xml"); + // Pretty-print the XML output (doesn't work in all cases) + outputProperties.put(javax.xml.transform.OutputKeys.INDENT, "no"); + // Omit the XML declaration, which can differ depending on the test's run context. + outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); + + /* Serialize builder document */ + StringWriter writer = new StringWriter(); + builder.toWriter(writer, outputProperties); + + assertEquals(EXAMPLE_XML_DOC, writer.toString()); + + /* Build XML document in segments*/ + BaseXMLBuilder projectsB = XMLBuilder_create("Projects"); + projectsB.e("java-xmlbuilder") + .a("language", "Java") + .a("scm","SVN") + .e("Location") + .a("type", "URL") + .t("http://code.google.com/p/java-xmlbuilder/"); + BaseXMLBuilder jets3tB = projectsB.e("JetS3t") + .a("language", "Java") + .a("scm","CVS"); + jets3tB.e("Location") + .a("type", "URL") + .t("http://jets3t.s3.amazonaws.com/index.html"); + + assertEquals(builder.asString(), projectsB.asString()); + } + + public void testParseAndXPath() throws Exception { + // Parse an existing XML document + BaseXMLBuilder builder = XMLBuilder_parse( + new InputSource(new StringReader(EXAMPLE_XML_DOC))); + assertEquals("Projects", builder.root().getElement().getNodeName()); + assertEquals("Invalid current element", "Projects", builder.getElement().getNodeName()); + + // Find the first Location element + builder = builder.xpathFind("//Location"); + assertEquals("Location", builder.getElement().getNodeName()); + assertEquals("http://code.google.com/p/java-xmlbuilder/", + builder.getElement().getTextContent()); + + // Find JetS3t's Location element + builder = builder.xpathFind("//JetS3t/Location"); + assertEquals("Location", builder.getElement().getNodeName()); + assertEquals("http://jets3t.s3.amazonaws.com/index.html", + builder.getElement().getTextContent()); + + // Find the project with the scm attribute 'CVS' (should be JetS3t) + builder = builder.xpathFind("//*[@scm = 'CVS']"); + assertEquals("JetS3t", builder.getElement().getNodeName()); + + // Try an invalid XPath that does not resolve to an element + try { + builder.xpathFind("//@language"); + fail("Non-Element XPath expression should have failed"); + } catch (Exception e) { + if (isRuntimeExceptionsOnly()) { + assertEquals(XMLBuilderRuntimeException.class, e.getClass()); + e = (Exception) e.getCause(); + } + assertEquals(XPathExpressionException.class, e.getClass()); + assertTrue(e.getMessage().contains("does not resolve to an Element")); + } + + /* Perform full-strength XPath queries that do not have to + * resolve to an Element, and do not return BaseXMLBuilder instances + */ + + // Find the Location value for the JetS3t project + String location = (String) builder.xpathQuery( + "//JetS3t/Location/.", XPathConstants.STRING); + assertEquals("http://jets3t.s3.amazonaws.com/index.html", location); + + // Count the number of projects (count returned as String) + String countAsString = (String) builder.xpathQuery( + "count(/Projects/*)", XPathConstants.STRING); + assertEquals("2", countAsString); + + // Count the number of projects (count returned as "Number" - actually Double) + Number countAsNumber = (Number) builder.xpathQuery( + "count(/Projects/*)", XPathConstants.NUMBER); + assertEquals(2.0, countAsNumber); + + // Find all nodes under Projects + NodeList nodes = (NodeList) builder.xpathQuery( + "/Projects/*", XPathConstants.NODESET); + assertEquals(2, nodes.getLength()); + assertEquals("JetS3t", nodes.item(1).getNodeName()); + + // Returns null if nothing found when a NODE type is requested... + assertNull(builder.xpathQuery("//WrongName", XPathConstants.NODE)); + // ... or an empty String if a STRING type is requested... + assertEquals("", builder.xpathQuery("//WrongName", XPathConstants.STRING)); + // ... or NaN if a NUMBER type is requested... + assertEquals(Double.NaN, builder.xpathQuery("//WrongName", XPathConstants.NUMBER)); + + /* Add a new XML element at a specific XPath location in an existing document */ + + // Use XPath to get a builder at the insert location + BaseXMLBuilder xpathLocB = builder.xpathFind("//JetS3t"); + assertEquals("JetS3t", xpathLocB.getElement().getNodeName()); + + // Append a new element with the location's builder + BaseXMLBuilder location2B = xpathLocB.elem("Location2").attr("type", "Testing"); + assertEquals("Location2", location2B.getElement().getNodeName()); + assertEquals("JetS3t", location2B.up().getElement().getNodeName()); + assertEquals(xpathLocB.getElement(), location2B.up().getElement()); + assertEquals(builder.root(), location2B.root()); + + // Sanity-check the entire resultant XML document + Properties outputProperties = new Properties(); + outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); + String xmlAsString = location2B.asString(outputProperties); + + assertFalse(EXAMPLE_XML_DOC.equals(xmlAsString)); + assertTrue(xmlAsString.contains("")); + assertEquals( + EXAMPLE_XML_DOC_START + "" + EXAMPLE_XML_DOC_END, + xmlAsString); + } + + public void testParseAndAmendDocWithWhitespaceNodes() throws Exception { + // Parse example XML document and output with indenting, to add whitespace nodes + Properties outputProperties = new Properties(); + outputProperties.put(OutputKeys.INDENT, "yes"); + outputProperties.put("{http://xml.apache.org/xslt}indent-amount", "2"); + String xmlWithWhitespaceNodes = + XMLBuilder_parse(EXAMPLE_XML_DOC).asString(outputProperties); + + // Re-parse document that now has whitespace nodes + BaseXMLBuilder builder = XMLBuilder_parse(xmlWithWhitespaceNodes); + + // Ensure we can add a node to the document (re issue #17) + builder.xpathFind("//JetS3t") + .elem("AnotherLocation").attr("type", "Testing"); + String xmlWithAmendments = builder.asString(outputProperties); + assertTrue(xmlWithAmendments.contains("")); + } + + public void testStripWhitespaceNodesFromDocument() throws Exception { + // Parse example XML document and output with indenting, to add whitespace nodes + Properties outputProperties = new Properties(); + outputProperties.put(OutputKeys.INDENT, "yes"); + outputProperties.put("{http://xml.apache.org/xslt}indent-amount", "2"); + String xmlWithWhitespaceNodes = + XMLBuilder_parse(EXAMPLE_XML_DOC).asString(outputProperties); + + // Re-parse document that now has whitespace text nodes + BaseXMLBuilder builder = XMLBuilder_parse(xmlWithWhitespaceNodes); + assertTrue(builder.asString().contains("\n")); + assertTrue(builder.asString().contains(" ")); + + // Strip whitespace nodes + builder.stripWhitespaceOnlyTextNodes(); + assertFalse(builder.asString().contains("\n")); + assertFalse(builder.asString().contains(" ")); + } + + public void testSimpleXpath() throws Exception { + String xmlDoc = ""; + BaseXMLBuilder builder = XMLBuilder_parse(xmlDoc); + BaseXMLBuilder builderNode = builder.xpathFind("report_objects"); + assertTrue("report_objects".equals(builderNode.getElement().getNodeName())); + assertTrue("".equals(builderNode.elementAsString())); + } + + /** + * Test for issue #11: https://code.google.com/p/java-xmlbuilder/issues/detail?id=11 + * @throws Exception + */ + public void testAddElementsInLoop() throws Exception { + BaseXMLBuilder builder = XMLBuilder_create("DocRoot"); + BaseXMLBuilder parentBuilder = builder.element("Parent"); + + // Add set of elements to Parent using a loop... + for (int i = 1; i <= 10; i++) { + parentBuilder.elem("IntegerValue" + i).text("" + i); + } + + // ...and confirm element set is within parent after a call to up() + parentBuilder.up(); + + assertEquals("Parent", parentBuilder.getElement().getNodeName()); + assertEquals("DocRoot", builder.getElement().getNodeName()); + assertEquals(1, builder.getElement().getChildNodes().getLength()); + assertEquals("Parent", builder.getElement().getChildNodes().item(0).getNodeName()); + assertEquals(10, parentBuilder.getElement().getChildNodes().getLength()); + assertEquals("IntegerValue1", parentBuilder.getElement().getChildNodes().item(0).getNodeName()); + assertEquals("1", parentBuilder.getElement().getChildNodes().item(0).getTextContent()); + } + + public void testTraversalDuringBuild() throws Exception { + BaseXMLBuilder builder = XMLBuilder_create("ElemDepth1") + .e("ElemDepth2") + .e("ElemDepth3") + .e("ElemDepth4"); + assertEquals("ElemDepth3", builder.up().getElement().getNodeName()); + assertEquals("ElemDepth1", builder.up(3).getElement().getNodeName()); + // Traverse too far up the node tree... + assertEquals("ElemDepth1", builder.up(4).getElement().getNodeName()); + // Traverse way too far up the node tree... + assertEquals("ElemDepth1", builder.up(100).getElement().getNodeName()); + } + + public void testImport() throws Exception { + BaseXMLBuilder importer = XMLBuilder_create("Importer") + .elem("Imported") + .elem("Element") + .elem("Goes").attr("are-we-there-yet", "almost") + .elem("Here"); + BaseXMLBuilder importee = XMLBuilder_create("Importee") + .elem("Importee").attr("awating-my", "new-home") + .elem("IsEntireSubtree") + .elem("Included"); + importer.importXMLBuilder(importee); + + // Ensure we're at the same point in the XML doc + assertEquals("Here", importer.getElement().getNodeName()); + + try { + importer.xpathFind("//Importee"); + importer.xpathFind("//IsEntireSubtree"); + importer.xpathFind("//IsEntireSubtree"); + importer.xpathFind("//Included"); + } catch (XPathExpressionException e) { + fail("XMLBuilder import failed: " + e.getMessage()); + } + + BaseXMLBuilder invalidImporter = XMLBuilder_create("InvalidImporter") + .text("BadBadBad"); + try { + invalidImporter.importXMLBuilder(importee); + fail("Should not be able to import XMLBuilder into " + + "an element containing text nodes"); + } catch (IllegalStateException e) { + // Expected + } + } + + public void testCDataNodes() throws Exception { + String text = "Text data -- left as it is"; + String textForBytes = "Byte data is automatically base64-encoded"; + String textEncoded = Base64.encodeBytes(textForBytes.getBytes("UTF-8")); + + BaseXMLBuilder builder = XMLBuilder_create("TestCDataNodes") + .elem("CDataTextElem") + .cdata(text) + .up() + .elem("CDataBytesElem") + .cdata(textForBytes.getBytes("UTF-8")); + + Node cdataTextNode = builder.xpathFind("//CDataTextElem") + .getElement().getChildNodes().item(0); + assertEquals(Node.CDATA_SECTION_NODE, cdataTextNode.getNodeType()); + assertEquals(text, cdataTextNode.getNodeValue()); + + Node cdataBytesNode = builder.xpathFind("//CDataBytesElem") + .getElement().getChildNodes().item(0); + assertEquals(Node.CDATA_SECTION_NODE, cdataBytesNode.getNodeType()); + assertEquals(textEncoded, cdataBytesNode.getNodeValue()); + String base64Decoded = new String(Base64.decode(cdataBytesNode.getNodeValue())); + assertEquals(textForBytes, base64Decoded); + } + + public void testElementAsString() throws Exception { + BaseXMLBuilder builder = XMLBuilder_create("This") + .elem("Is").elem("My").text("Test"); + // By default, entire XML document is serialized regardless of starting-point + assertEquals("Test", builder.asString()); + assertEquals("Test", builder.xpathFind("//My").asString()); + // Serialize a specific Element and its descendants with elementAsString + assertEquals("Test", builder.xpathFind("//My").elementAsString()); + } + + public void testNamespaces() throws Exception { + BaseXMLBuilder builder = XMLBuilder_create("NamespaceTest", "urn:default") + .namespace("prefix1", "urn:ns1") + + .element("NSDefaultImplicit").up() + .element("NSDefaultExplicit", "urn:default").up() + + .element("NS1Explicit", "urn:ns1").up() + .element("prefix1:NS1WithPrefixExplicit", "urn:ns1").up() + .element("prefix1:NS1WithPrefixImplicit").up(); + + // Build a namespace context from the builder's document + NamespaceContextImpl context = builder.buildDocumentNamespaceContext(); + + // All elements in a namespaced document inherit a namespace URI, + // for namespaced document any non-namespaced XPath query will fail. + try { + builder.xpathFind("//:NSDefaultImplicit"); + fail("Namespaced xpath query without context is invalid"); + } catch (Exception e) { + if (isRuntimeExceptionsOnly()) { + assertEquals(XMLBuilderRuntimeException.class, e.getClass()); + e = (Exception) e.getCause(); + } + assertEquals(XPathExpressionException.class, e.getClass()); + } + try { + builder.xpathFind("//NSDefaultImplicit", context); + fail("XPath query without prefixes on namespaced docs is invalid"); + } catch (Exception e) { + if (isRuntimeExceptionsOnly()) { + assertEquals(XMLBuilderRuntimeException.class, e.getClass()); + e = (Exception) e.getCause(); + } + assertEquals(XPathExpressionException.class, e.getClass()); + } + + // Find nodes with default namespace + builder.xpathFind("/:NamespaceTest", context); + builder.xpathFind("//:NSDefaultImplicit", context); + builder.xpathFind("//:NSDefaultExplicit", context); + + // Must use namespace-aware xpath to find namespaced nodes + try { + builder.xpathFind("//NSDefaultExplicit"); + fail(); + } catch (Exception e) { + if (isRuntimeExceptionsOnly()) { + assertEquals(XMLBuilderRuntimeException.class, e.getClass()); + e = (Exception) e.getCause(); + } + assertEquals(XPathExpressionException.class, e.getClass()); + } + try { + builder.xpathFind("//:NSDefaultExplicit"); + fail(); + } catch (Exception e) { + if (isRuntimeExceptionsOnly()) { + assertEquals(XMLBuilderRuntimeException.class, e.getClass()); + e = (Exception) e.getCause(); + } + assertEquals(XPathExpressionException.class, e.getClass()); + } + try { + builder.xpathFind("//NSDefaultExplicit", context); + fail(); + } catch (Exception e) { + if (isRuntimeExceptionsOnly()) { + assertEquals(XMLBuilderRuntimeException.class, e.getClass()); + e = (Exception) e.getCause(); + } + assertEquals(XPathExpressionException.class, e.getClass()); + } + + // Find node with namespace prefix + builder.xpathFind("//prefix1:NS1Explicit", context); + builder.xpathFind("//prefix1:NS1WithPrefixExplicit", context); + builder.xpathFind("//prefix1:NS1WithPrefixImplicit", context); + + // Find nodes with user-defined prefix "aliases" + context.addNamespace("default-alias", "urn:default"); + context.addNamespace("prefix1-alias", "urn:ns1"); + builder.xpathFind("//default-alias:NSDefaultExplicit", context); + builder.xpathFind("//prefix1-alias:NS1Explicit", context); + + // User can override context mappings, for better or worse + context.addNamespace("", "urn:default"); + builder.xpathFind("//:NSDefaultExplicit", context); + + context.addNamespace("", "urn:wrong"); + try { + builder.xpathFind("//:NSDefaultExplicit", context); + fail(); + } catch (Exception e) { + if (isRuntimeExceptionsOnly()) { + assertEquals(XMLBuilderRuntimeException.class, e.getClass()); + e = (Exception) e.getCause(); + } + assertEquals(XPathExpressionException.class, e.getClass()); + } + + // Users are not prevented from creating elements that reference + // an undefined namespace prefix -- user beware + builder.element("undefined-prefix:ElementName"); + } + + public void testElementBefore() throws Exception { + BaseXMLBuilder builder = XMLBuilder_create("TestDocument", "urn:default") + .namespace("custom", "urn:custom") + .elem("Before").up() + .elem("After"); + NamespaceContextImpl context = builder.buildDocumentNamespaceContext(); + + // Ensure XML structure is correct before insert + assertEquals("" + + "", builder.asString()); + + // Insert an element before the "After" element, no explicit namespace (will use default) + BaseXMLBuilder testDoc = XMLBuilder_parse(builder.asString()) + .xpathFind("/:TestDocument/:After", context); + BaseXMLBuilder insertedBuilder = testDoc.elementBefore("Inserted"); + assertEquals("Inserted", insertedBuilder.getElement().getNodeName()); + assertEquals("" + + "", testDoc.asString()); + + // Insert another element, this time with a custom namespace prefix + insertedBuilder = insertedBuilder.elementBefore("custom:InsertedAgain"); + assertEquals("custom:InsertedAgain", insertedBuilder.getElement().getNodeName()); + assertEquals("" + + "", + testDoc.asString()); + + // Insert another element, this time with a custom namespace ref + insertedBuilder = insertedBuilder.elementBefore("InsertedYetAgain", "urn:custom2"); + assertEquals("InsertedYetAgain", insertedBuilder.getElement().getNodeName()); + assertEquals("" + + "" + + "", + testDoc.asString()); + } + + public void testTextNodes() throws Exception { + BaseXMLBuilder builder = XMLBuilder_create("TestDocument") + .elem("TextElement") + .text("Initial"); + + BaseXMLBuilder textElementBuilder = builder.xpathFind("//TextElement"); + assertEquals("Initial", textElementBuilder.getElement().getTextContent()); + + // By default, text methods append value to existing text + textElementBuilder.text("Appended"); + assertEquals("InitialAppended", textElementBuilder.getElement().getTextContent()); + + // Use boolean flag to replace text nodes with a new value + textElementBuilder.text("Replacement", true); + assertEquals("Replacement", textElementBuilder.getElement().getTextContent()); + + // Fail-fast if a null text value is provided. + try { + textElementBuilder.text(null); + fail("null text value should cause IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("Illegal null text value", ex.getMessage()); + } + + try { + textElementBuilder.text(null, true); + fail("null text value should cause IllegalArgumentException"); + } catch (IllegalArgumentException ex) { + assertEquals("Illegal null text value", ex.getMessage()); + } + + } + + public void testProcessingInstructionNodes() throws Exception { + // Add instruction to root document element node (usual append-in-node behaviour) + BaseXMLBuilder builder = XMLBuilder_create("TestDocument").instruction("test", "data"); + assertEquals("", builder.asString()); + + // Add instruction after the root document element (not within it) + builder = XMLBuilder_create("TestDocument3").document().instruction("test", "data"); + assertEquals("", builder.asString().trim()); + + // Insert instruction as first node of the root document + builder = XMLBuilder_create("TestDocument3").insertInstruction("test", "data"); + assertEquals( + "", + // Remove newlines from XML as this differs across platforms + builder.asString().replace("\n", "")); + + // Insert instruction as first node of the root document, second example + builder = XMLBuilder_create("TestDocument4").elem("ChildElem") + .root().insertInstruction("test", "data"); + assertEquals( + "", + // Remove newlines from XML as this differs across platforms + builder.asString().replace("\n", "")); + } + + /** + * Test for strange issue raised by user on comments form where OutputKeys.STANDALONE setting + * in transformer is ignored. + * + * @throws Exception + */ + public void testSetStandaloneToYes() throws Exception { + String xmlDoc = ""; + BaseXMLBuilder builder = XMLBuilder_parse( + new InputSource(new StringReader(xmlDoc))); + + // Basic output settings + Properties outputProperties = new Properties(); + outputProperties.put(javax.xml.transform.OutputKeys.VERSION, "1.0"); + outputProperties.put(javax.xml.transform.OutputKeys.METHOD, "xml"); + outputProperties.put(javax.xml.transform.OutputKeys.ENCODING, "UTF-8"); + + // Use Document@setXmlStandalone(true) to ensure OutputKeys.STANDALONE is respected. + builder.getDocument().setXmlStandalone(true); + outputProperties.put(javax.xml.transform.OutputKeys.STANDALONE, "yes"); + + /* Serialize builder document */ + StringWriter writer = new StringWriter(); + builder.toWriter(writer, outputProperties); + + assertEquals( + "" + xmlDoc, + writer.toString()); + } + + /** + * Test the {@link BaseXMLBuilder#asString(Properties)} method output a document + * of the correct size when the document is moderately large, re issue + * #1 (on GitHub). + * + * @throws Exception + */ + public void testModerateDocumentSizeAsString() throws Exception { + // Create a moderate document around 0.5 MB + long expectedByteSize = 505021; + BaseXMLBuilder builder = XMLBuilder_create("RootNode"); + for (int i = 0; i < 5000; i++) { + builder + .e("TreeRoot") + .e("TreeTrunk") + .e("TreeBranch") + .e("TreeLeaf") + .t("Some Aphids"); + + } + // Omit XML declaration, which will otherwise be included in file + // via #toWriter but not in string via #asString + Properties outputProperties = new Properties(); + outputProperties.put( + javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); + // Ensure XML as string has expected length... + String xmlString = builder.asString(outputProperties); + assertEquals(expectedByteSize, xmlString.length()); + // ...and matches size of XML written to file + File f = File.createTempFile( + "java-xmlbuilder-testmoderatedocumentsizeasstring", ".xml"); + builder.toWriter(new FileWriter(f), outputProperties); + assertEquals(expectedByteSize, f.length()); + f.delete(); + } + +} diff --git a/src/test/java/com/jamesmurty/utils/TestXMLBuilder2.java b/src/test/java/com/jamesmurty/utils/TestXMLBuilder2.java new file mode 100644 index 0000000..ed0865a --- /dev/null +++ b/src/test/java/com/jamesmurty/utils/TestXMLBuilder2.java @@ -0,0 +1,15 @@ +package com.jamesmurty.utils; + +public class TestXMLBuilder2 extends BaseXMLBuilderTests { + + @Override + public Class XMLBuilderToTest() throws Exception { + return XMLBuilder2.class; + } + + @Override + protected boolean isRuntimeExceptionsOnly() { + return true; + } + +} diff --git a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java index bc44251..6e6fe9b 100644 --- a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java +++ b/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java @@ -1,593 +1,15 @@ package com.jamesmurty.utils; -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; -import java.io.UnsupportedEncodingException; -import java.util.Properties; +public class TestXMLBuilder extends BaseXMLBuilderTests { -import javax.xml.parsers.FactoryConfigurationError; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.transform.OutputKeys; -import javax.xml.transform.TransformerException; -import javax.xml.xpath.XPathConstants; -import javax.xml.xpath.XPathExpressionException; - -import junit.framework.TestCase; -import net.iharder.Base64; - -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -public class TestXmlBuilder extends TestCase { - - public static final String EXAMPLE_XML_DOC_START = - "" + - "" + - "http://code.google.com/p/java-xmlbuilder/" + - "" + - "" + - "http://jets3t.s3.amazonaws.com/index.html"; - - public static final String EXAMPLE_XML_DOC_END = - "" + - ""; - - public static final String EXAMPLE_XML_DOC = EXAMPLE_XML_DOC_START + EXAMPLE_XML_DOC_END; - - public void testXmlDocumentCreation() throws ParserConfigurationException, - FactoryConfigurationError, TransformerException - { - /* Build XML document in-place */ - XMLBuilder builder = XMLBuilder.create("Projects") - .e("java-xmlbuilder") - .a("language", "Java") - .a("scm","SVN") - .e("Location") - .a("type", "URL") - .t("http://code.google.com/p/java-xmlbuilder/") - .up() - .up() - .e("JetS3t") - .a("language", "Java") - .a("scm","CVS") - .e("Location") - .a("type", "URL") - .t("http://jets3t.s3.amazonaws.com/index.html"); - - /* Set output properties */ - Properties outputProperties = new Properties(); - // Explicitly identify the output as an XML document - outputProperties.put(javax.xml.transform.OutputKeys.METHOD, "xml"); - // Pretty-print the XML output (doesn't work in all cases) - outputProperties.put(javax.xml.transform.OutputKeys.INDENT, "no"); - // Omit the XML declaration, which can differ depending on the test's run context. - outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); - - /* Serialize builder document */ - StringWriter writer = new StringWriter(); - builder.toWriter(writer, outputProperties); - - assertEquals(EXAMPLE_XML_DOC, writer.toString()); - - /* Build XML document in segments*/ - XMLBuilder projectsB = XMLBuilder.create("Projects"); - projectsB.e("java-xmlbuilder") - .a("language", "Java") - .a("scm","SVN") - .e("Location") - .a("type", "URL") - .t("http://code.google.com/p/java-xmlbuilder/"); - XMLBuilder jets3tB = projectsB.e("JetS3t") - .a("language", "Java") - .a("scm","CVS"); - jets3tB.e("Location") - .a("type", "URL") - .t("http://jets3t.s3.amazonaws.com/index.html"); - - assertEquals(builder.asString(), projectsB.asString()); - } - - public void testParseAndXPath() throws ParserConfigurationException, SAXException, - IOException, XPathExpressionException, TransformerException - { - // Parse an existing XML document - XMLBuilder builder = XMLBuilder.parse( - new InputSource(new StringReader(EXAMPLE_XML_DOC))); - assertEquals("Projects", builder.root().getElement().getNodeName()); - assertEquals("Invalid current element", "Projects", builder.getElement().getNodeName()); - - // Find the first Location element - builder = builder.xpathFind("//Location"); - assertEquals("Location", builder.getElement().getNodeName()); - assertEquals("http://code.google.com/p/java-xmlbuilder/", - builder.getElement().getTextContent()); - - // Find JetS3t's Location element - builder = builder.xpathFind("//JetS3t/Location"); - assertEquals("Location", builder.getElement().getNodeName()); - assertEquals("http://jets3t.s3.amazonaws.com/index.html", - builder.getElement().getTextContent()); - - // Find the project with the scm attribute 'CVS' (should be JetS3t) - builder = builder.xpathFind("//*[@scm = 'CVS']"); - assertEquals("JetS3t", builder.getElement().getNodeName()); - - // Try an invalid XPath that does not resolve to an element - try { - builder.xpathFind("//@language"); - fail("Non-Element XPath expression should have failed"); - } catch (XPathExpressionException e) { - assertTrue(e.getMessage().contains("does not resolve to an Element")); - } - - /* Perform full-strength XPath queries that do not have to - * resolve to an Element, and do not return XMLBuilder instances - */ - - // Find the Location value for the JetS3t project - String location = (String) builder.xpathQuery( - "//JetS3t/Location/.", XPathConstants.STRING); - assertEquals("http://jets3t.s3.amazonaws.com/index.html", location); - - // Count the number of projects (count returned as String) - String countAsString = (String) builder.xpathQuery( - "count(/Projects/*)", XPathConstants.STRING); - assertEquals("2", countAsString); - - // Count the number of projects (count returned as "Number" - actually Double) - Number countAsNumber = (Number) builder.xpathQuery( - "count(/Projects/*)", XPathConstants.NUMBER); - assertEquals(2.0, countAsNumber); - - // Find all nodes under Projects - NodeList nodes = (NodeList) builder.xpathQuery( - "/Projects/*", XPathConstants.NODESET); - assertEquals(2, nodes.getLength()); - assertEquals("JetS3t", nodes.item(1).getNodeName()); - - // Returns null if nothing found when a NODE type is requested... - assertNull(builder.xpathQuery("//WrongName", XPathConstants.NODE)); - // ... or an empty String if a STRING type is requested... - assertEquals("", builder.xpathQuery("//WrongName", XPathConstants.STRING)); - // ... or NaN if a NUMBER type is requested... - assertEquals(Double.NaN, builder.xpathQuery("//WrongName", XPathConstants.NUMBER)); - - /* Add a new XML element at a specific XPath location in an existing document */ - - // Use XPath to get a builder at the insert location - XMLBuilder xpathLocB = builder.xpathFind("//JetS3t"); - assertEquals("JetS3t", xpathLocB.getElement().getNodeName()); - - // Append a new element with the location's builder - XMLBuilder location2B = xpathLocB.elem("Location2").attr("type", "Testing"); - assertEquals("Location2", location2B.getElement().getNodeName()); - assertEquals("JetS3t", location2B.up().getElement().getNodeName()); - assertEquals(xpathLocB.getElement(), location2B.up().getElement()); - assertEquals(builder.root(), location2B.root()); - - // Sanity-check the entire resultant XML document - Properties outputProperties = new Properties(); - outputProperties.put(javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); - String xmlAsString = location2B.asString(outputProperties); - - assertFalse(EXAMPLE_XML_DOC.equals(xmlAsString)); - assertTrue(xmlAsString.contains("")); - assertEquals( - EXAMPLE_XML_DOC_START + "" + EXAMPLE_XML_DOC_END, - xmlAsString); - } - - public void testParseAndAmendDocWithWhitespaceNodes() - throws ParserConfigurationException, SAXException, IOException, - XPathExpressionException, TransformerException - { - // Parse example XML document and output with indenting, to add whitespace nodes - Properties outputProperties = new Properties(); - outputProperties.put(OutputKeys.INDENT, "yes"); - outputProperties.put("{http://xml.apache.org/xslt}indent-amount", "2"); - String xmlWithWhitespaceNodes = - XMLBuilder.parse(EXAMPLE_XML_DOC).asString(outputProperties); - - // Re-parse document that now has whitespace nodes - XMLBuilder builder = XMLBuilder.parse(xmlWithWhitespaceNodes); - - // Ensure we can add a node to the document (re issue #17) - builder.xpathFind("//JetS3t") - .elem("AnotherLocation").attr("type", "Testing"); - String xmlWithAmendments = builder.asString(outputProperties); - assertTrue(xmlWithAmendments.contains("")); - } - - public void testStripWhitespaceNodesFromDocument() - throws ParserConfigurationException, SAXException, IOException, - XPathExpressionException, TransformerException - { - // Parse example XML document and output with indenting, to add whitespace nodes - Properties outputProperties = new Properties(); - outputProperties.put(OutputKeys.INDENT, "yes"); - outputProperties.put("{http://xml.apache.org/xslt}indent-amount", "2"); - String xmlWithWhitespaceNodes = - XMLBuilder.parse(EXAMPLE_XML_DOC).asString(outputProperties); - - // Re-parse document that now has whitespace text nodes - XMLBuilder builder = XMLBuilder.parse(xmlWithWhitespaceNodes); - assertTrue(builder.asString().contains("\n")); - assertTrue(builder.asString().contains(" ")); - - // Strip whitespace nodes - builder.stripWhitespaceOnlyTextNodes(); - assertFalse(builder.asString().contains("\n")); - assertFalse(builder.asString().contains(" ")); - } - - public void testSimpleXpath() throws Exception { - String xmlDoc = ""; - XMLBuilder builder = XMLBuilder.parse(xmlDoc); - XMLBuilder builderNode = builder.xpathFind("report_objects"); - assertTrue("report_objects".equals(builderNode.getElement().getNodeName())); - assertTrue("".equals(builderNode.elementAsString())); - } - - /** - * Test for issue #11: https://code.google.com/p/java-xmlbuilder/issues/detail?id=11 - * @throws Exception - */ - public void testAddElementsInLoop() throws Exception { - XMLBuilder builder = XMLBuilder.create("DocRoot"); - XMLBuilder parentBuilder = builder.element("Parent"); - - // Add set of elements to Parent using a loop... - for (int i = 1; i <= 10; i++) { - parentBuilder.elem("IntegerValue" + i).text("" + i); - } - - // ...and confirm element set is within parent after a call to up() - parentBuilder.up(); - - assertEquals("Parent", parentBuilder.getElement().getNodeName()); - assertEquals("DocRoot", builder.getElement().getNodeName()); - assertEquals(1, builder.getElement().getChildNodes().getLength()); - assertEquals("Parent", builder.getElement().getChildNodes().item(0).getNodeName()); - assertEquals(10, parentBuilder.getElement().getChildNodes().getLength()); - assertEquals("IntegerValue1", parentBuilder.getElement().getChildNodes().item(0).getNodeName()); - assertEquals("1", parentBuilder.getElement().getChildNodes().item(0).getTextContent()); - } - - public void testTraversalDuringBuild() throws ParserConfigurationException, SAXException, - IOException, XPathExpressionException, TransformerException - { - XMLBuilder builder = XMLBuilder.create("ElemDepth1") - .e("ElemDepth2") - .e("ElemDepth3") - .e("ElemDepth4"); - assertEquals("ElemDepth3", builder.up().getElement().getNodeName()); - assertEquals("ElemDepth1", builder.up(3).getElement().getNodeName()); - // Traverse too far up the node tree... - assertEquals("ElemDepth1", builder.up(4).getElement().getNodeName()); - // Traverse way too far up the node tree... - assertEquals("ElemDepth1", builder.up(100).getElement().getNodeName()); - } - - public void testImport() throws ParserConfigurationException, - FactoryConfigurationError - { - XMLBuilder importer = XMLBuilder.create("Importer") - .elem("Imported") - .elem("Element") - .elem("Goes").attr("are-we-there-yet", "almost") - .elem("Here"); - XMLBuilder importee = XMLBuilder.create("Importee") - .elem("Importee").attr("awating-my", "new-home") - .elem("IsEntireSubtree") - .elem("Included"); - importer.importXMLBuilder(importee); - - // Ensure we're at the same point in the XML doc - assertEquals("Here", importer.getElement().getNodeName()); - - try { - importer.xpathFind("//Importee"); - importer.xpathFind("//IsEntireSubtree"); - importer.xpathFind("//IsEntireSubtree"); - importer.xpathFind("//Included"); - } catch (XPathExpressionException e) { - fail("XMLBuilder import failed: " + e.getMessage()); - } - - XMLBuilder invalidImporter = XMLBuilder.create("InvalidImporter") - .text("BadBadBad"); - try { - invalidImporter.importXMLBuilder(importee); - fail("Should not be able to import XMLBuilder into " - + "an element containing text nodes"); - } catch (IllegalStateException e) { - // Expected - } + @Override + public Class XMLBuilderToTest() throws Exception { + return XMLBuilder.class; } - public void testCDataNodes() throws ParserConfigurationException, - FactoryConfigurationError, UnsupportedEncodingException, - XPathExpressionException, IOException - { - String text = "Text data -- left as it is"; - String textForBytes = "Byte data is automatically base64-encoded"; - String textEncoded = Base64.encodeBytes(textForBytes.getBytes("UTF-8")); - - XMLBuilder builder = XMLBuilder.create("TestCDataNodes") - .elem("CDataTextElem") - .cdata(text) - .up() - .elem("CDataBytesElem") - .cdata(textForBytes.getBytes("UTF-8")); - - Node cdataTextNode = builder.xpathFind("//CDataTextElem") - .getElement().getChildNodes().item(0); - assertEquals(Node.CDATA_SECTION_NODE, cdataTextNode.getNodeType()); - assertEquals(text, cdataTextNode.getNodeValue()); - - Node cdataBytesNode = builder.xpathFind("//CDataBytesElem") - .getElement().getChildNodes().item(0); - assertEquals(Node.CDATA_SECTION_NODE, cdataBytesNode.getNodeType()); - assertEquals(textEncoded, cdataBytesNode.getNodeValue()); - String base64Decoded = new String(Base64.decode(cdataBytesNode.getNodeValue())); - assertEquals(textForBytes, base64Decoded); - } - - public void testElementAsString() throws ParserConfigurationException, - FactoryConfigurationError, TransformerException, XPathExpressionException - { - XMLBuilder builder = XMLBuilder.create("This") - .elem("Is").elem("My").text("Test"); - // By default, entire XML document is serialized regardless of starting-point - assertEquals("Test", builder.asString()); - assertEquals("Test", builder.xpathFind("//My").asString()); - // Serialize a specific Element and its descendants with elementAsString - assertEquals("Test", builder.xpathFind("//My").elementAsString()); - } - - public void testNamespaces() throws ParserConfigurationException, - FactoryConfigurationError, TransformerException, XPathExpressionException - { - XMLBuilder builder = XMLBuilder - .create("NamespaceTest", "urn:default") - .namespace("prefix1", "urn:ns1") - - .element("NSDefaultImplicit").up() - .element("NSDefaultExplicit", "urn:default").up() - - .element("NS1Explicit", "urn:ns1").up() - .element("prefix1:NS1WithPrefixExplicit", "urn:ns1").up() - .element("prefix1:NS1WithPrefixImplicit").up(); - - // Build a namespace context from the builder's document - NamespaceContextImpl context = builder.buildDocumentNamespaceContext(); - - // All elements in a namespaced document inherit a namespace URI, - // for namespaced document any non-namespaced XPath query will fail. - try { - builder.xpathFind("//:NSDefaultImplicit"); - fail("Namespaced xpath query without context is invalid"); - } catch (XPathExpressionException e) {} - try { - builder.xpathFind("//NSDefaultImplicit", context); - fail("XPath query without prefixes on namespaced docs is invalid"); - } catch (XPathExpressionException e) {} - - // Find nodes with default namespace - builder.xpathFind("/:NamespaceTest", context); - builder.xpathFind("//:NSDefaultImplicit", context); - builder.xpathFind("//:NSDefaultExplicit", context); - - // Must use namespace-aware xpath to find namespaced nodes - try { - builder.xpathFind("//NSDefaultExplicit"); - fail(); - } catch (XPathExpressionException e) {} - try { - builder.xpathFind("//:NSDefaultExplicit"); - fail(); - } catch (XPathExpressionException e) {} - try { - builder.xpathFind("//NSDefaultExplicit", context); - fail(); - } catch (XPathExpressionException e) {} - - // Find node with namespace prefix - builder.xpathFind("//prefix1:NS1Explicit", context); - builder.xpathFind("//prefix1:NS1WithPrefixExplicit", context); - builder.xpathFind("//prefix1:NS1WithPrefixImplicit", context); - - // Find nodes with user-defined prefix "aliases" - context.addNamespace("default-alias", "urn:default"); - context.addNamespace("prefix1-alias", "urn:ns1"); - builder.xpathFind("//default-alias:NSDefaultExplicit", context); - builder.xpathFind("//prefix1-alias:NS1Explicit", context); - - // User can override context mappings, for better or worse - context.addNamespace("", "urn:default"); - builder.xpathFind("//:NSDefaultExplicit", context); - - context.addNamespace("", "urn:wrong"); - try { - builder.xpathFind("//:NSDefaultExplicit", context); - fail(); - } catch (XPathExpressionException e) {} - - // Users are not prevented from creating elements that reference - // an undefined namespace prefix -- user beware - builder.element("undefined-prefix:ElementName"); - } - - public void testElementBefore() throws ParserConfigurationException, - FactoryConfigurationError, TransformerException, XPathExpressionException, - SAXException, IOException - { - XMLBuilder builder = XMLBuilder - .create("TestDocument", "urn:default") - .namespace("custom", "urn:custom") - .elem("Before").up() - .elem("After"); - NamespaceContextImpl context = builder.buildDocumentNamespaceContext(); - - // Ensure XML structure is correct before insert - assertEquals("" - + "", builder.asString()); - - // Insert an element before the "After" element, no explicit namespace (will use default) - XMLBuilder testDoc = XMLBuilder.parse(builder.asString()) - .xpathFind("/:TestDocument/:After", context); - XMLBuilder insertedBuilder = testDoc.elementBefore("Inserted"); - assertEquals("Inserted", insertedBuilder.getElement().getNodeName()); - assertEquals("" - + "", testDoc.asString()); - - // Insert another element, this time with a custom namespace prefix - insertedBuilder = insertedBuilder.elementBefore("custom:InsertedAgain"); - assertEquals("custom:InsertedAgain", insertedBuilder.getElement().getNodeName()); - assertEquals("" - + "", - testDoc.asString()); - - // Insert another element, this time with a custom namespace ref - insertedBuilder = insertedBuilder.elementBefore("InsertedYetAgain", "urn:custom2"); - assertEquals("InsertedYetAgain", insertedBuilder.getElement().getNodeName()); - assertEquals("" - + "" - + "", - testDoc.asString()); - } - - public void testTextNodes() - throws ParserConfigurationException, FactoryConfigurationError, XPathExpressionException - { - XMLBuilder builder = XMLBuilder - .create("TestDocument") - .elem("TextElement") - .text("Initial"); - - XMLBuilder textElementBuilder = builder.xpathFind("//TextElement"); - assertEquals("Initial", textElementBuilder.getElement().getTextContent()); - - // By default, text methods append value to existing text - textElementBuilder.text("Appended"); - assertEquals("InitialAppended", textElementBuilder.getElement().getTextContent()); - - // Use boolean flag to replace text nodes with a new value - textElementBuilder.text("Replacement", true); - assertEquals("Replacement", textElementBuilder.getElement().getTextContent()); - - // Fail-fast if a null text value is provided. - try { - textElementBuilder.text(null); - fail("null text value should cause IllegalArgumentException"); - } catch (IllegalArgumentException ex) { - assertEquals("Illegal null text value", ex.getMessage()); - } - - try { - textElementBuilder.text(null, true); - fail("null text value should cause IllegalArgumentException"); - } catch (IllegalArgumentException ex) { - assertEquals("Illegal null text value", ex.getMessage()); - } - - } - - public void testProcessingInstructionNodes() throws Exception { - // Add instruction to root document element node (usual append-in-node behaviour) - XMLBuilder builder = XMLBuilder - .create("TestDocument").instruction("test", "data"); - assertEquals("", builder.asString()); - - // Add instruction after the root document element (not within it) - builder = XMLBuilder.create("TestDocument3").document().instruction("test", "data"); - assertEquals("", builder.asString().trim()); - - // Insert instruction as first node of the root document - builder = XMLBuilder.create("TestDocument3").insertInstruction("test", "data"); - assertEquals( - "", - // Remove newlines from XML as this differs across platforms - builder.asString().replace("\n", "")); - - // Insert instruction as first node of the root document, second example - builder = XMLBuilder.create("TestDocument4").elem("ChildElem") - .root().insertInstruction("test", "data"); - assertEquals( - "", - // Remove newlines from XML as this differs across platforms - builder.asString().replace("\n", "")); - } - - /** - * Test for strange issue raised by user on comments form where OutputKeys.STANDALONE setting - * in transformer is ignored. - * - * @throws Exception - */ - public void testSetStandaloneToYes() throws Exception { - String xmlDoc = ""; - XMLBuilder builder = XMLBuilder.parse( - new InputSource(new StringReader(xmlDoc))); - - // Basic output settings - Properties outputProperties = new Properties(); - outputProperties.put(javax.xml.transform.OutputKeys.VERSION, "1.0"); - outputProperties.put(javax.xml.transform.OutputKeys.METHOD, "xml"); - outputProperties.put(javax.xml.transform.OutputKeys.ENCODING, "UTF-8"); - - // Use Document@setXmlStandalone(true) to ensure OutputKeys.STANDALONE is respected. - builder.getDocument().setXmlStandalone(true); - outputProperties.put(javax.xml.transform.OutputKeys.STANDALONE, "yes"); - - /* Serialize builder document */ - StringWriter writer = new StringWriter(); - builder.toWriter(writer, outputProperties); - - assertEquals( - "" + xmlDoc, - writer.toString()); - } - - /** - * Test the {@link XMLBuilder#asString(Properties)} method output a document - * of the correct size when the document is moderately large, re issue - * #1 (on GitHub). - * - * @throws Exception - */ - public void testModerateDocumentSizeAsString() throws Exception { - // Create a moderate document around 0.5 MB - long expectedByteSize = 505021; - XMLBuilder builder = XMLBuilder.create("RootNode"); - for (int i = 0; i < 5000; i++) { - builder - .e("TreeRoot") - .e("TreeTrunk") - .e("TreeBranch") - .e("TreeLeaf") - .t("Some Aphids"); - - } - // Omit XML declaration, which will otherwise be included in file - // via #toWriter but not in string via #asString - Properties outputProperties = new Properties(); - outputProperties.put( - javax.xml.transform.OutputKeys.OMIT_XML_DECLARATION, "yes"); - // Ensure XML as string has expected length... - String xmlString = builder.asString(outputProperties); - assertEquals(expectedByteSize, xmlString.length()); - // ...and matches size of XML written to file - File f = File.createTempFile( - "java-xmlbuilder-testmoderatedocumentsizeasstring", ".xml"); - builder.toWriter(new FileWriter(f), outputProperties); - assertEquals(expectedByteSize, f.length()); - f.delete(); + @Override + protected boolean isRuntimeExceptionsOnly() { + return false; } } From 7c131b989254adb89fd436a27f9d6019dc864296 Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 17 Jul 2014 14:42:49 +0100 Subject: [PATCH 13/43] Add unit tests for XMLBuilder2 exception wrapping feature, re #4 --- .../com/jamesmurty/utils/TestXMLBuilder2.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/test/java/com/jamesmurty/utils/TestXMLBuilder2.java b/src/test/java/com/jamesmurty/utils/TestXMLBuilder2.java index ed0865a..2bfb7ee 100644 --- a/src/test/java/com/jamesmurty/utils/TestXMLBuilder2.java +++ b/src/test/java/com/jamesmurty/utils/TestXMLBuilder2.java @@ -1,5 +1,8 @@ package com.jamesmurty.utils; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathExpressionException; + public class TestXMLBuilder2 extends BaseXMLBuilderTests { @Override @@ -12,4 +15,28 @@ protected boolean isRuntimeExceptionsOnly() { return true; } + // NOTE: No checked exceptions for API calls made in this test method + public void testNoCheckedExceptions() { + XMLBuilder2 builder = XMLBuilder2.create("Blah"); + builder = XMLBuilder2.parse(EXAMPLE_XML_DOC); + builder.stripWhitespaceOnlyTextNodes(); + builder.asString(); + builder.elementAsString(); + builder.xpathQuery("/*", XPathConstants.NODESET); + builder.xpathFind("/Projects"); + } + + public void testExceptionWrappedInXMLBuilderRuntimeException() { + XMLBuilder2 builder = XMLBuilder2.parse(EXAMPLE_XML_DOC); + try { + builder.xpathFind("/BadPath"); + fail("Expected XMLBuilderRuntimeException"); + } catch (XMLBuilderRuntimeException e) { + assertEquals(XMLBuilderRuntimeException.class, e.getClass()); + Throwable cause = e.getCause(); + assertEquals(XPathExpressionException.class, cause.getClass()); + assertTrue(cause.getMessage().contains("does not resolve to an Element")); + } + } + } From 886d129f0601ad5f0169057f4d53fb7a46defe79 Mon Sep 17 00:00:00 2001 From: James Murty Date: Mon, 21 Jul 2014 21:47:39 +0100 Subject: [PATCH 14/43] Maven: Perform GPG signing only when explicitly requested, re #3 Improved fix for GPG signing so I can turn it on explicitly when releasing to Maven Central, but it stays turned off for the rest of the time so it doesn't get in the way of everyone else. --- pom.xml | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/pom.xml b/pom.xml index bd3cf7f..7714d43 100644 --- a/pom.xml +++ b/pom.xml @@ -97,23 +97,38 @@
- - - org.apache.maven.plugins - maven-gpg-plugin - 1.4 - - - sign-artifacts - deploy - - sign - - - - + + + + release-sign-artifacts + + + gpgsign + true + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.4 + + + sign-artifacts + verify + + sign + + + + + + + + From 688a279b9672f9d7f980b42862bbf466474a23c1 Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 12:11:59 +0100 Subject: [PATCH 15/43] Hard-code GPG signing argument into POM to sign on release to Maven Central Because there is no sane way to pass command-line arguments through to the "release:perform" process. Christ I hate Maven... --- pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/pom.xml b/pom.xml index 7714d43..c0dc4a5 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,7 @@ v@{project.version} false + -Dgpgsign=true From 16fb39d5e4914e00d3f2feb5e9a141e6b8706847 Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 12:22:18 +0100 Subject: [PATCH 16/43] Made XMLBuilder implementation classes final, re #4 Prevent subclassing of these implementations since that would get weird quickly if someone did so without replacing all of the necessary pieces, and such a person would likely be better off creating a new implementation anyway based on BaseXMLBuilder. --- src/main/java/com/jamesmurty/utils/XMLBuilder.java | 2 +- src/main/java/com/jamesmurty/utils/XMLBuilder2.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder.java b/src/main/java/com/jamesmurty/utils/XMLBuilder.java index 0339e4d..9b1144a 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder.java @@ -60,7 +60,7 @@ * * @author James Murty */ -public class XMLBuilder extends BaseXMLBuilder { +public final class XMLBuilder extends BaseXMLBuilder { /** * Construct a new builder object that wraps the given XML document. diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java index 2a77171..047238d 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java @@ -64,7 +64,7 @@ * * @author James Murty */ -public class XMLBuilder2 extends BaseXMLBuilder { +public final class XMLBuilder2 extends BaseXMLBuilder { /** * Construct a new builder object that wraps the given XML document. From ccdd3601bc761191927f5a4cb71a45be4b38702b Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 12:24:59 +0100 Subject: [PATCH 17/43] Fix case of test class file name, re #4 --- .../jamesmurty/utils/{TestXmlBuilder.java => TestXMLBuilder.java} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/test/java/com/jamesmurty/utils/{TestXmlBuilder.java => TestXMLBuilder.java} (100%) diff --git a/src/test/java/com/jamesmurty/utils/TestXmlBuilder.java b/src/test/java/com/jamesmurty/utils/TestXMLBuilder.java similarity index 100% rename from src/test/java/com/jamesmurty/utils/TestXmlBuilder.java rename to src/test/java/com/jamesmurty/utils/TestXMLBuilder.java From 59365ccac5e1a8ed0f71f0e80eb0dc38eddeff6b Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 13:05:13 +0100 Subject: [PATCH 18/43] Narrow exception wrapping to only specific, known exceptions, re #4 --- .../com/jamesmurty/utils/XMLBuilder2.java | 95 ++++++++++++++++--- 1 file changed, 82 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java index 047238d..61ea8af 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java @@ -20,7 +20,9 @@ package com.jamesmurty.utils; import java.io.File; +import java.io.FileNotFoundException; import java.io.FileReader; +import java.io.IOException; import java.io.StringReader; import java.io.Writer; import java.util.Properties; @@ -29,11 +31,15 @@ import javax.xml.namespace.QName; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import javax.xml.xpath.XPathExpressionException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.xml.sax.InputSource; +import org.xml.sax.SAXException; /** * XML Builder is a utility that creates simple XML documents using relatively @@ -94,6 +100,7 @@ protected XMLBuilder2(Node myNode, Node parentNode) { } private static RuntimeException wrapExceptionAsRuntimeException(Exception e) { + // Don't wrap (or re-wrap) runtime exceptions. if (e instanceof RuntimeException) { return (RuntimeException) e; } else { @@ -113,12 +120,14 @@ private static RuntimeException wrapExceptionAsRuntimeException(Exception e) { * default namespace URI for document, ignored if null or empty. * @return * a builder node that can be used to add more nodes to the XML document. + * @throws XMLBuilderRuntimeException + * to wrap {@link ParserConfigurationException} */ public static XMLBuilder2 create(String name, String namespaceURI) { try { return new XMLBuilder2(createDocumentImpl(name, namespaceURI)); - } catch (Exception e) { + } catch (ParserConfigurationException e) { throw wrapExceptionAsRuntimeException(e); } } @@ -132,6 +141,8 @@ public static XMLBuilder2 create(String name, String namespaceURI) * the name of the document's root element. * @return * a builder node that can be used to add more nodes to the XML document. + * @throws XMLBuilderRuntimeException + * to wrap {@link ParserConfigurationException} */ public static XMLBuilder2 create(String name) { @@ -147,12 +158,19 @@ public static XMLBuilder2 create(String name) * an XML document input source that will be parsed into a DOM. * @return * a builder node that can be used to add more nodes to the XML document. + * @throws XMLBuilderRuntimeException + * to wrap {@link ParserConfigurationException}, {@link SAXException}, + * {@link IOException} */ public static XMLBuilder2 parse(InputSource inputSource) { try { return new XMLBuilder2(parseDocumentImpl(inputSource)); - } catch (Exception e) { + } catch (ParserConfigurationException e) { + throw wrapExceptionAsRuntimeException(e); + } catch (SAXException e) { + throw wrapExceptionAsRuntimeException(e); + } catch (IOException e) { throw wrapExceptionAsRuntimeException(e); } } @@ -181,23 +199,30 @@ public static XMLBuilder2 parse(String xmlString) * an XML document file that will be parsed into a DOM. * @return * a builder node that can be used to add more nodes to the XML document. + * @throws XMLBuilderRuntimeException + * to wrap {@link ParserConfigurationException}, {@link SAXException}, + * {@link IOException}, {@link FileNotFoundException} */ public static XMLBuilder2 parse(File xmlFile) { try { return XMLBuilder2.parse(new InputSource(new FileReader(xmlFile))); - } catch (Exception e) { + } catch (FileNotFoundException e) { throw wrapExceptionAsRuntimeException(e); } } + /** + * @throws XMLBuilderRuntimeException + * to wrap {@link XPathExpressionException} + */ @Override public XMLBuilder2 stripWhitespaceOnlyTextNodes() { try { super.stripWhitespaceOnlyTextNodesImpl(); return this; - } catch (Exception e) { + } catch (XPathExpressionException e) { throw wrapExceptionAsRuntimeException(e); } } @@ -213,13 +238,17 @@ public XMLBuilder2 root() { return new XMLBuilder2(getDocument()); } + /** + * @throws XMLBuilderRuntimeException + * to wrap {@link XPathExpressionException} + */ @Override public XMLBuilder2 xpathFind(String xpath, NamespaceContext nsContext) { try { Node foundNode = super.xpathFindImpl(xpath, nsContext); return new XMLBuilder2(foundNode, null); - } catch (Exception e) { + } catch (XPathExpressionException e) { throw wrapExceptionAsRuntimeException(e); } } @@ -424,78 +453,118 @@ public XMLBuilder2 document() { return new XMLBuilder2(getDocument(), null); } + /** + * @throws XMLBuilderRuntimeException + * to wrap {@link TransformerException} + * + */ @Override public String asString() { try { return super.asString(); - } catch (Exception e) { + } catch (TransformerException e) { throw wrapExceptionAsRuntimeException(e); } } + /** + * @throws XMLBuilderRuntimeException + * to wrap {@link TransformerException} + * + */ @Override public String asString(Properties properties) { try { return super.asString(properties); - } catch (Exception e) { + } catch (TransformerException e) { throw wrapExceptionAsRuntimeException(e); } } + /** + * @throws XMLBuilderRuntimeException + * to wrap {@link TransformerException} + * + */ @Override public String elementAsString() { try { return super.elementAsString(); - } catch (Exception e) { + } catch (TransformerException e) { throw wrapExceptionAsRuntimeException(e); } } + /** + * @throws XMLBuilderRuntimeException + * to wrap {@link TransformerException} + * + */ @Override public String elementAsString(Properties outputProperties) { try { return super.elementAsString(outputProperties); - } catch (Exception e) { + } catch (TransformerException e) { throw wrapExceptionAsRuntimeException(e); } } + /** + * @throws XMLBuilderRuntimeException + * to wrap {@link TransformerException} + * + */ @Override public void toWriter(boolean wholeDocument, Writer writer, Properties outputProperties) { try { super.toWriter(wholeDocument, writer, outputProperties); - } catch (Exception e) { + } catch (TransformerException e) { throw wrapExceptionAsRuntimeException(e); } } + /** + * @throws XMLBuilderRuntimeException + * to wrap {@link TransformerException} + * + */ @Override public void toWriter(Writer writer, Properties outputProperties) { try { super.toWriter(writer, outputProperties); - } catch (Exception e) { + } catch (TransformerException e) { throw wrapExceptionAsRuntimeException(e); } } + /** + * @throws XMLBuilderRuntimeException + * to wrap {@link XPathExpressionException} + * + */ @Override public Object xpathQuery(String xpath, QName type, NamespaceContext nsContext) { try { return super.xpathQuery(xpath, type, nsContext); - } catch (Exception e) { + } catch (XPathExpressionException e) { throw wrapExceptionAsRuntimeException(e); } } + /** + * @throws XMLBuilderRuntimeException + * to wrap {@link XPathExpressionException} + * + */ @Override public Object xpathQuery(String xpath, QName type) { try { return super.xpathQuery(xpath, type); - } catch (Exception e) { + } catch (XPathExpressionException e) { throw wrapExceptionAsRuntimeException(e); } } From b0f531df961a74bf4adfa8f9a50e3c2191307947 Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 13:06:17 +0100 Subject: [PATCH 19/43] Fix code and javadoc warnings, re #4 --- .../com/jamesmurty/utils/XMLBuilderRuntimeException.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilderRuntimeException.java b/src/main/java/com/jamesmurty/utils/XMLBuilderRuntimeException.java index fd4df22..259d505 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilderRuntimeException.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilderRuntimeException.java @@ -9,6 +9,12 @@ */ public class XMLBuilderRuntimeException extends RuntimeException { + private static final long serialVersionUID = -635323496745601589L; + + /** + * @param exception + * cause exception to be wrapped + */ public XMLBuilderRuntimeException(Exception exception) { super(exception); } From 37aab4c350c09494232d82e5ce9b24066b41d917 Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 13:13:22 +0100 Subject: [PATCH 20/43] Add documentation for new XMLBuilder2 class and API, re #4 --- CHANGES.md | 21 +++++++++++++++++++++ README.md | 19 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 7b6a569..dfa7694 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,27 @@ Release Notes for java-xmlbuilder ================================= +Version 1.1 - 22 July 2014 +-------------------------- + +Added a new `XMLBuilder2` implementation that avoids checked exceptions in the +API and throws runtime exceptions instead. This should make the library much +more pleasant to use, and your code much cleaner, in situations where low-level +exceptions are unlikely -- which is probably most situations where you would +use this library. + +For example when creating a new document with the `#create` method, instead of +needing to explicitly catch the unlikely `ParserConfigurationException`, if you +use `XMLBuilder2` this exception automatically gets wrapped in the new +`XMLBuilderRuntimeException` class and can be left to propagate out. + +Aside from the removal of checked exceptions, `XMLBuilder2` has the same API as +the original `XMLBuilder` and should therefore be a drop-in replacement in +existing code. + +For further discussion and rationale see: +https://github.com/jmurty/java-xmlbuilder/issues/4 + Version 1.0 - 6 March 2014 -------------------------- diff --git a/README.md b/README.md index 4c51a1a..1d651c3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,25 @@ further if you have special requirements. [Apache License Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) +XMLBuilder versus XMLBuilder2 +----------------------------- + +Since version 1.1 this library provides two builder implementations and APIs: + + * `XMLBuilder` – the original API – follows standard Java practice of + re-throwing lower level checked exceptions when you do things like create a + new document. + You must explicitly `catch` these checked exceptions in your codebase, even + though they are unlikely to occur in tested code. + * `XMLBuilder2` is a newer API that removes checked exceptions altogether, and + will instead wrap and propagate lower level exceptions in an unchecked + `XMLBuilderRuntimeException`. + Use this class if you don't like the code mess or overhead of try/catching + many low-level exceptions that are unlikely to occur in practice. + +Both these versions work identically apart from the handling of errors, so you +can use whichever version you prefer or "upgrade" from one to the other in +existing code. Quick Example ------------- From 1b97a615d1623d8b219ed6bc69d97b698c690e56 Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 13:15:44 +0100 Subject: [PATCH 21/43] Use XMLBuilder2 as first example in readme, re #4 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1d651c3..7998295 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Easily build XML documents using code structured like the final document. This code: ```java -XMLBuilder builder = XMLBuilder.create("Projects") +XMLBuilder2 builder = XMLBuilder2.create("Projects") .e("java-xmlbuilder").a("language", "Java").a("scm","SVN") .e("Location").a("type", "URL") .t("http://code.google.com/p/java-xmlbuilder/") @@ -73,7 +73,7 @@ Produces this XML document: Getting Started --------------- -See further example usage below and the +See further example usage below and in the [JavaDoc documentation](http://s3.jamesmurty.com/java-xmlbuilder/index.html). Download a Jar file containing the latest version From 75a5608d8c97d22ebfeb5ca0fe4a89117bd83ca3 Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 13:20:00 +0100 Subject: [PATCH 22/43] [maven-release-plugin] prepare release v1.1 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c0dc4a5..7665264 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ java-xmlbuilder jar - 1.1-SNAPSHOT + 1.1 java-xmlbuilder XML Builder is a utility that creates simple XML documents using relatively sparse Java code https://github.com/jmurty/java-xmlbuilder @@ -21,7 +21,7 @@ scm:git:github.com/jmurty/java-xmlbuilder.git scm:git:git@github.com:jmurty/java-xmlbuilder.git https://github.com/jmurty/java-xmlbuilder - HEAD + v1.1 From 92645b50492d41ca6ee11a1be9a846a23b0e204e Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 13:20:00 +0100 Subject: [PATCH 23/43] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 7665264..5a84930 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ java-xmlbuilder jar - 1.1 + 1.2-SNAPSHOT java-xmlbuilder XML Builder is a utility that creates simple XML documents using relatively sparse Java code https://github.com/jmurty/java-xmlbuilder @@ -21,7 +21,7 @@ scm:git:github.com/jmurty/java-xmlbuilder.git scm:git:git@github.com:jmurty/java-xmlbuilder.git https://github.com/jmurty/java-xmlbuilder - v1.1 + HEAD From 5d5caa583b14ade1d6713b8e73b58679de50361f Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 13:25:28 +0100 Subject: [PATCH 24/43] Update latest version in README following release of 1.1 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7998295..0740c1c 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ See further example usage below and in the [JavaDoc documentation](http://s3.jamesmurty.com/java-xmlbuilder/index.html). Download a Jar file containing the latest version -[java-xmlbuilder-1.0.jar](http://s3.jamesmurty.com/java-xmlbuilder/java-xmlbuilder-1.0.jar). +[java-xmlbuilder-1.1.jar](http://s3.jamesmurty.com/java-xmlbuilder/java-xmlbuilder-1.1.jar). Maven users can add this project as a dependency with the following additions to a POM.xml file: @@ -88,7 +88,7 @@ to a POM.xml file: com.jamesmurty.utils java-xmlbuilder - 1.0 + 1.1 . . . From e6fddca201790abab4f2c274341c0bb8835c3e73 Mon Sep 17 00:00:00 2001 From: James Murty Date: Tue, 22 Jul 2014 21:47:48 +0100 Subject: [PATCH 25/43] Disable external entities by default to prevent XXE injection attacks, re #6 XML Builder classes now explicitly enable or disable 'external-general-entities' and 'external-parameter-entities' features of the DocumentBuilderFactory when #create or #parse methods are used. To prevent XML External Entity (XXE) injection attacks, these features are disabled by default. They can only be enabled by passing a true boolean value to new versions of the #create and #parse methods that accept a flag for this feature. --- CHANGES.md | 11 ++ .../com/jamesmurty/utils/BaseXMLBuilder.java | 87 +++++++++++- .../java/com/jamesmurty/utils/XMLBuilder.java | 133 +++++++++++++++++- .../com/jamesmurty/utils/XMLBuilder2.java | 125 ++++++++++++++-- .../jamesmurty/utils/BaseXMLBuilderTests.java | 34 +++++ .../java/com/jamesmurty/utils/external.txt | 1 + 6 files changed, 376 insertions(+), 15 deletions(-) create mode 100644 src/test/java/com/jamesmurty/utils/external.txt diff --git a/CHANGES.md b/CHANGES.md index dfa7694..5154782 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,17 @@ Release Notes for java-xmlbuilder ================================= +Version 1.2 - Pending +--------------------- + +Fixes: + +* Prevent XML External Entity (XXE) injection attacks by disabling parsing of + general and parameter external entities by default (#6). External entities + are now only parsed if this feature is explicitly enabled by passing a boolean + flag value to the #create and #parse methods. + WARNING: This will break code that expects external entities to be parsed. + Version 1.1 - 22 July 2014 -------------------------- diff --git a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java index da71b9f..90e26d5 100644 --- a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java @@ -76,6 +76,13 @@ public abstract class BaseXMLBuilder { private static boolean isNamespaceAware = true; + /** + * If true, the builder will raise an {@link XMLBuilderRuntimeException} + * if external general and parameter entities cannot be explicitly enabled + * or disabled. + */ + public static boolean failIfExternalEntityParsingCannotBeConfigured = true; + /** * Construct a new builder object that wraps the given XML document. * This constructor is for internal use only. @@ -112,6 +119,78 @@ protected BaseXMLBuilder(Node myNode, Node parentNode) { } } + /** + * Explicitly enable or disable the 'external-general-entities' and + * 'external-parameter-entities' features of the underlying + * DocumentBuilderFactory. + * + * TODO This is a naive approach that simply tries to apply all known + * feature name/URL values in turn until one succeeds, or none do. + * + * @param factory + * factory which will have external general and parameter entities enabled + * or disabled. + * @param enableExternalEntities + * if true external entities will be explicitly enabled, otherwise they + * will be explicitly disabled. + */ + protected static void enableOrDisableExternalEntityParsing( + DocumentBuilderFactory factory, boolean enableExternalEntities) + { + // Feature list drawn from: + // https://www.owasp.org/index.php/XML_External_Entity_(XXE)_Processing + + /* Enable or disable external general entities */ + String[] externalGeneralEntitiesFeatures = { + // General + "http://xml.org/sax/features/external-general-entities", + // Xerces 1 + "http://xerces.apache.org/xerces-j/features.html#external-general-entities", + // Xerces 2 + "http://xerces.apache.org/xerces2-j/features.html#external-general-entities", + }; + boolean success = false; + for (String feature: externalGeneralEntitiesFeatures) { + try { + factory.setFeature(feature, enableExternalEntities); + success = true; + break; + } catch (ParserConfigurationException e) { + } + } + if (!success && failIfExternalEntityParsingCannotBeConfigured) { + throw new XMLBuilderRuntimeException( + new ParserConfigurationException( + "Failed to set 'external-general-entities' feature to " + + enableExternalEntities)); + } + + /* Enable or disable external parameter entities */ + String[] externalParameterEntitiesFeatures = { + // General + "http://xml.org/sax/features/external-parameter-entities", + // Xerces 1 + "http://xerces.apache.org/xerces-j/features.html#external-parameter-entities", + // Xerces 2 + "http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities", + }; + success = false; + for (String feature: externalParameterEntitiesFeatures) { + try { + factory.setFeature(feature, enableExternalEntities); + success = true; + break; + } catch (ParserConfigurationException e) { + } + } + if (!success && failIfExternalEntityParsingCannotBeConfigured) { + throw new XMLBuilderRuntimeException( + new ParserConfigurationException( + "Failed to set 'external-parameter-entities' feature to " + + enableExternalEntities)); + } + } + /** * Construct an XML Document with a default namespace with the given * root element. @@ -126,11 +205,13 @@ protected BaseXMLBuilder(Node myNode, Node parentNode) { * @throws FactoryConfigurationError * @throws ParserConfigurationException */ - protected static Document createDocumentImpl(String name, String namespaceURI) + protected static Document createDocumentImpl( + String name, String namespaceURI, boolean enableExternalEntities) throws ParserConfigurationException, FactoryConfigurationError { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(isNamespaceAware); + enableOrDisableExternalEntityParsing(factory, enableExternalEntities); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.newDocument(); Element rootElement = null; @@ -157,11 +238,13 @@ protected static Document createDocumentImpl(String name, String namespaceURI) * @throws IOException * @throws SAXException */ - protected static Document parseDocumentImpl(InputSource inputSource) + protected static Document parseDocumentImpl( + InputSource inputSource, boolean enableExternalEntities) throws ParserConfigurationException, SAXException, IOException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(isNamespaceAware); + enableOrDisableExternalEntityParsing(factory, enableExternalEntities); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(inputSource); return document; diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder.java b/src/main/java/com/jamesmurty/utils/XMLBuilder.java index 9b1144a..5663b44 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder.java @@ -89,6 +89,53 @@ protected XMLBuilder(Node myNode, Node parentNode) { super(myNode, parentNode); } + /** + * Construct a builder for new XML document with a default namespace. + * The document will be created with the given root element, and the builder + * returned by this method will serve as the starting-point for any further + * document additions. + * + * @param name + * the name of the document's root element. + * @param namespaceURI + * default namespace URI for document, ignored if null or empty. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. + * @return + * a builder node that can be used to add more nodes to the XML document. + * + * @throws FactoryConfigurationError + * @throws ParserConfigurationException + */ + public static XMLBuilder create(String name, String namespaceURI, + boolean enableExternalEntities) + throws ParserConfigurationException, FactoryConfigurationError + { + return new XMLBuilder( + createDocumentImpl(name, namespaceURI, enableExternalEntities)); + } + + /** + * Construct a builder for new XML document. The document will be created + * with the given root element, and the builder returned by this method + * will serve as the starting-point for any further document additions. + * + * @param name + * the name of the document's root element. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. + * @return + * a builder node that can be used to add more nodes to the XML document. + * + * @throws FactoryConfigurationError + * @throws ParserConfigurationException + */ + public static XMLBuilder create(String name, boolean enableExternalEntities) + throws ParserConfigurationException, FactoryConfigurationError + { + return create(name, null, enableExternalEntities); + } + /** * Construct a builder for new XML document with a default namespace. * The document will be created with the given root element, and the builder @@ -108,7 +155,7 @@ protected XMLBuilder(Node myNode, Node parentNode) { public static XMLBuilder create(String name, String namespaceURI) throws ParserConfigurationException, FactoryConfigurationError { - return new XMLBuilder(createDocumentImpl(name, namespaceURI)); + return create(name, namespaceURI, false); } /** @@ -130,6 +177,84 @@ public static XMLBuilder create(String name) return create(name, null); } + /** + * Construct a builder from an existing XML document. The provided XML + * document will be parsed and an XMLBuilder object referencing the + * document's root element will be returned. + * + * @param inputSource + * an XML document input source that will be parsed into a DOM. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. + * @return + * a builder node that can be used to add more nodes to the XML document. + * @throws ParserConfigurationException + * + * @throws FactoryConfigurationError + * @throws ParserConfigurationException + * @throws IOException + * @throws SAXException + */ + public static XMLBuilder parse( + InputSource inputSource, boolean enableExternalEntities) + throws ParserConfigurationException, SAXException, IOException + { + return new XMLBuilder( + parseDocumentImpl(inputSource, enableExternalEntities)); + } + + /** + * Construct a builder from an existing XML document string. + * The provided XML document will be parsed and an XMLBuilder + * object referencing the document's root element will be returned. + * + * @param xmlString + * an XML document string that will be parsed into a DOM. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. + * @return + * a builder node that can be used to add more nodes to the XML document. + * + * @throws ParserConfigurationException + * @throws FactoryConfigurationError + * @throws ParserConfigurationException + * @throws IOException + * @throws SAXException + */ + public static XMLBuilder parse( + String xmlString, boolean enableExternalEntities) + throws ParserConfigurationException, SAXException, IOException + { + return XMLBuilder.parse( + new InputSource(new StringReader(xmlString)), + enableExternalEntities); + } + + /** + * Construct a builder from an existing XML document file. + * The provided XML document will be parsed and an XMLBuilder + * object referencing the document's root element will be returned. + * + * @param xmlFile + * an XML document file that will be parsed into a DOM. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. + * @return + * a builder node that can be used to add more nodes to the XML document. + * + * @throws ParserConfigurationException + * @throws FactoryConfigurationError + * @throws ParserConfigurationException + * @throws IOException + * @throws SAXException + */ + public static XMLBuilder parse(File xmlFile, boolean enableExternalEntities) + throws ParserConfigurationException, SAXException, IOException + { + return XMLBuilder.parse( + new InputSource(new FileReader(xmlFile)), enableExternalEntities); + } + /** * Construct a builder from an existing XML document. The provided XML * document will be parsed and an XMLBuilder object referencing the @@ -149,7 +274,7 @@ public static XMLBuilder create(String name) public static XMLBuilder parse(InputSource inputSource) throws ParserConfigurationException, SAXException, IOException { - return new XMLBuilder(parseDocumentImpl(inputSource)); + return XMLBuilder.parse(inputSource, false); } /** @@ -171,7 +296,7 @@ public static XMLBuilder parse(InputSource inputSource) public static XMLBuilder parse(String xmlString) throws ParserConfigurationException, SAXException, IOException { - return XMLBuilder.parse(new InputSource(new StringReader(xmlString))); + return XMLBuilder.parse(xmlString, false); } /** @@ -193,7 +318,7 @@ public static XMLBuilder parse(String xmlString) public static XMLBuilder parse(File xmlFile) throws ParserConfigurationException, SAXException, IOException { - return XMLBuilder.parse(new InputSource(new FileReader(xmlFile))); + return XMLBuilder.parse(xmlFile, false); } @Override diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java index 61ea8af..013ad7c 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java @@ -118,20 +118,63 @@ private static RuntimeException wrapExceptionAsRuntimeException(Exception e) { * the name of the document's root element. * @param namespaceURI * default namespace URI for document, ignored if null or empty. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. * @return * a builder node that can be used to add more nodes to the XML document. * @throws XMLBuilderRuntimeException * to wrap {@link ParserConfigurationException} */ - public static XMLBuilder2 create(String name, String namespaceURI) + public static XMLBuilder2 create( + String name, String namespaceURI, boolean enableExternalEntities) { try { - return new XMLBuilder2(createDocumentImpl(name, namespaceURI)); + return new XMLBuilder2( + createDocumentImpl(name, namespaceURI, enableExternalEntities)); } catch (ParserConfigurationException e) { throw wrapExceptionAsRuntimeException(e); } } + /** + * Construct a builder for new XML document. The document will be created + * with the given root element, and the builder returned by this method + * will serve as the starting-point for any further document additions. + * + * @param name + * the name of the document's root element. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. + * @return + * a builder node that can be used to add more nodes to the XML document. + * @throws XMLBuilderRuntimeException + * to wrap {@link ParserConfigurationException} + */ + public static XMLBuilder2 create(String name, boolean enableExternalEntities) + { + return XMLBuilder2.create(name, null, enableExternalEntities); + } + + /** + * Construct a builder for new XML document with a default namespace. + * The document will be created with the given root element, and the builder + * returned by this method will serve as the starting-point for any further + * document additions. + * + * @param name + * the name of the document's root element. + * @param namespaceURI + * default namespace URI for document, ignored if null or empty. + * @return + * a builder node that can be used to add more nodes to the XML document. + * @throws XMLBuilderRuntimeException + * to wrap {@link ParserConfigurationException} + */ + public static XMLBuilder2 create(String name, String namespaceURI) + { + return XMLBuilder2.create(name, namespaceURI, false); + } + /** * Construct a builder for new XML document. The document will be created * with the given root element, and the builder returned by this method @@ -146,7 +189,7 @@ public static XMLBuilder2 create(String name, String namespaceURI) */ public static XMLBuilder2 create(String name) { - return create(name, null); + return XMLBuilder2.create(name, null, false); } /** @@ -156,16 +199,20 @@ public static XMLBuilder2 create(String name) * * @param inputSource * an XML document input source that will be parsed into a DOM. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. * @return * a builder node that can be used to add more nodes to the XML document. * @throws XMLBuilderRuntimeException * to wrap {@link ParserConfigurationException}, {@link SAXException}, * {@link IOException} */ - public static XMLBuilder2 parse(InputSource inputSource) + public static XMLBuilder2 parse( + InputSource inputSource, boolean enableExternalEntities) { try { - return new XMLBuilder2(parseDocumentImpl(inputSource)); + return new XMLBuilder2( + parseDocumentImpl(inputSource, enableExternalEntities)); } catch (ParserConfigurationException e) { throw wrapExceptionAsRuntimeException(e); } catch (SAXException e) { @@ -182,12 +229,17 @@ public static XMLBuilder2 parse(InputSource inputSource) * * @param xmlString * an XML document string that will be parsed into a DOM. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. * @return * a builder node that can be used to add more nodes to the XML document. */ - public static XMLBuilder2 parse(String xmlString) + public static XMLBuilder2 parse( + String xmlString, boolean enableExternalEntities) { - return XMLBuilder2.parse(new InputSource(new StringReader(xmlString))); + return XMLBuilder2.parse( + new InputSource(new StringReader(xmlString)), + enableExternalEntities); } /** @@ -197,21 +249,76 @@ public static XMLBuilder2 parse(String xmlString) * * @param xmlFile * an XML document file that will be parsed into a DOM. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. * @return * a builder node that can be used to add more nodes to the XML document. * @throws XMLBuilderRuntimeException * to wrap {@link ParserConfigurationException}, {@link SAXException}, * {@link IOException}, {@link FileNotFoundException} */ - public static XMLBuilder2 parse(File xmlFile) + public static XMLBuilder2 parse(File xmlFile, boolean enableExternalEntities) { try { - return XMLBuilder2.parse(new InputSource(new FileReader(xmlFile))); + return XMLBuilder2.parse( + new InputSource(new FileReader(xmlFile)), + enableExternalEntities); } catch (FileNotFoundException e) { throw wrapExceptionAsRuntimeException(e); } } + /** + * Construct a builder from an existing XML document. The provided XML + * document will be parsed and an XMLBuilder2 object referencing the + * document's root element will be returned. + * + * @param inputSource + * an XML document input source that will be parsed into a DOM. + * @return + * a builder node that can be used to add more nodes to the XML document. + * @throws XMLBuilderRuntimeException + * to wrap {@link ParserConfigurationException}, {@link SAXException}, + * {@link IOException} + */ + public static XMLBuilder2 parse(InputSource inputSource) + { + return XMLBuilder2.parse(inputSource, false); + } + + /** + * Construct a builder from an existing XML document string. + * The provided XML document will be parsed and an XMLBuilder2 + * object referencing the document's root element will be returned. + * + * @param xmlString + * an XML document string that will be parsed into a DOM. + * @return + * a builder node that can be used to add more nodes to the XML document. + */ + public static XMLBuilder2 parse(String xmlString) + { + return XMLBuilder2.parse(xmlString, false); + } + + /** + * Construct a builder from an existing XML document file. + * The provided XML document will be parsed and an XMLBuilder2 + * object referencing the document's root element will be returned. + * + * @param xmlFile + * an XML document file that will be parsed into a DOM. + * @return + * a builder node that can be used to add more nodes to the XML document. + * @throws XMLBuilderRuntimeException + * to wrap {@link ParserConfigurationException}, {@link SAXException}, + * {@link IOException}, {@link FileNotFoundException} + */ + public static XMLBuilder2 parse(File xmlFile) + { + return XMLBuilder2.parse(xmlFile, false); + } + /** * @throws XMLBuilderRuntimeException * to wrap {@link XPathExpressionException} diff --git a/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java b/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java index e488307..54959fc 100644 --- a/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java +++ b/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java @@ -6,6 +6,7 @@ import java.io.StringWriter; import java.util.Properties; +import javax.annotation.Resource; import javax.xml.transform.OutputKeys; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; @@ -52,6 +53,14 @@ protected BaseXMLBuilder XMLBuilder_parse(InputSource source) throws Exception { "parse", InputSource.class).invoke(null, source); } + protected BaseXMLBuilder XMLBuilder_parse( + String documentString, boolean enableExternalEntities) throws Exception + { + return (BaseXMLBuilder) XMLBuilderToTest().getMethod( + "parse", String.class, boolean.class).invoke( + null, documentString, enableExternalEntities); + } + protected BaseXMLBuilder XMLBuilder_parse(String documentString) throws Exception { return (BaseXMLBuilder) XMLBuilderToTest().getMethod( "parse", String.class).invoke(null, documentString); @@ -619,4 +628,29 @@ public void testModerateDocumentSizeAsString() throws Exception { f.delete(); } + /** + * Ensure XML Builder parse methods use a default configuration that + * prevents XML External Entity (XXE) injection attacks. + * + * @throws Exception + */ + public void testXMLBuilderParserImmuneToXXEAttackByDefault() throws Exception { + String externalFilePath = "src/test/java/com/jamesmurty/utils/external.txt"; + File externalFile = new File(externalFilePath); + String XML_DOC_WITH_XXE = + "" + + "" + + " ]>" + + EXAMPLE_XML_DOC_START + "&xx1;" + EXAMPLE_XML_DOC_END; + // By default, builder is immune from XXE injection + BaseXMLBuilder builder = XMLBuilder_parse(XML_DOC_WITH_XXE); + String parsedXml = builder.asString(); + assertFalse(parsedXml.indexOf("Injected XXE Data") >= 0); + // If you enable external entity processing, builder becomes subject to XXE injection + builder = XMLBuilder_parse(XML_DOC_WITH_XXE, true); + parsedXml = builder.asString(); + assertTrue(parsedXml.indexOf("Injected XXE Data") >= 0); + } + } diff --git a/src/test/java/com/jamesmurty/utils/external.txt b/src/test/java/com/jamesmurty/utils/external.txt new file mode 100644 index 0000000..c067079 --- /dev/null +++ b/src/test/java/com/jamesmurty/utils/external.txt @@ -0,0 +1 @@ +Injected XXE Data \ No newline at end of file From f1aeb78020d09b490e4e15dae697a7a8e148c330 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roland=20Hu=C3=9F?= Date: Mon, 30 May 2016 21:42:50 +0200 Subject: [PATCH 26/43] Fixed wrong delegation for ns() --- src/main/java/com/jamesmurty/utils/XMLBuilder2.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java index 013ad7c..7d6e5d7 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java @@ -526,7 +526,7 @@ public XMLBuilder2 namespace(String prefix, String namespaceURI) { @Override public XMLBuilder2 ns(String prefix, String namespaceURI) { - return attribute(prefix, namespaceURI); + return namespace(prefix, namespaceURI); } @Override From 5630c494f922a3007d8b8ec5b9bd923fe571a731 Mon Sep 17 00:00:00 2001 From: James Murty Date: Fri, 1 Sep 2017 22:36:42 +1000 Subject: [PATCH 27/43] Permit disabling of namespace awareness Permit users to disable namespace-awareness in the underlying `DocumentBuilderFactory` when constructing the builder with extended `create()` and `parse()` methods added in this commit. Namespace awareness is enabled by default unless you use the more explicit versions of these methods that take additional `enableExternalEntities` and `isNamespaceAware` parameters. --- CHANGES.md | 8 +++ .../com/jamesmurty/utils/BaseXMLBuilder.java | 20 +++++-- .../java/com/jamesmurty/utils/XMLBuilder.java | 55 +++++++++++++----- .../com/jamesmurty/utils/XMLBuilder2.java | 56 +++++++++++++------ .../jamesmurty/utils/BaseXMLBuilderTests.java | 47 ++++++++++++++-- 5 files changed, 146 insertions(+), 40 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 5154782..12b586b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,14 @@ Fixes: are now only parsed if this feature is explicitly enabled by passing a boolean flag value to the #create and #parse methods. WARNING: This will break code that expects external entities to be parsed. + +Enhancements: + +* Permit users to disable namespace-awareness in the underlying + DocumentBuilderFactory when constructing the builder with extended `create()` + and `parse()` methods. Namespace awareness is enabled by default unless you + use the more explicit versions of these methods that take additional + `enableExternalEntities` and `isNamespaceAware` parameters. Version 1.1 - 22 July 2014 -------------------------- diff --git a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java index 90e26d5..c9466f7 100644 --- a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2014 James Murty (www.jamesmurty.com) + * Copyright 2008-2017 James Murty (www.jamesmurty.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -74,8 +74,6 @@ public abstract class BaseXMLBuilder { */ private Node xmlNode = null; - private static boolean isNamespaceAware = true; - /** * If true, the builder will raise an {@link XMLBuilderRuntimeException} * if external general and parameter entities cannot be explicitly enabled @@ -199,6 +197,11 @@ protected static void enableOrDisableExternalEntityParsing( * the name of the document's root element. * @param namespaceURI * default namespace URI for document, ignored if null or empty. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * an XML Document. * @@ -206,7 +209,8 @@ protected static void enableOrDisableExternalEntityParsing( * @throws ParserConfigurationException */ protected static Document createDocumentImpl( - String name, String namespaceURI, boolean enableExternalEntities) + String name, String namespaceURI, boolean enableExternalEntities, + boolean isNamespaceAware) throws ParserConfigurationException, FactoryConfigurationError { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); @@ -229,6 +233,11 @@ protected static Document createDocumentImpl( * * @param inputSource * an XML document input source that will be parsed into a DOM. + * @param enableExternalEntities + * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. * @throws ParserConfigurationException @@ -239,7 +248,8 @@ protected static Document createDocumentImpl( * @throws SAXException */ protected static Document parseDocumentImpl( - InputSource inputSource, boolean enableExternalEntities) + InputSource inputSource, boolean enableExternalEntities, + boolean isNamespaceAware) throws ParserConfigurationException, SAXException, IOException { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder.java b/src/main/java/com/jamesmurty/utils/XMLBuilder.java index 5663b44..77c50b0 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2014 James Murty (www.jamesmurty.com) + * Copyright 2008-2017 James Murty (www.jamesmurty.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -101,6 +101,9 @@ protected XMLBuilder(Node myNode, Node parentNode) { * default namespace URI for document, ignored if null or empty. * @param enableExternalEntities * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. * @@ -108,11 +111,12 @@ protected XMLBuilder(Node myNode, Node parentNode) { * @throws ParserConfigurationException */ public static XMLBuilder create(String name, String namespaceURI, - boolean enableExternalEntities) + boolean enableExternalEntities, boolean isNamespaceAware) throws ParserConfigurationException, FactoryConfigurationError { return new XMLBuilder( - createDocumentImpl(name, namespaceURI, enableExternalEntities)); + createDocumentImpl( + name, namespaceURI, enableExternalEntities, isNamespaceAware)); } /** @@ -124,16 +128,20 @@ public static XMLBuilder create(String name, String namespaceURI, * the name of the document's root element. * @param enableExternalEntities * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. * * @throws FactoryConfigurationError * @throws ParserConfigurationException */ - public static XMLBuilder create(String name, boolean enableExternalEntities) + public static XMLBuilder create(String name, boolean enableExternalEntities, + boolean isNamespaceAware) throws ParserConfigurationException, FactoryConfigurationError { - return create(name, null, enableExternalEntities); + return create(name, null, enableExternalEntities, isNamespaceAware); } /** @@ -146,6 +154,7 @@ public static XMLBuilder create(String name, boolean enableExternalEntities) * the name of the document's root element. * @param namespaceURI * default namespace URI for document, ignored if null or empty. + * @return * a builder node that can be used to add more nodes to the XML document. * @@ -155,7 +164,7 @@ public static XMLBuilder create(String name, boolean enableExternalEntities) public static XMLBuilder create(String name, String namespaceURI) throws ParserConfigurationException, FactoryConfigurationError { - return create(name, namespaceURI, false); + return create(name, namespaceURI, false, true); } /** @@ -186,6 +195,9 @@ public static XMLBuilder create(String name) * an XML document input source that will be parsed into a DOM. * @param enableExternalEntities * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. * @throws ParserConfigurationException @@ -196,11 +208,13 @@ public static XMLBuilder create(String name) * @throws SAXException */ public static XMLBuilder parse( - InputSource inputSource, boolean enableExternalEntities) + InputSource inputSource, boolean enableExternalEntities, + boolean isNamespaceAware) throws ParserConfigurationException, SAXException, IOException { return new XMLBuilder( - parseDocumentImpl(inputSource, enableExternalEntities)); + parseDocumentImpl( + inputSource, enableExternalEntities, isNamespaceAware)); } /** @@ -212,6 +226,9 @@ public static XMLBuilder parse( * an XML document string that will be parsed into a DOM. * @param enableExternalEntities * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. * @@ -222,12 +239,14 @@ public static XMLBuilder parse( * @throws SAXException */ public static XMLBuilder parse( - String xmlString, boolean enableExternalEntities) + String xmlString, boolean enableExternalEntities, + boolean isNamespaceAware) throws ParserConfigurationException, SAXException, IOException { return XMLBuilder.parse( new InputSource(new StringReader(xmlString)), - enableExternalEntities); + enableExternalEntities, + isNamespaceAware); } /** @@ -239,6 +258,9 @@ public static XMLBuilder parse( * an XML document file that will be parsed into a DOM. * @param enableExternalEntities * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. * @@ -248,11 +270,14 @@ public static XMLBuilder parse( * @throws IOException * @throws SAXException */ - public static XMLBuilder parse(File xmlFile, boolean enableExternalEntities) + public static XMLBuilder parse(File xmlFile, boolean enableExternalEntities, + boolean isNamespaceAware) throws ParserConfigurationException, SAXException, IOException { return XMLBuilder.parse( - new InputSource(new FileReader(xmlFile)), enableExternalEntities); + new InputSource(new FileReader(xmlFile)), + enableExternalEntities, + isNamespaceAware); } /** @@ -274,7 +299,7 @@ public static XMLBuilder parse(File xmlFile, boolean enableExternalEntities) public static XMLBuilder parse(InputSource inputSource) throws ParserConfigurationException, SAXException, IOException { - return XMLBuilder.parse(inputSource, false); + return XMLBuilder.parse(inputSource, false, true); } /** @@ -296,7 +321,7 @@ public static XMLBuilder parse(InputSource inputSource) public static XMLBuilder parse(String xmlString) throws ParserConfigurationException, SAXException, IOException { - return XMLBuilder.parse(xmlString, false); + return XMLBuilder.parse(xmlString, false, true); } /** @@ -318,7 +343,7 @@ public static XMLBuilder parse(String xmlString) public static XMLBuilder parse(File xmlFile) throws ParserConfigurationException, SAXException, IOException { - return XMLBuilder.parse(xmlFile, false); + return XMLBuilder.parse(xmlFile, false, true); } @Override diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java index 013ad7c..fcfb492 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2014 James Murty (www.jamesmurty.com) + * Copyright 2008-2017 James Murty (www.jamesmurty.com) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -120,17 +120,22 @@ private static RuntimeException wrapExceptionAsRuntimeException(Exception e) { * default namespace URI for document, ignored if null or empty. * @param enableExternalEntities * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. * @throws XMLBuilderRuntimeException * to wrap {@link ParserConfigurationException} */ public static XMLBuilder2 create( - String name, String namespaceURI, boolean enableExternalEntities) + String name, String namespaceURI, boolean enableExternalEntities, + boolean isNamespaceAware) { try { return new XMLBuilder2( - createDocumentImpl(name, namespaceURI, enableExternalEntities)); + createDocumentImpl( + name, namespaceURI, enableExternalEntities, isNamespaceAware)); } catch (ParserConfigurationException e) { throw wrapExceptionAsRuntimeException(e); } @@ -145,14 +150,19 @@ public static XMLBuilder2 create( * the name of the document's root element. * @param enableExternalEntities * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. * @throws XMLBuilderRuntimeException * to wrap {@link ParserConfigurationException} */ - public static XMLBuilder2 create(String name, boolean enableExternalEntities) + public static XMLBuilder2 create(String name, + boolean enableExternalEntities, boolean isNamespaceAware) { - return XMLBuilder2.create(name, null, enableExternalEntities); + return XMLBuilder2.create( + name, null, enableExternalEntities, isNamespaceAware); } /** @@ -172,7 +182,7 @@ public static XMLBuilder2 create(String name, boolean enableExternalEntities) */ public static XMLBuilder2 create(String name, String namespaceURI) { - return XMLBuilder2.create(name, namespaceURI, false); + return XMLBuilder2.create(name, namespaceURI, false, true); } /** @@ -189,7 +199,7 @@ public static XMLBuilder2 create(String name, String namespaceURI) */ public static XMLBuilder2 create(String name) { - return XMLBuilder2.create(name, null, false); + return XMLBuilder2.create(name, null, false, true); } /** @@ -201,6 +211,9 @@ public static XMLBuilder2 create(String name) * an XML document input source that will be parsed into a DOM. * @param enableExternalEntities * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. * @throws XMLBuilderRuntimeException @@ -208,11 +221,13 @@ public static XMLBuilder2 create(String name) * {@link IOException} */ public static XMLBuilder2 parse( - InputSource inputSource, boolean enableExternalEntities) + InputSource inputSource, boolean enableExternalEntities, + boolean isNamespaceAware) { try { return new XMLBuilder2( - parseDocumentImpl(inputSource, enableExternalEntities)); + parseDocumentImpl( + inputSource, enableExternalEntities, isNamespaceAware)); } catch (ParserConfigurationException e) { throw wrapExceptionAsRuntimeException(e); } catch (SAXException e) { @@ -231,15 +246,19 @@ public static XMLBuilder2 parse( * an XML document string that will be parsed into a DOM. * @param enableExternalEntities * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. */ public static XMLBuilder2 parse( - String xmlString, boolean enableExternalEntities) + String xmlString, boolean enableExternalEntities, boolean isNamespaceAware) { return XMLBuilder2.parse( new InputSource(new StringReader(xmlString)), - enableExternalEntities); + enableExternalEntities, + isNamespaceAware); } /** @@ -251,18 +270,23 @@ public static XMLBuilder2 parse( * an XML document file that will be parsed into a DOM. * @param enableExternalEntities * enable external entities; beware of XML External Entity (XXE) injection. + * @param isNamespaceAware + * enable or disable namespace awareness in the underlying + * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. * @throws XMLBuilderRuntimeException * to wrap {@link ParserConfigurationException}, {@link SAXException}, * {@link IOException}, {@link FileNotFoundException} */ - public static XMLBuilder2 parse(File xmlFile, boolean enableExternalEntities) + public static XMLBuilder2 parse(File xmlFile, boolean enableExternalEntities, + boolean isNamespaceAware) { try { return XMLBuilder2.parse( new InputSource(new FileReader(xmlFile)), - enableExternalEntities); + enableExternalEntities, + isNamespaceAware); } catch (FileNotFoundException e) { throw wrapExceptionAsRuntimeException(e); } @@ -283,7 +307,7 @@ public static XMLBuilder2 parse(File xmlFile, boolean enableExternalEntities) */ public static XMLBuilder2 parse(InputSource inputSource) { - return XMLBuilder2.parse(inputSource, false); + return XMLBuilder2.parse(inputSource, false, true); } /** @@ -298,7 +322,7 @@ public static XMLBuilder2 parse(InputSource inputSource) */ public static XMLBuilder2 parse(String xmlString) { - return XMLBuilder2.parse(xmlString, false); + return XMLBuilder2.parse(xmlString, false, true); } /** @@ -316,7 +340,7 @@ public static XMLBuilder2 parse(String xmlString) */ public static XMLBuilder2 parse(File xmlFile) { - return XMLBuilder2.parse(xmlFile, false); + return XMLBuilder2.parse(xmlFile, false, true); } /** diff --git a/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java b/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java index 54959fc..ca8876f 100644 --- a/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java +++ b/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java @@ -54,11 +54,12 @@ protected BaseXMLBuilder XMLBuilder_parse(InputSource source) throws Exception { } protected BaseXMLBuilder XMLBuilder_parse( - String documentString, boolean enableExternalEntities) throws Exception + String documentString, boolean enableExternalEntities, + boolean isNamespaceAware) throws Exception { return (BaseXMLBuilder) XMLBuilderToTest().getMethod( - "parse", String.class, boolean.class).invoke( - null, documentString, enableExternalEntities); + "parse", String.class, boolean.class, boolean.class).invoke( + null, documentString, enableExternalEntities, isNamespaceAware); } protected BaseXMLBuilder XMLBuilder_parse(String documentString) throws Exception { @@ -469,6 +470,44 @@ public void testNamespaces() throws Exception { builder.element("undefined-prefix:ElementName"); } + public void testNamespaceUnawareBuilder() throws Exception { + String XML_WITH_NAMESPACES = + "" + + "Found me" + + ""; + + // Builder set to be unaware of namespaces can traverse DOM with + // namespaces without using namespace prefixes + BaseXMLBuilder result = XMLBuilder_parse( + XML_WITH_NAMESPACES, + false, // enableExternalEntities + false // isNamespaceAware + ).xpathFind("/NamespaceUnwareTest/NestedElement"); + assertEquals("Found me", result.getElement().getTextContent()); + + // Builder set to be aware of namespaces (per default) cannot traverse + // DOM with namespaces without using namespace prefixes + try { + result = XMLBuilder_parse(XML_WITH_NAMESPACES) + .xpathFind("/NamespaceUnwareTest/NestedElement"); + } catch (Exception ex) { + Throwable cause = null; + if (this instanceof TestXMLBuilder2) { + cause = ex.getCause(); // Exception wrapped in runtime ex + } else { + cause = ex; + } + assertEquals( + cause.getClass(), XPathExpressionException.class); + assertTrue( + cause.getMessage().contains( + "XPath expression \"/NamespaceUnwareTest/NestedElement\"" + + " does not resolve to an Element in context" + )); + } + assertEquals("Found me", result.getElement().getTextContent()); + } + public void testElementBefore() throws Exception { BaseXMLBuilder builder = XMLBuilder_create("TestDocument", "urn:default") .namespace("custom", "urn:custom") @@ -648,7 +687,7 @@ public void testXMLBuilderParserImmuneToXXEAttackByDefault() throws Exception { String parsedXml = builder.asString(); assertFalse(parsedXml.indexOf("Injected XXE Data") >= 0); // If you enable external entity processing, builder becomes subject to XXE injection - builder = XMLBuilder_parse(XML_DOC_WITH_XXE, true); + builder = XMLBuilder_parse(XML_DOC_WITH_XXE, true, true); parsedXml = builder.asString(); assertTrue(parsedXml.indexOf("Injected XXE Data") >= 0); } From 6483a913a5ee3561e8ebe0b23e5716f96eb447fb Mon Sep 17 00:00:00 2001 From: James Murty Date: Fri, 1 Sep 2017 22:43:29 +1000 Subject: [PATCH 28/43] Mention advanced features of create & parse constructors in readme --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index 0740c1c..1b579ec 100644 --- a/README.md +++ b/README.md @@ -351,6 +351,22 @@ To produce:
``` +### Configuring advanced features + +When creating or parsing a document you can enable and disable advanced +features by using the more explicit versions of the `parse()` and `create()` +constructors. + +You can: + +* use the `enableExternalEntities` flag to enable or disable external entities. + NOTE: you should leave these disabled, as they are by default, unless you + really need them because they open you to XML External Entity (XXE) injection + attacks. +* use the `isNamespaceAware` flag to enable or disable namespace awareness in + the underlying `DocumentBuilderFactory`. + + Release History --------------- From 4ce85f5a8d2db6f31d474aa3c13d0dd86bae3a82 Mon Sep 17 00:00:00 2001 From: James Murty Date: Fri, 1 Sep 2017 22:52:15 +1000 Subject: [PATCH 29/43] [maven-release-plugin] prepare release v1.2 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 5a84930..e91d5f6 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ java-xmlbuilder jar - 1.2-SNAPSHOT + 1.2 java-xmlbuilder XML Builder is a utility that creates simple XML documents using relatively sparse Java code https://github.com/jmurty/java-xmlbuilder @@ -21,7 +21,7 @@ scm:git:github.com/jmurty/java-xmlbuilder.git scm:git:git@github.com:jmurty/java-xmlbuilder.git https://github.com/jmurty/java-xmlbuilder - HEAD + v1.2 From 557e1ee332ab569344050934a822cc7da590c17b Mon Sep 17 00:00:00 2001 From: James Murty Date: Fri, 1 Sep 2017 22:52:16 +1000 Subject: [PATCH 30/43] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index e91d5f6..88d5285 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ java-xmlbuilder jar - 1.2 + 1.3-SNAPSHOT java-xmlbuilder XML Builder is a utility that creates simple XML documents using relatively sparse Java code https://github.com/jmurty/java-xmlbuilder @@ -21,7 +21,7 @@ scm:git:github.com/jmurty/java-xmlbuilder.git scm:git:git@github.com:jmurty/java-xmlbuilder.git https://github.com/jmurty/java-xmlbuilder - v1.2 + HEAD From f396a241f64c12d159dc7b8baba7616552516922 Mon Sep 17 00:00:00 2001 From: James Murty Date: Fri, 1 Sep 2017 23:06:23 +1000 Subject: [PATCH 31/43] Correct status of version 1.2 in changelog --- CHANGES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 12b586b..53f8b53 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,8 +1,8 @@ Release Notes for java-xmlbuilder ================================= -Version 1.2 - Pending ---------------------- +Version 1.2 - 1 September 2017 +------------------------------ Fixes: From 7c224b8e8ed79808509322cb141dab5a88dd3cec Mon Sep 17 00:00:00 2001 From: James Murty Date: Fri, 1 Sep 2017 23:08:07 +1000 Subject: [PATCH 32/43] Update Maven reference and direct download link to version 1.2 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1b579ec..9181ecb 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ See further example usage below and in the [JavaDoc documentation](http://s3.jamesmurty.com/java-xmlbuilder/index.html). Download a Jar file containing the latest version -[java-xmlbuilder-1.1.jar](http://s3.jamesmurty.com/java-xmlbuilder/java-xmlbuilder-1.1.jar). +[java-xmlbuilder-1.2.jar](http://s3.jamesmurty.com/java-xmlbuilder/java-xmlbuilder-1.2.jar). Maven users can add this project as a dependency with the following additions to a POM.xml file: @@ -88,7 +88,7 @@ to a POM.xml file: com.jamesmurty.utils java-xmlbuilder - 1.1 + 1.2 . . . From 13a78bcf436daa8ff9c3e515e4599342cc5f1b38 Mon Sep 17 00:00:00 2001 From: harkue Date: Wed, 8 Jul 2020 11:29:58 +0800 Subject: [PATCH 33/43] add maven central badge in `README.md` --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 9181ecb..2572c92 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ java-xmlbuilder =============== +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.jamesmurty.utils/java-xmlbuilder/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.jamesmurty.utils/java-xmlbuilder) + XML Builder is a utility that allows simple XML documents to be constructed using relatively sparse Java code. From 0b53d5c77101a61058d39baf5b2d26080a4e0e09 Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 8 Jul 2020 23:12:24 +1000 Subject: [PATCH 34/43] Remove unused `javax.annotation` import which breaks Java 11+ --- src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java b/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java index ca8876f..274d461 100644 --- a/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java +++ b/src/test/java/com/jamesmurty/utils/BaseXMLBuilderTests.java @@ -6,7 +6,6 @@ import java.io.StringWriter; import java.util.Properties; -import javax.annotation.Resource; import javax.xml.transform.OutputKeys; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; From 4c44a34bafbee864dbdfd1e232268cfbb246cde4 Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 8 Jul 2020 23:14:48 +1000 Subject: [PATCH 35/43] Update build source & target to version 7 Versions <7 is no longer supported by Maven --- pom.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pom.xml b/pom.xml index 88d5285..c887b62 100644 --- a/pom.xml +++ b/pom.xml @@ -8,6 +8,11 @@ java-xmlbuilder XML Builder is a utility that creates simple XML documents using relatively sparse Java code https://github.com/jmurty/java-xmlbuilder + + + 1.7 + 1.7 + From 42109b9bea60861410f87a9cec1f8e72d4f62cc3 Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 8 Jul 2020 23:36:43 +1000 Subject: [PATCH 36/43] Update links previously to my defunct jamesmurty.com site --- .gitignore | 2 +- README.md | 26 +++++++++---------- pom.xml | 2 +- .../com/jamesmurty/utils/BaseXMLBuilder.java | 4 +-- .../java/com/jamesmurty/utils/XMLBuilder.java | 2 +- .../com/jamesmurty/utils/XMLBuilder2.java | 2 +- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.gitignore b/.gitignore index 762f6e9..84a1064 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,8 @@ /.classpath /.project /.settings +/.envrc /mvn -/HOWTO_RELEASE.txt /bin /target /java-xmlbuilder diff --git a/README.md b/README.md index 2572c92..1d0495d 100644 --- a/README.md +++ b/README.md @@ -26,14 +26,14 @@ Since version 1.1 this library provides two builder implementations and APIs: * `XMLBuilder` – the original API – follows standard Java practice of re-throwing lower level checked exceptions when you do things like create a - new document. + new document. You must explicitly `catch` these checked exceptions in your codebase, even - though they are unlikely to occur in tested code. + though they are unlikely to occur in tested code. * `XMLBuilder2` is a newer API that removes checked exceptions altogether, and will instead wrap and propagate lower level exceptions in an unchecked - `XMLBuilderRuntimeException`. + `XMLBuilderRuntimeException`. Use this class if you don't like the code mess or overhead of try/catching - many low-level exceptions that are unlikely to occur in practice. + many low-level exceptions that are unlikely to occur in practice. Both these versions work identically apart from the handling of errors, so you can use whichever version you prefer or "upgrade" from one to the other in @@ -75,11 +75,11 @@ Produces this XML document: Getting Started --------------- -See further example usage below and in the -[JavaDoc documentation](http://s3.jamesmurty.com/java-xmlbuilder/index.html). +See further example usage below and in the +[JavaDoc documentation](http://s3.james.murty.com/java-xmlbuilder/index.html). Download a Jar file containing the latest version -[java-xmlbuilder-1.2.jar](http://s3.jamesmurty.com/java-xmlbuilder/java-xmlbuilder-1.2.jar). +[java-xmlbuilder-1.2.jar](http://s3.james.murty.com/java-xmlbuilder/java-xmlbuilder-1.2.jar). Maven users can add this project as a dependency with the following additions to a POM.xml file: @@ -195,7 +195,7 @@ methods. XMLBuilder builder = XMLBuilder.create("Projects") .e("java-xmlbuilder") .a("language", "Java") - .a("scm","SVN") + .a("scm","SVN") .e("Location") .a("type", "URL") .t("http://code.google.com/p/java-xmlbuilder/") @@ -223,7 +223,7 @@ The following methods are available for adding items to the XML document: ### Output -XMLBuilder includes two convenient methods for outputting a document. +XMLBuilder includes two convenient methods for outputting a document. You can use the `toWriter` method to print the document to an output stream or file: @@ -240,7 +240,7 @@ builder.asString(outputProperties); Both of these output methods take an `outputProperties` parameter that you can use to control how the output is generated. Any output properties you provide are forwarded to the underlying Transformer object that is used to serialize -the XML document. +the XML document. You might specify any non-standard properties like so: @@ -286,7 +286,7 @@ represents the document's root element, no matter deep an element hierarchy your code has built: ```java -org.w3c.dom.Element rootElement = +org.w3c.dom.Element rootElement = XMLBuilder.create("This") .e("Element") .e("Hierarchy") @@ -361,13 +361,13 @@ constructors. You can: -* use the `enableExternalEntities` flag to enable or disable external entities. +* use the `enableExternalEntities` flag to enable or disable external entities. NOTE: you should leave these disabled, as they are by default, unless you really need them because they open you to XML External Entity (XXE) injection attacks. * use the `isNamespaceAware` flag to enable or disable namespace awareness in the underlying `DocumentBuilderFactory`. - + Release History --------------- diff --git a/pom.xml b/pom.xml index c887b62..7150194 100644 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ jmurty James Murty - http://jamesmurty.com + https://github.com/jmurty developer diff --git a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java index c9466f7..f5cabd8 100644 --- a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2017 James Murty (www.jamesmurty.com) + * Copyright 2008-2020 James Murty (github.com/jmurty) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -1438,4 +1438,4 @@ protected String getPrefixFromQualifiedName(String qualifiedName) { } } -} \ No newline at end of file +} diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder.java b/src/main/java/com/jamesmurty/utils/XMLBuilder.java index 77c50b0..2679002 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2017 James Murty (www.jamesmurty.com) + * Copyright 2008-2020 James Murty (github.com/jmurty) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java index 96473d7..6f0cda6 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder2.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder2.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2017 James Murty (www.jamesmurty.com) + * Copyright 2008-2020 James Murty (github.com/jmurty) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From e39ad5f26f7bf706d6f61f2878a26dc2cb4a07ae Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 8 Jul 2020 23:51:37 +1000 Subject: [PATCH 37/43] Various hacks to get Docs to build with Java 12 --- pom.xml | 3 + .../com/jamesmurty/utils/BaseXMLBuilder.java | 36 +++++---- .../java/com/jamesmurty/utils/XMLBuilder.java | 76 +++++++++---------- 3 files changed, 61 insertions(+), 54 deletions(-) diff --git a/pom.xml b/pom.xml index 7150194..55dcd8b 100644 --- a/pom.xml +++ b/pom.xml @@ -94,6 +94,9 @@ org.apache.maven.plugins maven-javadoc-plugin 2.9 + + 7 + attach-javadocs diff --git a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java index f5cabd8..3122a30 100644 --- a/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java @@ -205,8 +205,8 @@ protected static void enableOrDisableExternalEntityParsing( * @return * an XML Document. * - * @throws FactoryConfigurationError - * @throws ParserConfigurationException + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz */ protected static Document createDocumentImpl( String name, String namespaceURI, boolean enableExternalEntities, @@ -240,12 +240,12 @@ protected static Document createDocumentImpl( * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. - * @throws ParserConfigurationException * - * @throws FactoryConfigurationError - * @throws ParserConfigurationException - * @throws IOException - * @throws SAXException + * @throws ParserConfigurationException xyz + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz + * @throws IOException xyz + * @throws SAXException xyz */ protected static Document parseDocumentImpl( InputSource inputSource, boolean enableExternalEntities, @@ -268,7 +268,7 @@ protected static Document parseDocumentImpl( * Uses approach I documented on StackOverflow: * http://stackoverflow.com/a/979606/4970 * - * @throws XPathExpressionException + * @throws XPathExpressionException xyz */ protected void stripWhitespaceOnlyTextNodesImpl() throws XPathExpressionException @@ -297,7 +297,7 @@ protected void stripWhitespaceOnlyTextNodesImpl() * * @return * a builder node at the same location as before the operation. - * @throws XPathExpressionException + * @throws XPathExpressionException xyz */ public abstract BaseXMLBuilder stripWhitespaceOnlyTextNodes() throws XPathExpressionException; @@ -533,7 +533,7 @@ protected String lookupNamespaceURIImpl(String name) { * the name of the XML element. * @param namespaceURI * a namespace URI - * @return + * @return xyz * * @throws IllegalStateException * if you attempt to add a child element to an XML node that already @@ -757,6 +757,7 @@ protected Element elementImpl(String name, String namespaceURI) { * * @param name * the name of the XML element. + * @return xyz * * @throws IllegalStateException * if you attempt to add a sibling element to a node where there are already @@ -776,6 +777,7 @@ protected Element elementBeforeImpl(String name) { * the name of the XML element. * @param namespaceURI * a namespace URI + * @return xyz * * @throws IllegalStateException * if you attempt to add a sibling element to a node where there are already @@ -1253,6 +1255,8 @@ protected Node upImpl(int steps) { } /** + * @param anXmlElement xyz + * * @throws IllegalStateException * if the current element contains any child text nodes that aren't pure whitespace. * We allow whitespace so parsed XML documents containing indenting or pretty-printing @@ -1298,7 +1302,7 @@ protected void assertElementContainsNoOrWhitespaceOnlyTextNodes(Node anXmlElemen * null or an empty Properties object, in which case the default output * properties will be applied. * - * @throws TransformerException + * @throws TransformerException xyz */ public void toWriter(boolean wholeDocument, Writer writer, Properties outputProperties) throws TransformerException { @@ -1337,7 +1341,7 @@ public void toWriter(boolean wholeDocument, Writer writer, Properties outputProp * null or an empty Properties object, in which case the default output * properties will be applied. * - * @throws TransformerException + * @throws TransformerException xyz */ public void toWriter(Writer writer, Properties outputProperties) throws TransformerException { @@ -1358,7 +1362,7 @@ public void toWriter(Writer writer, Properties outputProperties) * @return * the XML document as a string * - * @throws TransformerException + * @throws TransformerException xyz */ public String asString(Properties outputProperties) throws TransformerException { StringWriter writer = new StringWriter(); @@ -1380,7 +1384,7 @@ public String asString(Properties outputProperties) throws TransformerException * @return * the XML document as a string * - * @throws TransformerException + * @throws TransformerException xyz */ public String elementAsString(Properties outputProperties) throws TransformerException { StringWriter writer = new StringWriter(); @@ -1395,7 +1399,7 @@ public String elementAsString(Properties outputProperties) throws TransformerExc * the XML document as a string without the XML declaration at the * beginning of the output. * - * @throws TransformerException + * @throws TransformerException xyz */ public String asString() throws TransformerException { Properties outputProperties = new Properties(); @@ -1411,7 +1415,7 @@ public String asString() throws TransformerException { * the XML document as a string without the XML declaration at the * beginning of the output. * - * @throws TransformerException + * @throws TransformerException xyz */ public String elementAsString() throws TransformerException { Properties outputProperties = new Properties(); diff --git a/src/main/java/com/jamesmurty/utils/XMLBuilder.java b/src/main/java/com/jamesmurty/utils/XMLBuilder.java index 2679002..144293f 100644 --- a/src/main/java/com/jamesmurty/utils/XMLBuilder.java +++ b/src/main/java/com/jamesmurty/utils/XMLBuilder.java @@ -107,8 +107,8 @@ protected XMLBuilder(Node myNode, Node parentNode) { * @return * a builder node that can be used to add more nodes to the XML document. * - * @throws FactoryConfigurationError - * @throws ParserConfigurationException + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz */ public static XMLBuilder create(String name, String namespaceURI, boolean enableExternalEntities, boolean isNamespaceAware) @@ -134,8 +134,8 @@ public static XMLBuilder create(String name, String namespaceURI, * @return * a builder node that can be used to add more nodes to the XML document. * - * @throws FactoryConfigurationError - * @throws ParserConfigurationException + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz */ public static XMLBuilder create(String name, boolean enableExternalEntities, boolean isNamespaceAware) @@ -158,8 +158,8 @@ public static XMLBuilder create(String name, boolean enableExternalEntities, * @return * a builder node that can be used to add more nodes to the XML document. * - * @throws FactoryConfigurationError - * @throws ParserConfigurationException + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz */ public static XMLBuilder create(String name, String namespaceURI) throws ParserConfigurationException, FactoryConfigurationError @@ -177,8 +177,8 @@ public static XMLBuilder create(String name, String namespaceURI) * @return * a builder node that can be used to add more nodes to the XML document. * - * @throws FactoryConfigurationError - * @throws ParserConfigurationException + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz */ public static XMLBuilder create(String name) throws ParserConfigurationException, FactoryConfigurationError @@ -200,12 +200,12 @@ public static XMLBuilder create(String name) * {@link DocumentBuilderFactory} * @return * a builder node that can be used to add more nodes to the XML document. - * @throws ParserConfigurationException * - * @throws FactoryConfigurationError - * @throws ParserConfigurationException - * @throws IOException - * @throws SAXException + * @throws ParserConfigurationException xyz + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz + * @throws IOException xyz + * @throws SAXException xyz */ public static XMLBuilder parse( InputSource inputSource, boolean enableExternalEntities, @@ -232,11 +232,11 @@ public static XMLBuilder parse( * @return * a builder node that can be used to add more nodes to the XML document. * - * @throws ParserConfigurationException - * @throws FactoryConfigurationError - * @throws ParserConfigurationException - * @throws IOException - * @throws SAXException + * @throws ParserConfigurationException xyz + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz + * @throws IOException xyz + * @throws SAXException xyz */ public static XMLBuilder parse( String xmlString, boolean enableExternalEntities, @@ -264,11 +264,11 @@ public static XMLBuilder parse( * @return * a builder node that can be used to add more nodes to the XML document. * - * @throws ParserConfigurationException - * @throws FactoryConfigurationError - * @throws ParserConfigurationException - * @throws IOException - * @throws SAXException + * @throws ParserConfigurationException xyz + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz + * @throws IOException xyz + * @throws SAXException xyz */ public static XMLBuilder parse(File xmlFile, boolean enableExternalEntities, boolean isNamespaceAware) @@ -289,12 +289,12 @@ public static XMLBuilder parse(File xmlFile, boolean enableExternalEntities, * an XML document input source that will be parsed into a DOM. * @return * a builder node that can be used to add more nodes to the XML document. - * @throws ParserConfigurationException * - * @throws FactoryConfigurationError - * @throws ParserConfigurationException - * @throws IOException - * @throws SAXException + * @throws ParserConfigurationException xyz + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz + * @throws IOException xyz + * @throws SAXException xyz */ public static XMLBuilder parse(InputSource inputSource) throws ParserConfigurationException, SAXException, IOException @@ -312,11 +312,11 @@ public static XMLBuilder parse(InputSource inputSource) * @return * a builder node that can be used to add more nodes to the XML document. * - * @throws ParserConfigurationException - * @throws FactoryConfigurationError - * @throws ParserConfigurationException - * @throws IOException - * @throws SAXException + * @throws ParserConfigurationException xyz + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz + * @throws IOException xyz + * @throws SAXException xyz */ public static XMLBuilder parse(String xmlString) throws ParserConfigurationException, SAXException, IOException @@ -334,11 +334,11 @@ public static XMLBuilder parse(String xmlString) * @return * a builder node that can be used to add more nodes to the XML document. * - * @throws ParserConfigurationException - * @throws FactoryConfigurationError - * @throws ParserConfigurationException - * @throws IOException - * @throws SAXException + * @throws ParserConfigurationException xyz + * @throws FactoryConfigurationError xyz + * @throws ParserConfigurationException xyz + * @throws IOException xyz + * @throws SAXException xyz */ public static XMLBuilder parse(File xmlFile) throws ParserConfigurationException, SAXException, IOException From 564a3297d9c3737bf36f32d8886a7dde45d45c6e Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 8 Jul 2020 23:54:08 +1000 Subject: [PATCH 38/43] Release 1.3 to Maven repository --- CHANGES.md | 28 ++++++++++++++++++++-------- README.md | 4 ++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 53f8b53..4b2435f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,18 @@ Release Notes for java-xmlbuilder ================================= +Version 1.3 - 8 July 2020 +------------------------- + +Fixes: + +* Update source version from 1.5 to 1.7 to work with Java versions 11+ which no + longer supports `javax.annotation` and because 6 no longer builds with Maven. + +* Tweaks to pom.xml to get Maven builds to work with Java 12. + +* Replace references to my defunct website with working alternatives. + Version 1.2 - 1 September 2017 ------------------------------ @@ -9,16 +21,16 @@ Fixes: * Prevent XML External Entity (XXE) injection attacks by disabling parsing of general and parameter external entities by default (#6). External entities are now only parsed if this feature is explicitly enabled by passing a boolean - flag value to the #create and #parse methods. + flag value to the #create and #parse methods. WARNING: This will break code that expects external entities to be parsed. - + Enhancements: * Permit users to disable namespace-awareness in the underlying DocumentBuilderFactory when constructing the builder with extended `create()` and `parse()` methods. Namespace awareness is enabled by default unless you use the more explicit versions of these methods that take additional - `enableExternalEntities` and `isNamespaceAware` parameters. + `enableExternalEntities` and `isNamespaceAware` parameters. Version 1.1 - 22 July 2014 -------------------------- @@ -27,12 +39,12 @@ Added a new `XMLBuilder2` implementation that avoids checked exceptions in the API and throws runtime exceptions instead. This should make the library much more pleasant to use, and your code much cleaner, in situations where low-level exceptions are unlikely -- which is probably most situations where you would -use this library. +use this library. For example when creating a new document with the `#create` method, instead of needing to explicitly catch the unlikely `ParserConfigurationException`, if you use `XMLBuilder2` this exception automatically gets wrapped in the new -`XMLBuilderRuntimeException` class and can be left to propagate out. +`XMLBuilderRuntimeException` class and can be left to propagate out. Aside from the removal of checked exceptions, `XMLBuilder2` has the same API as the original `XMLBuilder` and should therefore be a drop-in replacement in @@ -45,9 +57,9 @@ Version 1.0 - 6 March 2014 -------------------------- Jumped version number from 0.7 to 1.0 to better reflect this project's age -and stability, as well as to celebrate the move to GitHub. +and stability, as well as to celebrate the move to GitHub. -* Migrated project from +* Migrated project from [Google Code](https://code.google.com/p/java-xmlbuilder/) to [GitHub](https://github.com/jmurty/java-xmlbuilder). Whew, that's better! * Test cases for edge-case issues and questions reported by users. @@ -91,7 +103,7 @@ Version 0.3 - 2 July 2010 * First release to Maven repository. * Parse existing XML documents with `parse` method. -* Find specific nodes in document with an XPath with `xpathFind`. +* Find specific nodes in document with an XPath with `xpathFind`. * Added JUnit tests Version 0.2 - 6 January 2009 diff --git a/README.md b/README.md index 1d0495d..7465cc8 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ See further example usage below and in the [JavaDoc documentation](http://s3.james.murty.com/java-xmlbuilder/index.html). Download a Jar file containing the latest version -[java-xmlbuilder-1.2.jar](http://s3.james.murty.com/java-xmlbuilder/java-xmlbuilder-1.2.jar). +[java-xmlbuilder-1.3.jar](http://s3.james.murty.com/java-xmlbuilder/java-xmlbuilder-1.3.jar). Maven users can add this project as a dependency with the following additions to a POM.xml file: @@ -90,7 +90,7 @@ to a POM.xml file: com.jamesmurty.utils java-xmlbuilder - 1.2 + 1.3 . . . From b1d49f7dd5a2374c4dcedfc05a1164a943d8c75f Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 8 Jul 2020 23:58:11 +1000 Subject: [PATCH 39/43] [maven-release-plugin] prepare release v1.3 --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 55dcd8b..c7c4c3c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ java-xmlbuilder jar - 1.3-SNAPSHOT + 1.3 java-xmlbuilder XML Builder is a utility that creates simple XML documents using relatively sparse Java code https://github.com/jmurty/java-xmlbuilder @@ -26,7 +26,7 @@ scm:git:github.com/jmurty/java-xmlbuilder.git scm:git:git@github.com:jmurty/java-xmlbuilder.git https://github.com/jmurty/java-xmlbuilder - HEAD + v1.3 From 6c5a027dd49d4d52915125a66f96423d43c44568 Mon Sep 17 00:00:00 2001 From: James Murty Date: Wed, 8 Jul 2020 23:58:11 +1000 Subject: [PATCH 40/43] [maven-release-plugin] prepare for next development iteration --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index c7c4c3c..ec4603a 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ java-xmlbuilder jar - 1.3 + 1.4-SNAPSHOT java-xmlbuilder XML Builder is a utility that creates simple XML documents using relatively sparse Java code https://github.com/jmurty/java-xmlbuilder @@ -26,7 +26,7 @@ scm:git:github.com/jmurty/java-xmlbuilder.git scm:git:git@github.com:jmurty/java-xmlbuilder.git https://github.com/jmurty/java-xmlbuilder - v1.3 + HEAD From 01d118fd43c720741089a5d7a47cd8e7d28d9ee8 Mon Sep 17 00:00:00 2001 From: James Murty Date: Thu, 9 Jul 2020 00:48:30 +1000 Subject: [PATCH 41/43] Fix faulty links to docs and 1.3 Jar file --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7465cc8..f85ff71 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,10 @@ Getting Started --------------- See further example usage below and in the -[JavaDoc documentation](http://s3.james.murty.com/java-xmlbuilder/index.html). +[JavaDoc documentation](http://s3.james.murty.co/java-xmlbuilder/index.html). Download a Jar file containing the latest version -[java-xmlbuilder-1.3.jar](http://s3.james.murty.com/java-xmlbuilder/java-xmlbuilder-1.3.jar). +[java-xmlbuilder-1.3.jar](http://s3.james.murty.co/java-xmlbuilder/java-xmlbuilder-1.3.jar). Maven users can add this project as a dependency with the following additions to a POM.xml file: From e6e817dddb10173bd6b27073038f3db5ae97b177 Mon Sep 17 00:00:00 2001 From: harkue Date: Fri, 10 Jul 2020 08:44:03 +0800 Subject: [PATCH 42/43] set up github action for CI build and codecov add build and codecov badge in README.md --- .github/workflows/codecov.yml | 28 ++++++++++++++++++++++++++++ .github/workflows/maven.yml | 31 +++++++++++++++++++++++++++++++ README.md | 4 ++++ pom.xml | 14 +++++++++++++- 4 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/codecov.yml create mode 100644 .github/workflows/maven.yml diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 0000000..5b388a8 --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,28 @@ +# This is a basic workflow to help you get started with Actions + +name: Codecov + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + push: + pull_request: + + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Run cobertura + run: mvn cobertura:cobertura + + - name: Upload report + run: bash <(curl -s https://codecov.io/bash) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000..87ce66d --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,31 @@ +# This workflow will build a Java project with Maven +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Build + +on: + push: + pull_request: + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + java: [7, 8, 9.0.x, 10, 11, 12, 13] + + steps: + - uses: actions/checkout@v2 + + - name: Set up JDK + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + + - name: Clean Build + run: mvn clean + + - name: Build with Maven + run: mvn -B package --file pom.xml \ No newline at end of file diff --git a/README.md b/README.md index f85ff71..51bda75 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,10 @@ java-xmlbuilder [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.jamesmurty.utils/java-xmlbuilder/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.jamesmurty.utils/java-xmlbuilder) +![Build](https://github.com/jmurty/java-xmlbuilder/workflows/Build/badge.svg) ![Codecov](https://github.com/jmurty/java-xmlbuilder/workflows/Codecov/badge.svg) + +[![codecov](https://codecov.io/gh/jmurty/java-xmlbuilder/branch/master/graph/badge.svg)](https://codecov.io/gh/jmurty/java-xmlbuilder) + XML Builder is a utility that allows simple XML documents to be constructed using relatively sparse Java code. diff --git a/pom.xml b/pom.xml index ec4603a..36d109a 100644 --- a/pom.xml +++ b/pom.xml @@ -106,7 +106,19 @@ - + + + org.codehaus.mojo + cobertura-maven-plugin + 2.7 + + + html + xml + + + + From 0215c9b89fc404626afc5d37589727b488c37e96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2020 03:14:30 +0000 Subject: [PATCH 43/43] Bump junit from 4.11 to 4.13.1 Bumps [junit](https://github.com/junit-team/junit4) from 4.11 to 4.13.1. - [Release notes](https://github.com/junit-team/junit4/releases) - [Changelog](https://github.com/junit-team/junit4/blob/main/doc/ReleaseNotes4.11.md) - [Commits](https://github.com/junit-team/junit4/compare/r4.11...r4.13.1) Signed-off-by: dependabot[bot] --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 36d109a..cecbdb2 100644 --- a/pom.xml +++ b/pom.xml @@ -51,7 +51,7 @@ junit junit - 4.11 + 4.13.1 test