[Aprog]
[aprog.git] / Aprog / src / net / sourceforge / aprog / xml / XMLTools.java
blob30af97ce32f80eff1eba7b02bea695fb9fa1546a
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.io.OutputStreamWriter;
35 import java.util.ArrayList;
36 import java.util.LinkedHashMap;
37 import java.util.List;
38 import java.util.Map;
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;
73 /**
75 * @author codistmonk (creation 2010-07-02)
77 public final class XMLTools {
79 /**
80 * @throws IllegalInstantiationException To prevent instantiation
82 private XMLTools() {
83 throw new IllegalInstantiationException();
86 /**
87 * {@value}.
89 public static final String XML_1_UTF8_NOT_STANDALONE =
90 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>";
92 /**
93 * {@value}.
95 public static final String XML_1_UTF8_STANDALONE =
96 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
98 /**
100 * @param node
101 * <br>Not null
102 * @return
103 * <br>Not null
105 public static final String getQualifiedName(final Node node) {
106 return node.getPrefix() == null ? node.getNodeName() : node.getPrefix() + ":" + node.getLocalName();
111 * @param xmlInput
112 * <br>Not null
113 * @return
114 * <br>Not null
115 * <br>New
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
125 * <br>Not null
126 * <br>Input-output
127 * @return
128 * <br>Not null
129 * <br>New
130 * @throws RuntimeException if an error occurs
132 public static final Document parse(final InputStream xmlInputStream) {
133 return parse(new InputSource(xmlInputStream));
138 * @param inputSource
139 * <br>Not null
140 * <br>Input-output
141 * @return
142 * <br>Not null
143 * <br>New
144 * @throws RuntimeException if an error occurs
146 public static final Document parse(final InputSource inputSource) {
147 try {
148 return DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputSource);
149 } catch (final Exception exception) {
150 throw unchecked(exception);
156 * @return
157 * <br>Not null
158 * <br>New
160 public static final Document newDocument() {
161 try {
162 final Document result = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
164 result.setXmlStandalone(true);
166 return result;
167 } catch (final ParserConfigurationException exception) {
168 throw unchecked(exception);
174 * @param document
175 * <br>Not null
176 * <br>Input-output
177 * @return {@code document}
178 * <br>Not null
180 public static final Document normalize(final Document document) {
181 document.normalize();
183 return document;
188 * @param document
189 * <br>Not null
190 * <br>Input-output
191 * @return {@code document}
192 * <br>Not null
194 public static final Document standalone(final Document document) {
195 document.setXmlStandalone(true);
197 return document;
202 * @param node
203 * <br>Not null
204 * @param outputFile
205 * <br>Not null
206 * <br>Input-output
207 * @param indent
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);
216 * @param node
217 * <br>Not null
218 * @param output
219 * <br>Not null
220 * <br>Input-output
221 * @param 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);
230 * @param node
231 * <br>Not null
232 * @param output
233 * <br>Not null
234 * <br>Input-output
235 * @param indent
236 * <br>Range: {@code [0 .. Integer.MAX_VALUE]}
238 public static final void write(final Node node, final Result output, final int indent) {
239 try {
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
261 * <br>Not null
262 * @return
263 * <br>Not null
264 * <br>New
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());
272 return result;
277 * @param nodeList
278 * <br>Not null
279 * @return
280 * <br>Not null
281 * <br>New
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));
290 return result;
294 * Validates the XML input against the specified DTD or schema.
296 * @param xmlInputStream
297 * <br>Not null
298 * @param dtdOrSchema
299 * <br>Not null
300 * @return An empty list if validation succeeds
301 * <br>Not null
302 * <br>New
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);
308 try {
309 if (schemaLanguage != null) {
310 final Validator validator = SchemaFactory.newInstance(schemaLanguage)
311 .newSchema(dtdOrSchema).newValidator();
313 validator.validate(new StreamSource(xmlInputStream));
314 } else {
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() {
329 @Override
330 public final void error(final SAXParseException exception) throws SAXException {
331 exceptions.add(exception);
333 getLoggerForThisMethod().log(Level.WARNING, "", exceptions);
336 @Override
337 public final void fatalError(final SAXParseException exception) throws SAXException {
338 exceptions.add(exception);
340 getLoggerForThisMethod().log(Level.WARNING, "", exceptions);
342 throw exception;
345 @Override
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);
362 return exceptions;
366 * Determines the schema language from the argument's system id.
368 * @param dtdOrSchema
369 * <br>Not null
370 * @return {@code null} for a DTD
371 * <br>Maybe null
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")) {
385 return null;
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)}.
395 * @param context
396 * <br>Maybe null
397 * @param xPath
398 * <br>Not null
399 * @return
400 * <br>Maybe null
401 * <br>Not New
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)}.
410 * @param context
411 * <br>Maybe null
412 * @param xPath
413 * <br>Not null
414 * @return
415 * <br>Maybe null
416 * <br>Not New
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
427 * @param context
428 * <br>Maybe null
429 * @param xPath
430 * <br>Not null
431 * @return
432 * <br>Maybe null
433 * <br>Not New
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
445 * @param context
446 * <br>Maybe null
447 * @param xPath
448 * <br>Not null
449 * @return
450 * <br>Maybe null
451 * <br>Not New
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}.
462 * @param context
463 * <br>Maybe null
464 * @param xPath
465 * <br>Not null
466 * @return
467 * <br>Maybe null
468 * <br>Not New
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}.
478 * @param context
479 * <br>Maybe null
480 * @param xPath
481 * <br>Not null
482 * @return
483 * <br>Maybe null
484 * <br>Not New
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}.
494 * @param context
495 * <br>Maybe null
496 * @param xPath
497 * <br>Not null
498 * @return
499 * <br>Maybe null
500 * <br>Not New
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
511 * @param context
512 * <br>Maybe null
513 * @param xPath
514 * <br>Not null
515 * @param returnType
516 * <br>Not null
517 * <br>Range: {
518 * {@link XPathConstants#BOOLEAN},
519 * {@link XPathConstants#NODE},
520 * {@link XPathConstants#NODESET},
521 * {@link XPathConstants#NUMBER},
522 * {@link XPathConstants#STRING}
524 * @return
525 * <br>Maybe null
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) {
530 try {
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]"}
551 * </ul>
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]"}
555 * </ul>
558 * @param context
559 * <br>Not null
560 * @param quasiXPath
561 * <br>Not null
562 * @return
563 * <br>Maybe null
564 * <br>Maybe new
566 public static final Node getOrCreateNode(final Node context, final String quasiXPath) {
567 final String[] pathElements = quasiXPath.split("/");
568 Node result = context;
570 try {
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);
576 } else {
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);
597 return result;
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.
605 * @param parent
606 * <br>Not null
607 * <br>Input-output
608 * @param xPathElement
609 * <br>Not null
610 * @return
611 * <br>Not null
612 * <br>New
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);
620 if (isAttribute) {
621 ((Element) parent).setAttributeNode((Attr) result);
622 } else {
623 parent.appendChild(result);
626 return result;
631 * @param node
632 * <br>Not null
633 * @return
634 * <br>Maybe null
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
647 * <br>Not null
648 * @return
649 * <br>Not null
650 * <br>New
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)
658 .split("=|and");
660 for (int i = 0; i < keyValues.length; i += 2) {
661 result.put(keyValues[i], getString(null, keyValues[i + 1]));
665 return result;