4 * Copyright 2010 Codist Monk.
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 package net
.sourceforge
.aprog
.xml
;
27 import static java
.util
.Arrays
.asList
;
28 import static java
.util
.Collections
.unmodifiableList
;
30 import static net
.sourceforge
.aprog
.tools
.Tools
.*;
32 import java
.io
.ByteArrayInputStream
;
33 import java
.io
.ByteArrayOutputStream
;
35 import java
.io
.InputStream
;
36 import java
.io
.OutputStream
;
37 import java
.io
.OutputStreamWriter
;
38 import java
.util
.ArrayList
;
39 import java
.util
.LinkedHashMap
;
40 import java
.util
.List
;
41 import java
.util
.Locale
;
43 import java
.util
.logging
.Level
;
45 import javax
.xml
.XMLConstants
;
46 import javax
.xml
.namespace
.QName
;
47 import javax
.xml
.parsers
.DocumentBuilderFactory
;
48 import javax
.xml
.parsers
.ParserConfigurationException
;
49 import javax
.xml
.parsers
.SAXParserFactory
;
50 import javax
.xml
.transform
.OutputKeys
;
51 import javax
.xml
.transform
.Result
;
52 import javax
.xml
.transform
.Source
;
53 import javax
.xml
.transform
.Transformer
;
54 import javax
.xml
.transform
.TransformerConfigurationException
;
55 import javax
.xml
.transform
.TransformerException
;
56 import javax
.xml
.transform
.TransformerFactory
;
57 import javax
.xml
.transform
.dom
.DOMSource
;
58 import javax
.xml
.transform
.stream
.StreamResult
;
59 import javax
.xml
.transform
.stream
.StreamSource
;
60 import javax
.xml
.validation
.SchemaFactory
;
61 import javax
.xml
.validation
.Validator
;
62 import javax
.xml
.xpath
.XPathConstants
;
63 import javax
.xml
.xpath
.XPathFactory
;
65 import net
.sourceforge
.aprog
.tools
.IllegalInstantiationException
;
67 import org
.w3c
.dom
.Attr
;
68 import org
.w3c
.dom
.Document
;
69 import org
.w3c
.dom
.Element
;
70 import org
.w3c
.dom
.Node
;
71 import org
.w3c
.dom
.NodeList
;
72 import org
.w3c
.dom
.events
.DocumentEvent
;
73 import org
.w3c
.dom
.events
.EventListener
;
74 import org
.w3c
.dom
.events
.EventTarget
;
75 import org
.w3c
.dom
.events
.MutationEvent
;
76 import org
.xml
.sax
.InputSource
;
77 import org
.xml
.sax
.SAXException
;
78 import org
.xml
.sax
.SAXParseException
;
79 import org
.xml
.sax
.helpers
.DefaultHandler
;
83 * @author codistmonk (creation 2010-07-02)
85 public final class XMLTools
{
88 * @throws IllegalInstantiationException To prevent instantiation
91 throw new IllegalInstantiationException();
97 public static final String XML_1_0_UTF8
=
98 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
103 public static final String XML_1_0_UTF8_STANDALONE_NO
=
104 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>";
109 public static final String XML_1_0_UTF8_STANDALONE_YES
=
110 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
115 public static final String DOM_EVENT_SUBTREE_MODIFIED
= "DOMSubtreeModified";
120 public static final String DOM_EVENT_NODE_INSERTED
= "DOMNodeInserted";
125 public static final String DOM_EVENT_NODE_REMOVED
= "DOMNodeRemoved";
130 public static final String DOM_EVENT_NODE_REMOVED_FROM_DOCUMENT
= "DOMNodeRemovedFromDocument";
135 public static final String DOM_EVENT_NODE_INSERTED_INTO_DOCUMENT
= "DOMNodeInsertedIntoDocument";
140 public static final String DOM_EVENT_ATTRIBUTE_MODIFIED
= "DOMAttrModified";
145 public static final String DOM_EVENT_CHARACTER_DATA_MODIFIED
= "DOMCharacterDataModified";
147 public static final List
<String
> DOM_EVENT_TYPES
= unmodifiableList(asList(
148 DOM_EVENT_ATTRIBUTE_MODIFIED
,
149 DOM_EVENT_CHARACTER_DATA_MODIFIED
,
150 DOM_EVENT_NODE_INSERTED
,
151 DOM_EVENT_NODE_INSERTED_INTO_DOCUMENT
,
152 DOM_EVENT_NODE_REMOVED
,
153 DOM_EVENT_NODE_REMOVED_FROM_DOCUMENT
,
154 DOM_EVENT_SUBTREE_MODIFIED
165 * @throws IllegalArgumentException If {@code node} doesn't have the "events 2.0" feature
167 public static final void addDOMEventListener(final Node node
, final EventListener listener
) {
168 checkHasEventsFeature(node
);
170 for (final String eventType
: DOM_EVENT_TYPES
) {
171 ((EventTarget
) node
).addEventListener(eventType
, listener
, false);
183 * @throws IllegalArgumentException If {@code node} doesn't have the "events 2.0" feature
185 public static final void removeDOMEventListener(final Node node
, final EventListener listener
) {
186 checkHasEventsFeature(node
);
188 for (final String eventType
: DOM_EVENT_TYPES
) {
189 ((EventTarget
) node
).removeEventListener(eventType
, listener
, false);
197 * @param namespaceURI
199 * @param qualifiedName
205 public static final Node
rename(final Node node
, final String namespaceURI
, final String qualifiedName
) {
206 final String oldQualifiedName
= getQualifiedName(node
);
207 final Document document
= getOwnerDocument(node
);
208 final Node result
= document
.renameNode(node
, namespaceURI
, qualifiedName
);
210 if (hasEventsFeature(node
) && node
.getNodeType() == Node
.ELEMENT_NODE
) {
211 final MutationEvent event
= (MutationEvent
) ((DocumentEvent
) document
).createEvent("MutationEvent");
213 event
.initMutationEvent(DOM_EVENT_SUBTREE_MODIFIED
, true, true,
214 node
, oldQualifiedName
, qualifiedName
,
217 ((EventTarget
) result
).dispatchEvent(event
);
227 * @throws IllegalArgumentException If {@code node} doesn't have the "events 2.0" feature
229 public static final void checkHasEventsFeature(final Node node
) {
230 if (!hasEventsFeature(node
)) {
231 throw new IllegalArgumentException("Events 2.0 feature unavailable for node " + node
);
239 * @return {@code true} if {@code node} has the "events 2.0" feature
241 public static final boolean hasEventsFeature(final Node node
) {
242 return XMLTools
.getOwnerDocument(node
).getImplementation().hasFeature("events", "2.0");
252 public static final String
getQualifiedName(final Node node
) {
253 return node
.getPrefix() == null ? node
.getNodeName() : node
.getPrefix() + ":" + node
.getLocalName();
263 * @throws RuntimeException if an error occurs
265 public static final Document
parse(final String xmlInput
) {
266 return parse(new ByteArrayInputStream(xmlInput
.getBytes()));
271 * @param xmlInputStream
277 * @throws RuntimeException if an error occurs
279 public static final Document
parse(final InputStream xmlInputStream
) {
280 return parse(new InputSource(xmlInputStream
));
291 * @throws RuntimeException if an error occurs
293 public static final Document
parse(final InputSource inputSource
) {
295 return DocumentBuilderFactory
.newInstance().newDocumentBuilder().parse(inputSource
);
296 } catch (final Exception exception
) {
297 throw unchecked(exception
);
307 public static final Document
newDocument() {
309 final Document result
= DocumentBuilderFactory
.newInstance().newDocumentBuilder().newDocument();
311 result
.setXmlStandalone(true);
314 } catch (final ParserConfigurationException exception
) {
315 throw unchecked(exception
);
324 * @return {@code document}
327 public static final Document
normalize(final Document document
) {
328 document
.normalize();
338 * @return {@code document}
341 public static final Document
standalone(final Document document
) {
342 document
.setXmlStandalone(true);
355 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
357 public static final void write(final Node node
, final File outputFile
, final int indent
) {
358 write(node
, new StreamResult(outputFile
.getAbsolutePath()), indent
);
369 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
371 public static final void write(final Node node
, final OutputStream output
, final int indent
) {
372 write(node
, new StreamResult(new OutputStreamWriter(output
)), indent
);
383 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
385 public static final void write(final Node node
, final Result output
, final int indent
) {
387 final TransformerFactory transformerFactory
= TransformerFactory
.newInstance();
389 transformerFactory
.setAttribute("indent-number", indent
);
391 final Transformer transformer
= transformerFactory
.newTransformer();
393 transformer
.setOutputProperty(OutputKeys
.METHOD
, "xml");
394 transformer
.setOutputProperty(OutputKeys
.INDENT
, indent
!= 0 ?
"yes" : "no");
395 transformer
.setOutputProperty(OutputKeys
.OMIT_XML_DECLARATION
,
396 node
instanceof Document ?
"no" : "yes");
397 transformer
.setOutputProperty(OutputKeys
.STANDALONE
, "yes");
399 transformer
.transform(new DOMSource(node
), output
);
400 } catch (final Exception exception
) {
401 throw unchecked(exception
);
407 * @param resourceName
413 public static final Source
getResourceAsSource(final String resourceName
) {
414 final ClassLoader classLoader
= getCallerClass().getClassLoader();
415 final Source result
= new StreamSource(classLoader
.getResourceAsStream(resourceName
));
417 result
.setSystemId(classLoader
.getResource(resourceName
).toString());
430 public static final List
<Node
> toList(final NodeList nodeList
) {
431 final List
<Node
> result
= new ArrayList
<Node
>();
433 for (int i
= 0; i
< nodeList
.getLength(); ++i
) {
434 result
.add(nodeList
.item(i
));
441 * Validates the XML input against the specified DTD or schema.
443 * @param xmlInputStream
447 * @return An empty list if validation succeeds
451 public static final List
<Throwable
> validate(final InputStream xmlInputStream
, final Source dtdOrSchema
) {
452 final List
<Throwable
> exceptions
= new ArrayList
<Throwable
>();
453 final String schemaLanguage
= getSchemaLanguage(dtdOrSchema
);
456 if (schemaLanguage
!= null) {
457 final Validator validator
= SchemaFactory
.newInstance(schemaLanguage
)
458 .newSchema(dtdOrSchema
).newValidator();
460 validator
.validate(new StreamSource(xmlInputStream
));
462 final Transformer addDoctypeInformation
= TransformerFactory
.newInstance().newTransformer();
464 addDoctypeInformation
.setOutputProperty(OutputKeys
.DOCTYPE_SYSTEM
, dtdOrSchema
.getSystemId());
466 final ByteArrayOutputStream buffer
= new ByteArrayOutputStream();
468 addDoctypeInformation
.transform(new StreamSource(xmlInputStream
), new StreamResult(buffer
));
470 final SAXParserFactory saxParserFactory
= SAXParserFactory
.newInstance();
472 saxParserFactory
.setValidating(true);
473 saxParserFactory
.newSAXParser().parse(
474 new ByteArrayInputStream(buffer
.toByteArray()), new DefaultHandler() {
477 public final void error(final SAXParseException exception
) throws SAXException
{
478 exceptions
.add(exception
);
480 getLoggerForThisMethod().log(Level
.WARNING
, "", exceptions
);
484 public final void fatalError(final SAXParseException exception
) throws SAXException
{
485 exceptions
.add(exception
);
487 getLoggerForThisMethod().log(Level
.WARNING
, "", exceptions
);
493 public final void warning(final SAXParseException exception
) throws SAXException
{
494 getLoggerForThisMethod().log(Level
.WARNING
, "", exceptions
);
499 } catch (final TransformerConfigurationException exception
) {
500 throw unchecked(exception
);
501 } catch (final TransformerException exception
) {
502 exceptions
.add(exception
);
503 } catch (final SAXException exception
) {
504 exceptions
.add(exception
);
505 } catch (final Exception exception
) {
506 throw unchecked(exception
);
513 * Determines the schema language from the argument's system id.
517 * @return {@code null} for a DTD
519 * <br>Range: { {@code null},
520 * {@link XMLConstants#W3C_XML_SCHEMA_NS_URI}, {@link XMLConstants#RELAXNG_NS_URI} }
521 * @throws IllegalArgumentException If {@code dtdOrSchema}'s system id does not indicate
522 * a DTD or a schema (XSD or RNG)
524 public static final String
getSchemaLanguage(final Source dtdOrSchema
) {
525 final String dtdOrSchemaId
= dtdOrSchema
.getSystemId().toLowerCase(Locale
.ENGLISH
);
527 if (dtdOrSchemaId
.endsWith(".xsd")) {
528 return XMLConstants
.W3C_XML_SCHEMA_NS_URI
;
529 } else if (dtdOrSchemaId
.endsWith(".rng") || dtdOrSchemaId
.endsWith(".rnc")) {
530 return XMLConstants
.RELAXNG_NS_URI
;
531 } else if (dtdOrSchemaId
.endsWith(".dtd")) {
535 throw new IllegalArgumentException("Unsupported extension for " + dtdOrSchema
+
536 " (the name must end with .xsd, .rng, .rnc or .dtd (case-insensitive))");
540 * Calls {@link #getNode(java.lang.Object, java.lang.String)}.
550 public static final Node
getNode(final Node context
, final String xPath
) {
551 return getNode((Object
) context
, xPath
);
555 * Calls {@link #getNodeSet(java.lang.Object, java.lang.String)}.
565 public static final NodeList
getNodeList(final Node context
, final String xPath
) {
566 return getNodeSet((Object
) context
, xPath
);
570 * Calls {@link #toList(NodeList)} with the value returned by {@link #getNodeList(Node, String)}.
580 public static final List
<Node
> getNodes(final Node context
, final String xPath
) {
581 return toList(getNodeList(context
, xPath
));
585 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
586 * with {@code returnType == XPathConstants.NODE}.
588 * @param <N> The expected node type
597 @SuppressWarnings("unchecked")
598 public static final <N
> N
getNode(final Object context
, final String xPath
) {
599 return (N
) get(context
, xPath
, XPathConstants
.NODE
);
603 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
604 * with {@code returnType == XPathConstants.NODESET}.
606 * @param <S> The expected node set type
615 @SuppressWarnings("unchecked")
616 public static final <S
> S
getNodeSet(final Object context
, final String xPath
) {
617 return (S
) get(context
, xPath
, XPathConstants
.NODESET
);
621 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
622 * with {@code returnType == XPathConstants.BOOLEAN}.
632 public static final Boolean
getBoolean(final Object context
, final String xPath
) {
633 return (Boolean
) get(context
, xPath
, XPathConstants
.BOOLEAN
);
637 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
638 * with {@code returnType == XPathConstants.NUMBER}.
648 public static final Number
getNumber(final Object context
, final String xPath
) {
649 return (Number
) get(context
, xPath
, XPathConstants
.NUMBER
);
653 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
654 * with {@code returnType == XPathConstants.STRING}.
664 public static final String
getString(final Object context
, final String xPath
) {
665 return (String
) get(context
, xPath
, XPathConstants
.STRING
);
669 * Evaluates the compiled XPath expression in the specified context
670 * and returns the result as the specified type.
672 * @param <T> The expected return type
680 * {@link XPathConstants#BOOLEAN},
681 * {@link XPathConstants#NODE},
682 * {@link XPathConstants#NODESET},
683 * {@link XPathConstants#NUMBER},
684 * {@link XPathConstants#STRING}
688 * @throws RuntimeException If an error occurs
690 @SuppressWarnings("unchecked")
691 public static final <T
> T
get(final Object context
, final String xPath
, final QName returnType
) {
693 return (T
) XPathFactory
.newInstance().newXPath().compile(xPath
)
694 .evaluate(context
, returnType
);
695 } catch (final Exception exception
) {
696 throw unchecked(exception
);
701 * Evaluates the quasi-XPath expression in the specified context, and returns the corresponding node,
702 * creating it if necessary.
703 * <br>The second argument is called "quasi-XPath" because it allows non-XPath expressions
704 * like {@code "a/b[]"} which means "add an element b at the end of a".
705 * <br>If {@code quasiXPath} is a standard XPath expression and the corresponding node exists,
706 * then that node is returned.
707 * <br>When the node does not exist,
708 * {@code quasiXPath} is broken down into path elements separated by slashes ("/").
709 * <br>A path element can be created if it is of the form "@name", "name", "name[]" or "name[attributes]" where
710 * "atributes" must be a sequence of "@attribute=value" separated by "and".
711 * <br>Example of a valid quasi-XPath expression where each path element can be created if necessary:<ul>
712 * <li>{@code "a/b[]/c[@d='e' and @f=42]"}
714 * <br>Example of a valid quasi-XPath expression where
715 * the path elements cannot be created if they don't exist:<ul>
716 * <li>{@code "a[last()]/b[@c<42]/d[position()=33]/e['f'=@g]"}
728 public static final Node
getOrCreateNode(final Node context
, final String quasiXPath
) {
729 final String
[] pathElements
= quasiXPath
.split("/");
730 Node result
= context
;
733 for (int index
= 0; index
< pathElements
.length
&& result
!= null; ++index
) {
734 final String pathElement
= pathElements
[index
];
736 if (pathElement
.matches("\\w+\\[\\]")) {
737 result
= addChild(result
, pathElement
);
739 final Node parent
= result
;
740 result
= getNode(result
, pathElement
);
742 if (result
== null) {
743 final Map
<String
, String
> attributes
= getEqualityPredicates(pathElement
);
744 final Integer nameEnd
= pathElement
.indexOf("[");
745 final String childName
= nameEnd
< 0 ? pathElement
: pathElement
.substring(0, nameEnd
);
747 result
= addChild(parent
, childName
);
749 for (final Map
.Entry
<String
, String
> entry
: attributes
.entrySet()) {
750 addChild(result
, entry
.getKey()).setNodeValue(entry
.getValue());
755 } catch (final Exception exception
) {
756 throw unchecked(exception
);
763 * Creates and adds a new node to {@code parent}.
764 * <br>If {@code xPathElement} starts with "@",
765 * then the new node is an attribute, otherwise it is an element.
770 * @param xPathElement
776 public static final Node
addChild(final Node parent
, final String xPathElement
) {
777 final Boolean isAttribute
= xPathElement
.startsWith("@");
778 final String name
= isAttribute ? xPathElement
.substring(1) : xPathElement
.split("\\[")[0];
779 final Document document
= getOwnerDocument(parent
);
780 final Node result
= isAttribute ? document
.createAttribute(name
) : document
.createElement(name
);
783 ((Element
) parent
).setAttributeNode((Attr
) result
);
785 parent
.appendChild(result
);
798 public static final Document
getOwnerDocument(final Node node
) {
799 if (node
.getOwnerDocument() == null && node
instanceof Document
) {
800 return (Document
) node
;
803 return node
.getOwnerDocument();
808 * @param xPathElement
814 private static final Map
<String
, String
> getEqualityPredicates(final String xPathElement
) {
815 final LinkedHashMap
<String
, String
> result
= new LinkedHashMap
<String
, String
>();
816 final String trimmed
= xPathElement
.trim();
818 if (trimmed
.endsWith("]")) {
819 final String constraints
= trimmed
.substring(trimmed
.indexOf("[") + 1, trimmed
.length() - 1).trim();
820 final StringBuilder buffer
= new StringBuilder();
821 String attributeName
= null;
823 ScannerState state
= ScannerState
.ATTRIBUTE_NAME
;
825 while (i
< constraints
.length()) {
826 final char c
= constraints
.charAt(i
);
828 if (state
== ScannerState
.ATTRIBUTE_NAME
&& c
== '=') {
829 attributeName
= buffer
.toString().trim();
833 state
= ScannerState
.ATTRIBUTE_VALUE
;
834 } else if (state
== ScannerState
.ATTRIBUTE_VALUE
&& c
== '\\') {
836 buffer
.append(constraints
.charAt(++i
));
840 final String trimmedBuffer
= buffer
.toString().trim();
842 if (state
== ScannerState
.ATTRIBUTE_VALUE
&& trimmedBuffer
.length() > 1 && trimmedBuffer
.charAt(0) == trimmedBuffer
.charAt(trimmedBuffer
.length() - 1)) {
843 result
.put(attributeName
, getString(null, trimmedBuffer
));
847 state
= ScannerState
.AND
;
848 } else if (state
== ScannerState
.AND
&& "and".equalsIgnoreCase(trimmedBuffer
)) {
851 state
= ScannerState
.ATTRIBUTE_NAME
;
864 * @author codistmonk (creation 2010-07-29)
866 private enum ScannerState
{
868 ATTRIBUTE_NAME
, ATTRIBUTE_VALUE
, AND
;