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}