| /* |
| * 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 freemarker.ext.dom; |
| |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.lang.ref.WeakReference; |
| import java.net.MalformedURLException; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.WeakHashMap; |
| |
| import javax.xml.parsers.DocumentBuilder; |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| |
| import org.w3c.dom.Attr; |
| import org.w3c.dom.CDATASection; |
| import org.w3c.dom.CharacterData; |
| import org.w3c.dom.Document; |
| import org.w3c.dom.DocumentType; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| import org.w3c.dom.ProcessingInstruction; |
| import org.w3c.dom.Text; |
| import org.xml.sax.ErrorHandler; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.SAXException; |
| |
| import freemarker.core._UnexpectedTypeErrorExplainerTemplateModel; |
| import freemarker.ext.util.WrapperTemplateModel; |
| import freemarker.log.Logger; |
| import freemarker.template.AdapterTemplateModel; |
| import freemarker.template.DefaultObjectWrapper; |
| import freemarker.template.SimpleScalar; |
| import freemarker.template.TemplateBooleanModel; |
| import freemarker.template.TemplateDateModel; |
| import freemarker.template.TemplateHashModel; |
| import freemarker.template.TemplateModel; |
| import freemarker.template.TemplateModelException; |
| import freemarker.template.TemplateNodeModel; |
| import freemarker.template.TemplateNumberModel; |
| import freemarker.template.TemplateSequenceModel; |
| |
| /** |
| * A base class for wrapping a single W3C DOM Node as a FreeMarker template model. |
| * |
| * <p> |
| * Note that {@link DefaultObjectWrapper} automatically wraps W3C DOM {@link Node}-s into this, so you may not need to |
| * do that with this class manually. Though, before dropping the {@link Node}-s into the data-model, you may want to |
| * apply {@link NodeModel#simplify(Node)} on them. |
| * |
| * <p> |
| * Note that this class shouldn't be used to represent a result set of 0 or multiple nodes (we use {@link NodeListModel} |
| * then), but should be used to represent a node set of exactly 1 node. |
| */ |
| abstract public class NodeModel |
| implements TemplateNodeModel, TemplateHashModel, TemplateSequenceModel, |
| AdapterTemplateModel, WrapperTemplateModel, _UnexpectedTypeErrorExplainerTemplateModel { |
| |
| static private final Logger LOG = Logger.getLogger("freemarker.dom"); |
| |
| private static final Object STATIC_LOCK = new Object(); |
| |
| static private DocumentBuilderFactory docBuilderFactory; |
| |
| static private final Map xpathSupportMap = Collections.synchronizedMap(new WeakHashMap()); |
| |
| static private XPathSupport jaxenXPathSupport; |
| |
| static private ErrorHandler errorHandler; |
| |
| static Class xpathSupportClass; |
| |
| static { |
| try { |
| useDefaultXPathSupport(); |
| } catch (Exception e) { |
| // do nothing |
| } |
| if (xpathSupportClass == null && LOG.isWarnEnabled()) { |
| LOG.warn("No XPath support is available."); |
| } |
| } |
| |
| /** |
| * The W3C DOM Node being wrapped. |
| */ |
| final Node node; |
| private TemplateSequenceModel children; |
| private NodeModel parent; |
| |
| /** |
| * Sets the DOM Parser implementation to be used when building NodeModel |
| * objects from XML files. |
| */ |
| static public void setDocumentBuilderFactory(DocumentBuilderFactory docBuilderFactory) { |
| synchronized (STATIC_LOCK) { |
| NodeModel.docBuilderFactory = docBuilderFactory; |
| } |
| } |
| |
| /** |
| * @return the DOM Parser implementation that is used when |
| * building NodeModel objects from XML files. |
| */ |
| static public DocumentBuilderFactory getDocumentBuilderFactory() { |
| synchronized (STATIC_LOCK) { |
| if (docBuilderFactory == null) { |
| DocumentBuilderFactory newFactory = DocumentBuilderFactory.newInstance(); |
| newFactory.setNamespaceAware(true); |
| newFactory.setIgnoringElementContentWhitespace(true); |
| docBuilderFactory = newFactory; // We only write it out when the initialization was full |
| } |
| return docBuilderFactory; |
| } |
| } |
| |
| /** |
| * sets the error handler to use when parsing the document. |
| */ |
| static public void setErrorHandler(ErrorHandler errorHandler) { |
| synchronized (STATIC_LOCK) { |
| NodeModel.errorHandler = errorHandler; |
| } |
| } |
| |
| /** |
| * @since 2.3.20 |
| */ |
| static public ErrorHandler getErrorHandler() { |
| synchronized (STATIC_LOCK) { |
| return NodeModel.errorHandler; |
| } |
| } |
| |
| /** |
| * Create a NodeModel from a SAX input source. Adjacent text nodes will be merged (and CDATA sections |
| * are considered as text nodes). |
| * @param removeComments whether to remove all comment nodes |
| * (recursively) from the tree before processing |
| * @param removePIs whether to remove all processing instruction nodes |
| * (recursively from the tree before processing |
| */ |
| static public NodeModel parse(InputSource is, boolean removeComments, boolean removePIs) |
| throws SAXException, IOException, ParserConfigurationException { |
| DocumentBuilder builder = getDocumentBuilderFactory().newDocumentBuilder(); |
| ErrorHandler errorHandler = getErrorHandler(); |
| if (errorHandler != null) builder.setErrorHandler(errorHandler); |
| final Document doc; |
| try { |
| doc = builder.parse(is); |
| } catch (MalformedURLException e) { |
| // This typical error has an error message that is hard to understand, so let's translate it: |
| if (is.getSystemId() == null && is.getCharacterStream() == null && is.getByteStream() == null) { |
| throw new MalformedURLException( |
| "The SAX InputSource has systemId == null && characterStream == null && byteStream == null. " |
| + "This is often because it was created with a null InputStream or Reader, which is often because " |
| + "the XML file it should point to was not found. " |
| + "(The original exception was: " + e + ")"); |
| } else { |
| throw e; |
| } |
| } |
| if (removeComments && removePIs) { |
| simplify(doc); |
| } else { |
| if (removeComments) { |
| removeComments(doc); |
| } |
| if (removePIs) { |
| removePIs(doc); |
| } |
| mergeAdjacentText(doc); |
| } |
| return wrap(doc); |
| } |
| |
| /** |
| * Create a NodeModel from an XML input source. By default, |
| * all comments and processing instruction nodes are |
| * stripped from the tree. |
| */ |
| static public NodeModel parse(InputSource is) |
| throws SAXException, IOException, ParserConfigurationException { |
| return parse(is, true, true); |
| } |
| |
| |
| /** |
| * Create a NodeModel from an XML file. |
| * @param removeComments whether to remove all comment nodes |
| * (recursively) from the tree before processing |
| * @param removePIs whether to remove all processing instruction nodes |
| * (recursively from the tree before processing |
| */ |
| static public NodeModel parse(File f, boolean removeComments, boolean removePIs) |
| throws SAXException, IOException, ParserConfigurationException { |
| DocumentBuilder builder = getDocumentBuilderFactory().newDocumentBuilder(); |
| ErrorHandler errorHandler = getErrorHandler(); |
| if (errorHandler != null) builder.setErrorHandler(errorHandler); |
| Document doc = builder.parse(f); |
| if (removeComments) { |
| removeComments(doc); |
| } |
| if (removePIs) { |
| removePIs(doc); |
| } |
| mergeAdjacentText(doc); |
| return wrap(doc); |
| } |
| |
| /** |
| * Create a NodeModel from an XML file. By default, |
| * all comments and processing instruction nodes are |
| * stripped from the tree. |
| */ |
| static public NodeModel parse(File f) |
| throws SAXException, IOException, ParserConfigurationException { |
| return parse(f, true, true); |
| } |
| |
| protected NodeModel(Node node) { |
| this.node = node; |
| } |
| |
| /** |
| * @return the underling W3C DOM Node object that this TemplateNodeModel |
| * is wrapping. |
| */ |
| public Node getNode() { |
| return node; |
| } |
| |
| public TemplateModel get(String key) throws TemplateModelException { |
| if (key.startsWith("@@")) { |
| if (key.equals("@@text")) { |
| return new SimpleScalar(getText(node)); |
| } |
| if (key.equals("@@namespace")) { |
| String nsURI = node.getNamespaceURI(); |
| return nsURI == null ? null : new SimpleScalar(nsURI); |
| } |
| if (key.equals("@@local_name")) { |
| String localName = node.getLocalName(); |
| if (localName == null) { |
| localName = getNodeName(); |
| } |
| return new SimpleScalar(localName); |
| } |
| if (key.equals("@@markup")) { |
| StringBuilder buf = new StringBuilder(); |
| NodeOutputter nu = new NodeOutputter(node); |
| nu.outputContent(node, buf); |
| return new SimpleScalar(buf.toString()); |
| } |
| if (key.equals("@@nested_markup")) { |
| StringBuilder buf = new StringBuilder(); |
| NodeOutputter nu = new NodeOutputter(node); |
| nu.outputContent(node.getChildNodes(), buf); |
| return new SimpleScalar(buf.toString()); |
| } |
| if (key.equals("@@qname")) { |
| String qname = getQualifiedName(); |
| return qname == null ? null : new SimpleScalar(qname); |
| } |
| } |
| XPathSupport xps = getXPathSupport(); |
| if (xps != null) { |
| return xps.executeQuery(node, key); |
| } else { |
| throw new TemplateModelException( |
| "Can't try to resolve the XML query key, because no XPath support is available. " |
| + "It's either malformed or an XPath expression: " + key); |
| } |
| } |
| |
| public TemplateNodeModel getParentNode() { |
| if (parent == null) { |
| Node parentNode = node.getParentNode(); |
| if (parentNode == null) { |
| if (node instanceof Attr) { |
| parentNode = ((Attr) node).getOwnerElement(); |
| } |
| } |
| parent = wrap(parentNode); |
| } |
| return parent; |
| } |
| |
| public TemplateSequenceModel getChildNodes() { |
| if (children == null) { |
| children = new NodeListModel(node.getChildNodes(), this); |
| } |
| return children; |
| } |
| |
| public final String getNodeType() throws TemplateModelException { |
| short nodeType = node.getNodeType(); |
| switch (nodeType) { |
| case Node.ATTRIBUTE_NODE : return "attribute"; |
| case Node.CDATA_SECTION_NODE : return "text"; |
| case Node.COMMENT_NODE : return "comment"; |
| case Node.DOCUMENT_FRAGMENT_NODE : return "document_fragment"; |
| case Node.DOCUMENT_NODE : return "document"; |
| case Node.DOCUMENT_TYPE_NODE : return "document_type"; |
| case Node.ELEMENT_NODE : return "element"; |
| case Node.ENTITY_NODE : return "entity"; |
| case Node.ENTITY_REFERENCE_NODE : return "entity_reference"; |
| case Node.NOTATION_NODE : return "notation"; |
| case Node.PROCESSING_INSTRUCTION_NODE : return "pi"; |
| case Node.TEXT_NODE : return "text"; |
| } |
| throw new TemplateModelException("Unknown node type: " + nodeType + ". This should be impossible!"); |
| } |
| |
| public TemplateModel exec(List args) throws TemplateModelException { |
| if (args.size() != 1) { |
| throw new TemplateModelException("Expecting exactly one arguments"); |
| } |
| String query = (String) args.get(0); |
| // Now, we try to behave as if this is an XPath expression |
| XPathSupport xps = getXPathSupport(); |
| if (xps == null) { |
| throw new TemplateModelException("No XPath support available"); |
| } |
| return xps.executeQuery(node, query); |
| } |
| |
| public final int size() {return 1;} |
| |
| public final TemplateModel get(int i) { |
| return i == 0 ? this : null; |
| } |
| |
| public String getNodeNamespace() { |
| int nodeType = node.getNodeType(); |
| if (nodeType != Node.ATTRIBUTE_NODE && nodeType != Node.ELEMENT_NODE) { |
| return null; |
| } |
| String result = node.getNamespaceURI(); |
| if (result == null && nodeType == Node.ELEMENT_NODE) { |
| result = ""; |
| } else if ("".equals(result) && nodeType == Node.ATTRIBUTE_NODE) { |
| result = null; |
| } |
| return result; |
| } |
| |
| @Override |
| public final int hashCode() { |
| return node.hashCode(); |
| } |
| |
| @Override |
| public boolean equals(Object other) { |
| if (other == null) return false; |
| return other.getClass() == this.getClass() |
| && ((NodeModel) other).node.equals(this.node); |
| } |
| |
| static public NodeModel wrap(Node node) { |
| if (node == null) { |
| return null; |
| } |
| NodeModel result = null; |
| switch (node.getNodeType()) { |
| case Node.DOCUMENT_NODE : result = new DocumentModel((Document) node); break; |
| case Node.ELEMENT_NODE : result = new ElementModel((Element) node); break; |
| case Node.ATTRIBUTE_NODE : result = new AttributeNodeModel((Attr) node); break; |
| case Node.CDATA_SECTION_NODE : |
| case Node.COMMENT_NODE : |
| case Node.TEXT_NODE : result = new CharacterDataNodeModel((org.w3c.dom.CharacterData) node); break; |
| case Node.PROCESSING_INSTRUCTION_NODE : result = new PINodeModel((ProcessingInstruction) node); break; |
| case Node.DOCUMENT_TYPE_NODE : result = new DocumentTypeModel((DocumentType) node); break; |
| } |
| return result; |
| } |
| |
| /** |
| * Recursively removes all comment nodes |
| * from the subtree. |
| * |
| * @see #simplify |
| */ |
| static public void removeComments(Node node) { |
| NodeList children = node.getChildNodes(); |
| int i = 0; |
| int len = children.getLength(); |
| while (i < len) { |
| Node child = children.item(i); |
| if (child.hasChildNodes()) { |
| removeComments(child); |
| i++; |
| } else { |
| if (child.getNodeType() == Node.COMMENT_NODE) { |
| node.removeChild(child); |
| len--; |
| } else { |
| i++; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Recursively removes all processing instruction nodes |
| * from the subtree. |
| * |
| * @see #simplify |
| */ |
| static public void removePIs(Node node) { |
| NodeList children = node.getChildNodes(); |
| int i = 0; |
| int len = children.getLength(); |
| while (i < len) { |
| Node child = children.item(i); |
| if (child.hasChildNodes()) { |
| removePIs(child); |
| i++; |
| } else { |
| if (child.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) { |
| node.removeChild(child); |
| len--; |
| } else { |
| i++; |
| } |
| } |
| } |
| } |
| |
| /** |
| * Merges adjacent text/cdata nodes, so that there are no |
| * adjacent text/cdata nodes. Operates recursively |
| * on the entire subtree. You thus lose information |
| * about any CDATA sections occurring in the doc. |
| * |
| * @see #simplify |
| */ |
| static public void mergeAdjacentText(Node node) { |
| Node child = node.getFirstChild(); |
| while (child != null) { |
| if (child instanceof Text || child instanceof CDATASection) { |
| Node next = child.getNextSibling(); |
| if (next instanceof Text || next instanceof CDATASection) { |
| String fullText = child.getNodeValue() + next.getNodeValue(); |
| ((CharacterData) child).setData(fullText); |
| node.removeChild(next); |
| } |
| } else { |
| mergeAdjacentText(child); |
| } |
| child = child.getNextSibling(); |
| } |
| } |
| |
| /** |
| * Removes comments and processing instruction, and then unites adjacent text nodes. |
| * Note that CDATA sections count as text nodes. |
| */ |
| static public void simplify(Node node) { |
| NodeList children = node.getChildNodes(); |
| int i = 0; |
| int len = children.getLength(); |
| Node prevTextChild = null; |
| while (i < len) { |
| Node child = children.item(i); |
| if (child.hasChildNodes()) { |
| simplify(child); |
| prevTextChild = null; |
| i++; |
| } else { |
| int type = child.getNodeType(); |
| if (type == Node.PROCESSING_INSTRUCTION_NODE) { |
| node.removeChild(child); |
| len--; |
| } else if (type == Node.COMMENT_NODE) { |
| node.removeChild(child); |
| len--; |
| } else if (type == Node.TEXT_NODE || type == Node.CDATA_SECTION_NODE ) { |
| if (prevTextChild != null) { |
| CharacterData ptc = (CharacterData) prevTextChild; |
| ptc.setData(ptc.getNodeValue() + child.getNodeValue()); |
| node.removeChild(child); |
| len--; |
| } else { |
| prevTextChild = child; |
| i++; |
| } |
| } else { |
| prevTextChild = null; |
| i++; |
| } |
| } |
| } |
| } |
| |
| NodeModel getDocumentNodeModel() { |
| if (node instanceof Document) { |
| return this; |
| } else { |
| return wrap(node.getOwnerDocument()); |
| } |
| } |
| |
| /** |
| * Tells the system to use (restore) the default (initial) XPath system used by |
| * this FreeMarker version on this system. |
| */ |
| static public void useDefaultXPathSupport() { |
| synchronized (STATIC_LOCK) { |
| xpathSupportClass = null; |
| jaxenXPathSupport = null; |
| try { |
| useXalanXPathSupport(); |
| } catch (Exception e) { |
| ; // ignore |
| } |
| if (xpathSupportClass == null) try { |
| useSunInternalXPathSupport(); |
| } catch (Exception e) { |
| ; // ignore |
| } |
| if (xpathSupportClass == null) try { |
| useJaxenXPathSupport(); |
| } catch (Exception e) { |
| ; // ignore |
| } |
| } |
| } |
| |
| /** |
| * Convenience method. Tells the system to use Jaxen for XPath queries. |
| * @throws Exception if the Jaxen classes are not present. |
| */ |
| static public void useJaxenXPathSupport() throws Exception { |
| Class.forName("org.jaxen.dom.DOMXPath"); |
| Class c = Class.forName("freemarker.ext.dom.JaxenXPathSupport"); |
| jaxenXPathSupport = (XPathSupport) c.newInstance(); |
| synchronized (STATIC_LOCK) { |
| xpathSupportClass = c; |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Using Jaxen classes for XPath support"); |
| } |
| } |
| |
| /** |
| * Convenience method. Tells the system to use Xalan for XPath queries. |
| * @throws Exception if the Xalan XPath classes are not present. |
| */ |
| static public void useXalanXPathSupport() throws Exception { |
| Class.forName("org.apache.xpath.XPath"); |
| Class c = Class.forName("freemarker.ext.dom.XalanXPathSupport"); |
| synchronized (STATIC_LOCK) { |
| xpathSupportClass = c; |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Using Xalan classes for XPath support"); |
| } |
| } |
| |
| static public void useSunInternalXPathSupport() throws Exception { |
| Class.forName("com.sun.org.apache.xpath.internal.XPath"); |
| Class c = Class.forName("freemarker.ext.dom.SunInternalXalanXPathSupport"); |
| synchronized (STATIC_LOCK) { |
| xpathSupportClass = c; |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Using Sun's internal Xalan classes for XPath support"); |
| } |
| } |
| |
| /** |
| * Set an alternative implementation of freemarker.ext.dom.XPathSupport to use |
| * as the XPath engine. |
| * @param cl the class, or <code>null</code> to disable XPath support. |
| */ |
| static public void setXPathSupportClass(Class cl) { |
| if (cl != null && !XPathSupport.class.isAssignableFrom(cl)) { |
| throw new RuntimeException("Class " + cl.getName() |
| + " does not implement freemarker.ext.dom.XPathSupport"); |
| } |
| synchronized (STATIC_LOCK) { |
| xpathSupportClass = cl; |
| } |
| } |
| |
| /** |
| * Get the currently used freemarker.ext.dom.XPathSupport used as the XPath engine. |
| * Returns <code>null</code> if XPath support is disabled. |
| */ |
| static public Class getXPathSupportClass() { |
| synchronized (STATIC_LOCK) { |
| return xpathSupportClass; |
| } |
| } |
| |
| static private String getText(Node node) { |
| String result = ""; |
| if (node instanceof Text || node instanceof CDATASection) { |
| result = ((org.w3c.dom.CharacterData) node).getData(); |
| } else if (node instanceof Element) { |
| NodeList children = node.getChildNodes(); |
| for (int i = 0; i < children.getLength(); i++) { |
| result += getText(children.item(i)); |
| } |
| } else if (node instanceof Document) { |
| result = getText(((Document) node).getDocumentElement()); |
| } |
| return result; |
| } |
| |
| XPathSupport getXPathSupport() { |
| if (jaxenXPathSupport != null) { |
| return jaxenXPathSupport; |
| } |
| XPathSupport xps = null; |
| Document doc = node.getOwnerDocument(); |
| if (doc == null) { |
| doc = (Document) node; |
| } |
| synchronized (doc) { |
| WeakReference ref = (WeakReference) xpathSupportMap.get(doc); |
| if (ref != null) { |
| xps = (XPathSupport) ref.get(); |
| } |
| if (xps == null) { |
| try { |
| xps = (XPathSupport) xpathSupportClass.newInstance(); |
| xpathSupportMap.put(doc, new WeakReference(xps)); |
| } catch (Exception e) { |
| LOG.error("Error instantiating xpathSupport class", e); |
| } |
| } |
| } |
| return xps; |
| } |
| |
| |
| String getQualifiedName() throws TemplateModelException { |
| return getNodeName(); |
| } |
| |
| public Object getAdaptedObject(Class hint) { |
| return node; |
| } |
| |
| public Object getWrappedObject() { |
| return node; |
| } |
| |
| public Object[] explainTypeError(Class[] expectedClasses) { |
| for (int i = 0; i < expectedClasses.length; i++) { |
| Class expectedClass = expectedClasses[i]; |
| if (TemplateDateModel.class.isAssignableFrom(expectedClass) |
| || TemplateNumberModel.class.isAssignableFrom(expectedClass) |
| || TemplateBooleanModel.class.isAssignableFrom(expectedClass)) { |
| return new Object[] { |
| "XML node values are always strings (text), that is, they can't be used as number, " |
| + "date/time/datetime or boolean without explicit conversion (such as " |
| + "someNode?number, someNode?datetime.xs, someNode?date.xs, someNode?time.xs, " |
| + "someNode?boolean).", |
| }; |
| } |
| } |
| return null; |
| } |
| |
| } |