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