com.jamesmurty.utils.BaseXMLBuilder Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-xmlbuilder Show documentation
Show all versions of java-xmlbuilder Show documentation
XML Builder is a utility that creates simple XML documents using relatively sparse Java code
The newest version!
/*
* 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