/******************************************************************************* * Copyright 2012 Geoscience Australia * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package au.gov.ga.earthsci.common.util; import gov.nasa.worldwind.util.WWXML; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Array; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.util.ArrayList; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.transform.OutputKeys; import javax.xml.transform.Result; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.w3c.dom.Text; /** * Utility methods for XML handling. * * @author Michael de Hoog (michael.dehoog@ga.gov.au) */ public class XmlUtil { /** * Get the first {@link Text} child of the given parent. * * @param parent * Parent to search * @return First {@link Text} child of parent */ public static Text getFirstChildText(Node parent) { return getFirstChildImplementing(parent, Text.class); } /** * Get the first {@link Element} child of the given parent. * * @param parent * Parent to search * @return First {@link Element} child of parent */ public static Element getFirstChildElement(Node parent) { return getFirstChildImplementing(parent, Element.class); } /** * Get all children {@link Element}s of parent. * * @param parent * Parent to search * @return Array of {@link Element} children of parent */ public static Element[] getElements(Node parent) { return getChildrenImplementing(parent, Element.class); } /** * Get the first child node of parent that implements/subclasses the given * type. * * @param parent * Parent to search * @param nodeType * Type of child to search for * @return First child of parent that conforms to the given nodeType */ public static <N extends Node> N getFirstChildImplementing(Node parent, Class<N> nodeType) { return getNthChildImplementing(0, parent, nodeType); } /** * Get the index'th child node of parent that implements/subclasses the * given type. * * @param index * Child node index * @param parent * Parent to search * @param nodeType * Type of child to search for * @return index'th child of parent that conforms to the given nodeType */ public static <N extends Node> N getNthChildImplementing(int index, Node parent, Class<N> nodeType) { if (parent == null) { return null; } NodeList children = parent.getChildNodes(); if (children == null) { return null; } int count = 0; for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if (nodeType.isAssignableFrom(node.getClass())) { if (count++ == index) { @SuppressWarnings("unchecked") N n = (N) node; return n; } } } return null; } /** * Get all child nodes of parent that implement/subclass the given type. * * @param parent * Parent to search * @param nodeType * Type of child to search for * @return Array of children of parent that conform to the given nodeType */ public static <N extends Node> N[] getChildrenImplementing(Node parent, Class<N> nodeType) { if (parent == null) { return null; } NodeList children = parent.getChildNodes(); if (children == null) { return null; } int count = 0; for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if (nodeType.isAssignableFrom(node.getClass())) { count++; } } @SuppressWarnings("unchecked") N[] array = (N[]) Array.newInstance(nodeType, count); for (int i = 0, pos = 0; i < children.getLength(); i++) { Node node = children.item(i); if (nodeType.isAssignableFrom(node.getClass())) { @SuppressWarnings("unchecked") N n = (N) node; Array.set(array, pos++, n); } } return array; } /** * Returns the number of child elements of parent that have the given * element tag name. * * @param name * Element tag name to search for * @param parent * Parent to search * @return Count of child elements that have the given name */ public static int getCountChildElementsByTagName(String name, Element parent) { if (parent == null) { return 0; } NodeList children = parent.getChildNodes(); if (children == null) { return 0; } int count = 0; for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if (node instanceof Element) { Element e = (Element) node; if (e.getTagName().equals(name)) { count++; } } } return count; } /** * Return the index'th child element of parent that has the given element * tag name. * * @param index * Index of the child to return * @param name * Element tag name to search for * @param parent * Parent to search * @return index'th child element of parent that has the given name */ public static Element getChildElementByTagName(int index, String name, Element parent) { if (parent == null) { return null; } NodeList children = parent.getChildNodes(); if (children == null) { return null; } int count = 0; for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if (node instanceof Element) { Element e = (Element) node; if (e.getTagName().equals(name) && (count++ == index)) { return e; } } } return null; } /** * Return an array of direct child elements that have the given element tag * name. * * @param name * Element tag name to search for * @param parent * Parent to search * @return Array of child elements with the given name */ public static Element[] getChildElementsByTagName(String name, Element parent) { List<Element> elements = new ArrayList<Element>(); if (parent != null) { NodeList children = parent.getChildNodes(); if (children != null) { for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); if (node instanceof Element) { Element e = (Element) node; if (e.getTagName().equals(name)) { elements.add(e); } } } } } return elements.toArray(new Element[elements.size()]); } /** * Save the given document to an output stream. Output is nicely formatted, * with child elements indented by 4 spaces. * * @param doc * Document to save * @param outputStream * OutputStream to save to * @throws TransformerException * @throws IOException */ public static void saveDocumentToFormattedStream(Document doc, OutputStream outputStream) throws TransformerException, IOException { Source source = new DOMSource(doc); Result result = new StreamResult(outputStream); Transformer transformer = createTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$ transformer.setOutputProperty("{http://xml.apache.org/xalan}indent-amount", "4"); //$NON-NLS-1$ //$NON-NLS-2$ transformer.transform(source, result); } /** * Remove any whitespace text nodes from the DOM. Calling this before saving * a formatted document will fix the formatting indentation of elements * loaded from a different document. * * @param node * Node to remove whitespace nodes from * @param deep * Should this method recurse into the node's children? */ public static void removeWhitespace(Node node, boolean deep) { NodeList children = node.getChildNodes(); int length = children.getLength(); for (int i = 0; i < length; i++) { Node child = children.item(i); if (child.getNodeType() == Node.TEXT_NODE && length > 1) { Node previous = child.getPreviousSibling(); Node next = child.getNextSibling(); if ((previous == null || previous.getNodeType() == Node.ELEMENT_NODE || previous.getNodeType() == Node.COMMENT_NODE) && (next == null || next.getNodeType() == Node.ELEMENT_NODE || next.getNodeType() == Node.COMMENT_NODE)) { String content = child.getTextContent(); if (content.matches("\\s*")) //$NON-NLS-1$ { node.removeChild(child); i--; length--; } } } else if (deep && child.getNodeType() == Node.ELEMENT_NODE) { removeWhitespace(child, deep); } } } /** * Create a new {@link Transformer}. * * @throws TransformerConfigurationException */ public static Transformer createTransformer() throws TransformerConfigurationException { TransformerFactory transformerFactory = TransformerFactory.newInstance(); return transformerFactory.newTransformer(); } /** * Open the XML document referenced in the given source * <p/> * Supports: * <ul> * <li> {@link Document} * <li> {@link URI} (for supported protocols - see {@link URI#toURL()}) * <li>and all formats supported by {@link WWXML#openDocument(Object)} * </ul> */ public static Document openDocument(Object source) { if (source == null) { return null; } if (source instanceof Document) { return (Document) source; } if (source instanceof URI) { try { return WWXML.openDocument(((URI) source).toURL()); } catch (Exception e) { return null; } } try { return WWXML.openDocument(source); } catch (Exception e) { return null; } } /** * @return The XML element from a generic source. If the source is an * {@link Element}, will return the provided source. Otherwise, will * attempt to open the source as an XML {@link Document} and will * return the document element. * * @see {@link WWXML#openDocument(Object)} */ public static Element getElementFromSource(Object source) { if (source == null) { return null; } if (source instanceof Element) { return (Element) source; } else if (source instanceof Document) { return ((Document) source).getDocumentElement(); } else { Document document = openDocument(source); if (document != null) { return document.getDocumentElement(); } } return null; } /** * Return the text at the given path relative to the given context. * * @param context * The element from which the path is relative to * @param path * The path to the text value to return * * @return the text at the given path relative to the given context, or * <code>null</code> if none is found */ public static String getText(Element context, String path) { return getText(context, path, null); } /** * Return the text at the given path relative to the given context, * returning the provided default if none is found. * * @param context * The element from which the path is relative to * @param path * The path to the text value to return * @param def * The default value to return if none is found * * @return the text at the given path relative to the given context, or the * provided default if none is found. */ public static String getText(Element context, String path, String def) { return getText(context, path, def, null); } /** * Return the text at the given path relative to the given context, * returning the provided default if none is found. * * @param context * The element from which the path is relative to * @param path * The path to the text value to return * @param def * The default value to return if none is found * @param xpath * An {@link XPath} instance that can be reused across calls * * @return the text at the given path relative to the given context, or the * provided default if none is found. */ public static String getText(Element context, String path, String def, XPath xpath) { Node node = getNode(context, path, xpath); if (node == null) { return def; } return node.getTextContent(); } /** * Set the element text at the path to the given value. * * @param context * The element from which the path is relative to * @param path * The element path to the text value to set * @param value * Text value to set * @return Element set */ public static Element setTextElement(Element context, String path, String value) { return setTextElement(context, path, value, null); } /** * Set the element text at the path to the given value. * * @param context * The element from which the path is relative to * @param path * The element path to the text value to set * @param value * Text value to set * @param xpath * An {@link XPath} instance that can be reused across calls * @return Element set */ public static Element setTextElement(Element context, String path, String value, XPath xpath) { Element element = createElement(context, path, xpath); element.setTextContent(value); return element; } /** * Get the XML node at the given path. * * @param context * The element from which the path is relative to * @param path * The path of the node to get * @param xpath * An {@link XPath} instance that can be reused across calls * @return Node at the given path, or <code>null</code> if none exists */ public static Node getNode(Element context, String path, XPath xpath) { if (xpath == null) { xpath = WWXML.makeXPath(); } try { return (Node) xpath.evaluate(path, context, XPathConstants.NODE); } catch (XPathExpressionException e) { return null; } } /** * Create an element at the given path if one doesn't already exist. * * @param context * The element from which the path is relative to * @param path * The path of the element * @param xpath * An {@link XPath} instance that can be reused across calls * @return Element at path */ public static Element createElement(Element context, String path, XPath xpath) { Element element = WWXML.getElement(context, path, xpath); if (element == null) { element = WWXML.appendElementPath(context, path); } return element; } public static boolean getBoolean(Element context, String path, boolean def) { return getBoolean(context, path, def, null); } public static boolean getBoolean(Element context, String path, boolean def, XPath xpath) { Boolean b = WWXML.getBoolean(context, path, xpath); if (b == null) { return def; } return b; } public static double getDouble(Element context, String path, double def) { return getDouble(context, path, def, null); } public static double getDouble(Element context, String path, double def, XPath xpath) { Double d = WWXML.getDouble(context, path, xpath); if (d == null) { return def; } return d; } public static int getInteger(Element context, String path, int def) { return getInteger(context, path, def, null); } public static int getInteger(Element context, String path, int def, XPath xpath) { Integer i = WWXML.getInteger(context, path, xpath); if (i == null) { return def; } return i; } public static long getLong(Element context, String path, long def) { return getLong(context, path, def, null); } public static long getLong(Element context, String path, long def, XPath xpath) { Long i = WWXML.getLong(context, path, xpath); if (i == null) { return def; } return i; } /** * Return a URL created from the text identified at the given path relative * to the provided XML element. * * @param element * The XML element the path is relative to * @param path * The xpath expression identifying the text to use to generate * the URL * @param context * The URL context to use for construction of relative URLs * * @return URL created from the text identified at the given path * * @throws MalformedURLException * If the identified text cannot be converted to a valid URL */ public static URL getURL(Element element, String path, URL context) throws MalformedURLException { String text = getText(element, path); return textToURL(text, context); } /** * Return a URL created from the text identified at the given path relative * to the provided XML element. * * @param element * The XML element the path is relative to * @param path * The xpath expression identifying the text to use to generate * the URL * @param context * The URL context to use for construction of relative URLs * @param xpath * An {@link XPath} instance that can be reused between calls * * @return URL created from the text identified at the given path * * @throws MalformedURLException * If the identified text cannot be converted to a valid URL */ public static URL getURL(Element element, String path, URL context, XPath xpath) throws MalformedURLException { String text = WWXML.getText(element, path, xpath); return textToURL(text, context); } protected static URL textToURL(String text, URL context) throws MalformedURLException { if (text == null || text.length() == 0) { return null; } if (context == null) { return new URL(text); } return new URL(context, text); } /** * Returns all elements identified by the given XPath expression relative to * the provided element. * * @param context * The element from which the xpath expression is relative * @param path * The xpath expression to use for locating elements * @param xpath * An {@link XPath} instance that can be reused between calls * * @return All elements identified by the given XPath expression, or the * empty array if none are found. */ public static Element[] getElements(Element context, String path, XPath xpath) { Element[] elements = WWXML.getElements(context, path, xpath); return elements == null ? new Element[0] : elements; } /** * Create and return a new document builder */ public static DocumentBuilder createDocumentBuilder() { return WWXML.createDocumentBuilder(false); } }