1 package org.apache.velocity.anakia;
2
3 /*
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
11 *
12 * http://www.apache.org/licenses/LICENSE-2.0
13 *
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
19 * under the License.
20 */
21
22 import java.io.IOException;
23 import java.io.StringWriter;
24 import java.io.Writer;
25 import java.util.ArrayList;
26 import java.util.Collection;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.ListIterator;
30
31 import org.jdom.Attribute;
32 import org.jdom.CDATA;
33 import org.jdom.Comment;
34 import org.jdom.DocType;
35 import org.jdom.Document;
36 import org.jdom.Element;
37 import org.jdom.EntityRef;
38 import org.jdom.ProcessingInstruction;
39 import org.jdom.Text;
40 import org.jdom.output.XMLOutputter;
41
42 /**
43 * Provides a class for wrapping a list of JDOM objects primarily for use in template
44 * engines and other kinds of text transformation tools.
45 * It has a {@link #toString()} method that will output the XML serialized form of the
46 * nodes it contains - again focusing on template engine usage, as well as the
47 * {@link #selectNodes(String)} method that helps selecting a different set of nodes
48 * starting from the nodes in this list. The class also implements the {@link java.util.List}
49 * interface by simply delegating calls to the contained list (the {@link #subList(int, int)}
50 * method is implemented by delegating to the contained list and wrapping the returned
51 * sublist into a <code>NodeList</code>).
52 *
53 * @author <a href="mailto:szegedia@freemail.hu">Attila Szegedi</a>
54 * @version $Id: NodeList.java 463298 2006-10-12 16:10:32Z henning $
55 */
56 public class NodeList implements List, Cloneable
57 {
58 private static final AttributeXMLOutputter DEFAULT_OUTPUTTER =
59 new AttributeXMLOutputter();
60
61 /** The contained nodes */
62 private List nodes;
63
64 /**
65 * Creates an empty node list.
66 */
67 public NodeList()
68 {
69 nodes = new ArrayList();
70 }
71
72 /**
73 * Creates a node list that holds a single {@link Document} node.
74 * @param document
75 */
76 public NodeList(Document document)
77 {
78 this((Object)document);
79 }
80
81 /**
82 * Creates a node list that holds a single {@link Element} node.
83 * @param element
84 */
85 public NodeList(Element element)
86 {
87 this((Object)element);
88 }
89
90 private NodeList(Object object)
91 {
92 if(object == null)
93 {
94 throw new IllegalArgumentException(
95 "Cannot construct NodeList with null.");
96 }
97 nodes = new ArrayList(1);
98 nodes.add(object);
99 }
100
101 /**
102 * Creates a node list that holds a list of nodes.
103 * @param nodes the list of nodes this template should hold. The created
104 * template will copy the passed nodes list, so changes to the passed list
105 * will not affect the model.
106 */
107 public NodeList(List nodes)
108 {
109 this(nodes, true);
110 }
111
112 /**
113 * Creates a node list that holds a list of nodes.
114 * @param nodes the list of nodes this template should hold.
115 * @param copy if true, the created template will copy the passed nodes
116 * list, so changes to the passed list will not affect the model. If false,
117 * the model will reference the passed list and will sense changes in it,
118 * altough no operations on the list will be synchronized.
119 */
120 public NodeList(List nodes, boolean copy)
121 {
122 if(nodes == null)
123 {
124 throw new IllegalArgumentException(
125 "Cannot initialize NodeList with null list");
126 }
127 this.nodes = copy ? new ArrayList(nodes) : nodes;
128 }
129
130 /**
131 * Retrieves the underlying list used to store the nodes. Note however, that
132 * you can fully use the underlying list through the <code>List</code> interface
133 * of this class itself. You would probably access the underlying list only for
134 * synchronization purposes.
135 * @return The internal node List.
136 */
137 public List getList()
138 {
139 return nodes;
140 }
141
142 /**
143 * This method returns the string resulting from concatenation of string
144 * representations of its nodes. Each node is rendered using its XML
145 * serialization format. This greatly simplifies creating XML-transformation
146 * templates, as to output a node contained in variable x as XML fragment,
147 * you simply write ${x} in the template (or whatever your template engine
148 * uses as its expression syntax).
149 * @return The Nodelist as printable object.
150 */
151 public String toString()
152 {
153 if(nodes.isEmpty())
154 {
155 return "";
156 }
157
158 StringWriter sw = new StringWriter(nodes.size() * 128);
159 try
160 {
161 for(Iterator i = nodes.iterator(); i.hasNext();)
162 {
163 Object node = i.next();
164 if(node instanceof Element)
165 {
166 DEFAULT_OUTPUTTER.output((Element)node, sw);
167 }
168 else if(node instanceof Attribute)
169 {
170 DEFAULT_OUTPUTTER.output((Attribute)node, sw);
171 }
172 else if(node instanceof Text)
173 {
174 DEFAULT_OUTPUTTER.output((Text)node, sw);
175 }
176 else if(node instanceof Document)
177 {
178 DEFAULT_OUTPUTTER.output((Document)node, sw);
179 }
180 else if(node instanceof ProcessingInstruction)
181 {
182 DEFAULT_OUTPUTTER.output((ProcessingInstruction)node, sw);
183 }
184 else if(node instanceof Comment)
185 {
186 DEFAULT_OUTPUTTER.output((Comment)node, sw);
187 }
188 else if(node instanceof CDATA)
189 {
190 DEFAULT_OUTPUTTER.output((CDATA)node, sw);
191 }
192 else if(node instanceof DocType)
193 {
194 DEFAULT_OUTPUTTER.output((DocType)node, sw);
195 }
196 else if(node instanceof EntityRef)
197 {
198 DEFAULT_OUTPUTTER.output((EntityRef)node, sw);
199 }
200 else
201 {
202 throw new IllegalArgumentException(
203 "Cannot process a " +
204 (node == null
205 ? "null node"
206 : "node of class " + node.getClass().getName()));
207 }
208 }
209 }
210 catch(IOException e)
211 {
212 // Cannot happen as we work with a StringWriter in memory
213 throw new Error();
214 }
215 return sw.toString();
216 }
217
218 /**
219 * Returns a NodeList that contains the same nodes as this node list.
220 * @return A clone of this list.
221 * @throws CloneNotSupportedException if the contained list's class does
222 * not have an accessible no-arg constructor.
223 */
224 public Object clone()
225 throws CloneNotSupportedException
226 {
227 NodeList clonedList = (NodeList)super.clone();
228 clonedList.cloneNodes();
229 return clonedList;
230 }
231
232 private void cloneNodes()
233 throws CloneNotSupportedException
234 {
235 Class listClass = nodes.getClass();
236 try
237 {
238 List clonedNodes = (List)listClass.newInstance();
239 clonedNodes.addAll(nodes);
240 nodes = clonedNodes;
241 }
242 catch(IllegalAccessException e)
243 {
244 throw new CloneNotSupportedException("Cannot clone NodeList since"
245 + " there is no accessible no-arg constructor on class "
246 + listClass.getName());
247 }
248 catch(InstantiationException e)
249 {
250 // Cannot happen as listClass represents a concrete, non-primitive,
251 // non-array, non-void class - there's an instance of it in "nodes"
252 // which proves these assumptions.
253 throw new Error();
254 }
255 }
256
257 /**
258 * Returns the hash code of the contained list.
259 * @return The hashcode of the list.
260 */
261 public int hashCode()
262 {
263 return nodes.hashCode();
264 }
265
266 /**
267 * Tests for equality with another object.
268 * @param o the object to test for equality
269 * @return true if the other object is also a NodeList and their contained
270 * {@link List} objects evaluate as equals.
271 */
272 public boolean equals(Object o)
273 {
274 return o instanceof NodeList
275 ? ((NodeList)o).nodes.equals(nodes)
276 : false;
277 }
278
279 /**
280 * Applies an XPath expression to the node list and returns the resulting
281 * node list. In order for this method to work, your application must have
282 * access to <a href="http://code.werken.com">werken.xpath</a> library
283 * classes. The implementation does cache the parsed format of XPath
284 * expressions in a weak hash map, keyed by the string representation of
285 * the XPath expression. As the string object passed as the argument is
286 * usually kept in the parsed template, this ensures that each XPath
287 * expression is parsed only once during the lifetime of the template that
288 * first invoked it.
289 * @param xpathString the XPath expression you wish to apply
290 * @return a NodeList representing the nodes that are the result of
291 * application of the XPath to the current node list. It can be empty.
292 */
293 public NodeList selectNodes(String xpathString)
294 {
295 return new NodeList(XPathCache.getXPath(xpathString).applyTo(nodes), false);
296 }
297
298 // List methods implemented hereafter
299
300 /**
301 * @see java.util.List#add(java.lang.Object)
302 */
303 public boolean add(Object o)
304 {
305 return nodes.add(o);
306 }
307
308 /**
309 * @see java.util.List#add(int, java.lang.Object)
310 */
311 public void add(int index, Object o)
312 {
313 nodes.add(index, o);
314 }
315
316 /**
317 * @see java.util.List#addAll(java.util.Collection)
318 */
319 public boolean addAll(Collection c)
320 {
321 return nodes.addAll(c);
322 }
323
324 /**
325 * @see java.util.List#addAll(int, java.util.Collection)
326 */
327 public boolean addAll(int index, Collection c)
328 {
329 return nodes.addAll(index, c);
330 }
331
332 /**
333 * @see java.util.List#clear()
334 */
335 public void clear()
336 {
337 nodes.clear();
338 }
339
340 /**
341 * @see java.util.List#contains(java.lang.Object)
342 */
343 public boolean contains(Object o)
344 {
345 return nodes.contains(o);
346 }
347
348 /**
349 * @see java.util.List#containsAll(java.util.Collection)
350 */
351 public boolean containsAll(Collection c)
352 {
353 return nodes.containsAll(c);
354 }
355
356 /**
357 * @see java.util.List#get(int)
358 */
359 public Object get(int index)
360 {
361 return nodes.get(index);
362 }
363
364 /**
365 * @see java.util.List#indexOf(java.lang.Object)
366 */
367 public int indexOf(Object o)
368 {
369 return nodes.indexOf(o);
370 }
371
372 /**
373 * @see java.util.List#isEmpty()
374 */
375 public boolean isEmpty()
376 {
377 return nodes.isEmpty();
378 }
379
380 /**
381 * @see java.util.List#iterator()
382 */
383 public Iterator iterator()
384 {
385 return nodes.iterator();
386 }
387
388 /**
389 * @see java.util.List#lastIndexOf(java.lang.Object)
390 */
391 public int lastIndexOf(Object o)
392 {
393 return nodes.lastIndexOf(o);
394 }
395
396 /**
397 * @see java.util.List#listIterator()
398 */
399 public ListIterator listIterator()
400 {
401 return nodes.listIterator();
402 }
403
404 /**
405 * @see java.util.List#listIterator(int)
406 */
407 public ListIterator listIterator(int index)
408 {
409 return nodes.listIterator(index);
410 }
411
412 /**
413 * @see java.util.List#remove(int)
414 */
415 public Object remove(int index)
416 {
417 return nodes.remove(index);
418 }
419
420 /**
421 * @see java.util.List#remove(java.lang.Object)
422 */
423 public boolean remove(Object o)
424 {
425 return nodes.remove(o);
426 }
427
428 /**
429 * @see java.util.List#removeAll(java.util.Collection)
430 */
431 public boolean removeAll(Collection c)
432 {
433 return nodes.removeAll(c);
434 }
435
436 /**
437 * @see java.util.List#retainAll(java.util.Collection)
438 */
439 public boolean retainAll(Collection c)
440 {
441 return nodes.retainAll(c);
442 }
443
444 /**
445 * @see java.util.List#set(int, java.lang.Object)
446 */
447 public Object set(int index, Object o)
448 {
449 return nodes.set(index, o);
450 }
451
452 /**
453 * @see java.util.List#size()
454 */
455 public int size()
456 {
457 return nodes.size();
458 }
459
460 /**
461 * @see java.util.List#subList(int, int)
462 */
463 public List subList(int fromIndex, int toIndex)
464 {
465 return new NodeList(nodes.subList(fromIndex, toIndex));
466 }
467
468 /**
469 * @see java.util.List#toArray()
470 */
471 public Object[] toArray()
472 {
473 return nodes.toArray();
474 }
475
476 /**
477 * @see java.util.List#toArray(java.lang.Object[])
478 */
479 public Object[] toArray(Object[] a)
480 {
481 return nodes.toArray(a);
482 }
483
484 /**
485 * A special subclass of XMLOutputter that will be used to output
486 * Attribute nodes. As a subclass of XMLOutputter it can use its protected
487 * method escapeAttributeEntities() to serialize the attribute
488 * appropriately.
489 */
490 private static final class AttributeXMLOutputter extends XMLOutputter
491 {
492 /**
493 * @param attribute
494 * @param out
495 * @throws IOException
496 */
497 public void output(Attribute attribute, Writer out)
498 throws IOException
499 {
500 out.write(" ");
501 out.write(attribute.getQualifiedName());
502 out.write("=");
503
504 out.write("\"");
505 out.write(escapeAttributeEntities(attribute.getValue()));
506 out.write("\"");
507 }
508 }
509 }