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/.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/CHANGES.md b/CHANGES.md
index 7b6a569..4b2435f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,13 +1,65 @@
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
+------------------------------
+
+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.
+
+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
+--------------------------
+
+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
--------------------------
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.
@@ -51,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 4c51a1a..51bda75 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,12 @@
java-xmlbuilder
===============
+[](https://maven-badges.herokuapp.com/maven-central/com.jamesmurty.utils/java-xmlbuilder)
+
+ 
+
+[](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.
@@ -17,6 +23,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
-------------
@@ -26,7 +51,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/")
@@ -54,11 +79,11 @@ Produces this XML document:
Getting Started
---------------
-See further example usage below and 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.co/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.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:
@@ -69,7 +94,7 @@ to a POM.xml file:
com.jamesmurty.utilsjava-xmlbuilder
- 1.0
+ 1.3
. . .
@@ -174,7 +199,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/")
@@ -202,7 +227,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:
@@ -219,7 +244,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:
@@ -265,7 +290,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")
@@ -332,6 +357,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
---------------
diff --git a/pom.xml b/pom.xml
index 6d38f34..cecbdb2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,10 +4,15 @@
java-xmlbuilderjar
- 1.1-SNAPSHOT
+ 1.4-SNAPSHOTjava-xmlbuilderXML Builder is a utility that creates simple XML documents using relatively sparse Java codehttps://github.com/jmurty/java-xmlbuilder
+
+
+ 1.7
+ 1.7
+
@@ -28,7 +33,7 @@
jmurtyJames Murty
- http://jamesmurty.com
+ https://github.com/jmurtydeveloper
@@ -46,7 +51,7 @@
junitjunit
- 4.11
+ 4.13.1test
@@ -67,6 +72,7 @@
v@{project.version}false
+ -Dgpgsign=true
@@ -88,6 +94,9 @@
org.apache.maven.pluginsmaven-javadoc-plugin2.9
+
+ 7
+ attach-javadocs
@@ -99,21 +108,48 @@
- org.apache.maven.plugins
- maven-gpg-plugin
- 1.4
-
-
- sign-artifacts
- verify
-
- sign
-
-
-
+ org.codehaus.mojo
+ cobertura-maven-plugin
+ 2.7
+
+
+ html
+ xml
+
+
+
-
+
+
+
+ release-sign-artifacts
+
+
+ gpgsign
+ true
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-gpg-plugin
+ 1.4
+
+
+ sign-artifacts
+ verify
+
+ sign
+
+
+
+
+
+
+
+
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..3122a30
--- /dev/null
+++ b/src/main/java/com/jamesmurty/utils/BaseXMLBuilder.java
@@ -0,0 +1,1445 @@
+/*
+ * 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.
+ * 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;
+
+ /**
+ * 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.
+ *
+ * @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);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @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.
+ * @param isNamespaceAware
+ * enable or disable namespace awareness in the underlying
+ * {@link DocumentBuilderFactory}
+ * @return
+ * an XML Document.
+ *
+ * @throws FactoryConfigurationError xyz
+ * @throws ParserConfigurationException xyz
+ */
+ protected static Document createDocumentImpl(
+ String name, String namespaceURI, boolean enableExternalEntities,
+ boolean isNamespaceAware)
+ throws ParserConfigurationException, FactoryConfigurationError
+ {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(isNamespaceAware);
+ enableOrDisableExternalEntityParsing(factory, enableExternalEntities);
+ 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.
+ * @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 xyz
+ * @throws FactoryConfigurationError xyz
+ * @throws ParserConfigurationException xyz
+ * @throws IOException xyz
+ * @throws SAXException xyz
+ */
+ protected static Document parseDocumentImpl(
+ InputSource inputSource, boolean enableExternalEntities,
+ boolean isNamespaceAware)
+ 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;
+ }
+
+ /**
+ * 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 xyz
+ */
+ 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);
+ }
+ }
+
+ /**
+ * 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 xyz
+ */
+ public abstract BaseXMLBuilder stripWhitespaceOnlyTextNodes()
+ throws XPathExpressionException;
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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,
+ * 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
+ * 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
+ * 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 xyz
+ *
+ * @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 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.
+ *
+ * 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 xyz
+ *
+ * @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
+ * @return xyz
+ *
+ * @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);
+ }
+
+ /**
+ * 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.
+ *
+ * @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;
+ }
+
+ /**
+ * @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
+ * 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 xyz
+ */
+ 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