[Aprog]
[aprog.git] / Aprog / src / net / sourceforge / aprog / xml / XMLTools.java
bloba4a674f58e3d384bf3d581e9208ee7991b0af856
1 /*
2 * The MIT License
3 *
4 * Copyright 2010 Codist Monk.
5 *
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
22 * THE SOFTWARE.
25 package net.sourceforge.aprog.xml;
27 import static net.sourceforge.aprog.tools.Tools.*;
29 import java.io.ByteArrayInputStream;
30 import java.io.ByteArrayOutputStream;
31 import java.io.File;
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;
37 import java.util.Map;
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;
72 /**
74 * @author codistmonk (creation 2010-07-02)
76 public final class XMLTools {
78 /**
79 * @throws IllegalInstantiationException To prevent instantiation
81 private XMLTools() {
82 throw new IllegalInstantiationException();
85 /**
86 * {@value}.
88 public static final String XML_1_UTF8_NOT_STANDALONE =
89 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>";
91 /**
92 * {@value}.
94 public static final String XML_1_UTF8_STANDALONE =
95 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
97 /**
99 * @param node
100 * <br>Not null
101 * @return
102 * <br>Not null
104 public static final String getQualifiedName(final Node node) {
105 return node.getPrefix() == null ? node.getNodeName() : node.getPrefix() + ":" + node.getLocalName();
110 * @param xmlInput
111 * <br>Not null
112 * @return
113 * <br>Not null
114 * <br>New
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
124 * <br>Not null
125 * <br>Input-output
126 * @return
127 * <br>Not null
128 * <br>New
129 * @throws RuntimeException if an error occurs
131 public static final Document parse(final InputStream xmlInputStream) {
132 return parse(new InputSource(xmlInputStream));
137 * @param inputSource
138 * <br>Not null
139 * <br>Input-output
140 * @return
141 * <br>Not null
142 * <br>New
143 * @throws RuntimeException if an error occurs
145 public static final Document parse(final InputSource inputSource) {
146 try {
147 return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource);
148 } catch (final Exception exception) {
149 throw unchecked(exception);
155 * @return
156 * <br>Not null
157 * <br>New
159 public static final Document newDocument() {
160 try {
161 final Document result = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
163 result.setXmlStandalone(true);
165 return result;
166 } catch (final ParserConfigurationException exception) {
167 throw unchecked(exception);
173 * @param document
174 * <br>Not null
175 * <br>Input-output
176 * @return {@code document}
177 * <br>Not null
179 public static final Document normalize(final Document document) {
180 document.normalize();
182 return document;
187 * @param document
188 * <br>Not null
189 * <br>Input-output
190 * @return {@code document}
191 * <br>Not null
193 public static final Document standalone(final Document document) {
194 document.setXmlStandalone(true);
196 return document;
201 * @param node
202 * <br>Not null
203 * @param outputFile
204 * <br>Not null
205 * <br>Input-output
206 * @param indent
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);
215 * @param node
216 * <br>Not null
217 * @param output
218 * <br>Not null
219 * <br>Input-output
220 * @param 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);
229 * @param node
230 * <br>Not null
231 * @param output
232 * <br>Not null
233 * <br>Input-output
234 * @param indent
235 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
237 public static final void write(final Node node, final Result output, final int indent) {
238 try {
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
260 * <br>Not null
261 * @return
262 * <br>Not null
263 * <br>New
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());
271 return result;
276 * @param nodeList
277 * <br>Not null
278 * @return
279 * <br>Not null
280 * <br>New
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));
289 return result;
293 * Validates the XML input against the specified DTD or schema.
295 * @param xmlInputStream
296 * <br>Not null
297 * @param dtdOrSchema
298 * <br>Not null
299 * @return An empty list if validation succeeds
300 * <br>Not null
301 * <br>New
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);
307 try {
308 if (schemaLanguage != null) {
309 final Validator validator = SchemaFactory.newInstance(schemaLanguage)
310 .newSchema(dtdOrSchema).newValidator();
312 validator.validate(new StreamSource(xmlInputStream));
313 } else {
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() {
328 @Override
329 public final void error(final SAXParseException exception) throws SAXException {
330 exceptions.add(exception);
332 getLoggerForThisMethod().log(Level.WARNING, "", exceptions);
335 @Override
336 public final void fatalError(final SAXParseException exception) throws SAXException {
337 exceptions.add(exception);
339 getLoggerForThisMethod().log(Level.WARNING, "", exceptions);
341 throw exception;
344 @Override
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);
361 return exceptions;
365 * Determines the schema language from the argument's system id.
367 * @param dtdOrSchema
368 * <br>Not null
369 * @return {@code null} for a DTD
370 * <br>Maybe null
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")) {
384 return null;
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)}.
394 * @param context
395 * <br>Maybe null
396 * @param xPath
397 * <br>Not null
398 * @return
399 * <br>Maybe null
400 * <br>Not New
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)}.
409 * @param context
410 * <br>Maybe null
411 * @param xPath
412 * <br>Not null
413 * @return
414 * <br>Maybe null
415 * <br>Not New
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
426 * @param context
427 * <br>Maybe null
428 * @param xPath
429 * <br>Not null
430 * @return
431 * <br>Maybe null
432 * <br>Not New
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
444 * @param context
445 * <br>Maybe null
446 * @param xPath
447 * <br>Not null
448 * @return
449 * <br>Maybe null
450 * <br>Not New
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}.
461 * @param context
462 * <br>Maybe null
463 * @param xPath
464 * <br>Not null
465 * @return
466 * <br>Maybe null
467 * <br>Not New
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}.
477 * @param context
478 * <br>Maybe null
479 * @param xPath
480 * <br>Not null
481 * @return
482 * <br>Maybe null
483 * <br>Not New
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}.
493 * @param context
494 * <br>Maybe null
495 * @param xPath
496 * <br>Not null
497 * @return
498 * <br>Maybe null
499 * <br>Not New
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
510 * @param context
511 * <br>Maybe null
512 * @param xPath
513 * <br>Not null
514 * @param returnType
515 * <br>Not null
516 * <br>Range: {
517 * {@link XPathConstants#BOOLEAN},
518 * {@link XPathConstants#NODE},
519 * {@link XPathConstants#NODESET},
520 * {@link XPathConstants#NUMBER},
521 * {@link XPathConstants#STRING}
523 * @return
524 * <br>Maybe null
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) {
529 try {
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]"}
550 * </ul>
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]"}
554 * </ul>
557 * @param context
558 * <br>Not null
559 * @param quasiXPath
560 * <br>Not null
561 * @return
562 * <br>Maybe null
563 * <br>Maybe new
565 public static final Node getOrCreateNode(final Node context, final String quasiXPath) {
566 final String[] pathElements = quasiXPath.split("/");
567 Node result = context;
569 try {
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);
575 } else {
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);
596 return result;
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.
604 * @param parent
605 * <br>Not null
606 * <br>Input-output
607 * @param xPathElement
608 * <br>Not null
609 * @return
610 * <br>Not null
611 * <br>New
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);
619 if (isAttribute) {
620 ((Element) parent).setAttributeNode((Attr) result);
621 } else {
622 parent.appendChild(result);
625 return result;
630 * @param node
631 * <br>Not null
632 * @return
633 * <br>Maybe null
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
646 * <br>Not null
647 * @return
648 * <br>Not null
649 * <br>New
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)
657 .split("=|and");
659 for (int i = 0; i < keyValues.length; i += 2) {
660 result.put(keyValues[i], getString(null, keyValues[i + 1]));
664 return result;