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 net
.sourceforge
.aprog
.tools
.Tools
.*;
29 import java
.io
.ByteArrayInputStream
;
30 import java
.io
.ByteArrayOutputStream
;
32 import java
.io
.InputStream
;
33 import java
.io
.OutputStream
;
34 import java
.io
.OutputStreamWriter
;
35 import java
.util
.ArrayList
;
36 import java
.util
.LinkedHashMap
;
37 import java
.util
.List
;
39 import java
.util
.logging
.Level
;
41 import javax
.xml
.XMLConstants
;
42 import javax
.xml
.namespace
.QName
;
43 import javax
.xml
.parsers
.DocumentBuilderFactory
;
44 import javax
.xml
.parsers
.ParserConfigurationException
;
45 import javax
.xml
.parsers
.SAXParserFactory
;
46 import javax
.xml
.transform
.OutputKeys
;
47 import javax
.xml
.transform
.Result
;
48 import javax
.xml
.transform
.Source
;
49 import javax
.xml
.transform
.Transformer
;
50 import javax
.xml
.transform
.TransformerConfigurationException
;
51 import javax
.xml
.transform
.TransformerException
;
52 import javax
.xml
.transform
.TransformerFactory
;
53 import javax
.xml
.transform
.dom
.DOMSource
;
54 import javax
.xml
.transform
.stream
.StreamResult
;
55 import javax
.xml
.transform
.stream
.StreamSource
;
56 import javax
.xml
.validation
.SchemaFactory
;
57 import javax
.xml
.validation
.Validator
;
58 import javax
.xml
.xpath
.XPathConstants
;
59 import javax
.xml
.xpath
.XPathFactory
;
61 import net
.sourceforge
.aprog
.tools
.IllegalInstantiationException
;
63 import org
.w3c
.dom
.Attr
;
64 import org
.w3c
.dom
.Document
;
65 import org
.w3c
.dom
.Element
;
66 import org
.w3c
.dom
.Node
;
67 import org
.w3c
.dom
.NodeList
;
68 import org
.xml
.sax
.InputSource
;
69 import org
.xml
.sax
.SAXException
;
70 import org
.xml
.sax
.SAXParseException
;
71 import org
.xml
.sax
.helpers
.DefaultHandler
;
75 * @author codistmonk (creation 2010-07-02)
77 public final class XMLTools
{
80 * @throws IllegalInstantiationException To prevent instantiation
83 throw new IllegalInstantiationException();
89 public static final String XML_1_UTF8_NOT_STANDALONE
=
90 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>";
95 public static final String XML_1_UTF8_STANDALONE
=
96 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
105 public static final String
getQualifiedName(final Node node
) {
106 return node
.getPrefix() == null ? node
.getNodeName() : node
.getPrefix() + ":" + node
.getLocalName();
116 * @throws RuntimeException if an error occurs
118 public static final Document
parse(final String xmlInput
) {
119 return parse(new ByteArrayInputStream(xmlInput
.getBytes()));
124 * @param xmlInputStream
130 * @throws RuntimeException if an error occurs
132 public static final Document
parse(final InputStream xmlInputStream
) {
133 return parse(new InputSource(xmlInputStream
));
144 * @throws RuntimeException if an error occurs
146 public static final Document
parse(final InputSource inputSource
) {
148 return DocumentBuilderFactory
.newInstance().newDocumentBuilder().parse(inputSource
);
149 } catch (final Exception exception
) {
150 throw unchecked(exception
);
160 public static final Document
newDocument() {
162 final Document result
= DocumentBuilderFactory
.newInstance().newDocumentBuilder().newDocument();
164 result
.setXmlStandalone(true);
167 } catch (final ParserConfigurationException exception
) {
168 throw unchecked(exception
);
177 * @return {@code document}
180 public static final Document
normalize(final Document document
) {
181 document
.normalize();
191 * @return {@code document}
194 public static final Document
standalone(final Document document
) {
195 document
.setXmlStandalone(true);
208 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
210 public static final void write(final Node node
, final File outputFile
, final int indent
) {
211 write(node
, new StreamResult(outputFile
.getAbsolutePath()), indent
);
222 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
224 public static final void write(final Node node
, final OutputStream output
, final int indent
) {
225 write(node
, new StreamResult(new OutputStreamWriter(output
)), indent
);
236 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
238 public static final void write(final Node node
, final Result output
, final int indent
) {
240 final TransformerFactory transformerFactory
= TransformerFactory
.newInstance();
242 transformerFactory
.setAttribute("indent-number", indent
);
244 final Transformer transformer
= transformerFactory
.newTransformer();
246 transformer
.setOutputProperty(OutputKeys
.METHOD
, "xml");
247 transformer
.setOutputProperty(OutputKeys
.INDENT
, indent
!= 0 ?
"yes" : "no");
248 transformer
.setOutputProperty(OutputKeys
.OMIT_XML_DECLARATION
,
249 node
instanceof Document ?
"no" : "yes");
250 transformer
.setOutputProperty(OutputKeys
.STANDALONE
, "yes");
252 transformer
.transform(new DOMSource(node
), output
);
253 } catch (final Exception exception
) {
254 throw unchecked(exception
);
260 * @param resourceName
266 public static final Source
getResourceAsSource(final String resourceName
) {
267 final ClassLoader classLoader
= getCallerClass().getClassLoader();
268 final Source result
= new StreamSource(classLoader
.getResourceAsStream(resourceName
));
270 result
.setSystemId(classLoader
.getResource(resourceName
).toString());
283 public static final List
<Node
> toList(final NodeList nodeList
) {
284 final List
<Node
> result
= new ArrayList
<Node
>();
286 for (int i
= 0; i
< nodeList
.getLength(); ++i
) {
287 result
.add(nodeList
.item(i
));
294 * Validates the XML input against the specified DTD or schema.
296 * @param xmlInputStream
300 * @return An empty list if validation succeeds
304 public static final List
<Throwable
> validate(final InputStream xmlInputStream
, final Source dtdOrSchema
) {
305 final List
<Throwable
> exceptions
= new ArrayList
<Throwable
>();
306 final String schemaLanguage
= getSchemaLanguage(dtdOrSchema
);
309 if (schemaLanguage
!= null) {
310 final Validator validator
= SchemaFactory
.newInstance(schemaLanguage
)
311 .newSchema(dtdOrSchema
).newValidator();
313 validator
.validate(new StreamSource(xmlInputStream
));
315 final Transformer addDoctypeInformation
= TransformerFactory
.newInstance().newTransformer();
317 addDoctypeInformation
.setOutputProperty(OutputKeys
.DOCTYPE_SYSTEM
, dtdOrSchema
.getSystemId());
319 final ByteArrayOutputStream buffer
= new ByteArrayOutputStream();
321 addDoctypeInformation
.transform(new StreamSource(xmlInputStream
), new StreamResult(buffer
));
323 final SAXParserFactory saxParserFactory
= SAXParserFactory
.newInstance();
325 saxParserFactory
.setValidating(true);
326 saxParserFactory
.newSAXParser().parse(
327 new ByteArrayInputStream(buffer
.toByteArray()), new DefaultHandler() {
330 public final void error(final SAXParseException exception
) throws SAXException
{
331 exceptions
.add(exception
);
333 getLoggerForThisMethod().log(Level
.WARNING
, "", exceptions
);
337 public final void fatalError(final SAXParseException exception
) throws SAXException
{
338 exceptions
.add(exception
);
340 getLoggerForThisMethod().log(Level
.WARNING
, "", exceptions
);
346 public final void warning(final SAXParseException exception
) throws SAXException
{
347 getLoggerForThisMethod().log(Level
.WARNING
, "", exceptions
);
352 } catch (final TransformerConfigurationException exception
) {
353 throw unchecked(exception
);
354 } catch (final TransformerException exception
) {
355 exceptions
.add(exception
);
356 } catch (final SAXException exception
) {
357 exceptions
.add(exception
);
358 } catch (final Exception exception
) {
359 throw unchecked(exception
);
366 * Determines the schema language from the argument's system id.
370 * @return {@code null} for a DTD
372 * <br>Range: { {@code null},
373 * {@link XMLConstants#W3C_XML_SCHEMA_NS_URI}, {@link XMLConstants#RELAXNG_NS_URI} }
374 * @throws IllegalArgumentException If {@code dtdOrSchema}'s system id does not indicate
375 * a DTD or a schema (XSD or RNG)
377 public static final String
getSchemaLanguage(final Source dtdOrSchema
) {
378 final String dtdOrSchemaId
= dtdOrSchema
.getSystemId().toLowerCase();
380 if (dtdOrSchemaId
.endsWith(".xsd")) {
381 return XMLConstants
.W3C_XML_SCHEMA_NS_URI
;
382 } else if (dtdOrSchemaId
.endsWith(".rng") || dtdOrSchemaId
.endsWith(".rnc")) {
383 return XMLConstants
.RELAXNG_NS_URI
;
384 } else if (dtdOrSchemaId
.endsWith(".dtd")) {
388 throw new IllegalArgumentException("Unsupported extension for " + dtdOrSchema
+
389 " (the name must end with .xsd, .rng, .rnc or .dtd (case-insensitive))");
393 * Calls {@link #getNode(java.lang.Object, java.lang.String)}.
403 public static final Node
getNode(final Node context
, final String xPath
) {
404 return getNode((Object
) context
, xPath
);
408 * Calls {@link #getNodes(java.lang.Object, java.lang.String)}.
418 public static final NodeList
getNodes(final Node context
, final String xPath
) {
419 return getNodes((Object
) context
, xPath
);
423 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
424 * with {@code returnType == XPathConstants.NODE}.
426 * @param <N> The expected node type
435 @SuppressWarnings("unchecked")
436 public static final <N
> N
getNode(final Object context
, final String xPath
) {
437 return (N
) get(context
, xPath
, XPathConstants
.NODE
);
441 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
442 * with {@code returnType == XPathConstants.NODESET}.
444 * @param <S> The expected node set type
453 @SuppressWarnings("unchecked")
454 public static final <S
> S
getNodes(final Object context
, final String xPath
) {
455 return (S
) get(context
, xPath
, XPathConstants
.NODESET
);
459 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
460 * with {@code returnType == XPathConstants.BOOLEAN}.
470 public static final Boolean
getBoolean(final Object context
, final String xPath
) {
471 return (Boolean
) get(context
, xPath
, XPathConstants
.BOOLEAN
);
475 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
476 * with {@code returnType == XPathConstants.NUMBER}.
486 public static final Number
getNumber(final Object context
, final String xPath
) {
487 return (Number
) get(context
, xPath
, XPathConstants
.NUMBER
);
491 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
492 * with {@code returnType == XPathConstants.STRING}.
502 public static final String
getString(final Object context
, final String xPath
) {
503 return (String
) get(context
, xPath
, XPathConstants
.STRING
);
507 * Evaluates the compiled XPath expression in the specified context
508 * and returns the result as the specified type.
510 * @param <T> The expected return type
518 * {@link XPathConstants#BOOLEAN},
519 * {@link XPathConstants#NODE},
520 * {@link XPathConstants#NODESET},
521 * {@link XPathConstants#NUMBER},
522 * {@link XPathConstants#STRING}
526 * @throws RuntimeException If an error occurs
528 @SuppressWarnings("unchecked")
529 public static final <T
> T
get(final Object context
, final String xPath
, final QName returnType
) {
531 return (T
) XPathFactory
.newInstance().newXPath().compile(xPath
)
532 .evaluate(context
, returnType
);
533 } catch (final Exception exception
) {
534 throw unchecked(exception
);
539 * Evaluates the quasi-XPath expression in the specified context, and returns the corresponding node,
540 * creating it if necessary.
541 * <br>The second argument is called "quasi-XPath" because it allows non-XPath expressions
542 * like {@code "a/b[]"} which means "add an element b at the end of a".
543 * <br>If {@code quasiXPath} is a standard XPath expression and the corresponding node exists,
544 * then that node is returned.
545 * <br>When the node does not exist,
546 * {@code quasiXPath} is broken down into path elements separated by slashes ("/").
547 * <br>A path element can be created if it is of the form "@name", "name", "name[]" or "name[attributes]" where
548 * "atributes" must be a sequence of "@attribute=value" separated by "and".
549 * <br>Example of a valid quasi-XPath expression where each path element can be created if necessary:<ul>
550 * <li>{@code "a/b[]/c[@d='e' and @f=42]"}
552 * <br>Example of a valid quasi-XPath expression where
553 * the path elements cannot be created if they don't exist:<ul>
554 * <li>{@code "a[last()]/b[@c<42]/d[position()=33]/e['f'=@g]"}
566 public static final Node
getOrCreateNode(final Node context
, final String quasiXPath
) {
567 final String
[] pathElements
= quasiXPath
.split("/");
568 Node result
= context
;
571 for (int index
= 0; index
< pathElements
.length
&& result
!= null; ++index
) {
572 final String pathElement
= pathElements
[index
];
574 if (pathElement
.matches("\\w+\\[\\]")) {
575 result
= addChild(result
, pathElement
);
577 final Node parent
= result
;
578 result
= getNode(result
, pathElement
);
580 if (result
== null) {
581 final Map
<String
, String
> attributes
= getEqualityPredicates(pathElement
);
582 final Integer nameEnd
= pathElement
.indexOf("[");
583 final String childName
= nameEnd
< 0 ? pathElement
: pathElement
.substring(0, nameEnd
);
585 result
= addChild(parent
, childName
);
587 for (final Map
.Entry
<String
, String
> entry
: attributes
.entrySet()) {
588 addChild(result
, entry
.getKey()).setNodeValue(entry
.getValue());
593 } catch (final Exception exception
) {
594 throw unchecked(exception
);
601 * Creates and adds a new node to {@code parent}.
602 * <br>If {@code xPathElement} starts with "@",
603 * then the new node is an attribute, otherwise it is an element.
608 * @param xPathElement
614 public static final Node
addChild(final Node parent
, final String xPathElement
) {
615 final Boolean isAttribute
= xPathElement
.startsWith("@");
616 final String name
= isAttribute ? xPathElement
.substring(1) : xPathElement
.split("\\[")[0];
617 final Document document
= getOwnerDocument(parent
);
618 final Node result
= isAttribute ? document
.createAttribute(name
) : document
.createElement(name
);
621 ((Element
) parent
).setAttributeNode((Attr
) result
);
623 parent
.appendChild(result
);
636 public static final Document
getOwnerDocument(final Node node
) {
637 if (node
.getOwnerDocument() == null && node
instanceof Document
) {
638 return (Document
) node
;
641 return node
.getOwnerDocument();
646 * @param xPathElement
652 private static final Map
<String
, String
> getEqualityPredicates(final String xPathElement
) {
653 final LinkedHashMap
<String
, String
> result
= new LinkedHashMap
<String
, String
>();
655 if (xPathElement
.trim().endsWith("]")) {
656 final String
[] keyValues
= xPathElement
657 .substring(xPathElement
.indexOf("[") + 1, xPathElement
.length() - 1)
660 for (int i
= 0; i
< keyValues
.length
; i
+= 2) {
661 result
.put(keyValues
[i
], getString(null, keyValues
[i
+ 1]));