com.helger.xml.XMLHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ph-xml Show documentation
Show all versions of ph-xml Show documentation
Java 1.8+ Library with XML handling routines
/*
* Copyright (C) 2014-2023 Philip Helger (www.helger.com)
* philip[at]helger[dot]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.
*/
package com.helger.xml;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;
import javax.annotation.concurrent.NotThreadSafe;
import javax.xml.XMLConstants;
import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.EntityReference;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.PresentForCodeCoverage;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.builder.IBuilder;
import com.helger.commons.collection.ArrayHelper;
import com.helger.commons.collection.CollectionHelper;
import com.helger.commons.collection.impl.CommonsArrayList;
import com.helger.commons.collection.impl.CommonsLinkedHashMap;
import com.helger.commons.collection.impl.ICommonsList;
import com.helger.commons.collection.impl.ICommonsOrderedMap;
import com.helger.commons.collection.iterate.IIterableIterator;
import com.helger.commons.equals.EqualsHelper;
import com.helger.commons.string.StringHelper;
/**
* This class contains multiple XML utility methods.
*
* @author Philip Helger
*/
@Immutable
public final class XMLHelper
{
@PresentForCodeCoverage
private static final XMLHelper INSTANCE = new XMLHelper ();
private XMLHelper ()
{}
/**
* Get the owner document of the passed node. If the node itself is a
* document, only a cast is performed.
*
* @param aNode
* The node to get the document from. May be null
.
* @return null
if the passed node was null
.
*/
@Nullable
public static Document getOwnerDocument (@Nullable final Node aNode)
{
if (aNode == null)
return null;
if (aNode instanceof Document)
return (Document) aNode;
return aNode.getOwnerDocument ();
}
@Nullable
public static Element getDocumentElement (@Nullable final Node aNode)
{
final Document aDoc = getOwnerDocument (aNode);
return aDoc == null ? null : aDoc.getDocumentElement ();
}
@Nullable
public static String getNamespaceURI (@Nullable final Node aNode)
{
if (aNode instanceof Document)
{
// Recurse into document element
return getNamespaceURI (((Document) aNode).getDocumentElement ());
}
if (aNode != null)
return aNode.getNamespaceURI ();
return null;
}
@Nonnull
public static String getLocalNameOrTagName (@Nonnull final Element aElement)
{
String ret = aElement.getLocalName ();
if (ret == null)
ret = aElement.getTagName ();
return ret;
}
@Nonnull
public static String getLocalNameOrName (@Nonnull final Attr aAttr)
{
String ret = aAttr.getLocalName ();
if (ret == null)
ret = aAttr.getName ();
return ret;
}
@Nullable
public static String getElementName (@Nullable final Node aNode)
{
if (aNode instanceof Document)
{
// Recurse into document element
return getElementName (((Document) aNode).getDocumentElement ());
}
if (aNode instanceof Element)
{
return getLocalNameOrTagName ((Element) aNode);
}
return null;
}
public static boolean hasNoNamespaceURI (@Nonnull final Node aNode)
{
return StringHelper.hasNoText (aNode.getNamespaceURI ());
}
public static boolean hasNamespaceURI (@Nullable final Node aNode, @Nullable final String sNamespaceURI)
{
final String sNSURI = aNode == null ? null : aNode.getNamespaceURI ();
return sNSURI != null && sNSURI.equals (sNamespaceURI);
}
/**
* Check if the passed node is a text node. This includes all nodes derived
* from {@link Text} (Text and CData) or {@link EntityReference} nodes.
*
* @param aNode
* The node to be checked.
* @return true
if the passed node is a text node,
* false
otherwise.
*/
public static boolean isInlineNode (@Nullable final Node aNode)
{
return aNode instanceof Text || aNode instanceof EntityReference;
}
@Nonnegative
public static int getLength (@Nullable final NodeList aNL)
{
return aNL == null ? 0 : aNL.getLength ();
}
public static boolean isEmpty (@Nullable final NodeList aNL)
{
return aNL == null || aNL.getLength () == 0;
}
@Nonnull
public static Predicate super Node> filterNodeIsElement ()
{
return x -> x != null && x.getNodeType () == Node.ELEMENT_NODE;
}
@Nonnull
public static Predicate super Element> filterElementWithNamespace ()
{
return x -> x != null && StringHelper.hasText (x.getNamespaceURI ());
}
@Nonnull
public static Predicate super Element> filterElementWithoutNamespace ()
{
return x -> x != null && hasNoNamespaceURI (x);
}
@Nonnull
public static Predicate super Element> filterElementWithNamespace (@Nullable final String sNamespaceURI)
{
return x -> x != null && hasNamespaceURI (x, sNamespaceURI);
}
@Nonnull
public static Predicate super Element> filterElementWithNamespaceAndLocalName (@Nullable final String sNamespaceURI,
@Nonnull @Nonempty final String sLocalName)
{
ValueEnforcer.notEmpty (sLocalName, "LocalName");
return x -> x != null && hasNamespaceURI (x, sNamespaceURI) && x.getLocalName ().equals (sLocalName);
}
@Nonnull
public static Predicate super Element> filterElementWithTagName (@Nonnull @Nonempty final String sTagName)
{
ValueEnforcer.notEmpty (sTagName, "TagName");
return x -> EqualsHelper.equals (getElementName (x), sTagName);
}
@Nonnull
public static Predicate super Element> filterElementWithTagNameNoNS (@Nonnull @Nonempty final String sTagName)
{
ValueEnforcer.notEmpty (sTagName, "TagName");
return x -> hasNoNamespaceURI (x) && x.getTagName ().equals (sTagName);
}
/**
* Get the first direct child element of the passed element.
*
* @param aStartNode
* The element to start searching. May be null
.
* @return null
if the passed element does not have any direct
* child element.
*/
@Nullable
public static Element getFirstChildElement (@Nullable final Node aStartNode)
{
if (aStartNode == null)
return null;
return NodeListIterator.createChildNodeIterator (aStartNode)
.findFirstMapped (filterNodeIsElement (), Element.class::cast);
}
/**
* Check if the passed node has at least one direct child element or not.
*
* @param aStartNode
* The parent element to be searched. May be null
.
* @return true
if the passed node has at least one child
* element, false
otherwise.
*/
public static boolean hasChildElementNodes (@Nullable final Node aStartNode)
{
if (aStartNode == null)
return false;
return NodeListIterator.createChildNodeIterator (aStartNode).containsAny (filterNodeIsElement ());
}
/**
* Search all child nodes of the given for the first element that has the
* specified tag name.
*
* @param aStartNode
* The parent element to be searched. May be null
.
* @param sTagName
* The tag name to search.
* @return null
if the parent element has no such child element.
*/
@Nullable
public static Element getFirstChildElementOfName (@Nullable final Node aStartNode,
@Nonnull @Nonempty final String sTagName)
{
if (aStartNode == null)
return null;
return new ChildElementIterator (aStartNode).findFirst (filterElementWithTagName (sTagName));
}
/**
* Search all child nodes of the given for the first element that has the
* specified tag name.
*
* @param aStartNode
* The parent element to be searched. May be null
.
* @param sNamespaceURI
* Namespace URI to search. May be null
.
* @param sLocalName
* The tag name to search.
* @return null
if the parent element has no such child element.
*/
@Nullable
public static Element getFirstChildElementOfName (@Nullable final Node aStartNode,
@Nullable final String sNamespaceURI,
@Nonnull @Nonempty final String sLocalName)
{
if (aStartNode == null)
return null;
return new ChildElementIterator (aStartNode).findFirst (filterElementWithNamespaceAndLocalName (sNamespaceURI,
sLocalName));
}
/**
* Find a direct child using multiple levels, starting from a given start
* element.
*
* @param aStartElement
* The element to start from. May be null
.
* @param aTagNames
* The child elements to be found in order. May neither be
* null
nor empty and may not contain null
* elements.
* @return null
if no such child element was found, of if the
* start element was null
.
* @see #getFirstChildElementOfName(Node, String)
* @since 10.1.2
*/
@Nullable
public static Element getChildElementOfNames (@Nullable final Element aStartElement,
@Nonnull final String... aTagNames)
{
ValueEnforcer.notEmptyNoNullValue (aTagNames, "TagNames");
Element aCurElement = aStartElement;
if (aCurElement != null)
for (final String sTagName : aTagNames)
{
aCurElement = getFirstChildElementOfName (aCurElement, sTagName);
if (aCurElement == null)
return null;
}
return aCurElement;
}
@Nonnull
public static Node append (@Nonnull final Node aParentNode, @Nullable final Object aChild)
{
ValueEnforcer.notNull (aParentNode, "ParentNode");
if (aChild != null)
if (aChild instanceof Document)
{
// Special handling for Document comes first, as this is a special case
// of "Node"
// Cannot add complete documents!
append (aParentNode, ((Document) aChild).getDocumentElement ());
}
else
if (aChild instanceof Node)
{
// directly append Node
final Node aChildNode = (Node) aChild;
final Document aParentDoc = getOwnerDocument (aParentNode);
if (getOwnerDocument (aChildNode).equals (aParentDoc))
{
// Nodes have the same parent
aParentNode.appendChild (aChildNode);
}
else
{
// Node to be added belongs to a different document
aParentNode.appendChild (aParentDoc.adoptNode (aChildNode.cloneNode (true)));
}
}
else
if (aChild instanceof String)
{
// append a string node
aParentNode.appendChild (getOwnerDocument (aParentNode).createTextNode ((String) aChild));
}
else
if (aChild instanceof Iterable >)
{
// it's a nested collection -> recursion
for (final Object aSubChild : (Iterable >) aChild)
append (aParentNode, aSubChild);
}
else
if (ArrayHelper.isArray (aChild))
{
// it's a nested collection -> recursion
for (final Object aSubChild : (Object []) aChild)
append (aParentNode, aSubChild);
}
else
{
// unsupported type
throw new IllegalArgumentException ("Passed object cannot be appended to a DOMNode (type=" +
aChild.getClass ().getName () +
".");
}
return aParentNode;
}
public static void append (@Nonnull final Node aParentNode, @Nonnull final Iterable > aNodesToAppend)
{
ValueEnforcer.notNull (aParentNode, "ParentNode");
for (final Object aNode : aNodesToAppend)
append (aParentNode, aNode);
}
@Nonnegative
public static int getDirectChildElementCount (@Nullable final Element aParent)
{
return aParent == null ? 0 : CollectionHelper.getSize (getChildElementIterator (aParent));
}
@Nonnegative
public static int getDirectChildElementCountNoNS (@Nullable final Element aParent)
{
return aParent == null ? 0 : CollectionHelper.getSize (getChildElementIteratorNoNS (aParent));
}
@Nonnegative
public static int getDirectChildElementCount (@Nullable final Element aParent,
@Nonnull @Nonempty final String sTagName)
{
return aParent == null ? 0 : CollectionHelper.getSize (getChildElementIterator (aParent, sTagName));
}
@Nonnegative
public static int getDirectChildElementCountNoNS (@Nullable final Element aParent,
@Nonnull @Nonempty final String sTagName)
{
return aParent == null ? 0 : CollectionHelper.getSize (getChildElementIteratorNoNS (aParent, sTagName));
}
@Nonnegative
public static int getDirectChildElementCountNS (@Nullable final Element aParent, @Nullable final String sNamespaceURI)
{
return aParent == null ? 0 : CollectionHelper.getSize (getChildElementIteratorNS (aParent, sNamespaceURI));
}
@Nonnegative
public static int getDirectChildElementCountNS (@Nullable final Element aParent,
@Nullable final String sNamespaceURI,
@Nonnull @Nonempty final String sLocalName)
{
return aParent == null ? 0
: CollectionHelper.getSize (getChildElementIteratorNS (aParent, sNamespaceURI, sLocalName));
}
/**
* Get an iterator over all child elements.
*
* @param aStartNode
* the parent element
* @return a non-null Iterator
*/
@Nonnull
public static IIterableIterator getChildElementIterator (@Nullable final Node aStartNode)
{
return new ChildElementIterator (aStartNode);
}
/**
* Get an iterator over all child elements that have no namespace.
*
* @param aStartNode
* the parent element
* @return a non-null Iterator
*/
@Nonnull
public static IIterableIterator getChildElementIteratorNoNS (@Nullable final Node aStartNode)
{
return new ChildElementIterator (aStartNode).withFilter (filterElementWithoutNamespace ());
}
/**
* Get an iterator over all child elements that have no namespace and the
* desired tag name.
*
* @param aStartNode
* the parent element
* @param sTagName
* the name of the tag that is desired
* @return a non-null Iterator
* @throws IllegalArgumentException
* if the passed tag name is null or empty
*/
@Nonnull
public static IIterableIterator getChildElementIteratorNoNS (@Nullable final Node aStartNode,
@Nonnull @Nonempty final String sTagName)
{
return new ChildElementIterator (aStartNode).withFilter (filterElementWithTagNameNoNS (sTagName));
}
/**
* Get an iterator over all child elements that have the desired tag name (but
* potentially a namespace URI).
*
* @param aStartNode
* the parent element
* @param sTagName
* the name of the tag that is desired
* @return a non-null Iterator
* @throws IllegalArgumentException
* if the passed tag name is null or empty
*/
@Nonnull
public static IIterableIterator getChildElementIterator (@Nullable final Node aStartNode,
@Nonnull @Nonempty final String sTagName)
{
return new ChildElementIterator (aStartNode).withFilter (filterElementWithTagName (sTagName));
}
@Nonnull
public static IIterableIterator getChildElementIteratorNS (@Nullable final Node aStartNode,
@Nullable final String sNamespaceURI)
{
return new ChildElementIterator (aStartNode).withFilter (filterElementWithNamespace (sNamespaceURI));
}
@Nonnull
public static IIterableIterator getChildElementIteratorNS (@Nullable final Node aStartNode,
@Nullable final String sNamespaceURI,
@Nonnull @Nonempty final String sLocalName)
{
return new ChildElementIterator (aStartNode).withFilter (filterElementWithNamespaceAndLocalName (sNamespaceURI,
sLocalName));
}
public static boolean hasSameElementName (@Nonnull final Element aFirst, @Nonnull final Element aSecond)
{
final String sFirstNS = aFirst.getNamespaceURI ();
final String sSecondNS = aSecond.getNamespaceURI ();
if (StringHelper.hasText (sFirstNS))
{
// NS + local name
return sFirstNS.equals (sSecondNS) && aFirst.getLocalName ().equals (aSecond.getLocalName ());
}
// No NS + tag name
return StringHelper.hasNoText (sSecondNS) && aFirst.getTagName ().equals (aSecond.getTagName ());
}
@Nonnull
private static String _getPathToNode (@Nonnull final Node aNode,
@Nonnull final String sSep,
final boolean bExcludeDocumentNode,
final boolean bZeroBasedIndex,
final boolean bForceUseIndex,
final boolean bTrailingSeparator,
final boolean bCompareIncludingNamespaceURI,
@Nullable final NamespaceContext aNamespaceCtx)
{
ValueEnforcer.notNull (aNode, "Node");
ValueEnforcer.notNull (sSep, "Separator");
final Function funGetNSPrefix = aNamespaceCtx == null ? ns -> "" : ns -> {
if (StringHelper.hasText (ns))
{
final String sPrefix = aNamespaceCtx.getPrefix (ns);
if (StringHelper.hasText (sPrefix))
return sPrefix + ":";
}
return "";
};
final StringBuilder aRet = new StringBuilder (128);
Node aCurNode = aNode;
while (aCurNode != null)
{
final short nNodeType = aCurNode.getNodeType ();
if (bExcludeDocumentNode && nNodeType == Node.DOCUMENT_NODE && aRet.length () > 0)
{
// Avoid printing the content of the document node, if something else
// is already present
// Add leading separator
aRet.insert (0, sSep);
break;
}
final String sNamespaceURI = aCurNode.getNamespaceURI ();
final StringBuilder aName = new StringBuilder ();
if (nNodeType == Node.ATTRIBUTE_NODE)
aName.append ('@');
if (StringHelper.hasText (sNamespaceURI))
{
aName.append (funGetNSPrefix.apply (sNamespaceURI));
aName.append (aCurNode.getLocalName ());
}
else
aName.append (aCurNode.getNodeName ());
final Node aParentNode;
if (nNodeType == Node.ATTRIBUTE_NODE)
aParentNode = ((Attr) aCurNode).getOwnerElement ();
else
aParentNode = aCurNode.getParentNode ();
// Is there a parent node to work on?
if (aParentNode != null)
{
// Attribute nodes don't have a parent node, so it is not possible to
// construct the path
if (nNodeType == Node.ELEMENT_NODE)
{
// Differentiate between root element and nested child element
if (aParentNode.getNodeType () == Node.ELEMENT_NODE)
{
// get index of current element in parent element
final Element aCurElement = (Element) aCurNode;
int nParentChildCountWithName = 0;
int nMatchingIndex = -1;
for (final Element aCurParentChild : new ChildElementIterator (aParentNode))
{
if (EqualsHelper.identityEqual (aCurParentChild, aCurNode))
{
// 0-based index
nMatchingIndex = nParentChildCountWithName;
}
final boolean bMatches;
if (bCompareIncludingNamespaceURI)
bMatches = hasSameElementName (aCurParentChild, aCurElement);
else
bMatches = aCurParentChild.getTagName ().equals (aCurElement.getTagName ());
if (bMatches)
++nParentChildCountWithName;
}
if (nMatchingIndex < 0)
throw new IllegalStateException ("Failed to find Node with name '" +
aCurElement.getTagName () +
"' at parent");
if (nParentChildCountWithName > 1 || bForceUseIndex)
{
// Append index only, if more than one element is present
aName.append ('[').append (bZeroBasedIndex ? nMatchingIndex : nMatchingIndex + 1).append (']');
}
}
else
{
// This is the root element - special case
if (bForceUseIndex)
{
// Append index only, if more than one element is present
aName.append ('[').append (bZeroBasedIndex ? 0 : 1).append (']');
}
}
}
}
if (aRet.length () > 0)
{
// Avoid trailing separator
aRet.insert (0, sSep);
}
aRet.insert (0, aName);
// goto parent
aCurNode = aParentNode;
}
if (bTrailingSeparator && aRet.length () > 0)
aRet.append (sSep);
return aRet.toString ();
}
/**
* Builder class for the different possibilities to get the path of a node
*
* @author Philip Helger
* @since 10.2.2
*/
@NotThreadSafe
public static class PathToNodeBuilder implements IBuilder
{
private Node m_aNode;
private String m_sSeperator;
private boolean m_bExcludeDocumentNode;
private boolean m_bZeroBasedIndex;
private boolean m_bForceUseIndex;
private boolean m_bTrailingSeparator;
private boolean m_bCompareIncludingNamespaceURI;
private NamespaceContext m_aNamespaceCtx;
public PathToNodeBuilder ()
{
separator ("/");
excludeDocumentNode (true);
zeroBasedIndex (false);
forceUseIndex (false);
trailingSeparator (false);
compareIncludingNamespaceURI (true);
}
/**
* The node to get the path from.
*
* @param a
* Node to be used. Should not be null
.
* @return this for chaining
*/
@Nonnull
public PathToNodeBuilder node (@Nullable final Node a)
{
m_aNode = a;
return this;
}
/**
* Set the separator to be used.
*
* @param c
* The separator char
* @return this for chaining
*/
@Nonnull
public PathToNodeBuilder separator (final char c)
{
return separator (Character.toString (c));
}
/**
* Set the separator to be used.
*
* @param s
* The separator string. Should not be null
.
* @return this for chaining
*/
@Nonnull
public PathToNodeBuilder separator (@Nullable final String s)
{
m_sSeperator = s;
return this;
}
/**
* Determine to include or exclude the document node in the resulting path.
*
* @param b
* true
to exclude it, false
to include it
* @return this for chaining
*/
@Nonnull
public PathToNodeBuilder excludeDocumentNode (final boolean b)
{
m_bExcludeDocumentNode = b;
return this;
}
/**
* Shortcut to exclude the document Node from the resulting output
*
* @return this for chaining
* @see #excludeDocumentNode(boolean)
*/
@Nonnull
public PathToNodeBuilder excludeDocumentNode ()
{
return excludeDocumentNode (true);
}
/**
* Shortcut to include the document Node in the resulting output
*
* @return this for chaining
* @see #excludeDocumentNode(boolean)
*/
@Nonnull
public PathToNodeBuilder includeDocumentNode ()
{
return excludeDocumentNode (false);
}
/**
* Determine whether a 0-based index or a 1-based index should be used. For
* XPath usage etc. a 1-based index should be used. For a 0-based index the
* first element uses [0]
and for a 1-based index this is
* [1]
.
*
* @param b
* true
to use a 0-based index, false
* @return this for chaining.
*/
@Nonnull
public PathToNodeBuilder zeroBasedIndex (final boolean b)
{
m_bZeroBasedIndex = b;
return this;
}
/**
* Shortcut to enable the usage of a zero based index.
*
* @return this for chaining
* @see #zeroBasedIndex(boolean)
*/
@Nonnull
public PathToNodeBuilder zeroBasedIndex ()
{
return zeroBasedIndex (true);
}
/**
* Shortcut to enable the usage of a one based index.
*
* @return this for chaining
* @see #zeroBasedIndex(boolean)
*/
@Nonnull
public PathToNodeBuilder oneBasedIndex ()
{
return zeroBasedIndex (false);
}
/**
* Enable or disable the force of an index. If the index is forced, the
* [0]
or [1]
, depending on
* {@link #zeroBasedIndex(boolean)} is always emitted, even if only one
* element exists. If this is disabled and only element exists, the index is
* not emitted.
*
* @param b
* true
to force the index, false
to omit
* it if possible.
* @return this for chaining
*/
@Nonnull
public PathToNodeBuilder forceUseIndex (final boolean b)
{
m_bForceUseIndex = b;
return this;
}
/**
* Enable or disable the usage of a trailing separator. If enabled, the
* output is e.g. element/
compared to the output
* element
if the trailing separator is disabled.
*
* @param b
* true
to use a trailing separator, false
* to omit it.
* @return this for chaining
*/
@Nonnull
public PathToNodeBuilder trailingSeparator (final boolean b)
{
m_bTrailingSeparator = b;
return this;
}
/**
* Compare with namespace URI and local name, or just with the tag name.
*
* @param b
* true
to compare with namespace URI,
* false
to compare without namespace URI
* @return this for chaining
*/
@Nonnull
public PathToNodeBuilder compareIncludingNamespaceURI (final boolean b)
{
m_bCompareIncludingNamespaceURI = b;
return this;
}
/**
* Set the optional namespace context to be used for emitting prefixes. This
* is optional.
*
* @param a
* The namespace context to be used. May be null
.
* @return this for chaining
*/
@Nonnull
public PathToNodeBuilder namespaceContext (@Nullable final NamespaceContext a)
{
m_aNamespaceCtx = a;
return this;
}
@Nonnull
public String build ()
{
if (m_aNode == null)
throw new IllegalStateException ("A source Node need to be provided");
// Empty separator makes not much sense, but who knows....
if (m_sSeperator == null)
throw new IllegalStateException ("A non-null separator needs to be provided");
return _getPathToNode (m_aNode,
m_sSeperator,
m_bExcludeDocumentNode,
m_bZeroBasedIndex,
m_bForceUseIndex,
m_bTrailingSeparator,
m_bCompareIncludingNamespaceURI,
m_aNamespaceCtx);
}
}
@Nonnull
public static PathToNodeBuilder pathToNodeBuilder ()
{
return new PathToNodeBuilder ();
}
/**
* Shortcut for {@link #getPathToNode(Node, String)} using "/" as the
* separator.
*
* @param aNode
* The node to check.
* @return A non-null
path.
*/
@Nonnull
public static String getPathToNode (@Nonnull final Node aNode)
{
return getPathToNode (aNode, "/");
}
/**
* Get the path from root node to the passed node. This includes all nodes up
* to the document node!
*
* @param aNode
* The node to start. May not be null
.
* @param sSep
* The separator string to use. May not be null
.
* @return The path to the node.
*/
@Nonnull
public static String getPathToNode (@Nonnull final Node aNode, @Nonnull final String sSep)
{
return pathToNodeBuilder ().node (aNode)
.separator (sSep)
.includeDocumentNode ()
.zeroBasedIndex ()
.forceUseIndex (true)
.trailingSeparator (true)
.compareIncludingNamespaceURI (false)
.build ();
}
/**
* Shortcut for {@link #getPathToNode2(Node,String)} using "/" as the
* separator.
*
* @param aNode
* The node to check.
* @return A non-null
path.
*/
@Nonnull
public static String getPathToNode2 (@Nonnull final Node aNode)
{
return getPathToNode2 (aNode, "/");
}
/**
* Get the path from root node to the passed node. This includes all nodes but
* excluding the document node!
*
* @param aNode
* The node to start. May not be null
.
* @param sSep
* The separator string to use. May not be null
.
* @return The path to the node.
*/
@Nonnull
public static String getPathToNode2 (@Nonnull final Node aNode, @Nonnull final String sSep)
{
return pathToNodeBuilder ().node (aNode)
.separator (sSep)
.excludeDocumentNode ()
.zeroBasedIndex ()
.forceUseIndex (false)
.trailingSeparator (false)
.compareIncludingNamespaceURI (false)
.build ();
}
/**
* Remove all child nodes of the given node.
*
* @param aElement
* The element whose children are to be removed.
*/
public static void removeAllChildElements (@Nonnull final Element aElement)
{
ValueEnforcer.notNull (aElement, "Element");
while (aElement.getChildNodes ().getLength () > 0)
aElement.removeChild (aElement.getChildNodes ().item (0));
}
/**
* Get the content of the first Text child element of the passed element.
*
* @param aStartNode
* the element to scan for a TextNode child
* @return null
if the element contains no text node as child
*/
@Nullable
public static String getFirstChildText (@Nullable final Node aStartNode)
{
return NodeListIterator.createChildNodeIterator (aStartNode)
.findFirstMapped (x -> x instanceof Text && !((Text) x).isElementContentWhitespace (),
x -> ((Text) x).getData ());
}
/**
* The latest version of XercesJ 2.9 returns an empty string for non existing
* attributes. To differentiate between empty attributes and non-existing
* attributes, this method returns null for non existing attributes.
*
* @param aElement
* the source element to get the attribute from
* @param sAttrName
* the name of the attribute to query
* @return null
if the attribute does not exists, the string
* value otherwise
*/
@Nullable
public static String getAttributeValue (@Nonnull final Element aElement, @Nonnull final String sAttrName)
{
return getAttributeValue (aElement, sAttrName, null);
}
/**
* The latest version of XercesJ 2.9 returns an empty string for non existing
* attributes. To differentiate between empty attributes and non-existing
* attributes, this method returns a default value for non existing
* attributes.
*
* @param aElement
* the source element to get the attribute from. May not be
* null
.
* @param sAttrName
* the name of the attribute to query. May not be null
.
* @param sDefault
* the value to be returned if the attribute is not present.
* @return the default value if the attribute does not exists, the string
* value otherwise
*/
@Nullable
public static String getAttributeValue (@Nonnull final Element aElement,
@Nonnull final String sAttrName,
@Nullable final String sDefault)
{
final Attr aAttr = aElement.getAttributeNode (sAttrName);
return aAttr == null ? sDefault : aAttr.getValue ();
}
/**
* The latest version of XercesJ 2.9 returns an empty string for non existing
* attributes. To differentiate between empty attributes and non-existing
* attributes, this method returns null for non existing attributes.
*
* @param aElement
* the source element to get the attribute from
* @param sNamespaceURI
* The namespace URI of the attribute to retrieve. May be
* null
.
* @param sAttrName
* the name of the attribute to query
* @return null
if the attribute does not exists, the string
* value otherwise
*/
@Nullable
public static String getAttributeValueNS (@Nonnull final Element aElement,
@Nullable final String sNamespaceURI,
@Nonnull final String sAttrName)
{
return getAttributeValueNS (aElement, sNamespaceURI, sAttrName, null);
}
/**
* The latest version of XercesJ 2.9 returns an empty string for non existing
* attributes. To differentiate between empty attributes and non-existing
* attributes, this method returns a default value for non existing
* attributes.
*
* @param aElement
* the source element to get the attribute from. May not be
* null
.
* @param sNamespaceURI
* The namespace URI of the attribute to retrieve. May be
* null
.
* @param sAttrName
* the name of the attribute to query. May not be null
.
* @param sDefault
* the value to be returned if the attribute is not present.
* @return the default value if the attribute does not exists, the string
* value otherwise
*/
@Nullable
public static String getAttributeValueNS (@Nonnull final Element aElement,
@Nullable final String sNamespaceURI,
@Nonnull final String sAttrName,
@Nullable final String sDefault)
{
final Attr aAttr = aElement.getAttributeNodeNS (sNamespaceURI, sAttrName);
return aAttr == null ? sDefault : aAttr.getValue ();
}
@Nonnull
@ReturnsMutableCopy
public static ICommonsList getAllAttributesAsList (@Nullable final Element aSrcNode)
{
final ICommonsList ret = new CommonsArrayList <> ();
NamedNodeMapIterator.createAttributeIterator (aSrcNode).forEach (x -> ret.add ((Attr) x));
return ret;
}
@Nonnull
@ReturnsMutableCopy
public static ICommonsOrderedMap getAllAttributesAsMap (@Nullable final Element aSrcNode)
{
final ICommonsOrderedMap ret = new CommonsLinkedHashMap <> ();
// Cast needed for Oracle JDK 8
forAllAttributes (aSrcNode, (BiConsumer super String, ? super String>) ret::put);
return ret;
}
public static void forAllAttributes (@Nullable final Element aSrcNode,
@Nonnull final Consumer super Attr> aConsumer)
{
NamedNodeMapIterator.createAttributeIterator (aSrcNode).forEach (x -> aConsumer.accept ((Attr) x));
}
public static void forAllAttributes (@Nullable final Element aSrcNode,
@Nonnull final BiConsumer super String, ? super String> aConsumer)
{
forAllAttributes (aSrcNode, x -> aConsumer.accept (x.getName (), x.getValue ()));
}
/**
* Get the full qualified attribute name to use for the given namespace
* prefix. The result will e.g. be xmlns
or
* {http://www.w3.org/2000/xmlns/}xmlns:foo
.
*
* @param sNSPrefix
* The namespace prefix to build the attribute name from. May be
* null
or empty.
* @return If the namespace prefix is empty (if it equals
* {@link XMLConstants#DEFAULT_NS_PREFIX} or null
) than
* "xmlns" is returned, else "xmlns:prefix" is returned.
*/
@Nonnull
public static QName getXMLNSAttrQName (@Nullable final String sNSPrefix)
{
if (sNSPrefix != null)
ValueEnforcer.isFalse (sNSPrefix.contains (CXML.XML_PREFIX_NAMESPACE_SEP_STR),
() -> "prefix is invalid: " + sNSPrefix);
if (sNSPrefix == null || sNSPrefix.equals (XMLConstants.DEFAULT_NS_PREFIX))
{
// Default (empty) namespace prefix
return new QName (XMLConstants.XMLNS_ATTRIBUTE_NS_URI, XMLConstants.XMLNS_ATTRIBUTE);
}
// Named XML namespace prefix
return new QName (XMLConstants.XMLNS_ATTRIBUTE_NS_URI, sNSPrefix, XMLConstants.XMLNS_ATTRIBUTE);
}
/**
* Get the namespace prefix of the passed element in a safe way.
*
* @param aElement
* The element to be queried. May be null
.
* @return {@link XMLConstants#DEFAULT_NS_PREFIX} or the provided prefix.
* Never null
.
* @since 8.4.1
*/
@Nonnull
public static String getPrefix (@Nullable final Element aElement)
{
final String sPrefix = aElement == null ? null : aElement.getPrefix ();
return sPrefix == null ? XMLConstants.DEFAULT_NS_PREFIX : sPrefix;
}
/**
* Get the QName of the passed element. If the passed element has no namespace
* URI, only the tag name is used. Otherwise namespace URI, local name and
* prefix are used.
*
* @param aElement
* The element to be used. May not be null
.
* @return The created {@link QName}.
* @since 8.4.1
*/
@Nonnull
public static QName getQName (@Nonnull final Element aElement)
{
final String sNamespaceURI = aElement.getNamespaceURI ();
if (sNamespaceURI == null)
return new QName (aElement.getTagName ());
return new QName (sNamespaceURI, aElement.getLocalName (), getPrefix (aElement));
}
/**
* Iterate all child nodes of the provided element NOT recursive. The provided
* consumer is invoked for every child node. Please note: the Consumer is not
* invoked for the parent element itself.
*
* @param aParent
* The parent node to start from. May not be null
.
* @param aConsumer
* The Consumer to be invoked for every node. May not be
* null
.
* @since 10.1.7
*/
public static void iterateChildren (@Nonnull final Node aParent, @Nonnull final Consumer super Node> aConsumer)
{
ValueEnforcer.notNull (aParent, "Parent");
ValueEnforcer.notNull (aConsumer, "Consumer");
final NodeList aNodeList = aParent.getChildNodes ();
if (aNodeList != null)
{
final int nChildCount = aNodeList.getLength ();
for (int i = 0; i < nChildCount; ++i)
{
final Node aCurrent = aNodeList.item (i);
aConsumer.accept (aCurrent);
}
}
}
private static void _recursiveIterateChildren (@Nonnull final Node aParent,
@Nonnull final Consumer super Node> aConsumer)
{
final NodeList aNodeList = aParent.getChildNodes ();
if (aNodeList != null)
{
final int nChildCount = aNodeList.getLength ();
for (int i = 0; i < nChildCount; ++i)
{
final Node aCurrent = aNodeList.item (i);
aConsumer.accept (aCurrent);
_recursiveIterateChildren (aCurrent, aConsumer);
}
}
}
/**
* Recursively iterate all children of the provided element. The provided
* consumer is invoked for every child node. Please note: the Consumer is not
* invoked for the parent element itself.
*
* @param aParent
* The parent node to start from. May not be null
.
* @param aConsumer
* The Consumer to be invoked for every node. May not be
* null
.
* @since 10.1.7
*/
public static void recursiveIterateChildren (@Nonnull final Node aParent,
@Nonnull final Consumer super Node> aConsumer)
{
ValueEnforcer.notNull (aParent, "Parent");
ValueEnforcer.notNull (aConsumer, "Consumer");
// Use a private method to avoid the ValueEnforcer is called over and over
// again
_recursiveIterateChildren (aParent, aConsumer);
}
}