All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.velocity.tools.XmlUtils Maven / Gradle / Ivy

Go to download

Generic tools that can be used in any context. PLEASE NOTE: this is a temporary fork to unblock projects migrating to Jakarta, but I won't continue maintaining it in the future as the Velocity team doesn't understand the value of Jakarta. I strongly suggest you plan a switch to a more modern template engine such as Thymeleaf.

The newest version!
/*
 * 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;

/**
 * 

Utility class for simplifying parsing of xml documents. Documents are not validated, and * loading of external files (xinclude, external entities, DTDs, etc.) are disabled.

* * @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 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 reusableBuilder = new ThreadLocal() { @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> builderPool = new LinkedBlockingDeque>(); // 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(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 getNodes(String xpath, Node context) throws XPathExpressionException { List 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 getElements(String xpath, Node context) throws XPathExpressionException { List 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; } /** *

Builds the xpath expression for a node (tries to use id/name nodes when possible to get a unique path)

* @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 hierarchy = new Stack(); 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.COMMENT_NODE: case Node.ELEMENT_NODE: 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 true 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") ); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy