001: /*
002: * Copyright (c) 2003 The Visigoth Software Society. All rights
003: * reserved.
004: *
005: * Redistribution and use in source and binary forms, with or without
006: * modification, are permitted provided that the following conditions
007: * are met:
008: *
009: * 1. Redistributions of source code must retain the above copyright
010: * notice, this list of conditions and the following disclaimer.
011: *
012: * 2. Redistributions in binary form must reproduce the above copyright
013: * notice, this list of conditions and the following disclaimer in
014: * the documentation and/or other materials provided with the
015: * distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowledgement:
019: * "This product includes software developed by the
020: * Visigoth Software Society (http://www.visigoths.org/)."
021: * Alternately, this acknowledgement may appear in the software itself,
022: * if and wherever such third-party acknowledgements normally appear.
023: *
024: * 4. Neither the name "FreeMarker", "Visigoth", nor any of the names of the
025: * project contributors may be used to endorse or promote products derived
026: * from this software without prior written permission. For written
027: * permission, please contact visigoths@visigoths.org.
028: *
029: * 5. Products derived from this software may not be called "FreeMarker" or "Visigoth"
030: * nor may "FreeMarker" or "Visigoth" appear in their names
031: * without prior written permission of the Visigoth Software Society.
032: *
033: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
034: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
035: * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
036: * DISCLAIMED. IN NO EVENT SHALL THE VISIGOTH SOFTWARE SOCIETY OR
037: * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
038: * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
039: * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
040: * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
041: * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
042: * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
043: * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
044: * SUCH DAMAGE.
045: * ====================================================================
046: *
047: * This software consists of voluntary contributions made by many
048: * individuals on behalf of the Visigoth Software Society. For more
049: * information on the Visigoth Software Society, please see
050: * http://www.visigoths.org/
051: */
052:
053: package freemarker.ext.xml;
054:
055: import java.io.StringWriter;
056: import java.util.ArrayList;
057: import java.util.Collection;
058: import java.util.HashSet;
059: import java.util.Iterator;
060: import java.util.List;
061: import java.util.Set;
062:
063: import freemarker.log.Logger;
064: import freemarker.template.TemplateHashModel;
065: import freemarker.template.TemplateMethodModel;
066: import freemarker.template.TemplateModel;
067: import freemarker.template.TemplateModelException;
068: import freemarker.template.TemplateNodeModel;
069: import freemarker.template.TemplateScalarModel;
070: import freemarker.template.TemplateSequenceModel;
071: import freemarker.template.utility.ClassUtil;
072: import freemarker.template.utility.Collections12;
073:
074: /**
075: * <p>A data model adapter for three widespread XML document object model
076: * representations: W3C DOM, dom4j, and JDOM. The adapter automatically
077: * recognizes the used XML object model and provides a unified interface for it
078: * toward the template. The model provides access to all XML InfoSet features
079: * of the XML document and includes XPath support if it has access to the XPath-
080: * evaluator library Jaxen. The model's philosophy (which closely follows that
081: * of XML InfoSet and XPath) is as follows: it always wraps a list of XML nodes
082: * (the "nodelist"). The list can be empty, can have a single element, or can
083: * have multiple elements. Every operation applied to the model is applied to
084: * all nodes in its nodelist. You usually start with a single- element nodelist,
085: * usually the root element node or the document node of the XML tree.
086: * Additionally, the nodes can contain String objects as a result of certain
087: * evaluations (getting the names of elements, values of attributes, etc.)</p>
088: * <p><strong>Implementation note:</strong> If you are using W3C DOM documents
089: * built by the Crimson XML parser (or you are using the built-in JDK 1.4 XML
090: * parser, which is essentially Crimson), make sure you call
091: * <tt>setNamespaceAware(true)</tt> on the
092: * <tt>javax.xml.parsers.DocumentBuilderFactory</tt> instance used for document
093: * building even when your documents don't use XML namespaces. Failing to do so,
094: * you will experience incorrect behavior when using the documents wrapped with
095: * this model.</p>
096: *
097: * @deprecated Use {@link freemarker.ext.dom.NodeModel} instead.
098: * @version $Id: NodeListModel.java,v 1.15 2004/01/06 17:06:43 szegedia Exp $
099: * @author Attila Szegedi
100: */
101: public class NodeListModel implements TemplateHashModel,
102: TemplateMethodModel, TemplateScalarModel,
103: TemplateSequenceModel, TemplateNodeModel {
104: private static final Logger logger = Logger
105: .getLogger("freemarker.xml");
106:
107: private static final Class DOM_NODE_CLASS = getClass("org.w3c.dom.Node");
108: private static final Class DOM4J_NODE_CLASS = getClass("org.dom4j.Node");
109: private static final Navigator DOM_NAVIGATOR = getNavigator("Dom");
110: private static final Navigator DOM4J_NAVIGATOR = getNavigator("Dom4j");
111: private static final Navigator JDOM_NAVIGATOR = getNavigator("Jdom");
112: private static final Namespaces.Factory NS_FACTORY = getNamespacesFactory();
113:
114: // The navigator object that implements document model-specific behavior.
115: private final Navigator navigator;
116: // The contained nodes
117: private final List nodes;
118: // The namespaces object (potentially shared by multiple models)
119: private Namespaces namespaces;
120:
121: /**
122: * Creates a new NodeListModel, wrapping the passed nodes.
123: * @param nodes you can pass it a single XML node from any supported
124: * document model, or a Java collection containing any number of nodes.
125: * Passing null is prohibited. To create an empty model, pass it an empty
126: * collection. If a collection is passed, all passed nodes must belong to
127: * the same XML object model, i.e. you can't mix JDOM and dom4j in a single
128: * instance of NodeListModel. The model itself doesn't check for this condition,
129: * as it can be time consuming, but will throw spurious
130: * {@link ClassCastException}s when it encounters mixed objects.
131: * @throws IllegalArgumentException if you pass null
132: */
133: public NodeListModel(Object nodes) {
134: Object node = nodes;
135: if (nodes instanceof Collection) {
136: this .nodes = new ArrayList((Collection) nodes);
137: node = this .nodes.isEmpty() ? null : this .nodes.get(0);
138: } else if (nodes != null) {
139: this .nodes = Collections12.singletonList(nodes);
140: } else {
141: throw new IllegalArgumentException("nodes == null");
142: }
143: if (DOM_NODE_CLASS != null && DOM_NODE_CLASS.isInstance(node)) {
144: navigator = DOM_NAVIGATOR;
145: } else if (DOM4J_NODE_CLASS != null
146: && DOM4J_NODE_CLASS.isInstance(node)) {
147: navigator = DOM4J_NAVIGATOR;
148: } else {
149: // Assume JDOM
150: navigator = JDOM_NAVIGATOR;
151: }
152: namespaces = NS_FACTORY.create();
153: }
154:
155: private NodeListModel(Navigator navigator, List nodes,
156: Namespaces namespaces) {
157: this .navigator = navigator;
158: this .nodes = nodes;
159: this .namespaces = namespaces;
160: }
161:
162: private NodeListModel deriveModel(List derivedNodes) {
163: namespaces.markShared();
164: return new NodeListModel(navigator, derivedNodes, namespaces);
165: }
166:
167: /**
168: * Returns the number of nodes in this model's nodelist.
169: * @see freemarker.template.TemplateSequenceModel#size()
170: */
171: public int size() {
172: return nodes.size();
173: }
174:
175: /**
176: * Evaluates an XPath expression on XML nodes in this model.
177: * @param arguments the arguments to the method invocation. Expectes exactly
178: * one argument - the XPath expression.
179: * @return a new NodeListModel with nodes selected by applying the XPath
180: * expression to this model's nodelist.
181: * @see freemarker.template.TemplateMethodModel#exec(List)
182: */
183: public Object exec(List arguments) throws TemplateModelException {
184: if (arguments.size() != 1) {
185: throw new TemplateModelException(
186: "Expecting exactly one argument - an XPath expression");
187: }
188: return deriveModel(navigator.applyXPath(nodes,
189: (String) arguments.get(0), namespaces));
190: }
191:
192: /**
193: * Returns the string representation of the wrapped nodes. String objects in
194: * the nodelist are rendered as-is (with no XML escaping applied). All other
195: * nodes are rendered in the default XML serialization format ("plain XML").
196: * This makes the model quite suited for use as an XML-transformation tool.
197: * @return the string representation of the wrapped nodes. String objects
198: * in the nodelist are rendered as-is (with no XML escaping applied). All
199: * other nodes are rendered in the default XML serialization format ("plain
200: * XML").
201: * @see freemarker.template.TemplateScalarModel#getAsString()
202: */
203: public String getAsString() throws TemplateModelException {
204: StringWriter sw = new StringWriter(size() * 128);
205: for (Iterator iter = nodes.iterator(); iter.hasNext();) {
206: Object o = iter.next();
207: if (o instanceof String) {
208: sw.write((String) o);
209: } else {
210: navigator.getAsString(o, sw);
211: }
212: }
213: return sw.toString();
214: }
215:
216: /**
217: * Selects a single node from this model's nodelist by its list index and
218: * returns a new NodeListModel containing that single node.
219: * @param index the ordinal number of the selected node
220: * @see freemarker.template.TemplateSequenceModel#get(int)
221: */
222: public TemplateModel get(int index) {
223: return deriveModel(Collections12
224: .singletonList(nodes.get(index)));
225: }
226:
227: /**
228: * Returns a new NodeListModel containing the nodes that result from applying
229: * an operator to this model's nodes.
230: * @param key the operator to apply to nodes. Available operators are:
231: * <table border="1">
232: * <thead>
233: * <tr>
234: * <th align="left">Key name</th>
235: * <th align="left">Evaluates to</th>
236: * </tr>
237: * </thead>
238: * <tbody>
239: * <tr>
240: * <td><tt>*</tt> or <tt>_children</tt></td>
241: * <td>all direct element children of current nodes (non-recursive).
242: * Applicable to element and document nodes.</td>
243: * </tr>
244: * <tr>
245: * <td><tt>@*</tt> or <tt>_attributes</tt></td>
246: * <td>all attributes of current nodes. Applicable to elements only.
247: * </td>
248: * </tr>
249: * <tr>
250: * <td><tt>@<i>attributeName</i></tt></td>
251: * <td>named attributes of current nodes. Applicable to elements,
252: * doctypes and processing instructions. On doctypes it supports
253: * attributes <tt>publicId</tt>, <tt>systemId</tt> and
254: * <tt>elementName</tt>. On processing instructions, it supports
255: * attributes <tt>target</tt> and <tt>data</tt>, as well as any
256: * other attribute name specified in data as
257: * <tt>name="value"</tt> pair on dom4j or JDOM models.
258: * The attribute nodes for doctype and processing instruction are
259: * synthetic, and as such have no parent. Note, however that
260: * <tt>@*</tt> does NOT operate on doctypes or processing
261: * instructions.</td>
262: * </tr>
263: *
264: * <tr>
265: * <td><tt>_ancestor</tt></td>
266: * <td>all ancestors up to root element (recursive) of current nodes.
267: * Applicable to same node types as <tt>_parent</tt>.</td>
268: * </tr>
269: * <tr>
270: * <td><tt>_ancestorOrSelf</tt></td>
271: * <td>all ancestors of current nodes plus current nodes. Applicable
272: * to same node types as <tt>_parent</tt>.</td>
273: * </tr>
274: * <tr>
275: * <td><tt>_cname</tt></td>
276: * <td>the canonical names of current nodes (namespace URI + local
277: * name), one string per node (non-recursive). Applicable to
278: * elements and attributes</td>
279: * </tr>
280: * <tr>
281: * <td><tt>_content</tt></td>
282: * <td>the complete content of current nodes, including children
283: * elements, text, entity references, and processing instructions
284: * (non-recursive). Applicable to elements and documents.</td>
285: * </tr>
286: * <tr>
287: * <td><tt>_descendant</tt></td>
288: * <td>all recursive descendant element children of current nodes.
289: * Applicable to document and element nodes.</td>
290: * </tr>
291: * <tr>
292: * <td><tt>_descendantOrSelf</tt></td>
293: * <td>all recursive descendant element children of current nodes
294: * plus current nodes. Applicable to document and element nodes.
295: * </td>
296: * </tr>
297: * <tr>
298: * <td><tt>_document</tt></td>
299: * <td>all documents the current nodes belong to. Applicable to all
300: * nodes except text.</td>
301: * </tr>
302: * <tr>
303: * <td><tt>_doctype</tt></td>
304: * <td>doctypes of the current nodes. Applicable to document nodes
305: * only.</td>
306: * </tr>
307: * <tr>
308: * <td><tt>_filterType</tt></td>
309: * <td>is a filter-by-type template method model. When called, it
310: * will yield a node list that contains only those current nodes
311: * whose type matches one of types passed as argument. You can pass
312: * as many string arguments as you want, each representing one of
313: * the types to select: "attribute", "cdata",
314: * "comment", "document",
315: * "documentType", "element",
316: * "entity", "entityReference",
317: * "namespace", "processingInstruction", or
318: * "text".</td>
319: * </tr>
320: * <tr>
321: * <td><tt>_name</tt></td>
322: * <td>the names of current nodes, one string per node
323: * (non-recursive). Applicable to elements and attributes
324: * (returns their local names), entity references, processing
325: * instructions (returns its target), doctypes (returns its public
326: * ID)</td>
327: * </tr>
328: * <tr>
329: * <td><tt>_nsprefix</tt></td>
330: * <td>the namespace prefixes of current nodes, one string per node
331: * (non-recursive). Applicable to elements and attributes</td>
332: * </tr>
333: * <tr>
334: * <td><tt>_nsuri</tt></td>
335: * <td>the namespace URIs of current nodes, one string per node
336: * (non-recursive). Applicable to elements and attributes</td>
337: * </tr>
338: * <tr>
339: * <td><tt>_parent</tt></td>
340: * <td>parent elements of current nodes. Applicable to element,
341: * attribute, comment, entity, processing instruction.</td>
342: * </tr>
343: * <tr>
344: * <td><tt>_qname</tt></td>
345: * <td>the qualified names of current nodes in
346: * <tt>[namespacePrefix:]localName</tt> form, one string per node
347: * (non-recursive). Applicable to elements and attributes</td>
348: * </tr>
349: * <tr>
350: * <td><tt>_registerNamespace(prefix, uri)</tt></td>
351: * <td>register a XML namespace with the specified prefix and URI for
352: * the current node list and all node lists that are derived from
353: * the current node list. After registering, you can use the
354: * <tt>nodelist["prefix:localname"]</tt> or
355: * <tt>nodelist["@prefix:localname"]</tt> syntaxes to
356: * reach elements and attributes whose names are namespace-scoped.
357: * Note that the namespace prefix need not match the actual prefix
358: * used by the XML document itself since namespaces are compared
359: * solely by their URI.</td>
360: * </tr>
361: * <tr>
362: * <td><tt>_text</tt></td>
363: * <td>the text of current nodes, one string per node
364: * (non-recursive). Applicable to elements, attributes, comments,
365: * processing instructions (returns its data) and CDATA sections.
366: * The reserved XML characters ('<' and '&') are NOT
367: * escaped.</td>
368: * </tr>
369: * <tr>
370: * <td><tt>_type</tt></td>
371: * <td>Returns a string describing the type of nodes, one
372: * string per node. The returned values are "attribute",
373: * "cdata", "comment", "document",
374: * "documentType", "element",
375: * "entity", "entityReference",
376: * "namespace", "processingInstruction",
377: * "text", or "unknown".</td>
378: * </tr>
379: * <tr>
380: * <td><tt>_unique</tt></td>
381: * <td>a copy of the current nodes that keeps only the first
382: * occurrence of every node, eliminating duplicates. Duplicates can
383: * occur in the node list by applying uptree-traversals
384: * <tt>_parent</tt>, <tt>_ancestor</tt>, <tt>_ancestorOrSelf</tt>,
385: * and <tt>_document</tt> on a node list with multiple elements.
386: * I.e. <tt>foo._children._parent</tt> will return a node list that
387: * has duplicates of nodes in foo - each node will have the number
388: * of occurrences equal to the number of its children. In these
389: * cases, use <tt>foo._children._parent._unique</tt> to eliminate
390: * duplicates. Applicable to all node types.</td>
391: * </tr>
392: * <tr>
393: * <td>any other key</td>
394: * <td>element children of current nodes with name matching the key.
395: * This allows for convenience child traversal in
396: * <tt>book.chapter.title</tt> style syntax. Applicable to document
397: * and element nodes.</td>
398: * </tr>
399: * </tbody>
400: * </table>
401: * @return a new NodeListModel containing the nodes that result from applying
402: * the operator to this model's nodes.
403: * @see freemarker.template.TemplateHashModel#get(String)
404: */
405: public TemplateModel get(String key) throws TemplateModelException {
406: // Try a built-in navigator operator
407: NodeOperator op = navigator.getOperator(key);
408: String localName = null;
409: String namespaceUri = "";
410: // If not a nav op, then check for special keys.
411: if (op == null && key.length() > 0 && key.charAt(0) == '_') {
412: if (key.equals("_unique")) {
413: return deriveModel(removeDuplicates(nodes));
414: } else if (key.equals("_filterType")
415: || key.equals("_ftype")) {
416: return new FilterByType();
417: } else if (key.equals("_registerNamespace")) {
418: if (namespaces.isShared()) {
419: namespaces = (Namespaces) namespaces.clone();
420: }
421: }
422: }
423: // Last, do a named child element or attribute lookup
424: if (op == null) {
425: int colon = key.indexOf(':');
426: if (colon == -1) {
427: // No namespace prefix specified
428: localName = key;
429: } else {
430: // Namespace prefix specified
431: localName = key.substring(colon + 1);
432: String prefix = key.substring(0, colon);
433: namespaceUri = namespaces
434: .translateNamespacePrefixToUri(prefix);
435: if (namespaceUri == null) {
436: throw new TemplateModelException(
437: "Namespace prefix " + prefix
438: + " is not registered.");
439: }
440: }
441: if (localName.charAt(0) == '@') {
442: op = navigator.getAttributeOperator();
443: localName = localName.substring(1);
444: } else {
445: op = navigator.getChildrenOperator();
446: }
447: }
448: List result = new ArrayList();
449: for (Iterator iter = nodes.iterator(); iter.hasNext();) {
450: try {
451: op
452: .process(iter.next(), localName, namespaceUri,
453: result);
454: } catch (RuntimeException e) {
455: throw new TemplateModelException(e);
456: }
457: }
458: return deriveModel(result);
459: }
460:
461: /**
462: * Returns true if this NodeListModel contains no nodes.
463: * @see freemarker.template.TemplateHashModel#isEmpty()
464: */
465: public boolean isEmpty() {
466: return nodes.isEmpty();
467: }
468:
469: /**
470: * Registers a namespace prefix-URI pair for subsequent use in {@link
471: * #get(String)} as well as for use in XPath expressions.
472: * @param prefix the namespace prefix to use for the namespace
473: * @param uri the namespace URI that identifies the namespace.
474: */
475: public void registerNamespace(String prefix, String uri) {
476: if (namespaces.isShared()) {
477: namespaces = (Namespaces) namespaces.clone();
478: }
479: namespaces.registerNamespace(prefix, uri);
480: }
481:
482: private class FilterByType implements TemplateMethodModel {
483: public Object exec(List arguments) {
484: List filteredNodes = new ArrayList();
485: for (Iterator iter = arguments.iterator(); iter.hasNext();) {
486: Object node = iter.next();
487: if (arguments.contains(navigator.getType(node))) {
488: filteredNodes.add(node);
489: }
490: }
491: return deriveModel(filteredNodes);
492: }
493: }
494:
495: private static final List removeDuplicates(List list) {
496: int s = list.size();
497: ArrayList ulist = new ArrayList(s);
498: Set set = new HashSet(s * 4 / 3, .75f);
499: Iterator it = list.iterator();
500: while (it.hasNext()) {
501: Object o = it.next();
502: if (set.add(o)) {
503: ulist.add(o);
504: }
505: }
506: return ulist;
507: }
508:
509: private static Class getClass(String className) {
510: try {
511: return ClassUtil.forName(className);
512: } catch (Exception e) {
513: if (logger.isDebugEnabled()) {
514: logger.debug("Couldn't load class " + className, e);
515: }
516: return null;
517: }
518: }
519:
520: private static Namespaces.Factory getNamespacesFactory() {
521: Namespaces.Factory factory = getNamespacesFactory("JaxenNamespaces");
522: if (factory == null) {
523: factory = getNamespacesFactory("Namespaces");
524: }
525: return factory;
526: }
527:
528: private static Namespaces.Factory getNamespacesFactory(String clazz) {
529: try {
530: return (Namespaces.Factory) ClassUtil.forName(
531: "freemarker.ext.xml." + clazz).getDeclaredField(
532: "FACTORY").get(null);
533: } catch (Throwable t) {
534: if (logger.isDebugEnabled()) {
535: logger.debug("Could not load " + clazz, t);
536: }
537: return null;
538: }
539: }
540:
541: private static Navigator getNavigator(String navType) {
542: try {
543: Navigator nav = (Navigator) ClassUtil.forName(
544: "freemarker.ext.xml." + navType + "Navigator")
545: .getDeclaredConstructor(new Class[] {})
546: .newInstance(new Object[] {});
547: return nav;
548: } catch (Throwable t) {
549: if (logger.isDebugEnabled()) {
550: logger.debug("Could not load navigator for " + navType,
551: t);
552: }
553: return null;
554: }
555: }
556:
557: public TemplateSequenceModel getChildNodes()
558: throws TemplateModelException {
559: return (TemplateSequenceModel) get("_content");
560: }
561:
562: public String getNodeName() throws TemplateModelException {
563: return getUniqueText((NodeListModel) get("_name"), "name");
564: }
565:
566: public String getNodeNamespace() throws TemplateModelException {
567: return getUniqueText((NodeListModel) get("_nsuri"), "namespace");
568: }
569:
570: public String getNodeType() throws TemplateModelException {
571: return getUniqueText((NodeListModel) get("_type"), "type");
572: }
573:
574: public TemplateNodeModel getParentNode()
575: throws TemplateModelException {
576: return (TemplateNodeModel) get("_parent");
577: }
578:
579: private String getUniqueText(NodeListModel model, String property)
580: throws TemplateModelException {
581: String s1 = null;
582: Set s = null;
583: for (Iterator it = model.nodes.iterator(); it.hasNext();) {
584: String s2 = (String) it.next();
585: if (s2 != null) {
586: // No text yet, make this text the current text
587: if (s1 == null) {
588: s1 = s2;
589: }
590: // else if there's already a text and they differ, start
591: // accumulating them for an error message
592: else if (!s1.equals(s2)) {
593: if (s == null) {
594: s = new HashSet();
595: s.add(s1);
596: }
597: s.add(s2);
598: }
599: }
600: }
601: // If the set for the error messages is empty, return the retval
602: if (s == null) {
603: return s1;
604: }
605: // Else throw an exception signaling ambiguity
606: throw new TemplateModelException("Value for node " + property
607: + " is ambiguos: " + s);
608: }
609: }
|