001package biweekly.util;
002
003import java.io.IOException;
004import java.io.InputStream;
005import java.io.Reader;
006import java.io.StringReader;
007import java.io.StringWriter;
008import java.io.Writer;
009import java.util.ArrayList;
010import java.util.HashMap;
011import java.util.List;
012import java.util.Map;
013
014import javax.xml.namespace.QName;
015import javax.xml.parsers.DocumentBuilder;
016import javax.xml.parsers.DocumentBuilderFactory;
017import javax.xml.parsers.ParserConfigurationException;
018import javax.xml.transform.OutputKeys;
019import javax.xml.transform.Transformer;
020import javax.xml.transform.TransformerConfigurationException;
021import javax.xml.transform.TransformerException;
022import javax.xml.transform.TransformerFactory;
023import javax.xml.transform.TransformerFactoryConfigurationError;
024import javax.xml.transform.dom.DOMSource;
025import javax.xml.transform.stream.StreamResult;
026
027import org.w3c.dom.Document;
028import org.w3c.dom.Element;
029import org.w3c.dom.Node;
030import org.w3c.dom.NodeList;
031import org.xml.sax.InputSource;
032import org.xml.sax.SAXException;
033
034/*
035 Copyright (c) 2013-2015, Michael Angstadt
036 All rights reserved.
037
038 Redistribution and use in source and binary forms, with or without
039 modification, are permitted provided that the following conditions are met: 
040
041 1. Redistributions of source code must retain the above copyright notice, this
042 list of conditions and the following disclaimer. 
043 2. Redistributions in binary form must reproduce the above copyright notice,
044 this list of conditions and the following disclaimer in the documentation
045 and/or other materials provided with the distribution. 
046
047 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
048 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
049 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
050 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
051 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
052 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
053 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
054 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
055 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
056 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
057
058 The views and conclusions contained in the software and documentation are those
059 of the authors and should not be interpreted as representing official policies, 
060 either expressed or implied, of the FreeBSD Project.
061 */
062
063/**
064 * Generic XML utility methods.
065 * @author Michael Angstadt
066 */
067public class XmlUtils {
068        /**
069         * Creates a new XML document.
070         * @return the XML document
071         */
072        public static Document createDocument() {
073                try {
074                        DocumentBuilderFactory fact = DocumentBuilderFactory.newInstance();
075                        fact.setNamespaceAware(true);
076                        DocumentBuilder db = fact.newDocumentBuilder();
077                        return db.newDocument();
078                } catch (ParserConfigurationException e) {
079                        //will probably never be thrown because we're not doing anything fancy with the configuration
080                        throw new RuntimeException(e);
081                }
082        }
083
084        /**
085         * Parses an XML string into a DOM.
086         * @param xml the XML string
087         * @return the parsed DOM
088         * @throws SAXException if the string is not valid XML
089         */
090        public static Document toDocument(String xml) throws SAXException {
091                try {
092                        return toDocument(new StringReader(xml));
093                } catch (IOException e) {
094                        //reading from string
095                        throw new RuntimeException(e);
096                }
097        }
098
099        /**
100         * Parses an XML document from an input stream.
101         * @param in the input stream
102         * @return the parsed DOM
103         * @throws SAXException if the XML is not valid
104         * @throws IOException if there is a problem reading from the input stream
105         */
106        public static Document toDocument(InputStream in) throws SAXException, IOException {
107                return toDocument(new InputSource(in));
108        }
109
110        /**
111         * <p>
112         * Parses an XML document from a reader.
113         * </p>
114         * <p>
115         * Note that use of this method is discouraged. It ignores the character
116         * encoding that is defined within the XML document itself, and should only
117         * be used if the encoding is undefined or if the encoding needs to be
118         * ignored for whatever reason. The {@link #toDocument(InputStream)} method
119         * should be used instead, since it takes the XML document's character
120         * encoding into account when parsing.
121         * </p>
122         * @param reader the reader
123         * @return the parsed DOM
124         * @throws SAXException if the XML is not valid
125         * @throws IOException if there is a problem reading from the reader
126         * @see <a
127         * href="http://stackoverflow.com/q/3482494/13379">http://stackoverflow.com/q/3482494/13379</a>
128         */
129        public static Document toDocument(Reader reader) throws SAXException, IOException {
130                return toDocument(new InputSource(reader));
131        }
132
133        private static Document toDocument(InputSource in) throws SAXException, IOException {
134                try {
135                        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
136                        dbf.setNamespaceAware(true);
137                        dbf.setIgnoringComments(true);
138                        DocumentBuilder db = dbf.newDocumentBuilder();
139                        return db.parse(in);
140                } catch (ParserConfigurationException e) {
141                        //will probably never be thrown because we're not doing anything fancy with the configuration
142                        throw new RuntimeException(e);
143                }
144        }
145
146        /**
147         * Converts an XML node to a string.
148         * @param node the XML node
149         * @return the string
150         */
151        public static String toString(Node node) {
152                return toString(node, new HashMap<String, String>());
153        }
154
155        /**
156         * Converts an XML node to a string.
157         * @param node the XML node
158         * @param prettyPrint true to pretty print, false not to
159         * @return the string
160         */
161        public static String toString(Node node, boolean prettyPrint) {
162                Map<String, String> properties = new HashMap<String, String>();
163                if (prettyPrint) {
164                        properties.put(OutputKeys.INDENT, "yes");
165                        properties.put("{http://xml.apache.org/xslt}indent-amount", "2");
166                }
167                return toString(node, properties);
168        }
169
170        /**
171         * Converts an XML node to a string.
172         * @param node the XML node
173         * @param outputProperties the output properties
174         * @return the string
175         */
176        public static String toString(Node node, Map<String, String> outputProperties) {
177                try {
178                        StringWriter writer = new StringWriter();
179                        toWriter(node, writer, outputProperties);
180                        return writer.toString();
181                } catch (TransformerException e) {
182                        //should never be thrown because we're writing to string
183                        throw new RuntimeException(e);
184                }
185        }
186
187        /**
188         * Writes an XML node to a writer.
189         * @param node the XML node
190         * @param writer the writer
191         * @throws TransformerException if there's a problem writing to the writer
192         */
193        public static void toWriter(Node node, Writer writer) throws TransformerException {
194                toWriter(node, writer, new HashMap<String, String>());
195        }
196
197        /**
198         * Writes an XML node to a writer.
199         * @param node the XML node
200         * @param writer the writer
201         * @param outputProperties the output properties
202         * @throws TransformerException if there's a problem writing to the writer
203         */
204        public static void toWriter(Node node, Writer writer, Map<String, String> outputProperties) throws TransformerException {
205                try {
206                        Transformer transformer = TransformerFactory.newInstance().newTransformer();
207                        for (Map.Entry<String, String> property : outputProperties.entrySet()) {
208                                try {
209                                        transformer.setOutputProperty(property.getKey(), property.getValue());
210                                } catch (IllegalArgumentException e) {
211                                        //ignore invalid output properties
212                                }
213                        }
214
215                        DOMSource source = new DOMSource(node);
216                        StreamResult result = new StreamResult(writer);
217                        transformer.transform(source, result);
218                } catch (TransformerConfigurationException e) {
219                        //no complex configurations
220                } catch (TransformerFactoryConfigurationError e) {
221                        //no complex configurations
222                }
223        }
224
225        /**
226         * Gets all the elements out of a {@link NodeList}.
227         * @param nodeList the node list
228         * @return the elements
229         */
230        public static List<Element> toElementList(NodeList nodeList) {
231                List<Element> elements = new ArrayList<Element>();
232                for (int i = 0; i < nodeList.getLength(); i++) {
233                        Node node = nodeList.item(i);
234                        if (node instanceof Element) {
235                                elements.add((Element) node);
236                        }
237                }
238                return elements;
239        }
240
241        /**
242         * Gets the root element of a document.
243         * @param parent the document
244         * @return the root element
245         */
246        public static Element getRootElement(Document parent) {
247                return getFirstChildElement((Node) parent);
248        }
249
250        /**
251         * Gets the first child element of an element.
252         * @param parent the parent element
253         * @return the first child element or null if there are no child elements
254         */
255        public static Element getFirstChildElement(Element parent) {
256                return getFirstChildElement((Node) parent);
257        }
258
259        /**
260         * Gets the first child element of a node.
261         * @param parent the node
262         * @return the first child element or null if there are no child elements
263         */
264        private static Element getFirstChildElement(Node parent) {
265                NodeList nodeList = parent.getChildNodes();
266                for (int i = 0; i < nodeList.getLength(); i++) {
267                        Node node = nodeList.item(i);
268                        if (node instanceof Element) {
269                                return (Element) node;
270                        }
271                }
272                return null;
273        }
274
275        /**
276         * Determines if a node has a particular qualified name.
277         * @param node the node
278         * @param qname the qualified name
279         * @return true if the node has the given qualified name, false if not
280         */
281        public static boolean hasQName(Node node, QName qname) {
282                return qname.getNamespaceURI().equals(node.getNamespaceURI()) && qname.getLocalPart().equals(node.getLocalName());
283        }
284
285        private XmlUtils() {
286                //hide
287        }
288}