/* * Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky * * Licensed 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.jdom; import java.io.IOException; import java.io.Writer; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import org.jaxen.Context; import org.jaxen.JaxenException; import org.jaxen.NamespaceContext; import org.jaxen.jdom.JDOMXPath; import org.jdom.Attribute; import org.jdom.CDATA; import org.jdom.Comment; import org.jdom.DocType; import org.jdom.Document; import org.jdom.Element; import org.jdom.EntityRef; import org.jdom.Namespace; import org.jdom.ProcessingInstruction; import org.jdom.Text; import org.jdom.output.XMLOutputter; import freemarker.template.SimpleScalar; import freemarker.template.TemplateCollectionModel; import freemarker.template.TemplateHashModel; import freemarker.template.TemplateMethodModel; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateModelIterator; import freemarker.template.TemplateScalarModel; import freemarker.template.TemplateSequenceModel; /** * Provides a template for wrapping JDOM objects. It is capable of storing not only * a single JDOM node, but a list of JDOM nodes at once (hence the name). * Each node is an instance of any of the core JDOM node classes (except namespaces, * which are not supported at the moment), or String for representing text. * See individual method documentation for exact details on how the class works. In * short: * <ul> * <li>{@link #getAsString()} will render all contained nodes as XML fragment, * <li>{@link #exec(List)} provides full XPath functionality implemented on top of * the <a href="http://www.jaxen.org">Jaxen</a> library,</li> * <li>{@link #get(String)} provides node traversal, copying and filtering - somewhat * less expressive than XPath, however it does not require the external library and * it evaluates somewhat faster</li> * <li>being a {@link TemplateCollectionModel} allows to iterate the contained node list, and</li> * <li>being a {@link TemplateSequenceModel} allows to access the contained nodes by index and query the node count.</li> * </ul> * * <p><b>Note:</b> There is a JDOM independent re-implementation of this class: * {@link freemarker.ext.xml.NodeListModel freemarker.ext.xml.NodeListModel} * * @deprecated Use {@link freemarker.ext.dom.NodeModel} instead. */ @Deprecated public class NodeListModel implements TemplateHashModel, TemplateMethodModel, TemplateCollectionModel, TemplateSequenceModel, TemplateScalarModel { private static final AttributeXMLOutputter OUTPUT = new AttributeXMLOutputter(); // A convenience singleton for representing a node list without nodes. private static final NodeListModel EMPTY = new NodeListModel(null, false); // Cache of already parsed XPath expressions private static final Map XPATH_CACHE = new WeakHashMap(); private static final NamedNodeOperator NAMED_CHILDREN_OP = new NamedChildrenOp(); private static final NamedNodeOperator NAMED_ATTRIBUTE_OP = new NamedAttributeOp(); private static final NodeOperator ALL_ATTRIBUTES_OP = new AllAttributesOp(); private static final NodeOperator ALL_CHILDREN_OP = new AllChildrenOp(); private static final Map OPERATIONS = createOperations(); private static final Map SPECIAL_OPERATIONS = createSpecialOperations(); private static final int SPECIAL_OPERATION_COPY = 0; private static final int SPECIAL_OPERATION_UNIQUE = 1; private static final int SPECIAL_OPERATION_FILTER_NAME = 2; private static final int SPECIAL_OPERATION_FILTER_TYPE = 3; private static final int SPECIAL_OPERATION_QUERY_TYPE = 4; private static final int SPECIAL_OPERATION_REGISTER_NAMESPACE = 5; private static final int SPECIAL_OPERATION_PLAINTEXT = 6; // The contained nodes private final List nodes; private final Map namespaces; /** * Creates a node list that holds a single {@link Document} node. */ public NodeListModel(Document document) { nodes = document == null ? Collections.EMPTY_LIST : Collections.singletonList(document); namespaces = new HashMap(); } /** * Creates a node list that holds a single {@link Element} node. */ public NodeListModel(Element element) { nodes = element == null ? Collections.EMPTY_LIST : Collections.singletonList(element); namespaces = new HashMap(); } private NodeListModel(Object object, Map namespaces) { nodes = object == null ? Collections.EMPTY_LIST : Collections.singletonList(object); this.namespaces = namespaces; } /** * Creates a node list that holds a list of nodes. * @param nodes the list of nodes this template should hold. The created template * will copy the passed nodes list, so changes to the passed list will not affect * the model. */ public NodeListModel(List nodes) { this(nodes, true); } /** * Creates a node list that holds a list of nodes. * @param nodes the list of nodes this template should hold. * @param copy if true, the created template will copy the passed nodes list, * so changes to the passed list will not affect the model. If false, the model * will reference the passed list and will sense changes in it, although no * operations on the list will be synchronized. */ public NodeListModel(List nodes, boolean copy) { this.nodes = copy && nodes != null ? new ArrayList(nodes) : (nodes == null ? Collections.EMPTY_LIST : nodes); namespaces = new HashMap(); } private NodeListModel(List nodes, Map namespaces) { this.nodes = nodes == null ? Collections.EMPTY_LIST : nodes; this.namespaces = namespaces; } private static final NodeListModel createNodeListModel(List list, Map namespaces) { if (list == null || list.isEmpty()) { if (namespaces.isEmpty()) { return EMPTY; } else { return new NodeListModel(Collections.EMPTY_LIST, namespaces); } } if (list.size() == 1) return new NodeListModel(list.get(0), namespaces); return new NodeListModel(list, namespaces); } /** * Returns true if this model contains no nodes. */ public boolean isEmpty() { return nodes.isEmpty(); } /** * This method returns the string resulting from concatenation * of string representations of its nodes. Each node is rendered using its XML * serialization format, while text (String) is rendered as itself. This greatly * simplifies creating XML-transformation templates, as to output a node contained * in variable x as XML fragment, you simply write ${x} in the template. */ public String getAsString() throws TemplateModelException { if (isEmpty()) return ""; java.io.StringWriter sw = new java.io.StringWriter(nodes.size() * 128); try { for (Iterator i = nodes.iterator(); i.hasNext(); ) { Object node = i.next(); if (node instanceof Element) OUTPUT.output((Element) node, sw); else if (node instanceof Attribute) OUTPUT.output((Attribute) node, sw); else if (node instanceof String) sw.write(OUTPUT.escapeElementEntities(node.toString())); else if (node instanceof Text) OUTPUT.output((Text) node, sw); else if (node instanceof Document) OUTPUT.output((Document) node, sw); else if (node instanceof ProcessingInstruction) OUTPUT.output((ProcessingInstruction) node, sw); else if (node instanceof Comment) OUTPUT.output((Comment) node, sw); else if (node instanceof CDATA) OUTPUT.output((CDATA) node, sw); else if (node instanceof DocType) OUTPUT.output((DocType) node, sw); else if (node instanceof EntityRef) OUTPUT.output((EntityRef) node, sw); else throw new TemplateModelException(node.getClass().getName() + " is not a core JDOM class"); } } catch (IOException e) { throw new TemplateModelException(e.getMessage()); } return sw.toString(); } /** * Provides node list traversal as well as special functions: filtering by name, * filtering by node type, shallow-copying, and duplicate removal. * While not as powerful as the full XPath support built into the * {@link #exec(List)} method, it does not require the external Jaxen * library to be present at run time. Below are listed the recognized keys. * In key descriptions, "applicable to this-and-that node type" means that if * a key is applied to a node list that contains a node of non-applicable type * a TemplateMethodModel will be thrown. However, you can use <tt>_ftype</tt> * key to explicitly filter out undesired node types prior to applying the * restricted-applicability key. Also "current nodes" means nodes contained in this * set. * <ul> * <li><tt>*</tt> or <tt>_children</tt>: all direct element children of current nodes (non-recursive). Applicable * to element and document nodes.</li> * <li><tt>@*</tt> or <tt>_attributes</tt>: all attributes of current nodes. Applicable to elements only.</li> * <li><tt>_content</tt> the complete content of current nodes (non-recursive). * Applicable to elements and documents.</li> * <li><tt>_text</tt>: 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 escaped.</li> * <li><tt>_plaintext</tt>: same as <tt>_text</tt>, but does not escape any characters, * and instead of returning a NodeList returns a SimpleScalar.</li> * <li><tt>_name</tt>: the names of current nodes, one string per node (non-recursive). * Applicable to elements and attributes (returns their local name), * entities, processing instructions (returns its target), doctypes * (returns its public ID)</li> * <li><tt>_qname</tt>: the qualified names of current nodes in <tt>[namespacePrefix:]localName</tt> * form, one string per node (non-recursive). Applicable to elements and attributes</li> * <li><tt>_cname</tt>: the canonical names of current nodes (namespace URI + local name), * one string per node (non-recursive). Applicable to elements and attributes</li> * <li><tt>_nsprefix</tt>: namespace prefixes of current nodes, * one string per node (non-recursive). Applicable to elements and attributes</li> * <li><tt>_nsuri</tt>: namespace URIs of current nodes, * one string per node (non-recursive). Applicable to elements and attributes</li> * <li><tt>_parent</tt>: parent elements of current nodes. Applicable to element, attribute, comment, * entity, processing instruction.</li> * <li><tt>_ancestor</tt>: all ancestors up to root element (recursive) of current nodes. Applicable * to same node types as <tt>_parent</tt>.</li> * <li><tt>_ancestorOrSelf</tt>: all ancestors of current nodes plus current nodes. Applicable * to same node types as <tt>_parent</tt>.</li> * <li><tt>_descendant</tt>: all recursive descendant element children of current nodes. Applicable to * document and element nodes. * <li><tt>_descendantOrSelf</tt>: all recursive descendant element children of current nodes * plus current nodes. Applicable to document and element nodes. * <li><tt>_document</tt>: all documents the current nodes belong to. * Applicable to all nodes except text. * <li><tt>_doctype</tt>: doctypes of the current nodes. * Applicable to document nodes only. * <li><tt>_fname</tt>: is a filter-by-name template method model. When called, * it will yield a node list that contains only those current nodes whose name * matches one of names passed as argument. Attribute names should NOT be prefixed with the * at sign (@). Applicable on all node types, however has no effect on unnamed nodes.</li> * <li><tt>_ftype</tt>: 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 should pass a single string to this method * containing the characters of all types to keep. Valid characters are: * e (Element), a (Attribute), n (Entity), d (Document), t (DocType), * c (Comment), p (ProcessingInstruction), x (text). If the string anywhere contains * the exclamation mark (!), the filter's effect is inverted.</li> * <li><tt>_type</tt>: Returns a one-character String SimpleScalar containing * the typecode of the first node in the node list. Valid characters are: * e (Element), a (Attribute), n (Entity), d (Document), t (DocType), * c (Comment), p (ProcessingInstruction), x (text). If the type of the node * is unknown, returns '?'. If the node list is empty, returns an empty string scalar.</li> * <li><tt>_unique</tt>: 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>. * 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.</li> * <li><tt>_copy</tt>: a copy of the current node list. It is a shallow copy that * shares the underlying node list with this node list, however it has a * separate namespace registry, so it can be used to guarantee that subsequent * changes to the set of registered namespaces does not affect the node lists * that were used to create this node list. Applicable to all node types.</li> * <li><tt>_registerNamespace(prefix, uri)</tt>: 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> 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. You can also register * namespaces from Java code using the * {@link #registerNamespace(String, String)} method. * </li> * <li><tt>@attributeName</tt>: 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. * 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.</li> * <li>any other key: element children of current nodes with name matching the key. * This allows for convenience child traversal in <tt>book.chapter.title</tt> style syntax. * Note that <tt>nodeset.childname</tt> is technically equivalent to * <tt>nodeset._children._fname("childname")</tt>, but is both shorter to write * and evaluates faster. Applicable to document and element nodes.</li> * </ul> * The order of nodes in the resulting set is the order of evaluation of the key * on each node in this set from left to right. Evaluation of the key on a single * node always yields the results in "natural" order (that of the document preorder * traversal), even for uptree traversals. As a consequence, if this node list's nodes * are listed in natural order, applying any of the keys will produce a node list that * is also naturally ordered. As a special case, all node lists that are directly or * indirectly generated from a single Document or Element node through repeated * invocations of this method will be naturally ordered. * @param key a key that identifies a required set of nodes * @return a new NodeListModel that represents the requested set of nodes. */ public TemplateModel get(String key) throws TemplateModelException { if (isEmpty()) return EMPTY; if (key == null || key.length() == 0) throw new TemplateModelException("Invalid key [" + key + "]"); NodeOperator op = null; NamedNodeOperator nop = null; String name = null; switch (key.charAt(0)) { case '@': { if (key.length() != 2 || key.charAt(1) != '*') { // Generic attribute key nop = NAMED_ATTRIBUTE_OP; name = key.substring(1); } else // It is @* op = ALL_ATTRIBUTES_OP; break; } case '*': { if (key.length() == 1) op = ALL_CHILDREN_OP; else // Explicitly disallow any other identifier starting with asterisk throw new TemplateModelException("Invalid key [" + key + "]"); break; } case 'x': case '_': { op = (NodeOperator) OPERATIONS.get(key); if (op == null) { // Some special operation? Integer specop = (Integer) SPECIAL_OPERATIONS.get(key); if (specop != null) { switch (specop.intValue()) { case SPECIAL_OPERATION_COPY: { synchronized (namespaces) { return new NodeListModel(nodes, (Map) ((HashMap) namespaces).clone()); } } case SPECIAL_OPERATION_UNIQUE: return new NodeListModel(removeDuplicates(nodes), namespaces); case SPECIAL_OPERATION_FILTER_NAME: return new NameFilter(); case SPECIAL_OPERATION_FILTER_TYPE: return new TypeFilter(); case SPECIAL_OPERATION_QUERY_TYPE: return getType(); case SPECIAL_OPERATION_REGISTER_NAMESPACE: return new RegisterNamespace(); case SPECIAL_OPERATION_PLAINTEXT: return getPlainText(); } } } break; } } if (op == null && nop == null) { nop = NAMED_CHILDREN_OP; name = key; } List list = null; if (op != null) list = evaluateElementOperation(op, nodes); else { String localName = name; Namespace namespace = Namespace.NO_NAMESPACE; int colon = name.indexOf(':'); if (colon != -1) { localName = name.substring(colon + 1); String nsPrefix = name.substring(0, colon); synchronized (namespaces) { namespace = (Namespace) namespaces.get(nsPrefix); } if (namespace == null) { if (nsPrefix.equals("xml")) namespace = Namespace.XML_NAMESPACE; else throw new TemplateModelException("Unregistered namespace prefix '" + nsPrefix + "'"); } } list = evaluateNamedElementOperation(nop, localName, namespace, nodes); } return createNodeListModel(list, namespaces); } private TemplateModel getType() { if (nodes.size() == 0) return new SimpleScalar(""); Object firstNode = nodes.get(0); char code; if (firstNode instanceof Element) code = 'e'; else if (firstNode instanceof Text || firstNode instanceof String) code = 'x'; else if (firstNode instanceof Attribute) code = 'a'; else if (firstNode instanceof EntityRef) code = 'n'; else if (firstNode instanceof Document) code = 'd'; else if (firstNode instanceof DocType) code = 't'; else if (firstNode instanceof Comment) code = 'c'; else if (firstNode instanceof ProcessingInstruction) code = 'p'; else code = '?'; return new SimpleScalar(new String(new char[] { code})); } private SimpleScalar getPlainText() throws TemplateModelException { List list = evaluateElementOperation((TextOp) OPERATIONS.get("_text"), nodes); StringBuilder buf = new StringBuilder(); for (Iterator it = list.iterator(); it.hasNext(); ) { buf.append(it.next()); } return new SimpleScalar(buf.toString()); } public TemplateModelIterator iterator() { return new TemplateModelIterator() { private final Iterator it = nodes.iterator(); public TemplateModel next() { return it.hasNext() ? new NodeListModel(it.next(), namespaces) : null; } public boolean hasNext() { return it.hasNext(); } }; } /** * Retrieves the i-th element of the node list. */ public TemplateModel get(int i) throws TemplateModelException { try { return new NodeListModel(nodes.get(i), namespaces); } catch (IndexOutOfBoundsException e) { throw new TemplateModelException("Index out of bounds: " + e.getMessage()); } } public int size() { return nodes.size(); } /** * Applies an XPath expression to the node list and returns the resulting node list. * In order for this method to work, your application must have access * <a href="http://www.jaxen.org">Jaxen</a> library classes. The * implementation does cache the parsed format of XPath expressions in a weak hash * map, keyed by the string representation of the XPath expression. As the string * object passed as the argument is usually kept in the parsed FreeMarker template, * this ensures that each XPath expression is parsed only once during the lifetime * of the FreeMarker template that contains it. * @param arguments the list of arguments. Must contain exactly one string that is * the XPath expression you wish to apply. The XPath expression can use any namespace * prefixes that were defined using the {@link #registerNamespace(String, String)} * method or the <code>nodelist._registerNamespace(prefix, uri)</code> expression in the * template. * @return a NodeListModel representing the nodes that are the result of application * of the XPath to the current node list. */ public Object exec(List arguments) throws TemplateModelException { if (arguments == null || arguments.size() != 1) throw new TemplateModelException("Exactly one argument required for execute() on NodeTemplate"); String xpathString = (String) arguments.get(0); JDOMXPathEx xpath = null; try { synchronized (XPATH_CACHE) { xpath = (JDOMXPathEx) XPATH_CACHE.get(xpathString); if (xpath == null) { xpath = new JDOMXPathEx(xpathString); XPATH_CACHE.put(xpathString, xpath); } } return createNodeListModel(xpath.selectNodes(nodes, namespaces), namespaces); } catch (Exception e) { throw new TemplateModelException("Could not evaulate XPath expression " + xpathString, e); } } /** * Registers an XML namespace with this node list. Once registered, you can * refer to the registered namespace using its prefix in the * {@link #get(String)} method from this node list and all other * node lists that are derived from this node list. Use the * <tt>nodelist["prefix:localname"]</tt> or the * <tt>nodelist["@prefix:localname"]</tt> syntax 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. You can also register * namespaces during template evaluation using the * <tt>nodelist._registerNamespace(prefix, uri)</tt> syntax in the template. * This mechanism is completely independent from the namespace declarations * in the XML document itself; its purpose is to give you an easy way * to refer to namespace-scoped elements in {@link #get(String)} and * in XPath expressions passed to {@link #exec(List)}. Note also that * the namespace prefix registry is shared among all node lists that * are created from a single node list - modifying the registry in one * affects all others as well. If you want to obtain a namespace * "detached" copy of the node list, use the <code>_copy</code> key on * it (or call <code>nodeList.get("_copy")</code> directly from your * Java code. The returned node list has all the namespaces that the * original node list has, but they can be manipulated independently * thereon. */ public void registerNamespace(String prefix, String uri) { synchronized (namespaces) { namespaces.put(prefix, Namespace.getNamespace(prefix, uri)); } } private interface NodeOperator { List operate(Object node) throws TemplateModelException; } private interface NamedNodeOperator { List operate(Object node, String localName, Namespace namespace) throws TemplateModelException; } private static final class AllChildrenOp implements NodeOperator { public List operate(Object node) { if (node instanceof Element) return((Element) node).getChildren(); else if (node instanceof Document) { Element root = ((Document) node).getRootElement(); return root == null ? Collections.EMPTY_LIST : Collections.singletonList(root); } // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; /* else throw new TemplateModelException("_allChildren can not be applied on " + node.getClass()); */ } } private static final class NamedChildrenOp implements NamedNodeOperator { public List operate(Object node, String localName, Namespace namespace) { if (node instanceof Element) { return((Element) node).getChildren(localName, namespace); } else if (node instanceof Document) { Element root = ((Document) node).getRootElement(); if (root != null && root.getName().equals(localName) && root.getNamespaceURI().equals(namespace.getURI())) { return Collections.singletonList(root); } else return Collections.EMPTY_LIST; } // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; /* else throw new TemplateModelException("_namedChildren can not be applied on " + node.getClass()); */ } } private static final class AllAttributesOp implements NodeOperator { public List operate(Object node) { // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) if (!(node instanceof Element)) { return null; } return ((Element) node).getAttributes(); /* else throw new TemplateModelException("_allAttributes can not be applied on " + node.getClass()); */ } } private static final class NamedAttributeOp implements NamedNodeOperator { public List operate(Object node, String localName, Namespace namespace) { Attribute attr = null; if (node instanceof Element) { Element element = (Element) node; attr = element.getAttribute(localName, namespace); } else if (node instanceof ProcessingInstruction) { ProcessingInstruction pi = (ProcessingInstruction) node; if ("target".equals(localName)) attr = new Attribute("target", pi.getTarget()); else if ("data".equals(localName)) attr = new Attribute("data", pi.getData()); else attr = new Attribute(localName, pi.getValue(localName)); } else if (node instanceof DocType) { DocType doctype = (DocType) node; if ("publicId".equals(localName)) attr = new Attribute("publicId", doctype.getPublicID()); else if ("systemId".equals(localName)) attr = new Attribute("systemId", doctype.getSystemID()); else if ("elementName".equals(localName)) attr = new Attribute("elementName", doctype.getElementName()); } // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) else { return null; } /* else throw new TemplateModelException("_allAttributes can not be applied on " + node.getClass()); */ return attr == null ? Collections.EMPTY_LIST : Collections.singletonList(attr); } } private static final class NameOp implements NodeOperator { public List operate(Object node) { if (node instanceof Element) return Collections.singletonList(((Element) node).getName()); else if (node instanceof Attribute) return Collections.singletonList(((Attribute) node).getName()); else if (node instanceof EntityRef) return Collections.singletonList(((EntityRef) node).getName()); else if (node instanceof ProcessingInstruction) return Collections.singletonList(((ProcessingInstruction) node).getTarget()); else if (node instanceof DocType) return Collections.singletonList(((DocType) node).getPublicID()); else return null; // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) // throw new TemplateModelException("_name can not be applied on " + node.getClass()); } } private static final class QNameOp implements NodeOperator { public List operate(Object node) { if (node instanceof Element) return Collections.singletonList(((Element) node).getQualifiedName()); else if (node instanceof Attribute) return Collections.singletonList(((Attribute) node).getQualifiedName()); // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; // throw new TemplateModelException("_qname can not be applied on " + node.getClass()); } } private static final class NamespaceUriOp implements NodeOperator { public List operate(Object node) { if (node instanceof Element) return Collections.singletonList(((Element) node).getNamespace().getURI()); else if (node instanceof Attribute) return Collections.singletonList(((Attribute) node).getNamespace().getURI()); // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; // throw new TemplateModelException("_nsuri can not be applied on " + node.getClass()); } } private static final class NamespacePrefixOp implements NodeOperator { public List operate(Object node) { if (node instanceof Element) return Collections.singletonList(((Element) node).getNamespace().getPrefix()); else if (node instanceof Attribute) return Collections.singletonList(((Attribute) node).getNamespace().getPrefix()); // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; // throw new TemplateModelException("_nsprefix can not be applied on " + node.getClass()); } } private static final class CanonicalNameOp implements NodeOperator { public List operate(Object node) { if (node instanceof Element) { Element element = (Element) node; return Collections.singletonList(element.getNamespace().getURI() + element.getName()); } else if (node instanceof Attribute) { Attribute attribute = (Attribute) node; return Collections.singletonList(attribute.getNamespace().getURI() + attribute.getName()); } // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; // throw new TemplateModelException("_cname can not be applied on " + node.getClass()); } } private static final Element getParent(Object node) { if (node instanceof Element) return((Element) node).getParent(); else if (node instanceof Attribute) return((Attribute) node).getParent(); else if (node instanceof Text) return((Text) node).getParent(); else if (node instanceof ProcessingInstruction) return((ProcessingInstruction) node).getParent(); else if (node instanceof Comment) return((Comment) node).getParent(); else if (node instanceof EntityRef) return((EntityRef) node).getParent(); else // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; // throw new TemplateModelException("_parent can not be applied on " + node.getClass()); } private static final class ParentOp implements NodeOperator { public List operate(Object node) { Element parent = getParent(node); return parent == null ? Collections.EMPTY_LIST : Collections.singletonList(parent); } } private static final class AncestorOp implements NodeOperator { public List operate(Object node) { Element parent = getParent(node); if (parent == null) return Collections.EMPTY_LIST; LinkedList list = new LinkedList(); do { list.addFirst(parent); parent = parent.getParent(); } while (parent != null); return list; } } private static final class AncestorOrSelfOp implements NodeOperator { public List operate(Object node) { Element parent = getParent(node); if (parent == null) return Collections.singletonList(node); LinkedList list = new LinkedList(); list.addFirst(node); do { list.addFirst(parent); parent = parent.getParent(); } while (parent != null); return list; } } private static class DescendantOp implements NodeOperator { public List operate(Object node) { LinkedList list = new LinkedList(); if (node instanceof Element) { addChildren((Element) node, list); } else if (node instanceof Document) { Element root = ((Document) node).getRootElement(); list.add(root); addChildren(root, list); } else // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; // throw new TemplateModelException("_descendant can not be applied on " + node.getClass()); return list; } private void addChildren(Element element, List list) { List children = element.getChildren(); Iterator it = children.iterator(); while (it.hasNext()) { Element child = (Element) it.next(); list.add(child); addChildren(child, list); } } } private static final class DescendantOrSelfOp extends DescendantOp { @Override public List operate(Object node) { LinkedList list = (LinkedList) super.operate(node); list.addFirst(node); return list; } } private static final class DocumentOp implements NodeOperator { public List operate(Object node) { Document doc = null; if (node instanceof Element) doc = ((Element) node).getDocument(); else if (node instanceof Attribute) { Element parent = ((Attribute) node).getParent(); doc = parent == null ? null : parent.getDocument(); } else if (node instanceof Text) { Element parent = ((Text) node).getParent(); doc = parent == null ? null : parent.getDocument(); } else if (node instanceof Document) doc = (Document) node; else if (node instanceof ProcessingInstruction) doc = ((ProcessingInstruction) node).getDocument(); else if (node instanceof EntityRef) doc = ((EntityRef) node).getDocument(); else if (node instanceof Comment) doc = ((Comment) node).getDocument(); else // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; // throw new TemplateModelException("_document can not be applied on " + node.getClass()); return doc == null ? Collections.EMPTY_LIST : Collections.singletonList(doc); } } private static final class DocTypeOp implements NodeOperator { public List operate(Object node) { if (node instanceof Document) { DocType doctype = ((Document) node).getDocType(); return doctype == null ? Collections.EMPTY_LIST : Collections.singletonList(doctype); } else // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; // throw new TemplateModelException("_doctype can not be applied on " + node.getClass()); } } private static final class ContentOp implements NodeOperator { public List operate(Object node) { if (node instanceof Element) return((Element) node).getContent(); else if (node instanceof Document) return((Document) node).getContent(); // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; // throw new TemplateModelException("_content can not be applied on " + node.getClass()); } } private static final class TextOp implements NodeOperator { public List operate(Object node) { if (node instanceof Element) return Collections.singletonList(((Element) node).getTextTrim()); if (node instanceof Attribute) return Collections.singletonList(((Attribute) node).getValue()); if (node instanceof CDATA) return Collections.singletonList(((CDATA) node).getText()); if (node instanceof Comment) return Collections.singletonList(((Comment) node).getText()); if (node instanceof ProcessingInstruction) return Collections.singletonList(((ProcessingInstruction) node).getData()); // With 2.1 semantics it makes more sense to just return a null and let the core // throw an InvalidReferenceException and the template writer can use ?exists etcetera. (JR) return null; // throw new TemplateModelException("_text can not be applied on " + node.getClass()); } } private static final List evaluateElementOperation(NodeOperator op, List nodes) throws TemplateModelException { int s = nodes.size(); List[] lists = new List[s]; int l = 0; { int i = 0; Iterator it = nodes.iterator(); while (it.hasNext()) { List list = op.operate(it.next()); if (list != null) { lists[i++] = list; l += list.size(); } } } List retval = new ArrayList(l); for (int i = 0; i < s; ++i) { if (lists[i] != null) { retval.addAll(lists[i]); } } return retval; } private static final List evaluateNamedElementOperation(NamedNodeOperator op, String localName, Namespace namespace, List nodes) throws TemplateModelException { int s = nodes.size(); List[] lists = new List[s]; int l = 0; { int i = 0; Iterator it = nodes.iterator(); while (it.hasNext()) { List list = op.operate(it.next(), localName, namespace); lists[i++] = list; l += list.size(); } } List retval = new ArrayList(l); for (int i = 0; i < s; ++i) retval.addAll(lists[i]); return retval; } 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); } ulist.trimToSize(); return ulist; } private static final Map createOperations() { Map map = new HashMap(); map.put("_ancestor", new AncestorOp()); map.put("_ancestorOrSelf", new AncestorOrSelfOp()); map.put("_attributes", ALL_ATTRIBUTES_OP); map.put("_children", ALL_CHILDREN_OP); map.put("_cname", new CanonicalNameOp()); map.put("_content", new ContentOp()); map.put("_descendant", new DescendantOp()); map.put("_descendantOrSelf", new DescendantOrSelfOp()); map.put("_document", new DocumentOp()); map.put("_doctype", new DocTypeOp()); map.put("_name", new NameOp()); map.put("_nsprefix", new NamespacePrefixOp()); map.put("_nsuri", new NamespaceUriOp()); map.put("_parent", new ParentOp()); map.put("_qname", new QNameOp()); map.put("_text", new TextOp()); return map; } private static final Map createSpecialOperations() { Map map = new HashMap(); Integer copy = Integer.valueOf(SPECIAL_OPERATION_COPY); Integer unique = Integer.valueOf(SPECIAL_OPERATION_UNIQUE); Integer fname = Integer.valueOf(SPECIAL_OPERATION_FILTER_NAME); Integer ftype = Integer.valueOf(SPECIAL_OPERATION_FILTER_TYPE); Integer type = Integer.valueOf(SPECIAL_OPERATION_QUERY_TYPE); Integer regns = Integer.valueOf(SPECIAL_OPERATION_REGISTER_NAMESPACE); Integer plaintext = Integer.valueOf(SPECIAL_OPERATION_PLAINTEXT); map.put("_copy", copy); map.put("_unique", unique); map.put("_fname", fname); map.put("_ftype", ftype); map.put("_type", type); map.put("_registerNamespace", regns); map.put("_plaintext", plaintext); // These are in for backward compatibility map.put("x_copy", copy); map.put("x_unique", unique); map.put("x_fname", fname); map.put("x_ftype", ftype); map.put("x_type", type); return map; } private final class RegisterNamespace implements TemplateMethodModel { public boolean isEmpty() { return false; } public Object exec(List arguments) throws TemplateModelException { if (arguments.size() != 2) throw new TemplateModelException("_registerNamespace(prefix, uri) requires two arguments"); registerNamespace((String) arguments.get(0), (String) arguments.get(1)); return TemplateScalarModel.EMPTY_STRING; } } private final class NameFilter implements TemplateMethodModel { public boolean isEmpty() { return false; } public Object exec(List arguments) { Set names = new HashSet(arguments); List list = new LinkedList(nodes); Iterator it = list.iterator(); while (it.hasNext()) { Object node = it.next(); String name = null; if (node instanceof Element) name = ((Element) node).getName(); else if (node instanceof Attribute) name = ((Attribute) node).getName(); else if (node instanceof ProcessingInstruction) name = ((ProcessingInstruction) node).getTarget(); else if (node instanceof EntityRef) name = ((EntityRef) node).getName(); else if (node instanceof DocType) name = ((DocType) node).getPublicID(); if (name == null || !names.contains(name)) it.remove(); } return createNodeListModel(list, namespaces); } } private final class TypeFilter implements TemplateMethodModel { public boolean isEmpty() { return false; } public Object exec(List arguments) throws TemplateModelException { if (arguments == null || arguments.size() == 0) throw new TemplateModelException("_type expects exactly one argument"); String arg = (String) arguments.get(0); boolean invert = arg.indexOf('!') != -1; // NOTE: true in each of these variables means 'remove', not 'keep' // This is so we don't invert their values in the loop. So, // a is true <--> (a is not present in the string) xor invert. boolean a = invert != (arg.indexOf('a') == -1); boolean c = invert != (arg.indexOf('c') == -1); boolean d = invert != (arg.indexOf('d') == -1); boolean e = invert != (arg.indexOf('e') == -1); boolean n = invert != (arg.indexOf('n') == -1); boolean p = invert != (arg.indexOf('p') == -1); boolean t = invert != (arg.indexOf('t') == -1); boolean x = invert != (arg.indexOf('x') == -1); LinkedList list = new LinkedList(nodes); Iterator it = list.iterator(); while (it.hasNext()) { Object node = it.next(); if ((node instanceof Element && e) || (node instanceof Attribute && a) || (node instanceof String && x) || (node instanceof Text && x) || (node instanceof ProcessingInstruction && p) || (node instanceof Comment && c) || (node instanceof EntityRef && n) || (node instanceof Document && d) || (node instanceof DocType && t)) it.remove(); } return createNodeListModel(list, namespaces); } } private static final class AttributeXMLOutputter extends XMLOutputter { public void output(Attribute attribute, Writer out) throws IOException { out.write(" "); out.write(attribute.getQualifiedName()); out.write("="); out.write("\""); out.write(escapeAttributeEntities(attribute.getValue())); out.write("\""); } } private static final class JDOMXPathEx extends JDOMXPath { JDOMXPathEx(String path) throws JaxenException { super(path); } public List selectNodes(Object object, Map namespaces) throws JaxenException { Context context = getContext(object); context.getContextSupport().setNamespaceContext(new NamespaceContextImpl(namespaces)); return selectNodesForContext(context); } private static final class NamespaceContextImpl implements NamespaceContext { private final Map namespaces; NamespaceContextImpl(Map namespaces) { this.namespaces = namespaces; } public String translateNamespacePrefixToUri(String prefix) { // Empty prefix always maps to empty URL in XPath if (prefix.length() == 0) { return prefix; } synchronized (namespaces) { Namespace ns = (Namespace) namespaces.get(prefix); return ns == null ? null : ns.getURI(); } } } } }