You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@velocity.apache.org by nb...@apache.org on 2008/07/10 01:55:50 UTC

svn commit: r675383 - in /velocity/tools/trunk: ./ examples/showcase/ examples/showcase/WEB-INF/ examples/showcase/WEB-INF/src/ src/main/java/org/apache/velocity/tools/generic/ src/test/java/ src/test/java/org/apache/velocity/tools/

Author: nbubna
Date: Wed Jul  9 16:55:50 2008
New Revision: 675383

URL: http://svn.apache.org/viewvc?rev=675383&view=rev
Log:
VELTOOLS-99 - create an XmlTool (inspired by Philippe Collignon's XmlGen in API and DVSL's Dom4jNodeImpl in implementation)

Added:
    velocity/tools/trunk/examples/showcase/WEB-INF/src/file.xml   (with props)
    velocity/tools/trunk/examples/showcase/xml.vm   (with props)
    velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/XmlTool.java   (with props)
    velocity/tools/trunk/src/test/java/file.xml   (with props)
    velocity/tools/trunk/src/test/java/org/apache/velocity/tools/XmlToolTests.java   (with props)
Modified:
    velocity/tools/trunk/build.properties
    velocity/tools/trunk/download.xml
    velocity/tools/trunk/examples/showcase/WEB-INF/src/resources.properties
    velocity/tools/trunk/examples/showcase/WEB-INF/tools.xml
    velocity/tools/trunk/examples/showcase/toolmenu.vm
    velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/tools.xml
    velocity/tools/trunk/test.xml

Modified: velocity/tools/trunk/build.properties
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/build.properties?rev=675383&r1=675382&r2=675383&view=diff
==============================================================================
--- velocity/tools/trunk/build.properties (original)
+++ velocity/tools/trunk/build.properties Wed Jul  9 16:55:50 2008
@@ -169,6 +169,7 @@
 commons-lang.jar=${lib.dir}/commons-lang-${jar.commons-lang.version}.jar
 commons-logging.jar=${lib.dir}/commons-logging-${jar.commons-logging.version}.jar
 commons-validator.jar=${lib.dir}/commons-validator-${jar.commons-validator.version}.jar
+dom4j.jar=${lib.dir}/dom4j-${jar.dom4j.version}.jar
 servlet.jar=${lib.dir}/servletapi-${jar.servletapi.version}.jar
 struts-core.jar=${lib.dir}/struts-core-${jar.struts-core.version}.jar
 struts-taglib.jar=${lib.dir}/struts-taglib-${jar.struts-taglib.version}.jar

Modified: velocity/tools/trunk/download.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/download.xml?rev=675383&r1=675382&r2=675383&view=diff
==============================================================================
--- velocity/tools/trunk/download.xml (original)
+++ velocity/tools/trunk/download.xml Wed Jul  9 16:55:50 2008
@@ -49,6 +49,7 @@
     <antcall target="commons-lang-download" />
     <antcall target="commons-logging-download" />
     <antcall target="commons-validator-download" />
+    <antcall target="dom4j-download" />
     <antcall target="oro-download" />
     <antcall target="servletapi-download" />
     <antcall target="sslext-download" />
@@ -60,7 +61,6 @@
   <target name="docs-download"
           depends="base-download"
           description="Download dependencies needed to render VelocityTools documentation from the central repository">
-    <antcall target="dom4j-download" />
     <antcall target="velocity-dvsl-download" />
   </target>
 

Added: velocity/tools/trunk/examples/showcase/WEB-INF/src/file.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/examples/showcase/WEB-INF/src/file.xml?rev=675383&view=auto
==============================================================================
--- velocity/tools/trunk/examples/showcase/WEB-INF/src/file.xml (added)
+++ velocity/tools/trunk/examples/showcase/WEB-INF/src/file.xml Wed Jul  9 16:55:50 2008
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<foo x="true" this="that">
+  <a name="test" baz="true"/>
+  <bar>woogie</bar>
+  <bar>wiggie</bar>
+</foo>
\ No newline at end of file

Propchange: velocity/tools/trunk/examples/showcase/WEB-INF/src/file.xml
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: velocity/tools/trunk/examples/showcase/WEB-INF/src/file.xml
------------------------------------------------------------------------------
    svn:executable = *

Propchange: velocity/tools/trunk/examples/showcase/WEB-INF/src/file.xml
------------------------------------------------------------------------------
    svn:mime-type = text/xml

Modified: velocity/tools/trunk/examples/showcase/WEB-INF/src/resources.properties
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/examples/showcase/WEB-INF/src/resources.properties?rev=675383&r1=675382&r2=675383&view=diff
==============================================================================
--- velocity/tools/trunk/examples/showcase/WEB-INF/src/resources.properties (original)
+++ velocity/tools/trunk/examples/showcase/WEB-INF/src/resources.properties Wed Jul  9 16:55:50 2008
@@ -68,6 +68,7 @@
 tools.text = ResourceTool
 tools.search = SearchTool
 tools.sorter = SortTool
+tools.xml = XmlTool
 
 ## demo common resources
 demo.function = Function
@@ -204,3 +205,68 @@
 determine if you are on the first or last iteration, get the number of iterations \
 completed, automatically stop before or exclude particular elements and easily \
 do all the above even with complex, nested loops.
+
+# xml.vm resources
+xml.intro = Tool for reading/navigating XML files.  This uses dom4j under the \
+covers to provide complete XPath support for traversing XML files.  \
+Most of the methods below will do nothing, as the default tool does not \
+have any XML content to work with.  For demo's sake, there is a <code>file.xml</code> \
+in the classpath that is automatically loaded for you to work with, \
+and we have turned of safe mode, so you can do things like \
+<code>$xml.read('tools.xml')</code>. \
+Still, the full demo at the bottom will likely be the most useful for you.
+xml.read_Object.param1 = ''file.xml''
+xml.read_Object = Returns <code>null</code> if safe mode is on; otherwise \
+this will accept url pointing to an XML document.  It will then parse that document \
+and return a new instance with that document's root element as its node.
+xml.parse_Object.param1 = ''<test><this>that</this></test>''
+xml.parse_Object = Accepts XML strings.  If the XML is valid, it will return a \
+new XmlTool instance with the XML's root as its internal node.
+xml.get_Object.param1 = ''bar[2]''
+xml.get_Object = This will first attempt to find an attribute with the \
+specified name and return its value.  If no such attribute \
+exists or its value is <code>null</code>, this will attempt to convert \
+the given value to a Number and get the result of get(Number).  If the number conversion fails, \
+then this will convert the object to a string. If that string \
+does not start contain a '/', it appends the result of getPath() and a '/' to the front of it. \
+Finally, it delegates the string to the find(String) method and returns the result of that.
+xml.getName = Asks <code>get(Object)</code> for "name". If none, this will return the result of getNodeName().
+xml.getNodeName = Returns the name of the root node. If the internal Node \
+list has more than one Node, it will only return the name of the first node in the list.
+xml.attr_Object.param1 = ''x''
+xml.attr_Object = Returns the value of the specified attribute for the first/sole \
+Node in the internal Node list for this instance, if that Node is an Element.  If it is a non-Element node type or \
+there is no value for that attribute in this element, then this will return <code>null</code>.
+xml.attributes = Returns a Map of all attributes for the first/sole \
+Node held internally by this instance.  If that Node is not an Element, this will return null.
+xml.isEmpty = Returns <code>true</code> if there are no Nodes internally held by this instance. 
+xml.size = Returns the number of Nodes internally held by this instance. 
+xml.iterator = Returns an Iterator that returns new XmlTool instances for each Node held internally by this instance.
+xml.getFirst = Returns a new instance that wraps only the first Node from this instance's internal Node list.
+xml.getLast = Returns a new instance that wraps only the last Node from this instance's internal Node list.
+xml.get_Number.param1 = 0
+xml.get_Number = Returns a new instance that wraps the specified Node from this instance's internal Node list.
+xml.node = Returns the first/sole Node from this instance's internal Node list, if any.
+xml.find_Object.param1 = ''/*/a[@*]''
+xml.find_Object = Converts the specified object to a String and calls <code>find(String)</code> with that.
+xml.find_String.param1 = ''@baz''
+xml.find_String = Performs an XPath selection on the current set of \
+Nodes held by this instance and returns a new instance that wraps those results. \
+If the specified value is null or this instance does not currently hold any nodes, then this will return  \
+<code>null</code>.  If the specified value, when converted to a string, does not contain a '/' character, then \
+it has "//" prepended to it.  This means that a call to <code>$xml.find("a")</code> is equivalent to calling \
+<code>$xml.find("//a")</code>.  The full range of XPath selectors is supported here.
+xml.getParent = Returns a new XmlTool instance that wraps the parent Element of the first/sole Node \
+being wrapped by this instance.
+xml.getPath = Returns the XPath that identifies the first/sole Node represented by this instance.
+xml.parents = Returns a new XmlTool instance that wraps the parent Elements of each of the Nodes \
+being wrapped by this instance.  This does not return all ancestors, just the immediate parents.
+xml.children = Returns a new XmlTool instance that wraps all the \
+child Elements of all the current internally held nodes that are Elements themselves.
+xml.getText = Returns the concatenated text content of all the internally held \
+nodes.  Obviously, this is most useful when only one node is held.
+xml.toString = If this instance has no XML Nodes, then this returns the result of <code>super.toString()</code>.  Otherwise, \
+it returns the XML (as a string) of all the internally held nodes \
+that are not Attributes. For attributes, only the value is used.
+xml.custom = $xml.find(''*[@name]'').attributes()
+

Modified: velocity/tools/trunk/examples/showcase/WEB-INF/tools.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/examples/showcase/WEB-INF/tools.xml?rev=675383&r1=675382&r2=675383&view=diff
==============================================================================
--- velocity/tools/trunk/examples/showcase/WEB-INF/tools.xml (original)
+++ velocity/tools/trunk/examples/showcase/WEB-INF/tools.xml Wed Jul  9 16:55:50 2008
@@ -32,5 +32,6 @@
     <tool key="date" format="yyyy-MM-dd" depth="2" skip="month"/>
     <tool key="convert" dateFormat="yyyy-MM-dd"/>
     <tool key="number" format="#0.0"/>
+    <tool key="xml" file="file.xml" safeMode="false"/>
   </toolbox>
 </tools>

Modified: velocity/tools/trunk/examples/showcase/toolmenu.vm
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/examples/showcase/toolmenu.vm?rev=675383&r1=675382&r2=675383&view=diff
==============================================================================
--- velocity/tools/trunk/examples/showcase/toolmenu.vm (original)
+++ velocity/tools/trunk/examples/showcase/toolmenu.vm Wed Jul  9 16:55:50 2008
@@ -44,4 +44,5 @@
 #toolMenuItem( $llink 'text' )
 #toolMenuItem( $llink 'search' )
 #toolMenuItem( $llink 'sorter' )
+#toolMenuItem( $llink 'xml' )
 </ul>

Added: velocity/tools/trunk/examples/showcase/xml.vm
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/examples/showcase/xml.vm?rev=675383&view=auto
==============================================================================
--- velocity/tools/trunk/examples/showcase/xml.vm (added)
+++ velocity/tools/trunk/examples/showcase/xml.vm Wed Jul  9 16:55:50 2008
@@ -0,0 +1,36 @@
+## Licensed to the Apache Software Foundation (ASF) under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  The ASF licenses this file
+## to you 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.
+#title( 'XmlTool' )
+<p>
+$text.demo.thisPage.insert("#doclink( 'XmlTool' true )").
+$text.get('xml.intro')
+</p>
+
+#set( $toolname = 'xml' )
+#set( $toolclass = $xml.class )
+#set( $toollink = $doclink )
+#set( $toolDemo = 
+"Document: ${esc.d}esc.xml(${esc.d}xml)
+
+bar text: ${esc.d}xml.bar.text
+
+baz attr value: ${esc.d}xml.a.baz
+
+last bar xml: ${esc.d}esc.xml(${esc.d}xml.bar.last)"
+)
+
+#parse( 'demo.vm' )

Propchange: velocity/tools/trunk/examples/showcase/xml.vm
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: velocity/tools/trunk/examples/showcase/xml.vm
------------------------------------------------------------------------------
    svn:executable = *

Propchange: velocity/tools/trunk/examples/showcase/xml.vm
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Added: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/XmlTool.java
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/XmlTool.java?rev=675383&view=auto
==============================================================================
--- velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/XmlTool.java (added)
+++ velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/XmlTool.java Wed Jul  9 16:55:50 2008
@@ -0,0 +1,677 @@
+package org.apache.velocity.tools.generic;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import java.io.File;
+import java.io.StringWriter;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import org.dom4j.Attribute;
+import org.dom4j.Node;
+import org.dom4j.Element;
+import org.dom4j.Document;
+import org.dom4j.DocumentHelper;
+import org.dom4j.io.XMLWriter;
+import org.dom4j.io.SAXReader;
+import org.apache.velocity.runtime.log.Log;
+import org.apache.velocity.tools.ClassUtils;
+import org.apache.velocity.tools.ConversionUtils;
+import org.apache.velocity.tools.ToolContext;
+import org.apache.velocity.tools.config.DefaultKey;
+
+/**
+ * <p>Tool for reading/navigating XML files.  This uses dom4j under the
+ * covers to provide complete XPath support for traversing XML files.</p>
+ * <p>Here's a short example:<pre>
+ * XML file:
+ *   &lt;foo&gt;&lt;bar&gt;woogie&lt;/bar&gt;&lt;a name="test"/&gt;&lt;/foo&gt;
+ *
+ * Template:
+ *   $doc.foo.bar
+ *   $doc.find('a')
+ *   $doc.a.name
+ *
+ * Output:
+ *   woogie
+ *   &lt;a name="test"/&gt;
+ *   test
+ *
+ * Configuration:
+ * &lt;tools&gt;
+ *   &lt;toolbox scope="application"&gt;
+ *     &lt;tool class="org.apache.velocity.tools.generic.XmlTool"
+ *              key="doc" file="doc.xml"/&gt;
+ *   &lt;/toolbox&gt;
+ * &lt;/tools&gt;
+ * </pre></p>
+ * <p>Note that this tool is included in the default GenericTools configuration
+ * under the key "xml", but unless you set safeMode="false" for it, you will
+ * only be able to parse XML strings.  Safe mode is on by default and blocks
+ * access to the {@link #read(Object)} method.</p>
+ *
+ * @author Nathan Bubna
+ * @version $Revision$ $Date: 2006-11-27 10:49:37 -0800 (Mon, 27 Nov 2006) $
+ * @since VelocityTools 2.0
+ */
+@DefaultKey("xml")
+public class XmlTool extends SafeConfig
+{
+    public static final String FILE_KEY = "file";
+
+    protected Log LOG;
+
+    private List<Node> nodes;
+
+    public XmlTool() {}
+
+    public XmlTool(Node node)
+    {
+        this(Collections.singletonList(node));
+    }
+
+    public XmlTool(List<Node> nodes)
+    {
+        this.nodes = nodes;
+    }
+
+
+    /**
+     * Looks for the "file" parameter and automatically uses
+     * {@link #read(String)} to parse the file and set the
+     * resulting {@link Document} as the root node for this
+     * instance.
+     */
+    protected void configure(ValueParser parser)
+    {
+        this.LOG = (Log)parser.get(ToolContext.LOG_KEY);
+
+        String file = parser.getString(FILE_KEY);
+        if (file != null)
+        {
+            try
+            {
+                read(file);
+            }
+            catch (Exception e)
+            {
+                throw new RuntimeException("Could not read XML file at: "+file, e);
+            }
+        }
+    }
+
+    /**
+     * Sets a singular root {@link Node} for this instance.
+     */
+    protected void setRoot(Node node)
+    {
+        if (node instanceof Document)
+        {
+            node = ((Document)node).getRootElement();
+        }
+        this.nodes = new ArrayList<Node>(1);
+        this.nodes.add(node);
+    }
+
+    //FIXME: dupe of FileFactoryConfiguration; move to one place
+    private URL getURL(String name) throws Exception
+    {
+        //TODO: grab the VelocityEngine so we can read files from there?
+        File file = new File(name);
+        if (file.exists())
+        {
+            return file.toURI().toURL();
+        }
+        URL url = ClassUtils.getResource(name, this);
+        if (url != null)
+        {
+            return url;
+        }
+        return new URL(name);
+    }
+
+    private void log(Object o, Throwable t)
+    {
+        if (LOG != null)
+        {
+            LOG.debug("XmlTool - "+o, t);
+        }
+    }
+
+    /**
+     * Creates a {@link URL} from the string and passes it to {@link #read(URL)}.
+     */
+    protected void read(String file) throws Exception
+    {
+        read(getURL(file));
+    }
+
+    /**
+     * Reads, parses and creates a {@link Document} from the
+     * given {@link URL} and uses it as the root {@link Node} for this instance.
+     */
+    protected void read(URL url) throws Exception
+    {
+        SAXReader reader = new SAXReader();
+        setRoot(reader.read(url));
+    }
+
+    /**
+     * Parses the given XML string and uses the resulting {@link Document}
+     * as the root {@link Node}.
+     */
+    protected void parse(String xml) throws Exception
+    {
+        setRoot(DocumentHelper.parseText(xml));
+    }
+
+
+    /**
+     * If safe mode is explicitly turned off for this tool, then
+     * this will accept either a {@link URL} or the string representation
+     * thereof.  If valid, it will return a new {@link XmlTool} instance
+     * with that document as the root {@link Node}.  If reading the URL
+     * or parsing its content fails or if safe mode is on (the default),
+     * this will return {@code null}.
+     */
+    public XmlTool read(Object o)
+    {
+        if (isSafeMode() || o == null)
+        {
+            return null;
+        }
+        try
+        {
+            XmlTool xml = new XmlTool();
+            if (o instanceof URL)
+            {
+                xml.read((URL)o);
+            }
+            else
+            {
+                String file = String.valueOf(o);
+                xml.read(file);
+            }
+            return xml;
+        }
+        catch (Exception e)
+        {
+            log("Failed to read XML from : "+o, e);
+            return null;
+        }
+    }
+
+    /**
+     * This accepts XML in form.  If the XML is valid, it will return a
+     * new {@link XmlTool} instance with the resulting XML document
+     * as the root {@link Node}.  If parsing the content fails,
+     * this will return {@code null}.
+     */
+    public XmlTool parse(Object o)
+    {
+        if (o == null)
+        {
+            return null;
+        }
+        String s = String.valueOf(o);
+        try
+        {
+            XmlTool xml = new XmlTool();
+            xml.parse(s);
+            return xml;
+        }
+        catch (Exception e)
+        {
+            log("Failed to parse XML from : "+o, e);
+            return null;
+        }
+    }
+
+
+    /**
+     * This will first attempt to find an attribute with the
+     * specified name and return its value.  If no such attribute
+     * exists or its value is {@code null}, this will attempt to convert
+     * the given value to a {@link Number} and get the result of
+     * {@link #get(Number)}.  If the number conversion fails,
+     * then this will convert the object to a string. If that string
+     * does not contain a '/', it appends the result of {@link #getPath()}
+     * and a '/' to the front of it.  Finally, it delegates the string to the
+     * {@link #find(String)} method and returns the result of that.
+     */
+    public Object get(Object o)
+    {
+        if (isEmpty() || o == null)
+        {
+            return null;
+        }
+        String attr = attr(o);
+        if (attr != null)
+        {
+            return attr;
+        }
+        Number i = ConversionUtils.toNumber(o);
+        if (i != null)
+        {
+            return get(i);
+        }
+        String s = String.valueOf(o);
+        if (s.length() == 0)
+        {
+            return null;
+        }
+        if (s.indexOf('/') < 0)
+        {
+            s = getPath()+'/'+s;
+        }
+        return find(s);
+    }
+
+
+    /**
+     * Asks {@link #get(Object)} for a "name" result.
+     * If none, this will return the result of {@link #getNodeName()}.
+     */
+    public Object getName()
+    {
+        // give attributes and child elements priority
+        Object name = get("name");
+        if (name != null)
+        {
+            return name;
+        }
+        return getNodeName();
+    }
+
+    /**
+     * Returns the name of the root node. If the internal {@link Node}
+     * list has more than one {@link Node}, it will only return the name
+     * of the first node in the list.
+     */
+    public String getNodeName()
+    {
+        if (isEmpty())
+        {
+            return null;
+        }
+        return node().getName();
+    }
+
+    /**
+     * Returns the XPath that identifies the first/sole {@link Node}
+     * represented by this instance.
+     */
+    public String getPath()
+    {
+        if (isEmpty())
+        {
+            return null;
+        }
+        return node().getPath();
+    }
+
+    /**
+     * Returns the value of the specified attribute for the first/sole
+     * {@link Node} in the internal Node list for this instance, if that
+     * Node is an {@link Element}.  If it is a non-Element node type or
+     * there is no value for that attribute in this element, then this
+     * will return {@code null}.
+     */
+    public String attr(Object o)
+    {
+        if (o == null)
+        {
+            return null;
+        }
+        String key = String.valueOf(o);
+        Node node = node();
+        if (node instanceof Element)
+        {
+            return ((Element)node).attributeValue(key);
+        }
+        return null;
+    }
+
+    /**
+     * Returns a {@link Map} of all attributes for the first/sole
+     * {@link Node} held internally by this instance.  If that Node is
+     * not an {@link Element}, this will return null.
+     */
+    public Map<String,String> attributes()
+    {
+        Node node = node();
+        if (node instanceof Element)
+        {
+            Map<String,String> attrs = new HashMap<String,String>();
+            for (Iterator i = ((Element)node).attributeIterator(); i.hasNext();)
+            {
+                Attribute a = (Attribute)i.next();
+                attrs.put(a.getName(), a.getValue());
+            }
+            return attrs;
+        }
+        return null;
+    }
+
+
+    /**
+     * Returns {@code true} if there are no {@link Node}s internally held
+     * by this instance.
+     */
+    public boolean isEmpty()
+    {
+        return (nodes == null || nodes.isEmpty());
+    }
+
+    /**
+     * Returns the number of {@link Node}s internally held by this instance.
+     */
+    public int size()
+    {
+        if (isEmpty())
+        {
+            return 0;
+        }
+        return nodes.size();
+    }
+
+    /**
+     * Returns an {@link Iterator} that returns new {@link XmlTool}
+     * instances for each {@link Node} held internally by this instance.
+     */
+    public Iterator<XmlTool> iterator()
+    {
+        if (isEmpty())
+        {
+            return null;
+        }
+        return new NodeIterator(nodes.iterator());
+    }
+
+    /**
+     * Returns an {@link XmlTool} that wraps only the
+     * first {@link Node} from this instance's internal Node list.
+     */
+    public XmlTool getFirst()
+    {
+        if (size() == 1)
+        {
+            return this;
+        }
+        return new XmlTool(node());
+    }
+
+    /**
+     * Returns an {@link XmlTool} that wraps only the
+     * last {@link Node} from this instance's internal Node list.
+     */
+    public XmlTool getLast()
+    {
+        if (size() == 1)
+        {
+            return this;
+        }
+        return new XmlTool(nodes.get(size() - 1));
+    }
+
+    /**
+     * Returns an {@link XmlTool} that wraps the specified
+     * {@link Node} from this instance's internal Node list.
+     */
+    public XmlTool get(Number n)
+    {
+        if (n == null)
+        {
+            return null;
+        }
+        int i = n.intValue();
+        if (i < 0 || i > size() - 1)
+        {
+            return null;
+        }
+        return new XmlTool(nodes.get(i));
+    }
+
+    /**
+     * Returns the first/sole {@link Node} from this
+     * instance's internal Node list, if any.
+     */
+    public Node node()
+    {
+        if (isEmpty())
+        {
+            return null;
+        }
+        return nodes.get(0);
+    }
+
+
+    /**
+     * Converts the specified object to a String and calls
+     * {@link #find(String)} with that.
+     */
+    public XmlTool find(Object o)
+    {
+        if (o == null || isEmpty())
+        {
+            return null;
+        }
+        return find(String.valueOf(o));
+    }
+
+    /**
+     * Performs an XPath selection on the current set of
+     * {@link Node}s held by this instance and returns a new
+     * {@link XmlTool} instance that wraps those results.
+     * If the specified value is null or this instance does
+     * not currently hold any nodes, then this will return 
+     * {@code null}.  If the specified value, when converted
+     * to a string, does not contain a '/' character, then
+     * it has "//" prepended to it.  This means that a call to
+     * {@code $xml.find("a")} is equivalent to calling
+     * {@code $xml.find("//a")}.  The full range of XPath
+     * selectors is supported here.
+     */
+    public XmlTool find(String xpath)
+    {
+        if (xpath == null || xpath.length() == 0)
+        {
+            return null;
+        }
+        if (xpath.indexOf('/') < 0)
+        {
+            xpath = "//"+xpath;
+        }
+        List<Node> found = new ArrayList<Node>();
+        for (Node n : nodes)
+        {
+            found.addAll((List<Node>)n.selectNodes(xpath));
+        }
+        if (found.isEmpty())
+        {
+            return null;
+        }
+        return new XmlTool(found);
+    }
+
+    /**
+     * Returns a new {@link XmlTool} instance that wraps
+     * the parent {@link Element} of the first/sole {@link Node}
+     * being wrapped by this instance.
+     */
+    public XmlTool getParent()
+    {
+        if (isEmpty())
+        {
+            return null;
+        }
+        Element parent = node().getParent();
+        if (parent == null)
+        {
+            return null;
+        }
+        return new XmlTool(parent);
+    }
+
+    /**
+     * Returns a new {@link XmlTool} instance that wraps
+     * the parent {@link Element}s of each of the {@link Node}s
+     * being wrapped by this instance.  This does not return
+     * all ancestors, just the immediate parents.
+     */
+    public XmlTool parents()
+    {
+        if (isEmpty())
+        {
+            return null;
+        }
+        if (size() == 1)
+        {
+            return getParent();
+        }
+        List<Node> parents = new ArrayList<Node>(size());
+        for (Node n : nodes)
+        {
+            Element parent = n.getParent();
+            if (parent != null && !parents.contains(parent))
+            {
+                parents.add(parent);
+            }
+        }
+        if (parents.isEmpty())
+        {
+            return null;
+        }
+        return new XmlTool(parents);
+    }
+
+    /**
+     * Returns a new {@link XmlTool} instance that wraps all the
+     * child {@link Element}s of all the current internally held nodes
+     * that are {@link Element}s themselves.
+     */
+    public XmlTool children()
+    {
+        if (isEmpty())
+        {
+            return null;
+        }
+        List<Node> kids = new ArrayList<Node>();
+        for (Node n : nodes)
+        {
+            if (n instanceof Element)
+            {
+                kids.addAll((List<Node>)((Element)n).elements());
+            }
+        }
+        return new XmlTool(kids);
+    }
+
+    /**
+     * Returns the concatenated text content of all the internally held
+     * nodes.  Obviously, this is most useful when only one node is held.
+     */
+    public String getText()
+    {
+        if (isEmpty())
+        {
+            return null;
+        }
+        StringBuilder out = new StringBuilder();
+        for (Node n : nodes)
+        {
+            String text = n.getText();
+            if (text != null)
+            {
+                out.append(text);
+            }
+        }
+        String result = out.toString().trim();
+        if (result.length() > 0)
+        {
+            return result;
+        }
+        return null;
+    }
+
+
+    /**
+     * If this instance has no XML {@link Node}s, then this
+     * returns the result of {@code super.toString()}.  Otherwise, it
+     * returns the XML (as a string) of all the internally held nodes
+     * that are not {@link Attribute}s. For attributes, only the value
+     * is used.
+     */
+    public String toString()
+    {
+        if (isEmpty())
+        {
+            return super.toString();
+        }
+        StringBuilder out = new StringBuilder();
+        for (Node n : nodes)
+        {
+            if (n instanceof Attribute)
+            {
+                out.append(n.getText().trim());
+            }
+            else
+            {
+                out.append(n.asXML());
+            }
+        }
+        return out.toString();
+    }
+
+    
+    /**
+     * Iterator implementation that wraps a Node list iterator
+     * to return new XmlTool instances for each item in the wrapped
+     * iterator.s
+     */
+    public static class NodeIterator implements Iterator<XmlTool>
+    {
+        private Iterator<Node> i;
+
+        public NodeIterator(Iterator<Node> i)
+        {
+            this.i = i;
+        }
+
+        public boolean hasNext()
+        {
+            return i.hasNext();
+        }
+
+        public XmlTool next()
+        {
+            return new XmlTool(i.next());
+        }
+
+        public void remove()
+        {
+            i.remove();
+        }
+    }
+}

Propchange: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/XmlTool.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/XmlTool.java
------------------------------------------------------------------------------
    svn:executable = *

Propchange: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/XmlTool.java
------------------------------------------------------------------------------
    svn:keywords = Revision

Propchange: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/XmlTool.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/tools.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/tools.xml?rev=675383&r1=675382&r2=675383&view=diff
==============================================================================
--- velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/tools.xml (original)
+++ velocity/tools/trunk/src/main/java/org/apache/velocity/tools/generic/tools.xml Wed Jul  9 16:55:50 2008
@@ -35,6 +35,7 @@
         <tool class="org.apache.velocity.tools.generic.NumberTool"/>
         <tool class="org.apache.velocity.tools.generic.ResourceTool"/>
         <tool class="org.apache.velocity.tools.generic.SortTool"/>
+        <tool class="org.apache.velocity.tools.generic.XmlTool"/>
     </toolbox>
     <toolbox scope="request">
         <tool class="org.apache.velocity.tools.generic.ContextTool"/>

Added: velocity/tools/trunk/src/test/java/file.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/test/java/file.xml?rev=675383&view=auto
==============================================================================
--- velocity/tools/trunk/src/test/java/file.xml (added)
+++ velocity/tools/trunk/src/test/java/file.xml Wed Jul  9 16:55:50 2008
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<foo>
+  <bar name="a"/>
+  <baz>woogie</baz>
+  <baz>wiggie</baz>
+</foo>
\ No newline at end of file

Propchange: velocity/tools/trunk/src/test/java/file.xml
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: velocity/tools/trunk/src/test/java/file.xml
------------------------------------------------------------------------------
    svn:executable = *

Propchange: velocity/tools/trunk/src/test/java/file.xml
------------------------------------------------------------------------------
    svn:mime-type = text/xml

Added: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/XmlToolTests.java
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/src/test/java/org/apache/velocity/tools/XmlToolTests.java?rev=675383&view=auto
==============================================================================
--- velocity/tools/trunk/src/test/java/org/apache/velocity/tools/XmlToolTests.java (added)
+++ velocity/tools/trunk/src/test/java/org/apache/velocity/tools/XmlToolTests.java Wed Jul  9 16:55:50 2008
@@ -0,0 +1,304 @@
+package org.apache.velocity.tools.generic;
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+import org.junit.*;
+import static org.junit.Assert.*;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import org.apache.velocity.tools.generic.ValueParser;
+import org.dom4j.Node;
+
+/**
+ * <p>Tests for XmlTool</p>
+ *
+ * @author Nathan Bubna
+ * @since VelocityTools 2.0
+ * @version $Id$
+ */
+public class XmlToolTests {
+
+    private static final String XML_FILE = "@test.file.dir@/file.xml";
+
+    private static final String XML_STRING =
+"<foo>\n  <bar name=\"a\"/>\n  <baz>woogie</baz>\n  <baz>wiggie</baz>\n</foo>";
+
+    public @Test void ctorXmlTool() throws Exception
+    {
+        try
+        {
+            new XmlTool();
+        }
+        catch (Exception e)
+        {
+            fail("Constructor 'XmlTool()' failed due to: " + e);
+        }
+    }
+
+    private XmlTool stringBased() throws Exception
+    {
+        XmlTool xml = new XmlTool();
+        xml.parse(XML_STRING);
+        return xml;
+    }
+
+    private XmlTool fileBased() throws Exception
+    {
+        XmlTool xml = new XmlTool();
+        xml.read(XML_FILE);
+        return xml;
+    }
+
+    public @Test void testStringFileEquals() throws Exception
+    {
+        String string = stringBased().toString();
+        String file = fileBased().toString();
+        //System.out.println("string:\n"+string+"\nfile:\n"+file);
+        assertEquals(string, file);
+    }
+
+    public @Test void methodAttr_Object() throws Exception
+    {
+        XmlTool xml = stringBased();
+        assertNull(xml.attr("href"));
+        xml = xml.find("bar");
+        assertNotNull(xml.attr("name"));
+        assertEquals("a", xml.attr("name"));
+    }
+
+    public @Test void methodAttributes() throws Exception
+    {
+        XmlTool xml = stringBased();
+        Map<String,String> result = xml.attributes();
+        assertTrue(result.isEmpty());
+        xml = xml.find("bar");
+        result = xml.attributes();
+        assertNotNull(result);
+        assertEquals(1, result.size());
+        assertEquals("a", result.get("name"));
+    }
+
+    public @Test void methodChildren() throws Exception
+    {
+        XmlTool xml = stringBased();
+        assertEquals(1, xml.size());
+        XmlTool result = xml.children();
+        assertEquals(3, result.size());
+    }
+
+    public @Test void methodGetParent() throws Exception
+    {
+        XmlTool xml = stringBased().find("bar");
+        assertNotNull(xml);
+        XmlTool foo = xml.getParent();
+        assertNotNull(foo);
+        assertEquals(1, foo.size());
+        assertNull(foo.getParent());
+    }
+
+    public @Test void methodParents() throws Exception
+    {
+        XmlTool foo = stringBased();
+        XmlTool xml = foo.find("bar");
+        assertEquals(foo.toString(), xml.parents().toString());
+        xml = foo.children();
+        assertEquals(3, xml.size());
+        foo = xml.parents();
+        assertEquals(1, foo.size());
+    }
+
+    public @Test void methodConfigure_ValueParser() throws Exception
+    {
+        XmlTool xml = new XmlTool();
+        Map<String,String> params = new HashMap<String,String>();
+        assertEquals("file", XmlTool.FILE_KEY);
+        params.put(XmlTool.FILE_KEY, XML_FILE);
+        xml.configure(params);
+        assertEquals(1, xml.size());
+        assertEquals("foo", xml.getName());
+    }
+
+    public @Test void methodFind_Object() throws Exception
+    {
+        XmlTool xml = stringBased();
+        XmlTool result = xml.find((Object)null);
+        assertNull(result);
+        assertEquals(xml.find("bar").toString(), xml.find("//bar").toString());
+        //TODO: test more xpath expressions?
+        //TODO: test expressions with no results
+    }
+
+    public @Test void methodGetFirst() throws Exception
+    {
+        XmlTool xml = stringBased();
+        assertSame(xml, xml.getFirst());
+        xml = xml.children();
+        assertEquals(3, xml.size());
+        xml = xml.getFirst();
+        assertEquals(1, xml.size());
+        assertEquals("a", xml.getName());
+    }
+
+    public @Test void methodGetLast() throws Exception
+    {
+        XmlTool xml = stringBased();
+        assertSame(xml, xml.getLast());
+        xml = xml.children();
+        assertEquals(3, xml.size());
+        xml = xml.getLast();
+        assertEquals(1, xml.size());
+        assertEquals("baz", xml.getName());
+        assertEquals("wiggie", xml.getText());
+    }
+
+    public @Test void methodGetName() throws Exception
+    {
+        XmlTool xml = stringBased();
+        assertEquals(1, xml.size());
+        assertEquals("foo", xml.getName());
+        xml = xml.find("bar");
+        assertEquals("a", xml.getName());
+    }
+
+    public @Test void methodGetNodeName() throws Exception
+    {
+        XmlTool xml = stringBased();
+        assertEquals("foo", xml.getNodeName());
+        xml = xml.find("baz");
+        assertEquals("baz", xml.getNodeName());
+    }
+
+    public @Test void methodGetText() throws Exception
+    {
+        XmlTool xml = stringBased();
+        //TODO: prepare the instance for testing
+        String result = xml.getText();
+        assertEquals((String)null, result);
+    }
+
+    public @Test void methodGet_Number() throws Exception
+    {
+        XmlTool xml = stringBased();
+        assertEquals(xml.toString(), xml.get(0).toString());
+        xml = xml.children();
+        assertEquals("bar", xml.get(0).getNodeName());
+        assertEquals("baz", xml.get(1).getName());
+        assertEquals("baz", xml.get(2).getName());
+        assertNull(xml.get(3));
+        assertNull(xml.get(-1));
+    }
+
+    public @Test void methodGet_Object() throws Exception
+    {
+        XmlTool xml = stringBased();
+        assertNull(xml.get(null));
+        assertNull(xml.get(""));
+        assertNull(xml.get("null"));
+        Object result = xml.get("bar");
+        assertNotNull(result);
+        assertTrue(result instanceof XmlTool);
+        xml = (XmlTool)result;
+        result = null;
+        assertNull(result);
+        result = xml.get("0");
+        assertNotNull(result);
+        assertEquals(result.toString(), xml.toString());
+        result = null;
+        assertNull(result);
+        result = xml.get("name");
+        assertNotNull(result);
+        assertEquals("a", result);
+    }
+
+    public @Test void methodIsEmpty() throws Exception
+    {
+        XmlTool xml = new XmlTool();
+        assertTrue(xml.isEmpty());
+        xml.parse(XML_STRING);
+        assertFalse(xml.isEmpty());
+    }
+
+    public @Test void methodIterator() throws Exception
+    {
+        XmlTool xml = new XmlTool();
+        assertNull(xml.iterator());
+        xml.parse(XML_STRING);
+        Iterator<XmlTool> i = xml.iterator();
+        assertNotNull(i);
+        XmlTool foo = i.next();
+        assertNotNull(foo);
+        assertEquals(foo.toString(), xml.toString());
+        xml = xml.children();
+        i = xml.iterator();
+        assertEquals("a", i.next().attr("name"));
+        assertEquals("baz", i.next().getName());
+        assertEquals("wiggie", i.next().getText());
+        assertFalse(i.hasNext());
+    }
+
+    public @Test void methodNode() throws Exception
+    {
+        XmlTool xml = new XmlTool();
+        assertNull(xml.node());
+        xml.parse(XML_STRING);
+        Node n = xml.node();
+        assertNotNull(n);
+    }
+
+    public @Test void methodParse_Object() throws Exception
+    {
+        XmlTool xml = new XmlTool();
+        assertNull(xml.parse((Object)null));
+        assertNull(xml.parse((Object)"><S asdf8 ~$"));
+        assertNotNull(xml.parse((Object)XML_STRING));
+        //TODO: test other strings?
+    }
+
+    public @Test void methodSize() throws Exception
+    {
+        XmlTool xml = new XmlTool();
+        assertEquals(0, xml.size());
+        xml.parse(XML_STRING);
+        assertEquals(1, xml.size());
+        xml = xml.children();
+        assertEquals(3, xml.size());
+        xml = xml.getLast();
+        assertEquals(1, xml.size());
+    }
+
+    public @Test void methodToString() throws Exception
+    {
+        XmlTool xml = new XmlTool();
+        assertTrue(xml.toString().startsWith(XmlTool.class.getName()));
+        xml.read(XML_FILE);
+        assertTrue(xml.toString().startsWith("<foo>"));
+        assertTrue(xml.toString().endsWith("</foo>"));
+        XmlTool bar = xml.find("bar");
+        assertEquals("<bar name=\"a\"/>", bar.toString());
+        XmlTool baz = (XmlTool)xml.get("baz");
+        assertEquals("<baz>woogie</baz><baz>wiggie</baz>", baz.toString());
+    }
+
+
+}
+        
\ No newline at end of file

Propchange: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/XmlToolTests.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/XmlToolTests.java
------------------------------------------------------------------------------
    svn:executable = *

Propchange: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/XmlToolTests.java
------------------------------------------------------------------------------
    svn:keywords = Revision

Propchange: velocity/tools/trunk/src/test/java/org/apache/velocity/tools/XmlToolTests.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: velocity/tools/trunk/test.xml
URL: http://svn.apache.org/viewvc/velocity/tools/trunk/test.xml?rev=675383&r1=675382&r2=675383&view=diff
==============================================================================
--- velocity/tools/trunk/test.xml (original)
+++ velocity/tools/trunk/test.xml Wed Jul  9 16:55:50 2008
@@ -63,6 +63,10 @@
     <pathconvert property="test.conf.dir.java" targetos="unix">
         <path location="${test.conf.dir}"/>
     </pathconvert>
+    <pathconvert property="test.file.dir" targetos="unix">
+        <path location="${test.src.dir}"/>
+    </pathconvert>
+    <filter token="test.file.dir" value="${test.file.dir}"/>
     <filter token="test.conf.dir" value="${test.conf.dir.java}"/>
     <filter token="test.log.dir" value="${test.log.dir}"/>
     <filter token="test.webcontainer.port" value="${test.webcontainer.port}"/>