| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.velocity.tools; |
| |
| import org.apache.velocity.runtime.RuntimeConstants; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.NamedNodeMap; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.xml.sax.ErrorHandler; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.SAXParseException; |
| |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.io.StringWriter; |
| import java.lang.ref.SoftReference; |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Stack; |
| import java.util.concurrent.LinkedBlockingDeque; |
| |
| import javax.xml.XMLConstants; |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| import javax.xml.transform.OutputKeys; |
| import javax.xml.transform.Transformer; |
| 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.XPathExpression; |
| import javax.xml.xpath.XPathExpressionException; |
| import javax.xml.xpath.XPathFactory; |
| |
| /** |
| * <p>Utility class for simplifying parsing of xml documents. Documents are not validated, and |
| * loading of external files (xinclude, external entities, DTDs, etc.) are disabled.</p> |
| * |
| * @author Claude Brisson |
| * @since 3.0 |
| * @version $$ |
| */ |
| public final class XmlUtils |
| { |
| /* several pieces of code were borrowed from the Apache Shindig XmlUtil class.*/ |
| |
| private static final Logger LOGGER = LoggerFactory.getLogger(XmlUtils.class); |
| |
| /** |
| * Handles xml errors so that they're not logged to stderr. |
| */ |
| private static final ErrorHandler errorHandler = new ErrorHandler() |
| { |
| public void error(SAXParseException exception) throws SAXException |
| { |
| throw exception; |
| } |
| public void fatalError(SAXParseException exception) throws SAXException |
| { |
| throw exception; |
| } |
| public void warning(SAXParseException exception) |
| { |
| LOGGER.info("warning during parsing", exception); |
| } |
| }; |
| |
| private static boolean canReuseBuilders = false; |
| |
| private static final DocumentBuilderFactory builderFactory = createDocumentBuilderFactory(); |
| |
| public static final DocumentBuilderFactory createDocumentBuilderFactory() |
| { |
| DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance(); |
| // Namespace support is required for <os:> elements |
| builderFactory.setNamespaceAware(true); |
| |
| // Disable various insecure and/or expensive options. |
| builderFactory.setValidating(false); |
| |
| // Can't disable doctypes entirely because they're usually harmless. External entity |
| // resolution, however, is both expensive and insecure. |
| try |
| { |
| builderFactory.setAttribute("http://xml.org/sax/features/external-general-entities", false); |
| } |
| catch (IllegalArgumentException e) |
| { |
| // Not supported by some very old parsers. |
| LOGGER.info("Error parsing external general entities: ", e); |
| } |
| |
| try |
| { |
| builderFactory.setAttribute("http://xml.org/sax/features/external-parameter-entities", false); |
| } |
| catch (IllegalArgumentException e) |
| { |
| // Not supported by some very old parsers. |
| LOGGER.info("Error parsing external parameter entities: ", e); |
| } |
| |
| try |
| { |
| builderFactory.setAttribute("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); |
| } |
| catch (IllegalArgumentException e) |
| { |
| // Only supported by Apache's XML parsers. |
| LOGGER.info("Error parsing external DTD: ", e); |
| } |
| |
| try |
| { |
| builderFactory.setAttribute(XMLConstants.FEATURE_SECURE_PROCESSING, true); |
| } |
| catch (IllegalArgumentException e) |
| { |
| // Not supported by older parsers. |
| LOGGER.info("Error parsing secure XML: ", e); |
| } |
| |
| return builderFactory; |
| } |
| |
| private static final ThreadLocal<DocumentBuilder> reusableBuilder |
| = new ThreadLocal<DocumentBuilder>() { |
| @Override |
| protected DocumentBuilder initialValue() { |
| try |
| { |
| LOGGER.trace("Created a new document builder"); |
| return builderFactory.newDocumentBuilder(); |
| } |
| catch (ParserConfigurationException e) |
| { |
| throw new RuntimeException(e); |
| } |
| } |
| }; |
| |
| static |
| { |
| try |
| { |
| DocumentBuilder builder = builderFactory.newDocumentBuilder(); |
| builder.reset(); |
| canReuseBuilders = true; |
| LOGGER.trace("reusing document builders"); |
| } |
| catch (UnsupportedOperationException e) |
| { |
| // Only supported by newer parsers (xerces 2.8.x+ for instance). |
| canReuseBuilders = false; |
| LOGGER.trace("not reusing document builders"); |
| } |
| catch (ParserConfigurationException e) |
| { |
| // Only supported by newer parsers (xerces 2.8.x+ for instance). |
| canReuseBuilders = false; |
| LOGGER.trace("not reusing document builders"); |
| } |
| } |
| |
| private static LinkedBlockingDeque<SoftReference<DocumentBuilder>> builderPool = new LinkedBlockingDeque<SoftReference<DocumentBuilder>>(); // contains only idle builders |
| private static int maxBuildersCount = 100; |
| private static int currentBuildersCount = 0; |
| private static final String BUILDER_MAX_INSTANCES_KEY = "velocity.tools.xml.documentbuilder.max.instances"; |
| |
| static |
| { |
| /* We're in a static code portion, so use a system property so that the DocumentBuilder pool size |
| * remains configurable. */ |
| try |
| { |
| String configuredMax = System.getProperty(BUILDER_MAX_INSTANCES_KEY); |
| if (configuredMax != null) |
| { |
| maxBuildersCount = Integer.parseInt(configuredMax); |
| } |
| } |
| catch(Exception e) |
| { |
| LOGGER.error("could not configure XML document builder max instances count", e); |
| } |
| } |
| |
| /** |
| * Get a document builder |
| * @return document builder |
| */ |
| private static synchronized DocumentBuilder getDocumentBuilder() |
| { |
| DocumentBuilder builder = null; |
| if (canReuseBuilders && builderPool.size() > 0) |
| { |
| builder = builderPool.pollFirst().get(); |
| } |
| if (builder == null) |
| { |
| if(!canReuseBuilders || currentBuildersCount < maxBuildersCount) |
| { |
| try |
| { |
| builder = builderFactory.newDocumentBuilder(); |
| builder.setErrorHandler(errorHandler); |
| ++currentBuildersCount; |
| } |
| catch(Exception e) |
| { |
| /* this is a fatal error */ |
| throw new RuntimeException("could not create a new XML DocumentBuilder instance", e); |
| } |
| } |
| else |
| { |
| try |
| { |
| LOGGER.warn("reached XML DocumentBuilder pool size limit, current thread needs to wait; you can increase pool size with the {} system property", BUILDER_MAX_INSTANCES_KEY); |
| builder = builderPool.takeFirst().get(); |
| } |
| catch(InterruptedException ie) |
| { |
| LOGGER.warn("caught an InterruptedException while waiting for a DocumentBuilder instance"); |
| } |
| } |
| } |
| return builder; |
| } |
| |
| /** |
| * Release the given document builder |
| * @param builder document builder |
| */ |
| private static synchronized void releaseBuilder(DocumentBuilder builder) |
| { |
| builder.reset(); |
| builderPool.addLast(new SoftReference<DocumentBuilder>(builder)); |
| } |
| |
| private XmlUtils() {} |
| |
| /** |
| * Extracts an attribute from a node. |
| * |
| * @param node target node |
| * @param attr attribute name |
| * @param def default value |
| * @return The value of the attribute, or def |
| */ |
| public static String getAttribute(Node node, String attr, String def) |
| { |
| NamedNodeMap attrs = node.getAttributes(); |
| Node val = attrs.getNamedItem(attr); |
| if (val != null) |
| { |
| return val.getNodeValue(); |
| } |
| return def; |
| } |
| |
| /** |
| * @param node target node |
| * @param attr attribute name |
| * @return The value of the given attribute, or null if not present. |
| */ |
| public static String getAttribute(Node node, String attr) |
| { |
| return getAttribute(node, attr, null); |
| } |
| |
| /** |
| * Retrieves an attribute as a boolean. |
| * |
| * @param node target node |
| * @param attr attribute name |
| * @param def default value |
| * @return True if the attribute exists and is not equal to "false" |
| * false if equal to "false", and def if not present. |
| */ |
| public static boolean getBoolAttribute(Node node, String attr, boolean def) |
| { |
| String value = getAttribute(node, attr); |
| if (value == null) |
| { |
| return def; |
| } |
| return Boolean.parseBoolean(value); |
| } |
| |
| /** |
| * @param node target node |
| * @param attr attribute name |
| * @return True if the attribute exists and is not equal to "false" |
| * false otherwise. |
| */ |
| public static boolean getBoolAttribute(Node node, String attr) |
| { |
| return getBoolAttribute(node, attr, false); |
| } |
| |
| /** |
| * @param node target node |
| * @param attr attribute name |
| * @param def default value |
| * @return An attribute coerced to an integer. |
| */ |
| public static int getIntAttribute(Node node, String attr, int def) |
| { |
| String value = getAttribute(node, attr); |
| if (value == null) |
| { |
| return def; |
| } |
| try |
| { |
| return Integer.parseInt(value); |
| } |
| catch (NumberFormatException e) |
| { |
| return def; |
| } |
| } |
| |
| /** |
| * @param node target node |
| * @param attr attribute name |
| * @return An attribute coerced to an integer. |
| */ |
| public static int getIntAttribute(Node node, String attr) |
| { |
| return getIntAttribute(node, attr, 0); |
| } |
| |
| /** |
| * Attempts to parse the input xml into a single element. |
| * @param xml xml stream reader |
| * @return The document object |
| */ |
| public static Element parse(Reader xml) |
| { |
| Element ret = null; |
| DocumentBuilder builder = getDocumentBuilder(); |
| try |
| { |
| ret = builder.parse(new InputSource(xml)).getDocumentElement(); |
| releaseBuilder(builder); |
| return ret; |
| } |
| catch(Exception e) |
| { |
| LOGGER.error("could not parse given xml", e); |
| } |
| finally |
| { |
| releaseBuilder(builder); |
| } |
| return ret; |
| } |
| |
| /** |
| * Attempts to parse the input xml into a single element. |
| * @param xml xml string |
| * @return The document object |
| */ |
| public static Element parse(String xml) |
| { |
| return parse(new StringReader(xml)); |
| } |
| |
| /** |
| * Search for nodes using an XPath expression |
| * @param xpath XPath expression |
| * @param context evaluation context |
| * @return org.w3c.NodeList of found nodes |
| * @throws XPathExpressionException |
| */ |
| public static NodeList search(String xpath, Node context) throws XPathExpressionException |
| { |
| NodeList ret = null; |
| XPath xp = XPathFactory.newInstance().newXPath(); |
| XPathExpression exp = xp.compile(xpath); |
| ret = (NodeList)exp.evaluate(context, XPathConstants.NODESET); |
| return ret; |
| } |
| |
| /** |
| * Search for nodes using an XPath expression |
| * @param xpath XPath expression |
| * @param context evaluation context |
| * @return List of found nodes |
| * @throws XPathExpressionException |
| */ |
| public static List<Node> getNodes(String xpath, Node context) throws XPathExpressionException |
| { |
| List<Node> ret = new ArrayList<>(); |
| NodeList lst = search(xpath, context); |
| for (int i = 0; i < lst.getLength(); ++i) |
| { |
| ret.add(lst.item(i)); |
| } |
| return ret; |
| } |
| |
| |
| /** |
| * Search for elements using an XPath expression |
| * @param xpath XPath expression |
| * @param context evaluation context |
| * @return List of found elements |
| * @throws XPathExpressionException |
| */ |
| public static List<Element> getElements(String xpath, Node context) throws XPathExpressionException |
| { |
| List<Element> ret = new ArrayList<>(); |
| NodeList lst = search(xpath, context); |
| for (int i = 0; i < lst.getLength(); ++i) |
| { |
| // will throw a ClassCastExpression if Node is not an Element, |
| // that's what we want |
| ret.add((Element)lst.item(i)); |
| } |
| return ret; |
| } |
| |
| /** |
| * <p>Builds the xpath expression for a node (tries to use id/name nodes when possible to get a unique path)</p> |
| * @param n target node |
| * @return node xpath |
| */ |
| // (borrow from http://stackoverflow.com/questions/5046174/get-xpath-from-the-org-w3c-dom-node ) |
| public static String nodePath(Node n) |
| { |
| // abort early |
| if (null == n) |
| return null; |
| |
| // declarations |
| Node parent = null; |
| Stack<Node> hierarchy = new Stack<Node>(); |
| StringBuffer buffer = new StringBuffer('/'); |
| |
| // push element on stack |
| hierarchy.push(n); |
| |
| switch (n.getNodeType()) { |
| case Node.ATTRIBUTE_NODE: |
| parent = ((Attr) n).getOwnerElement(); |
| break; |
| case Node.ELEMENT_NODE: |
| parent = n.getParentNode(); |
| break; |
| case Node.DOCUMENT_NODE: |
| parent = n.getParentNode(); |
| break; |
| default: |
| throw new IllegalStateException("Unexpected Node type" + n.getNodeType()); |
| } |
| |
| while (null != parent && parent.getNodeType() != Node.DOCUMENT_NODE) { |
| // push on stack |
| hierarchy.push(parent); |
| |
| // get parent of parent |
| parent = parent.getParentNode(); |
| } |
| |
| // construct xpath |
| Object obj = null; |
| while (!hierarchy.isEmpty() && null != (obj = hierarchy.pop())) { |
| Node node = (Node) obj; |
| boolean handled = false; |
| |
| if (node.getNodeType() == Node.ELEMENT_NODE) |
| { |
| Element e = (Element) node; |
| |
| // is this the root element? |
| if (buffer.length() == 1) |
| { |
| // root element - simply append element name |
| buffer.append(node.getNodeName()); |
| } |
| else |
| { |
| // child element - append slash and element name |
| buffer.append("/"); |
| buffer.append(node.getNodeName()); |
| |
| if (node.hasAttributes()) |
| { |
| // see if the element has a name or id attribute |
| if (e.hasAttribute("id")) |
| { |
| // id attribute found - use that |
| buffer.append("[@id='" + e.getAttribute("id") + "']"); |
| handled = true; |
| } |
| else if (e.hasAttribute("name")) |
| { |
| // name attribute found - use that |
| buffer.append("[@name='" + e.getAttribute("name") + "']"); |
| handled = true; |
| } |
| } |
| |
| if (!handled) |
| { |
| // no known attribute we could use - get sibling index |
| int prev_siblings = 1; |
| Node prev_sibling = node.getPreviousSibling(); |
| while (null != prev_sibling) |
| { |
| if (prev_sibling.getNodeType() == node.getNodeType()) |
| { |
| if (prev_sibling.getNodeName().equalsIgnoreCase( |
| node.getNodeName())) |
| { |
| prev_siblings++; |
| } |
| } |
| prev_sibling = prev_sibling.getPreviousSibling(); |
| } |
| buffer.append("[" + prev_siblings + "]"); |
| } |
| } |
| } |
| else if (node.getNodeType() == Node.ATTRIBUTE_NODE) |
| { |
| buffer.append("/@"); |
| buffer.append(node.getNodeName()); |
| } |
| } |
| // return buffer |
| return buffer.toString(); |
| } |
| |
| /** |
| * XML Node to string |
| * @param node XML node |
| * @return XML node string representation |
| */ |
| public static String nodeToString(Node node) |
| { |
| StringWriter sw = new StringWriter(); |
| try |
| { |
| Transformer t = TransformerFactory.newInstance().newTransformer(); |
| t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes"); |
| t.setOutputProperty(OutputKeys.INDENT, "no"); |
| /* CB - Since everything is already stored as strings in memory why shoud an encoding be required here? */ |
| t.setOutputProperty(OutputKeys.ENCODING, RuntimeConstants.ENCODING_DEFAULT); |
| t.transform(new DOMSource(node), new StreamResult(sw)); |
| } |
| catch (TransformerException te) |
| { |
| LOGGER.error("could not convert XML node to string", te); |
| } |
| return sw.toString(); |
| } |
| |
| /** |
| * Checkes whether the given mime type is an XML format |
| * @param mimeType mime type |
| * @return <code>true</code> if this mime type is an XML format |
| */ |
| public static boolean isXmlMimeType(String mimeType) |
| { |
| return mimeType != null && |
| ( |
| "text/xml".equals(mimeType) || |
| "application/xml".equals(mimeType) || |
| mimeType.endsWith("+xml") |
| ); |
| } |
| } |