001package ezvcard.util;
002
003import java.io.BufferedInputStream;
004import java.io.File;
005import java.io.FileInputStream;
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.Reader;
009import java.io.StringReader;
010import java.io.StringWriter;
011import java.io.Writer;
012import java.util.ArrayList;
013import java.util.HashMap;
014import java.util.List;
015import java.util.Map;
016
017import javax.xml.namespace.QName;
018import javax.xml.parsers.DocumentBuilder;
019import javax.xml.parsers.DocumentBuilderFactory;
020import javax.xml.parsers.ParserConfigurationException;
021import javax.xml.transform.Transformer;
022import javax.xml.transform.TransformerConfigurationException;
023import javax.xml.transform.TransformerException;
024import javax.xml.transform.TransformerFactory;
025import javax.xml.transform.TransformerFactoryConfigurationError;
026import javax.xml.transform.dom.DOMSource;
027import javax.xml.transform.stream.StreamResult;
028
029import org.w3c.dom.Document;
030import org.w3c.dom.Element;
031import org.w3c.dom.Node;
032import org.w3c.dom.NodeList;
033import org.xml.sax.InputSource;
034import org.xml.sax.SAXException;
035
036/*
037 Copyright (c) 2012-2020, Michael Angstadt
038 All rights reserved.
039
040 Redistribution and use in source and binary forms, with or without
041 modification, are permitted provided that the following conditions are met: 
042
043 1. Redistributions of source code must retain the above copyright notice, this
044 list of conditions and the following disclaimer. 
045 2. Redistributions in binary form must reproduce the above copyright notice,
046 this list of conditions and the following disclaimer in the documentation
047 and/or other materials provided with the distribution. 
048
049 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
050 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
051 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
052 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
053 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
054 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
055 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
056 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
057 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
058 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
059
060 The views and conclusions contained in the software and documentation are those
061 of the authors and should not be interpreted as representing official policies, 
062 either expressed or implied, of the FreeBSD Project.
063 */
064
065/**
066 * Generic XML utility methods.
067 * @author Michael Angstadt
068 */
069public final class XmlUtils {
070        /**
071         * Creates a new XML document.
072         * @return the XML document
073         */
074        public static Document createDocument() {
075                try {
076                        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
077                        DocumentBuilder builder = factory.newDocumentBuilder();
078                        return builder.newDocument();
079                } catch (ParserConfigurationException e) {
080                        //should never be thrown because we're not doing anything fancy with the configuration
081                        throw new RuntimeException(e);
082                }
083        }
084
085        /**
086         * Parses an XML string into a DOM.
087         * @param xml the XML string
088         * @return the parsed DOM
089         * @throws SAXException if the string is not valid XML
090         */
091        public static Document toDocument(String xml) throws SAXException {
092                try {
093                        return toDocument(new StringReader(xml));
094                } catch (IOException e) {
095                        //should never be thrown because we're reading from a string
096                        throw new RuntimeException(e);
097                }
098        }
099
100        /**
101         * Parses an XML document from a file.
102         * @param file the file
103         * @return the parsed DOM
104         * @throws SAXException if the XML is not valid
105         * @throws IOException if there is a problem reading from the file
106         */
107        public static Document toDocument(File file) throws SAXException, IOException {
108                InputStream in = new BufferedInputStream(new FileInputStream(file));
109                try {
110                        return XmlUtils.toDocument(in);
111                } finally {
112                        in.close();
113                }
114        }
115
116        /**
117         * Parses an XML document from an input stream.
118         * @param in the input stream
119         * @return the parsed DOM
120         * @throws SAXException if the XML is not valid
121         * @throws IOException if there is a problem reading from the input stream
122         */
123        public static Document toDocument(InputStream in) throws SAXException, IOException {
124                return toDocument(new InputSource(in));
125        }
126
127        /**
128         * <p>
129         * Parses an XML document from a reader.
130         * </p>
131         * <p>
132         * Note that use of this method is discouraged. It ignores the character
133         * encoding that is defined within the XML document itself, and should only
134         * be used if the encoding is undefined or if the encoding needs to be
135         * ignored for whatever reason. The {@link #toDocument(InputStream)} method
136         * should be used instead, since it takes the XML document's character
137         * encoding into account when parsing.
138         * </p>
139         * @param reader the reader
140         * @return the parsed DOM
141         * @throws SAXException if the XML is not valid
142         * @throws IOException if there is a problem reading from the reader
143         * @see <a
144         * href="http://stackoverflow.com/q/3482494/13379">http://stackoverflow.com/q/3482494/13379</a>
145         */
146        public static Document toDocument(Reader reader) throws SAXException, IOException {
147                return toDocument(new InputSource(reader));
148        }
149
150        private static Document toDocument(InputSource in) throws SAXException, IOException {
151                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
152                factory.setNamespaceAware(true);
153                factory.setIgnoringComments(true);
154                applyXXEProtection(factory);
155
156                DocumentBuilder builder;
157                try {
158                        builder = factory.newDocumentBuilder();
159                } catch (ParserConfigurationException e) {
160                        //should never be thrown because we're not doing anything fancy with the configuration
161                        throw new RuntimeException(e);
162                }
163
164                return builder.parse(in);
165        }
166
167        /**
168         * Configures a {@link DocumentBuilderFactory} to protect it against XML
169         * External Entity attacks.
170         * @param factory the factory
171         * @see <a href=
172         * "https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Prevention_Cheat_Sheet#Java">
173         * XXE Cheat Sheet</a>
174         */
175        public static void applyXXEProtection(DocumentBuilderFactory factory) {
176                Map<String, Boolean> features = new HashMap<String, Boolean>();
177                features.put("http://apache.org/xml/features/disallow-doctype-decl", true);
178                features.put("http://xml.org/sax/features/external-general-entities", false);
179                features.put("http://xml.org/sax/features/external-parameter-entities", false);
180                features.put("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
181
182                for (Map.Entry<String, Boolean> entry : features.entrySet()) {
183                        String feature = entry.getKey();
184                        Boolean value = entry.getValue();
185                        try {
186                                factory.setFeature(feature, value);
187                        } catch (ParserConfigurationException e) {
188                                //feature is not supported by the local XML engine, skip it
189                        }
190                }
191
192                factory.setXIncludeAware(false);
193                factory.setExpandEntityReferences(false);
194        }
195
196        /**
197         * Configures a {@link TransformerFactory} to protect it against XML
198         * External Entity attacks.
199         * @param factory the factory
200         * @see <a href=
201         * "https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Prevention_Cheat_Sheet#Java">
202         * XXE Cheat Sheet</a>
203         */
204        public static void applyXXEProtection(TransformerFactory factory) {
205                //@formatter:off
206                String[] attributes = {
207                        //XMLConstants.ACCESS_EXTERNAL_DTD (Java 7 only)
208                        "http://javax.xml.XMLConstants/property/accessExternalDTD",
209
210                        //XMLConstants.ACCESS_EXTERNAL_STYLESHEET (Java 7 only)
211                        "http://javax.xml.XMLConstants/property/accessExternalStylesheet"
212                };
213                //@formatter:on
214
215                for (String attribute : attributes) {
216                        try {
217                                factory.setAttribute(attribute, "");
218                        } catch (IllegalArgumentException e) {
219                                //attribute is not supported by the local XML engine, skip it
220                        }
221                }
222        }
223
224        /**
225         * Converts an XML node to a string.
226         * @param node the XML node
227         * @return the string
228         */
229        public static String toString(Node node) {
230                return toString(node, new HashMap<String, String>());
231        }
232
233        /**
234         * Converts an XML node to a string.
235         * @param node the XML node
236         * @param outputProperties the output properties
237         * @return the string
238         */
239        public static String toString(Node node, Map<String, String> outputProperties) {
240                try {
241                        StringWriter writer = new StringWriter();
242                        toWriter(node, writer, outputProperties);
243                        return writer.toString();
244                } catch (TransformerException e) {
245                        //should never be thrown because we're writing to a string
246                        throw new RuntimeException(e);
247                }
248        }
249
250        /**
251         * Writes an XML node to a writer.
252         * @param node the XML node
253         * @param writer the writer
254         * @throws TransformerException if there's a problem writing to the writer
255         */
256        public static void toWriter(Node node, Writer writer) throws TransformerException {
257                toWriter(node, writer, new HashMap<String, String>());
258        }
259
260        /**
261         * Writes an XML node to a writer.
262         * @param node the XML node
263         * @param writer the writer
264         * @param outputProperties the output properties
265         * @throws TransformerException if there's a problem writing to the writer
266         */
267        public static void toWriter(Node node, Writer writer, Map<String, String> outputProperties) throws TransformerException {
268                Transformer transformer;
269                try {
270                        transformer = TransformerFactory.newInstance().newTransformer();
271                } catch (TransformerConfigurationException e) {
272                        //should never be thrown because we're not doing anything fancy with the configuration
273                        throw new RuntimeException(e);
274                } catch (TransformerFactoryConfigurationError e) {
275                        //should never be thrown because we're not doing anything fancy with the configuration
276                        throw new RuntimeException(e);
277                }
278
279                assignOutputProperties(transformer, outputProperties);
280
281                DOMSource source = new DOMSource(node);
282                StreamResult result = new StreamResult(writer);
283                transformer.transform(source, result);
284        }
285
286        /**
287         * Assigns the given output properties to the given transformer, ignoring
288         * invalid output properties.
289         * @param transformer the transformer
290         * @param outputProperties the output properties
291         */
292        public static void assignOutputProperties(Transformer transformer, Map<String, String> outputProperties) {
293                for (Map.Entry<String, String> property : outputProperties.entrySet()) {
294                        try {
295                                transformer.setOutputProperty(property.getKey(), property.getValue());
296                        } catch (IllegalArgumentException e) {
297                                //ignore invalid output properties
298                        }
299                }
300        }
301
302        /**
303         * Gets all the elements out of a {@link NodeList}.
304         * @param nodeList the node list
305         * @return the elements
306         */
307        public static List<Element> toElementList(NodeList nodeList) {
308                List<Element> elements = new ArrayList<Element>();
309                for (int i = 0; i < nodeList.getLength(); i++) {
310                        Node node = nodeList.item(i);
311                        if (node instanceof Element) {
312                                elements.add((Element) node);
313                        }
314                }
315                return elements;
316        }
317
318        /**
319         * Gets the first child element of an element.
320         * @param parent the parent element
321         * @return the first child element or null if there are no child elements
322         */
323        public static Element getFirstChildElement(Element parent) {
324                return getFirstChildElement((Node) parent);
325        }
326
327        /**
328         * Gets the first child element of a node.
329         * @param parent the node
330         * @return the first child element or null if there are no child elements
331         */
332        private static Element getFirstChildElement(Node parent) {
333                NodeList nodeList = parent.getChildNodes();
334                for (int i = 0; i < nodeList.getLength(); i++) {
335                        Node node = nodeList.item(i);
336                        if (node instanceof Element) {
337                                return (Element) node;
338                        }
339                }
340                return null;
341        }
342
343        /**
344         * Determines if a node has a particular qualified name.
345         * @param node the node
346         * @param qname the qualified name
347         * @return true if the node has the given qualified name, false if not
348         */
349        public static boolean hasQName(Node node, QName qname) {
350                return qname.getNamespaceURI().equals(node.getNamespaceURI()) && qname.getLocalPart().equals(node.getLocalName());
351        }
352
353        private XmlUtils() {
354                //hide
355        }
356}