View Javadoc

1   package org.apache.velocity.tools.generic;
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.StringWriter;
23  import java.net.URL;
24  import java.util.ArrayList;
25  import java.util.Collections;
26  import java.util.HashMap;
27  import java.util.Iterator;
28  import java.util.List;
29  import java.util.Map;
30  import org.dom4j.Attribute;
31  import org.dom4j.Node;
32  import org.dom4j.Element;
33  import org.dom4j.Document;
34  import org.dom4j.DocumentHelper;
35  import org.dom4j.io.XMLWriter;
36  import org.dom4j.io.SAXReader;
37  import org.apache.velocity.runtime.log.Log;
38  import org.apache.velocity.tools.ConversionUtils;
39  import org.apache.velocity.tools.ToolContext;
40  import org.apache.velocity.tools.config.DefaultKey;
41  
42  /**
43   * <p>Tool for reading/navigating XML files.  This uses dom4j under the
44   * covers to provide complete XPath support for traversing XML files.</p>
45   * <p>Here's a short example:<pre>
46   * XML file:
47   *   &lt;foo&gt;&lt;bar&gt;woogie&lt;/bar&gt;&lt;a name="test"/&gt;&lt;/foo&gt;
48   *
49   * Template:
50   *   $foo.bar.text
51   *   $foo.find('a')
52   *   $foo.a.name
53   *
54   * Output:
55   *   woogie
56   *   &lt;a name="test"/&gt;
57   *   test
58   *
59   * Configuration:
60   * &lt;tools&gt;
61   *   &lt;toolbox scope="application"&gt;
62   *     &lt;tool class="org.apache.velocity.tools.generic.XmlTool"
63   *              key="foo" file="doc.xml"/&gt;
64   *   &lt;/toolbox&gt;
65   * &lt;/tools&gt;
66   * </pre></p>
67   * <p>Note that this tool is included in the default GenericTools configuration
68   * under the key "xml", but unless you set safeMode="false" for it, you will
69   * only be able to parse XML strings.  Safe mode is on by default and blocks
70   * access to the {@link #read(Object)} method.</p>
71   *
72   * @author Nathan Bubna
73   * @version $Revision: 749731 $ $Date: 2006-11-27 10:49:37 -0800 (Mon, 27 Nov 2006) $
74   * @since VelocityTools 2.0
75   */
76  @DefaultKey("xml")
77  public class XmlTool extends SafeConfig
78  {
79      public static final String FILE_KEY = "file";
80  
81      protected Log LOG;
82  
83      private List<Node> nodes;
84  
85      public XmlTool() {}
86  
87      public XmlTool(Node node)
88      {
89          this(Collections.singletonList(node));
90      }
91  
92      public XmlTool(List<Node> nodes)
93      {
94          this.nodes = nodes;
95      }
96  
97  
98      /**
99       * Looks for the "file" parameter and automatically uses
100      * {@link #read(String)} to parse the file and set the
101      * resulting {@link Document} as the root node for this
102      * instance.
103      */
104     protected void configure(ValueParser parser)
105     {
106         this.LOG = (Log)parser.getValue(ToolContext.LOG_KEY);
107 
108         String file = parser.getString(FILE_KEY);
109         if (file != null)
110         {
111             try
112             {
113                 read(file);
114             }
115             catch (IllegalArgumentException iae)
116             {
117                 throw iae;
118             }
119             catch (Exception e)
120             {
121                 throw new RuntimeException("Could not read XML file at: "+file, e);
122             }
123         }
124     }
125 
126     /**
127      * Sets a singular root {@link Node} for this instance.
128      */
129     protected void setRoot(Node node)
130     {
131         if (node instanceof Document)
132         {
133             node = ((Document)node).getRootElement();
134         }
135         this.nodes = new ArrayList<Node>(1);
136         this.nodes.add(node);
137     }
138 
139     private void log(Object o, Throwable t)
140     {
141         if (LOG != null)
142         {
143             LOG.debug("XmlTool - "+o, t);
144         }
145     }
146 
147     /**
148      * Creates a {@link URL} from the string and passes it to {@link #read(URL)}.
149      */
150     protected void read(String file) throws Exception
151     {
152         URL url = ConversionUtils.toURL(file, this);
153         if (url == null)
154         {
155             throw new IllegalArgumentException("Could not find file, classpath resource or standard URL for '"+file+"'.");
156         }
157         read(url);
158     }
159 
160     /**
161      * Reads, parses and creates a {@link Document} from the
162      * given {@link URL} and uses it as the root {@link Node} for this instance.
163      */
164     protected void read(URL url) throws Exception
165     {
166         SAXReader reader = new SAXReader();
167         setRoot(reader.read(url));
168     }
169 
170     /**
171      * Parses the given XML string and uses the resulting {@link Document}
172      * as the root {@link Node}.
173      */
174     protected void parse(String xml) throws Exception
175     {
176         setRoot(DocumentHelper.parseText(xml));
177     }
178 
179 
180     /**
181      * If safe mode is explicitly turned off for this tool, then
182      * this will accept either a {@link URL} or the string representation
183      * thereof.  If valid, it will return a new {@link XmlTool} instance
184      * with that document as the root {@link Node}.  If reading the URL
185      * or parsing its content fails or if safe mode is on (the default),
186      * this will return {@code null}.
187      */
188     public XmlTool read(Object o)
189     {
190         if (isSafeMode() || o == null)
191         {
192             return null;
193         }
194         try
195         {
196             XmlTool xml = new XmlTool();
197             if (o instanceof URL)
198             {
199                 xml.read((URL)o);
200             }
201             else
202             {
203                 String file = String.valueOf(o);
204                 xml.read(file);
205             }
206             return xml;
207         }
208         catch (Exception e)
209         {
210             log("Failed to read XML from : "+o, e);
211             return null;
212         }
213     }
214 
215     /**
216      * This accepts XML in form.  If the XML is valid, it will return a
217      * new {@link XmlTool} instance with the resulting XML document
218      * as the root {@link Node}.  If parsing the content fails,
219      * this will return {@code null}.
220      */
221     public XmlTool parse(Object o)
222     {
223         if (o == null)
224         {
225             return null;
226         }
227         String s = String.valueOf(o);
228         try
229         {
230             XmlTool xml = new XmlTool();
231             xml.parse(s);
232             return xml;
233         }
234         catch (Exception e)
235         {
236             log("Failed to parse XML from : "+o, e);
237             return null;
238         }
239     }
240 
241 
242     /**
243      * This will first attempt to find an attribute with the
244      * specified name and return its value.  If no such attribute
245      * exists or its value is {@code null}, this will attempt to convert
246      * the given value to a {@link Number} and get the result of
247      * {@link #get(Number)}.  If the number conversion fails,
248      * then this will convert the object to a string. If that string
249      * does not contain a '/', it appends the result of {@link #getPath()}
250      * and a '/' to the front of it.  Finally, it delegates the string to the
251      * {@link #find(String)} method and returns the result of that.
252      */
253     public Object get(Object o)
254     {
255         if (isEmpty() || o == null)
256         {
257             return null;
258         }
259         String attr = attr(o);
260         if (attr != null)
261         {
262             return attr;
263         }
264         Number i = ConversionUtils.toNumber(o);
265         if (i != null)
266         {
267             return get(i);
268         }
269         String s = String.valueOf(o);
270         if (s.length() == 0)
271         {
272             return null;
273         }
274         if (s.indexOf('/') < 0)
275         {
276             s = getPath()+'/'+s;
277         }
278         return find(s);
279     }
280 
281 
282     /**
283      * Asks {@link #get(Object)} for a "name" result.
284      * If none, this will return the result of {@link #getNodeName()}.
285      */
286     public Object getName()
287     {
288         // give attributes and child elements priority
289         Object name = get("name");
290         if (name != null)
291         {
292             return name;
293         }
294         return getNodeName();
295     }
296 
297     /**
298      * Returns the name of the root node. If the internal {@link Node}
299      * list has more than one {@link Node}, it will only return the name
300      * of the first node in the list.
301      */
302     public String getNodeName()
303     {
304         if (isEmpty())
305         {
306             return null;
307         }
308         return node().getName();
309     }
310 
311     /**
312      * Returns the XPath that identifies the first/sole {@link Node}
313      * represented by this instance.
314      */
315     public String getPath()
316     {
317         if (isEmpty())
318         {
319             return null;
320         }
321         return node().getPath();
322     }
323 
324     /**
325      * Returns the value of the specified attribute for the first/sole
326      * {@link Node} in the internal Node list for this instance, if that
327      * Node is an {@link Element}.  If it is a non-Element node type or
328      * there is no value for that attribute in this element, then this
329      * will return {@code null}.
330      */
331     public String attr(Object o)
332     {
333         if (o == null)
334         {
335             return null;
336         }
337         String key = String.valueOf(o);
338         Node node = node();
339         if (node instanceof Element)
340         {
341             return ((Element)node).attributeValue(key);
342         }
343         return null;
344     }
345 
346     /**
347      * Returns a {@link Map} of all attributes for the first/sole
348      * {@link Node} held internally by this instance.  If that Node is
349      * not an {@link Element}, this will return null.
350      */
351     public Map<String,String> attributes()
352     {
353         Node node = node();
354         if (node instanceof Element)
355         {
356             Map<String,String> attrs = new HashMap<String,String>();
357             for (Iterator i = ((Element)node).attributeIterator(); i.hasNext();)
358             {
359                 Attribute a = (Attribute)i.next();
360                 attrs.put(a.getName(), a.getValue());
361             }
362             return attrs;
363         }
364         return null;
365     }
366 
367 
368     /**
369      * Returns {@code true} if there are no {@link Node}s internally held
370      * by this instance.
371      */
372     public boolean isEmpty()
373     {
374         return (nodes == null || nodes.isEmpty());
375     }
376 
377     /**
378      * Returns the number of {@link Node}s internally held by this instance.
379      */
380     public int size()
381     {
382         if (isEmpty())
383         {
384             return 0;
385         }
386         return nodes.size();
387     }
388 
389     /**
390      * Returns an {@link Iterator} that returns new {@link XmlTool}
391      * instances for each {@link Node} held internally by this instance.
392      */
393     public Iterator<XmlTool> iterator()
394     {
395         if (isEmpty())
396         {
397             return null;
398         }
399         return new NodeIterator(nodes.iterator());
400     }
401 
402     /**
403      * Returns an {@link XmlTool} that wraps only the
404      * first {@link Node} from this instance's internal Node list.
405      */
406     public XmlTool getFirst()
407     {
408         if (size() == 1)
409         {
410             return this;
411         }
412         return new XmlTool(node());
413     }
414 
415     /**
416      * Returns an {@link XmlTool} that wraps only the
417      * last {@link Node} from this instance's internal Node list.
418      */
419     public XmlTool getLast()
420     {
421         if (size() == 1)
422         {
423             return this;
424         }
425         return new XmlTool(nodes.get(size() - 1));
426     }
427 
428     /**
429      * Returns an {@link XmlTool} that wraps the specified
430      * {@link Node} from this instance's internal Node list.
431      */
432     public XmlTool get(Number n)
433     {
434         if (n == null)
435         {
436             return null;
437         }
438         int i = n.intValue();
439         if (i < 0 || i > size() - 1)
440         {
441             return null;
442         }
443         return new XmlTool(nodes.get(i));
444     }
445 
446     /**
447      * Returns the first/sole {@link Node} from this
448      * instance's internal Node list, if any.
449      */
450     public Node node()
451     {
452         if (isEmpty())
453         {
454             return null;
455         }
456         return nodes.get(0);
457     }
458 
459 
460     /**
461      * Converts the specified object to a String and calls
462      * {@link #find(String)} with that.
463      */
464     public XmlTool find(Object o)
465     {
466         if (o == null || isEmpty())
467         {
468             return null;
469         }
470         return find(String.valueOf(o));
471     }
472 
473     /**
474      * Performs an XPath selection on the current set of
475      * {@link Node}s held by this instance and returns a new
476      * {@link XmlTool} instance that wraps those results.
477      * If the specified value is null or this instance does
478      * not currently hold any nodes, then this will return 
479      * {@code null}.  If the specified value, when converted
480      * to a string, does not contain a '/' character, then
481      * it has "//" prepended to it.  This means that a call to
482      * {@code $xml.find("a")} is equivalent to calling
483      * {@code $xml.find("//a")}.  The full range of XPath
484      * selectors is supported here.
485      */
486     public XmlTool find(String xpath)
487     {
488         if (xpath == null || xpath.length() == 0)
489         {
490             return null;
491         }
492         if (xpath.indexOf('/') < 0)
493         {
494             xpath = "//"+xpath;
495         }
496         List<Node> found = new ArrayList<Node>();
497         for (Node n : nodes)
498         {
499             found.addAll((List<Node>)n.selectNodes(xpath));
500         }
501         if (found.isEmpty())
502         {
503             return null;
504         }
505         return new XmlTool(found);
506     }
507 
508     /**
509      * Returns a new {@link XmlTool} instance that wraps
510      * the parent {@link Element} of the first/sole {@link Node}
511      * being wrapped by this instance.
512      */
513     public XmlTool getParent()
514     {
515         if (isEmpty())
516         {
517             return null;
518         }
519         Element parent = node().getParent();
520         if (parent == null)
521         {
522             return null;
523         }
524         return new XmlTool(parent);
525     }
526 
527     /**
528      * Returns a new {@link XmlTool} instance that wraps
529      * the parent {@link Element}s of each of the {@link Node}s
530      * being wrapped by this instance.  This does not return
531      * all ancestors, just the immediate parents.
532      */
533     public XmlTool parents()
534     {
535         if (isEmpty())
536         {
537             return null;
538         }
539         if (size() == 1)
540         {
541             return getParent();
542         }
543         List<Node> parents = new ArrayList<Node>(size());
544         for (Node n : nodes)
545         {
546             Element parent = n.getParent();
547             if (parent != null && !parents.contains(parent))
548             {
549                 parents.add(parent);
550             }
551         }
552         if (parents.isEmpty())
553         {
554             return null;
555         }
556         return new XmlTool(parents);
557     }
558 
559     /**
560      * Returns a new {@link XmlTool} instance that wraps all the
561      * child {@link Element}s of all the current internally held nodes
562      * that are {@link Element}s themselves.
563      */
564     public XmlTool children()
565     {
566         if (isEmpty())
567         {
568             return null;
569         }
570         List<Node> kids = new ArrayList<Node>();
571         for (Node n : nodes)
572         {
573             if (n instanceof Element)
574             {
575                 kids.addAll((List<Node>)((Element)n).elements());
576             }
577         }
578         return new XmlTool(kids);
579     }
580 
581     /**
582      * Returns the concatenated text content of all the internally held
583      * nodes.  Obviously, this is most useful when only one node is held.
584      */
585     public String getText()
586     {
587         if (isEmpty())
588         {
589             return null;
590         }
591         StringBuilder out = new StringBuilder();
592         for (Node n : nodes)
593         {
594             String text = n.getText();
595             if (text != null)
596             {
597                 out.append(text);
598             }
599         }
600         String result = out.toString().trim();
601         if (result.length() > 0)
602         {
603             return result;
604         }
605         return null;
606     }
607 
608 
609     /**
610      * If this instance has no XML {@link Node}s, then this
611      * returns the result of {@code super.toString()}.  Otherwise, it
612      * returns the XML (as a string) of all the internally held nodes
613      * that are not {@link Attribute}s. For attributes, only the value
614      * is used.
615      */
616     public String toString()
617     {
618         if (isEmpty())
619         {
620             return super.toString();
621         }
622         StringBuilder out = new StringBuilder();
623         for (Node n : nodes)
624         {
625             if (n instanceof Attribute)
626             {
627                 out.append(n.getText().trim());
628             }
629             else
630             {
631                 out.append(n.asXML());
632             }
633         }
634         return out.toString();
635     }
636 
637     
638     /**
639      * Iterator implementation that wraps a Node list iterator
640      * to return new XmlTool instances for each item in the wrapped
641      * iterator.s
642      */
643     public static class NodeIterator implements Iterator<XmlTool>
644     {
645         private Iterator<Node> i;
646 
647         public NodeIterator(Iterator<Node> i)
648         {
649             this.i = i;
650         }
651 
652         public boolean hasNext()
653         {
654             return i.hasNext();
655         }
656 
657         public XmlTool next()
658         {
659             return new XmlTool(i.next());
660         }
661 
662         public void remove()
663         {
664             i.remove();
665         }
666     }
667 }