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
.util
.ArrayList
;
35 import java
.util
.LinkedHashMap
;
36 import java
.util
.List
;
38 import java
.util
.logging
.Level
;
40 import javax
.xml
.XMLConstants
;
41 import javax
.xml
.namespace
.QName
;
42 import javax
.xml
.parsers
.DocumentBuilderFactory
;
43 import javax
.xml
.parsers
.ParserConfigurationException
;
44 import javax
.xml
.parsers
.SAXParserFactory
;
45 import javax
.xml
.transform
.OutputKeys
;
46 import javax
.xml
.transform
.Result
;
47 import javax
.xml
.transform
.Source
;
48 import javax
.xml
.transform
.Transformer
;
49 import javax
.xml
.transform
.TransformerConfigurationException
;
50 import javax
.xml
.transform
.TransformerException
;
51 import javax
.xml
.transform
.TransformerFactory
;
52 import javax
.xml
.transform
.dom
.DOMSource
;
53 import javax
.xml
.transform
.stream
.StreamResult
;
54 import javax
.xml
.transform
.stream
.StreamSource
;
55 import javax
.xml
.validation
.SchemaFactory
;
56 import javax
.xml
.validation
.Validator
;
57 import javax
.xml
.xpath
.XPathConstants
;
58 import javax
.xml
.xpath
.XPathFactory
;
60 import net
.sourceforge
.aprog
.tools
.IllegalInstantiationException
;
62 import org
.w3c
.dom
.Attr
;
63 import org
.w3c
.dom
.Document
;
64 import org
.w3c
.dom
.Element
;
65 import org
.w3c
.dom
.Node
;
66 import org
.w3c
.dom
.NodeList
;
67 import org
.xml
.sax
.InputSource
;
68 import org
.xml
.sax
.SAXException
;
69 import org
.xml
.sax
.SAXParseException
;
70 import org
.xml
.sax
.helpers
.DefaultHandler
;
74 * @author codistmonk (creation 2010-07-02)
76 public final class XMLTools
{
79 * @throws IllegalInstantiationException To prevent instantiation
82 throw new IllegalInstantiationException();
88 public static final String XML_1_UTF8_NOT_STANDALONE
=
89 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>";
94 public static final String XML_1_UTF8_STANDALONE
=
95 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
104 public static final String
getQualifiedName(final Node node
) {
105 return node
.getPrefix() == null ? node
.getNodeName() : node
.getPrefix() + ":" + node
.getLocalName();
115 * @throws RuntimeException if an error occurs
117 public static final Document
parse(final String xmlInput
) {
118 return parse(new ByteArrayInputStream(xmlInput
.getBytes()));
123 * @param xmlInputStream
129 * @throws RuntimeException if an error occurs
131 public static final Document
parse(final InputStream xmlInputStream
) {
132 return parse(new InputSource(xmlInputStream
));
143 * @throws RuntimeException if an error occurs
145 public static final Document
parse(final InputSource inputSource
) {
147 return DocumentBuilderFactory
.newInstance().newDocumentBuilder().parse(inputSource
);
148 } catch (final Exception exception
) {
149 throw unchecked(exception
);
159 public static final Document
newDocument() {
161 final Document result
= DocumentBuilderFactory
.newInstance().newDocumentBuilder().newDocument();
163 result
.setXmlStandalone(true);
166 } catch (final ParserConfigurationException exception
) {
167 throw unchecked(exception
);
176 * @return {@code document}
179 public static final Document
normalize(final Document document
) {
180 document
.normalize();
190 * @return {@code document}
193 public static final Document
standalone(final Document document
) {
194 document
.setXmlStandalone(true);
207 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
209 public static final void write(final Node node
, final File outputFile
, final int indent
) {
210 write(node
, new StreamResult(outputFile
.getAbsolutePath()), indent
);
221 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
223 public static final void write(final Node node
, final OutputStream output
, final int indent
) {
224 write(node
, new StreamResult(output
), indent
);
235 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
237 public static final void write(final Node node
, final Result output
, final int indent
) {
239 final TransformerFactory transformerFactory
= TransformerFactory
.newInstance();
241 transformerFactory
.setAttribute("indent-number", indent
);
243 final Transformer transformer
= transformerFactory
.newTransformer();
245 transformer
.setOutputProperty(OutputKeys
.METHOD
, "xml");
246 transformer
.setOutputProperty(OutputKeys
.INDENT
, indent
!= 0 ?
"yes" : "no");
247 transformer
.setOutputProperty(OutputKeys
.OMIT_XML_DECLARATION
,
248 node
instanceof Document ?
"no" : "yes");
249 transformer
.setOutputProperty(OutputKeys
.STANDALONE
, "yes");
251 transformer
.transform(new DOMSource(node
), output
);
252 } catch (final Exception exception
) {
253 throw unchecked(exception
);
259 * @param resourceName
265 public static final Source
getResourceAsSource(final String resourceName
) {
266 final ClassLoader classLoader
= getCallerClass().getClassLoader();
267 final Source result
= new StreamSource(classLoader
.getResourceAsStream(resourceName
));
269 result
.setSystemId(classLoader
.getResource(resourceName
).toString());
282 public static final List
<Node
> toList(final NodeList nodeList
) {
283 final List
<Node
> result
= new ArrayList
<Node
>();
285 for (int i
= 0; i
< nodeList
.getLength(); ++i
) {
286 result
.add(nodeList
.item(i
));
293 * Validates the XML input against the specified DTD or schema.
295 * @param xmlInputStream
299 * @return An empty list if validation succeeds
303 public static final List
<Throwable
> validate(final InputStream xmlInputStream
, final Source dtdOrSchema
) {
304 final List
<Throwable
> exceptions
= new ArrayList
<Throwable
>();
305 final String schemaLanguage
= getSchemaLanguage(dtdOrSchema
);
308 if (schemaLanguage
!= null) {
309 final Validator validator
= SchemaFactory
.newInstance(schemaLanguage
)
310 .newSchema(dtdOrSchema
).newValidator();
312 validator
.validate(new StreamSource(xmlInputStream
));
314 final Transformer addDoctypeInformation
= TransformerFactory
.newInstance().newTransformer();
316 addDoctypeInformation
.setOutputProperty(OutputKeys
.DOCTYPE_SYSTEM
, dtdOrSchema
.getSystemId());
318 final ByteArrayOutputStream buffer
= new ByteArrayOutputStream();
320 addDoctypeInformation
.transform(new StreamSource(xmlInputStream
), new StreamResult(buffer
));
322 final SAXParserFactory saxParserFactory
= SAXParserFactory
.newInstance();
324 saxParserFactory
.setValidating(true);
325 saxParserFactory
.newSAXParser().parse(
326 new ByteArrayInputStream(buffer
.toByteArray()), new DefaultHandler() {
329 public final void error(final SAXParseException exception
) throws SAXException
{
330 exceptions
.add(exception
);
332 getLoggerForThisMethod().log(Level
.WARNING
, "", exceptions
);
336 public final void fatalError(final SAXParseException exception
) throws SAXException
{
337 exceptions
.add(exception
);
339 getLoggerForThisMethod().log(Level
.WARNING
, "", exceptions
);
345 public final void warning(final SAXParseException exception
) throws SAXException
{
346 getLoggerForThisMethod().log(Level
.WARNING
, "", exceptions
);
351 } catch (final TransformerConfigurationException exception
) {
352 throw unchecked(exception
);
353 } catch (final TransformerException exception
) {
354 exceptions
.add(exception
);
355 } catch (final SAXException exception
) {
356 exceptions
.add(exception
);
357 } catch (final Exception exception
) {
358 throw unchecked(exception
);
365 * Determines the schema language from the argument's system id.
369 * @return {@code null} for a DTD
371 * <br>Range: { {@code null},
372 * {@link XMLConstants#W3C_XML_SCHEMA_NS_URI}, {@link XMLConstants#RELAXNG_NS_URI} }
373 * @throws IllegalArgumentException If {@code dtdOrSchema}'s system id does not indicate
374 * a DTD or a schema (XSD or RNG)
376 public static final String
getSchemaLanguage(final Source dtdOrSchema
) {
377 final String dtdOrSchemaId
= dtdOrSchema
.getSystemId().toLowerCase();
379 if (dtdOrSchemaId
.endsWith(".xsd")) {
380 return XMLConstants
.W3C_XML_SCHEMA_NS_URI
;
381 } else if (dtdOrSchemaId
.endsWith(".rng") || dtdOrSchemaId
.endsWith(".rnc")) {
382 return XMLConstants
.RELAXNG_NS_URI
;
383 } else if (dtdOrSchemaId
.endsWith(".dtd")) {
387 throw new IllegalArgumentException("Unsupported extension for " + dtdOrSchema
+
388 " (the name must end with .xsd, .rng, .rnc or .dtd (case-insensitive))");
392 * Calls {@link #getNode(java.lang.Object, java.lang.String)}.
402 public static final Node
getNode(final Node context
, final String xPath
) {
403 return getNode((Object
) context
, xPath
);
407 * Calls {@link #getNodes(java.lang.Object, java.lang.String)}.
417 public static final NodeList
getNodes(final Node context
, final String xPath
) {
418 return getNodes((Object
) context
, xPath
);
422 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
423 * with {@code returnType == XPathConstants.NODE}.
425 * @param <N> The expected node type
434 @SuppressWarnings("unchecked")
435 public static final <N
> N
getNode(final Object context
, final String xPath
) {
436 return (N
) get(context
, xPath
, XPathConstants
.NODE
);
440 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
441 * with {@code returnType == XPathConstants.NODESET}.
443 * @param <S> The expected node set type
452 @SuppressWarnings("unchecked")
453 public static final <S
> S
getNodes(final Object context
, final String xPath
) {
454 return (S
) get(context
, xPath
, XPathConstants
.NODESET
);
458 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
459 * with {@code returnType == XPathConstants.BOOLEAN}.
469 public static final Boolean
getBoolean(final Object context
, final String xPath
) {
470 return (Boolean
) get(context
, xPath
, XPathConstants
.BOOLEAN
);
474 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
475 * with {@code returnType == XPathConstants.NUMBER}.
485 public static final Number
getNumber(final Object context
, final String xPath
) {
486 return (Number
) get(context
, xPath
, XPathConstants
.NUMBER
);
490 * Calls {@link #get(java.lang.Object, java.lang.String, javax.xml.namespace.QName)}
491 * with {@code returnType == XPathConstants.STRING}.
501 public static final String
getString(final Object context
, final String xPath
) {
502 return (String
) get(context
, xPath
, XPathConstants
.STRING
);
506 * Evaluates the compiled XPath expression in the specified context
507 * and returns the result as the specified type.
509 * @param <T> The expected return type
517 * {@link XPathConstants#BOOLEAN},
518 * {@link XPathConstants#NODE},
519 * {@link XPathConstants#NODESET},
520 * {@link XPathConstants#NUMBER},
521 * {@link XPathConstants#STRING}
525 * @throws RuntimeException If an error occurs
527 @SuppressWarnings("unchecked")
528 public static final <T
> T
get(final Object context
, final String xPath
, final QName returnType
) {
530 return (T
) XPathFactory
.newInstance().newXPath().compile(xPath
)
531 .evaluate(context
, returnType
);
532 } catch (final Exception exception
) {
533 throw unchecked(exception
);
538 * Evaluates the quasi-XPath expression in the specified context, and returns the corresponding node,
539 * creating it if necessary.
540 * <br>The second argument is called "quasi-XPath" because it allows non-XPath expressions
541 * like {@code "a/b[]"} which means "add an element b at the end of a".
542 * <br>If {@code quasiXPath} is a standard XPath expression and the corresponding node exists,
543 * then that node is returned.
544 * <br>When the node does not exist,
545 * {@code quasiXPath} is broken down into path elements separated by slashes ("/").
546 * <br>A path element can be created if it is of the form "@name", "name", "name[]" or "name[attributes]" where
547 * "atributes" must be a sequence of "@attribute=value" separated by "and".
548 * <br>Example of a valid quasi-XPath expression where each path element can be created if necessary:<ul>
549 * <li>{@code "a/b[]/c[@d='e' and @f=42]"}
551 * <br>Example of a valid quasi-XPath expression where
552 * the path elements cannot be created if they don't exist:<ul>
553 * <li>{@code "a[last()]/b[@c<42]/d[position()=33]/e['f'=@g]"}
565 public static final Node
getOrCreateNode(final Node context
, final String quasiXPath
) {
566 final String
[] pathElements
= quasiXPath
.split("/");
567 Node result
= context
;
570 for (int index
= 0; index
< pathElements
.length
&& result
!= null; ++index
) {
571 final String pathElement
= pathElements
[index
];
573 if (pathElement
.matches("\\w+\\[\\]")) {
574 result
= addChild(result
, pathElement
);
576 final Node parent
= result
;
577 result
= getNode(result
, pathElement
);
579 if (result
== null) {
580 final Map
<String
, String
> attributes
= getEqualityPredicates(pathElement
);
581 final Integer nameEnd
= pathElement
.indexOf("[");
582 final String childName
= nameEnd
< 0 ? pathElement
: pathElement
.substring(0, nameEnd
);
584 result
= addChild(parent
, childName
);
586 for (final Map
.Entry
<String
, String
> entry
: attributes
.entrySet()) {
587 addChild(result
, entry
.getKey()).setNodeValue(entry
.getValue());
592 } catch (final Exception exception
) {
593 throw unchecked(exception
);
600 * Creates and adds a new node to {@code parent}.
601 * <br>If {@code xPathElement} starts with "@",
602 * then the new node is an attribute, otherwise it is an element.
607 * @param xPathElement
613 public static final Node
addChild(final Node parent
, final String xPathElement
) {
614 final Boolean isAttribute
= xPathElement
.startsWith("@");
615 final String name
= isAttribute ? xPathElement
.substring(1) : xPathElement
.split("\\[")[0];
616 final Document document
= getOwnerDocument(parent
);
617 final Node result
= isAttribute ? document
.createAttribute(name
) : document
.createElement(name
);
620 ((Element
) parent
).setAttributeNode((Attr
) result
);
622 parent
.appendChild(result
);
635 public static final Document
getOwnerDocument(final Node node
) {
636 if (node
.getOwnerDocument() == null && node
instanceof Document
) {
637 return (Document
) node
;
640 return node
.getOwnerDocument();
645 * @param xPathElement
651 private static final Map
<String
, String
> getEqualityPredicates(final String xPathElement
) {
652 final LinkedHashMap
<String
, String
> result
= new LinkedHashMap
<String
, String
>();
654 if (xPathElement
.trim().endsWith("]")) {
655 final String
[] keyValues
= xPathElement
656 .substring(xPathElement
.indexOf("[") + 1, xPathElement
.length() - 1)
659 for (int i
= 0; i
< keyValues
.length
; i
+= 2) {
660 result
.put(keyValues
[i
], getString(null, keyValues
[i
+ 1]));