freemarker.ext.xml.NodeListModel Maven / Gradle / Ivy
/*
* Copyright (c) 2003 The Visigoth Software Society. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
*
* 3. The end-user documentation included with the redistribution, if
* any, must include the following acknowledgement:
* "This product includes software developed by the
* Visigoth Software Society (http://www.visigoths.org/)."
* Alternately, this acknowledgement may appear in the software itself,
* if and wherever such third-party acknowledgements normally appear.
*
* 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
* project contributors may be used to endorse or promote products derived
* from this software without prior written permission. For written
* permission, please contact [email protected].
*
* 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
* nor may "FreeMarker" or "Visigoth" appear in their names
* without prior written permission of the Visigoth Software Society.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
* ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
* USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
* OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Visigoth Software Society. For more
* information on the Visigoth Software Society, please see
* http://www.visigoths.org/
*/
package freemarker.ext.xml;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
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;
import freemarker.template.utility.Collections12;
/**
* 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.
* @version $Id: NodeListModel.java,v 1.15 2004/01/06 17:06:43 szegedia Exp $
* @author Attila Szegedi
*/
public class NodeListModel
implements
TemplateHashModel,
TemplateMethodModel,
TemplateScalarModel,
TemplateSequenceModel,
TemplateNodeModel
{
private static final Logger logger = 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 = Collections12.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 new JaxenNamespaces();
}
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()
*/
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)
*/
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()
*/
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)
*/
public TemplateModel get(int index) {
return deriveModel(Collections12.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 name
* Evaluates to
*
*
*
*
* * or _children
* all direct element children of current nodes (non-recursive).
* Applicable to element and document nodes.
*
*
* @* or _attributes
* all attributes of current nodes. Applicable to elements only.
*
*
*
* @attributeName
* named 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.
*
*
*
* _ancestor
* all ancestors up to root element (recursive) of current nodes.
* Applicable to same node types as _parent.
*
*
* _ancestorOrSelf
* all ancestors of current nodes plus current nodes. Applicable
* to same node types as _parent.
*
*
* _cname
* the canonical names of current nodes (namespace URI + local
* name), one string per node (non-recursive). Applicable to
* elements and attributes
*
*
* _content
* the complete content of current nodes, including children
* elements, text, entity references, and processing instructions
* (non-recursive). Applicable to elements and documents.
*
*
* _descendant
* all recursive descendant element children of current nodes.
* Applicable to document and element nodes.
*
*
* _descendantOrSelf
* all recursive descendant element children of current nodes
* plus current nodes. Applicable to document and element nodes.
*
*
*
* _document
* all documents the current nodes belong to. Applicable to all
* nodes except text.
*
*
* _doctype
* doctypes of the current nodes. Applicable to document nodes
* only.
*
*
* _filterType
* 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".
*
*
* _name
* 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)
*
*
* _nsprefix
* the namespace prefixes of current nodes, one string per node
* (non-recursive). Applicable to elements and attributes
*
*
* _nsuri
* the namespace URIs of current nodes, one string per node
* (non-recursive). Applicable to elements and attributes
*
*
* _parent
* parent elements of current nodes. Applicable to element,
* attribute, comment, entity, processing instruction.
*
*
* _qname
* the 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"] syntaxes 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.
*
*
* _text
* 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.
*
*
* _type
* 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".
*
*
* _unique
* 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
* _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 key
* element 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)
*/
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();
}
}
}
// 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()
*/
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
{
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(logger.isDebugEnabled()) {
logger.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(logger.isDebugEnabled()) {
logger.debug("Could not load navigator for " + navType, t);
}
return null;
}
}
public TemplateSequenceModel getChildNodes() throws TemplateModelException
{
return (TemplateSequenceModel)get("_content");
}
public String getNodeName() throws TemplateModelException
{
return getUniqueText((NodeListModel)get("_name"), "name");
}
public String getNodeNamespace() throws TemplateModelException
{
return getUniqueText((NodeListModel)get("_nsuri"), "namespace");
}
public String getNodeType() throws TemplateModelException
{
return getUniqueText((NodeListModel)get("_type"), "type");
}
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);
}
}