| /* |
| * 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.xml; |
| |
| import java.io.StringWriter; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Set; |
| |
| import freemarker.log.Logger; |
| import freemarker.template.TemplateHashModel; |
| import freemarker.template.TemplateMethodModel; |
| import freemarker.template.TemplateModel; |
| import freemarker.template.TemplateModelException; |
| import freemarker.template.TemplateNodeModel; |
| import freemarker.template.TemplateScalarModel; |
| import freemarker.template.TemplateSequenceModel; |
| import freemarker.template.utility.ClassUtil; |
| |
| /** |
| * <p>A data model adapter for three widespread XML document object model |
| * representations: W3C DOM, dom4j, and JDOM. The adapter automatically |
| * recognizes the used XML object model and provides a unified interface for it |
| * toward the template. The model provides access to all XML InfoSet features |
| * of the XML document and includes XPath support if it has access to the XPath- |
| * evaluator library Jaxen. The model's philosophy (which closely follows that |
| * of XML InfoSet and XPath) is as follows: it always wraps a list of XML nodes |
| * (the "nodelist"). The list can be empty, can have a single element, or can |
| * have multiple elements. Every operation applied to the model is applied to |
| * all nodes in its nodelist. You usually start with a single- element nodelist, |
| * usually the root element node or the document node of the XML tree. |
| * Additionally, the nodes can contain String objects as a result of certain |
| * evaluations (getting the names of elements, values of attributes, etc.)</p> |
| * <p><strong>Implementation note:</strong> If you are using W3C DOM documents |
| * built by the Crimson XML parser (or you are using the built-in JDK 1.4 XML |
| * parser, which is essentially Crimson), make sure you call |
| * <tt>setNamespaceAware(true)</tt> on the |
| * <tt>javax.xml.parsers.DocumentBuilderFactory</tt> instance used for document |
| * building even when your documents don't use XML namespaces. Failing to do so, |
| * you will experience incorrect behavior when using the documents wrapped with |
| * this model.</p> |
| * |
| * @deprecated Use {@link freemarker.ext.dom.NodeModel} instead. |
| */ |
| @Deprecated |
| public class NodeListModel |
| implements |
| TemplateHashModel, |
| TemplateMethodModel, |
| TemplateScalarModel, |
| TemplateSequenceModel, |
| TemplateNodeModel { |
| private static final Logger LOG = Logger.getLogger("freemarker.xml"); |
| |
| private static final Class DOM_NODE_CLASS = getClass("org.w3c.dom.Node"); |
| private static final Class DOM4J_NODE_CLASS = getClass("org.dom4j.Node"); |
| private static final Navigator DOM_NAVIGATOR = getNavigator("Dom"); |
| private static final Navigator DOM4J_NAVIGATOR = getNavigator("Dom4j"); |
| private static final Navigator JDOM_NAVIGATOR = getNavigator("Jdom"); |
| private static volatile boolean useJaxenNamespaces = true; |
| |
| // The navigator object that implements document model-specific behavior. |
| private final Navigator navigator; |
| // The contained nodes |
| private final List nodes; |
| // The namespaces object (potentially shared by multiple models) |
| private Namespaces namespaces; |
| |
| /** |
| * Creates a new NodeListModel, wrapping the passed nodes. |
| * @param nodes you can pass it a single XML node from any supported |
| * document model, or a Java collection containing any number of nodes. |
| * Passing null is prohibited. To create an empty model, pass it an empty |
| * collection. If a collection is passed, all passed nodes must belong to |
| * the same XML object model, i.e. you can't mix JDOM and dom4j in a single |
| * instance of NodeListModel. The model itself doesn't check for this condition, |
| * as it can be time consuming, but will throw spurious |
| * {@link ClassCastException}s when it encounters mixed objects. |
| * @throws IllegalArgumentException if you pass null |
| */ |
| public NodeListModel(Object nodes) { |
| Object node = nodes; |
| if (nodes instanceof Collection) { |
| this.nodes = new ArrayList((Collection) nodes); |
| node = this.nodes.isEmpty() ? null : this.nodes.get(0); |
| } else if (nodes != null) { |
| this.nodes = Collections.singletonList(nodes); |
| } else { |
| throw new IllegalArgumentException("nodes == null"); |
| } |
| if (DOM_NODE_CLASS != null && DOM_NODE_CLASS.isInstance(node)) { |
| navigator = DOM_NAVIGATOR; |
| } else if (DOM4J_NODE_CLASS != null && DOM4J_NODE_CLASS.isInstance(node)) { |
| navigator = DOM4J_NAVIGATOR; |
| } else { |
| // Assume JDOM |
| navigator = JDOM_NAVIGATOR; |
| } |
| namespaces = createNamespaces(); |
| } |
| |
| private Namespaces createNamespaces() { |
| if (useJaxenNamespaces) { |
| try { |
| return (Namespaces) |
| Class.forName("freemarker.ext.xml._JaxenNamespaces") |
| .newInstance(); |
| } catch (Throwable t) { |
| useJaxenNamespaces = false; |
| } |
| } |
| return new Namespaces(); |
| } |
| |
| private NodeListModel(Navigator navigator, List nodes, Namespaces namespaces) { |
| this.navigator = navigator; |
| this.nodes = nodes; |
| this.namespaces = namespaces; |
| } |
| |
| private NodeListModel deriveModel(List derivedNodes) { |
| namespaces.markShared(); |
| return new NodeListModel(navigator, derivedNodes, namespaces); |
| } |
| |
| /** |
| * Returns the number of nodes in this model's nodelist. |
| * @see freemarker.template.TemplateSequenceModel#size() |
| */ |
| @Override |
| public int size() { |
| return nodes.size(); |
| } |
| |
| /** |
| * Evaluates an XPath expression on XML nodes in this model. |
| * @param arguments the arguments to the method invocation. Expectes exactly |
| * one argument - the XPath expression. |
| * @return a new NodeListModel with nodes selected by applying the XPath |
| * expression to this model's nodelist. |
| * @see freemarker.template.TemplateMethodModel#exec(List) |
| */ |
| @Override |
| public Object exec(List arguments) throws TemplateModelException { |
| if (arguments.size() != 1) { |
| throw new TemplateModelException( |
| "Expecting exactly one argument - an XPath expression"); |
| } |
| return deriveModel(navigator.applyXPath(nodes, (String) arguments.get(0), namespaces)); |
| } |
| |
| /** |
| * Returns the string representation of the wrapped nodes. String objects in |
| * the nodelist are rendered as-is (with no XML escaping applied). All other |
| * nodes are rendered in the default XML serialization format ("plain XML"). |
| * This makes the model quite suited for use as an XML-transformation tool. |
| * @return the string representation of the wrapped nodes. String objects |
| * in the nodelist are rendered as-is (with no XML escaping applied). All |
| * other nodes are rendered in the default XML serialization format ("plain |
| * XML"). |
| * @see freemarker.template.TemplateScalarModel#getAsString() |
| */ |
| @Override |
| public String getAsString() throws TemplateModelException { |
| StringWriter sw = new StringWriter(size() * 128); |
| for (Iterator iter = nodes.iterator(); iter.hasNext(); ) { |
| Object o = iter.next(); |
| if (o instanceof String) { |
| sw.write((String) o); |
| } else { |
| navigator.getAsString(o, sw); |
| } |
| } |
| return sw.toString(); |
| } |
| |
| /** |
| * Selects a single node from this model's nodelist by its list index and |
| * returns a new NodeListModel containing that single node. |
| * @param index the ordinal number of the selected node |
| * @see freemarker.template.TemplateSequenceModel#get(int) |
| */ |
| @Override |
| public TemplateModel get(int index) { |
| return deriveModel(Collections.singletonList(nodes.get(index))); |
| } |
| |
| /** |
| * Returns a new NodeListModel containing the nodes that result from applying |
| * an operator to this model's nodes. |
| * @param key the operator to apply to nodes. Available operators are: |
| * <table style="width: auto; border-collapse: collapse" border="1" summary="XML node hash keys"> |
| * <thead> |
| * <tr> |
| * <th align="left">Key name</th> |
| * <th align="left">Evaluates to</th> |
| * </tr> |
| * </thead> |
| * <tbody> |
| * <tr> |
| * <td><tt>*</tt> or <tt>_children</tt></td> |
| * <td>all direct element children of current nodes (non-recursive). |
| * Applicable to element and document nodes.</td> |
| * </tr> |
| * <tr> |
| * <td><tt>@*</tt> or <tt>_attributes</tt></td> |
| * <td>all attributes of current nodes. Applicable to elements only. |
| * </td> |
| * </tr> |
| * <tr> |
| * <td><tt>@<i>attributeName</i></tt></td> |
| * <td>named attributes of current nodes. Applicable to elements, |
| * doctypes and processing instructions. On doctypes it supports |
| * attributes <tt>publicId</tt>, <tt>systemId</tt> and |
| * <tt>elementName</tt>. On processing instructions, it supports |
| * attributes <tt>target</tt> and <tt>data</tt>, as well as any |
| * other attribute name specified in data as |
| * <tt>name="value"</tt> pair on dom4j or JDOM models. |
| * The attribute nodes for doctype and processing instruction are |
| * synthetic, and as such have no parent. Note, however that |
| * <tt>@*</tt> does NOT operate on doctypes or processing |
| * instructions.</td> |
| * </tr> |
| * |
| * <tr> |
| * <td><tt>_ancestor</tt></td> |
| * <td>all ancestors up to root element (recursive) of current nodes. |
| * Applicable to same node types as <tt>_parent</tt>.</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_ancestorOrSelf</tt></td> |
| * <td>all ancestors of current nodes plus current nodes. Applicable |
| * to same node types as <tt>_parent</tt>.</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_content</tt></td> |
| * <td>the complete content of current nodes, including children |
| * elements, text, entity references, and processing instructions |
| * (non-recursive). Applicable to elements and documents.</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_descendant</tt></td> |
| * <td>all recursive descendant element children of current nodes. |
| * Applicable to document and element nodes.</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_descendantOrSelf</tt></td> |
| * <td>all recursive descendant element children of current nodes |
| * plus current nodes. Applicable to document and element nodes. |
| * </td> |
| * </tr> |
| * <tr> |
| * <td><tt>_document</tt></td> |
| * <td>all documents the current nodes belong to. Applicable to all |
| * nodes except text.</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_doctype</tt></td> |
| * <td>doctypes of the current nodes. Applicable to document nodes |
| * only.</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_filterType</tt></td> |
| * <td>is a filter-by-type template method model. When called, it |
| * will yield a node list that contains only those current nodes |
| * whose type matches one of types passed as argument. You can pass |
| * as many string arguments as you want, each representing one of |
| * the types to select: "attribute", "cdata", |
| * "comment", "document", |
| * "documentType", "element", |
| * "entity", "entityReference", |
| * "namespace", "processingInstruction", or |
| * "text".</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_name</tt></td> |
| * <td>the names of current nodes, one string per node |
| * (non-recursive). Applicable to elements and attributes |
| * (returns their local names), entity references, processing |
| * instructions (returns its target), doctypes (returns its public |
| * ID)</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_nsprefix</tt></td> |
| * <td>the namespace prefixes of current nodes, one string per node |
| * (non-recursive). Applicable to elements and attributes</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_nsuri</tt></td> |
| * <td>the namespace URIs of current nodes, one string per node |
| * (non-recursive). Applicable to elements and attributes</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_parent</tt></td> |
| * <td>parent elements of current nodes. Applicable to element, |
| * attribute, comment, entity, processing instruction.</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_qname</tt></td> |
| * <td>the qualified names of current nodes in |
| * <tt>[namespacePrefix:]localName</tt> form, one string per node |
| * (non-recursive). Applicable to elements and attributes</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_registerNamespace(prefix, uri)</tt></td> |
| * <td>register a XML namespace with the specified prefix and URI for |
| * the current node list and all node lists that are derived from |
| * the current node list. After registering, you can use the |
| * <tt>nodelist["prefix:localname"]</tt>, or |
| * <tt>nodelist["@prefix:localname"]</tt> syntax |
| * (or <tt>nodelist.prefix\:localname</tt>, or <tt>nodelist.@prefix\:localname</tt>) |
| * to reach elements, and attributes whose names are namespace-scoped. |
| * Note that the namespace prefix need not match the actual prefix |
| * used by the XML document itself since namespaces are compared |
| * solely by their URI. Also note that if you do {@code doc.elem1._registerNamespace(...)}, |
| * and then later you use {@code doc.elem1} again, it will not have the prefix registered, |
| * because each time you use {@code doc.elem1}, it gives a completely new object. In this |
| * example, you certainly should have used {@code doc._registerNamespace(...)}. |
| * </tr> |
| * <tr> |
| * <td><tt>_text</tt></td> |
| * <td>the text of current nodes, one string per node |
| * (non-recursive). Applicable to elements, attributes, comments, |
| * processing instructions (returns its data) and CDATA sections. |
| * The reserved XML characters ('<' and '&') are NOT |
| * escaped.</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_type</tt></td> |
| * <td>Returns a string describing the type of nodes, one |
| * string per node. The returned values are "attribute", |
| * "cdata", "comment", "document", |
| * "documentType", "element", |
| * "entity", "entityReference", |
| * "namespace", "processingInstruction", |
| * "text", or "unknown".</td> |
| * </tr> |
| * <tr> |
| * <td><tt>_unique</tt></td> |
| * <td>a copy of the current nodes that keeps only the first |
| * occurrence of every node, eliminating duplicates. Duplicates can |
| * occur in the node list by applying uptree-traversals |
| * <tt>_parent</tt>, <tt>_ancestor</tt>, <tt>_ancestorOrSelf</tt>, |
| * and <tt>_document</tt> on a node list with multiple elements. |
| * I.e. <tt>foo._children._parent</tt> will return a node list that |
| * has duplicates of nodes in foo - each node will have the number |
| * of occurrences equal to the number of its children. In these |
| * cases, use <tt>foo._children._parent._unique</tt> to eliminate |
| * duplicates. Applicable to all node types.</td> |
| * </tr> |
| * <tr> |
| * <td>any other key</td> |
| * <td>element children of current nodes with name matching the key. |
| * This allows for convenience child traversal in |
| * <tt>book.chapter.title</tt> style syntax. Applicable to document |
| * and element nodes.</td> |
| * </tr> |
| * </tbody> |
| * </table> |
| * @return a new NodeListModel containing the nodes that result from applying |
| * the operator to this model's nodes. |
| * @see freemarker.template.TemplateHashModel#get(String) |
| */ |
| @Override |
| public TemplateModel get(String key) throws TemplateModelException { |
| // Try a built-in navigator operator |
| NodeOperator op = navigator.getOperator(key); |
| String localName = null; |
| String namespaceUri = ""; |
| // If not a nav op, then check for special keys. |
| if (op == null && key.length() > 0 && key.charAt(0) == '_') { |
| if (key.equals("_unique")) { |
| return deriveModel(removeDuplicates(nodes)); |
| } else if (key.equals("_filterType") || key.equals("_ftype")) { |
| return new FilterByType(); |
| } else if (key.equals("_registerNamespace")) { |
| if (namespaces.isShared()) { |
| namespaces = (Namespaces) namespaces.clone(); |
| } |
| return namespaces; |
| } |
| } |
| // Last, do a named child element or attribute lookup |
| if (op == null) { |
| int colon = key.indexOf(':'); |
| if (colon == -1) { |
| // No namespace prefix specified |
| localName = key; |
| } else { |
| // Namespace prefix specified |
| localName = key.substring(colon + 1); |
| String prefix = key.substring(0, colon); |
| namespaceUri = namespaces.translateNamespacePrefixToUri(prefix); |
| if (namespaceUri == null) { |
| throw new TemplateModelException("Namespace prefix " + prefix + " is not registered."); |
| } |
| } |
| if (localName.charAt(0) == '@') { |
| op = navigator.getAttributeOperator(); |
| localName = localName.substring(1); |
| } else { |
| op = navigator.getChildrenOperator(); |
| } |
| } |
| List result = new ArrayList(); |
| for (Iterator iter = nodes.iterator(); iter.hasNext(); ) { |
| try { |
| op.process(iter.next(), localName, namespaceUri, result); |
| } catch (RuntimeException e) { |
| throw new TemplateModelException(e); |
| } |
| } |
| return deriveModel(result); |
| } |
| |
| /** |
| * Returns true if this NodeListModel contains no nodes. |
| * @see freemarker.template.TemplateHashModel#isEmpty() |
| */ |
| @Override |
| public boolean isEmpty() { |
| return nodes.isEmpty(); |
| } |
| |
| /** |
| * Registers a namespace prefix-URI pair for subsequent use in {@link |
| * #get(String)} as well as for use in XPath expressions. |
| * @param prefix the namespace prefix to use for the namespace |
| * @param uri the namespace URI that identifies the namespace. |
| */ |
| public void registerNamespace(String prefix, String uri) { |
| if (namespaces.isShared()) { |
| namespaces = (Namespaces) namespaces.clone(); |
| } |
| namespaces.registerNamespace(prefix, uri); |
| } |
| |
| private class FilterByType |
| implements |
| TemplateMethodModel { |
| @Override |
| public Object exec(List arguments) { |
| List filteredNodes = new ArrayList(); |
| for (Iterator iter = arguments.iterator(); iter.hasNext(); ) { |
| Object node = iter.next(); |
| if (arguments.contains(navigator.getType(node))) { |
| filteredNodes.add(node); |
| } |
| } |
| return deriveModel(filteredNodes); |
| } |
| } |
| |
| private static final List removeDuplicates(List list) { |
| int s = list.size(); |
| ArrayList ulist = new ArrayList(s); |
| Set set = new HashSet(s * 4 / 3, .75f); |
| Iterator it = list.iterator(); |
| while (it.hasNext()) { |
| Object o = it.next(); |
| if (set.add(o)) { |
| ulist.add(o); |
| } |
| } |
| return ulist; |
| } |
| |
| private static Class getClass(String className) { |
| try { |
| return ClassUtil.forName(className); |
| } catch (Exception e) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Couldn't load class " + className, e); |
| } |
| return null; |
| } |
| } |
| |
| private static Navigator getNavigator(String navType) { |
| try { |
| return (Navigator) ClassUtil.forName("freemarker.ext.xml._" + |
| navType + "Navigator").newInstance(); |
| } catch (Throwable t) { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Could not load navigator for " + navType, t); |
| } |
| return null; |
| } |
| } |
| |
| @Override |
| public TemplateSequenceModel getChildNodes() throws TemplateModelException { |
| return (TemplateSequenceModel) get("_content"); |
| } |
| |
| @Override |
| public String getNodeName() throws TemplateModelException { |
| return getUniqueText((NodeListModel) get("_name"), "name"); |
| } |
| |
| @Override |
| public String getNodeNamespace() throws TemplateModelException { |
| return getUniqueText((NodeListModel) get("_nsuri"), "namespace"); |
| } |
| |
| @Override |
| public String getNodeType() throws TemplateModelException { |
| return getUniqueText((NodeListModel) get("_type"), "type"); |
| } |
| @Override |
| public TemplateNodeModel getParentNode() throws TemplateModelException { |
| return (TemplateNodeModel) get("_parent"); |
| } |
| |
| private String getUniqueText(NodeListModel model, String property) throws TemplateModelException { |
| String s1 = null; |
| Set s = null; |
| for (Iterator it = model.nodes.iterator(); it.hasNext(); ) { |
| String s2 = (String) it.next(); |
| if (s2 != null) { |
| // No text yet, make this text the current text |
| if (s1 == null) { |
| s1 = s2; |
| } |
| // else if there's already a text and they differ, start |
| // accumulating them for an error message |
| else if (!s1.equals(s2)) { |
| if (s == null) { |
| s = new HashSet(); |
| s.add(s1); |
| } |
| s.add(s2); |
| } |
| } |
| } |
| // If the set for the error messages is empty, return the retval |
| if (s == null) { |
| return s1; |
| } |
| // Else throw an exception signaling ambiguity |
| throw new TemplateModelException( |
| "Value for node " + property + " is ambiguos: " + s); |
| } |
| } |