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

freemarker.ext.xml.NodeListModel Maven / Gradle / Ivy

There is a newer version: 7.0.58
Show 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 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;

/**
 * 

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.)

*

Implementation note: 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 * setNamespaceAware(true) on the * javax.xml.parsers.DocumentBuilderFactory 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.

* * @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: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Key nameEvaluates to
* or _childrenall direct element children of current nodes (non-recursive). * Applicable to element and document nodes.
@* or _attributesall attributes of current nodes. Applicable to elements only. *
@attributeNamenamed attributes of current nodes. Applicable to elements, * doctypes and processing instructions. On doctypes it supports * attributes publicId, systemId and * elementName. On processing instructions, it supports * attributes target and data, as well as any * other attribute name specified in data as * name="value" 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 * @* does NOT operate on doctypes or processing * instructions.
_ancestorall ancestors up to root element (recursive) of current nodes. * Applicable to same node types as _parent.
_ancestorOrSelfall ancestors of current nodes plus current nodes. Applicable * to same node types as _parent.
_contentthe complete content of current nodes, including children * elements, text, entity references, and processing instructions * (non-recursive). Applicable to elements and documents.
_descendantall recursive descendant element children of current nodes. * Applicable to document and element nodes.
_descendantOrSelfall recursive descendant element children of current nodes * plus current nodes. Applicable to document and element nodes. *
_documentall documents the current nodes belong to. Applicable to all * nodes except text.
_doctypedoctypes of the current nodes. Applicable to document nodes * only.
_filterTypeis 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".
_namethe 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)
_nsprefixthe namespace prefixes of current nodes, one string per node * (non-recursive). Applicable to elements and attributes
_nsurithe namespace URIs of current nodes, one string per node * (non-recursive). Applicable to elements and attributes
_parentparent elements of current nodes. Applicable to element, * attribute, comment, entity, processing instruction.
_qnamethe qualified names of current nodes in * [namespacePrefix:]localName form, one string per node * (non-recursive). Applicable to elements and attributes
_registerNamespace(prefix, uri)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 * nodelist["prefix:localname"], or * nodelist["@prefix:localname"] syntax * (or nodelist.prefix\:localname, or nodelist.@prefix\:localname) * 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(...)}. *
_textthe 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.
_typeReturns 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".
_uniquea 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 * _parent, _ancestor, _ancestorOrSelf, * and _document on a node list with multiple elements. * I.e. foo._children._parent 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 foo._children._parent._unique to eliminate * duplicates. Applicable to all node types.
any other keyelement children of current nodes with name matching the key. * This allows for convenience child traversal in * book.chapter.title style syntax. Applicable to document * and element nodes.
* @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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy