blob: 8a07fc3f882bde97ce2ffb45eb9a9db4082c4ac7 [file] [log] [blame]
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.IOException;
import java.io.Reader;
import java.io.Serializable;
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.apache.velocity.tools.XmlUtils;
import org.apache.velocity.tools.ConversionUtils;
import org.apache.velocity.tools.config.DefaultKey;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;
import javax.xml.xpath.XPathExpressionException;
/**
* <p>Tool for reading/navigating XML files, with XPath expressions support.</p>
*
* <p>Configuration parameters:</p>
* <ul>
* <li><code>resource</code>=<i>file or classpath resource</i></li>
* <li><code>source</code>=<i>URL</i></li>
* </ul>
*
* <p>Here's a short example:</p>
* <pre>
* XML file:
* &lt;foo&gt;&lt;bar&gt;woogie&lt;/bar&gt;&lt;a name="test"/&gt;&lt;/foo&gt;
*
* Template:
* $foo.bar.text
* $foo.find('a')
* $foo.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="foo" source="doc.xml"/&gt;
* &lt;/toolbox&gt;
* &lt;/tools&gt;
* </pre>
* <p>Note that this tool is included in the default GenericTools configuration
* under the key "xml". You can read 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(String)} method.</p>
*
* @author Nathan Bubna
* @author Claude Brisson
* @version $Revision: 1769055 $ $Date: 2006-11-27 10:49:37 -0800 (Mon, 27 Nov 2006) $
* @since VelocityTools 2.0
*/
@DefaultKey("xml")
public class XmlTool extends SafeConfig implements Serializable
{
private static final long serialVersionUID = 8217744660691416595L;
/**
* ImportSupport utility which provides underlying i/o
*/
protected transient ImportSupport importSupport = null;
/**
* ImportSupport initialization
* @param config configuration values
*/
protected synchronized void initializeImportSupport(ValueParser config)
{
if (importSupport == null)
{
importSupport = new ImportSupport();
importSupport.configure(config);
}
}
/**
* Configuration.
* @param values configuration values
*/
protected void configure(ValueParser values)
{
super.configure(values);
initializeImportSupport(values);
String resource = values.getString(ImportSupport.RESOURCE_KEY);
if (resource != null)
{
read(resource);
}
else
{
String url = values.getString(ImportSupport.URL_KEY);
if (url != null)
{
/* temporary disable safe mode */
boolean safeMode = importSupport.isSafeMode();
importSupport.setSafeMode(false);
fetch(url);
importSupport.setSafeMode(safeMode);
}
}
}
/**
* Content nodes.
*/
private List<Node> nodes = null;
/**
* Default constructor.
*/
public XmlTool() {}
/**
* Builds an XmlTool around a node.
* @param node target node
*/
public XmlTool(Node node)
{
this(Collections.singletonList(node));
}
/**
* Builds an XmlTool around a nodes list.
* @param nodes nodes list
*/
public XmlTool(List<Node> nodes)
{
this.nodes = nodes;
}
/**
* Sets a singular root {@link Node} for this instance.
* @param node new root node
*/
protected void setRoot(Node node)
{
if (node == null)
{
this.nodes = null;
}
else
{
if (node instanceof Document)
{
node = ((Document) node).getDocumentElement();
}
this.nodes = new ArrayList<Node>(1);
this.nodes.add(node);
}
}
/**
* Parses the given XML string and uses the resulting {@link Document}
* as the root {@link Node}.
* @param xml XML string
* @return new XmlTool
*/
public XmlTool parse(String xml)
{
try
{
if (xml != null)
{
setRoot(XmlUtils.parse(xml));
}
}
catch (Exception e)
{
getLog().error("could not parse given XML string", e);
}
return this;
}
/**
* Reads and parses a local resource file
* @param resource resource path
* @return new XmlTool
*/
public XmlTool read(String resource)
{
Reader reader = null;
try
{
if (importSupport == null)
{
initializeImportSupport(new ValueParser());
}
reader = importSupport.getResourceReader(resource);
if (reader != null)
{
setRoot(XmlUtils.parse(reader));
}
}
catch (Exception e)
{
getLog().error("could not read XML resource {}", resource, e);
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (IOException ioe) {}
}
}
return this;
}
/**
* Reads and parses a remote or local URL
* @param url resource URL
* @return new XmlTool
*/
public XmlTool fetch(String url)
{
Reader reader = null;
try
{
if (importSupport == null)
{
initializeImportSupport(new ValueParser());
}
reader = importSupport.acquireReader(url);
if (reader != null)
{
setRoot(XmlUtils.parse(reader));
}
}
catch (Exception e)
{
getLog().error("could not fetch XML content from URL {}", url, e);
}
finally
{
if (reader != null)
{
try
{
reader.close();
}
catch (IOException ioe) {}
}
}
return this;
}
/**
* 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.
* @param o attribute name, number, xpath relative expression
* @return found value or null
*/
public Object get(Object o)
{
if (isEmpty() || o == null)
{
return null;
}
String attr = attr(o);
if (attr != null && attr.length() > 0)
{
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()}.
* @return found value or null
*/
public Object getName()
{
// give attributes and child elements priority
Object name = get("name");
if (name != null && !"".equals(name))
{
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.
* @return node name
*/
public String getNodeName()
{
if (isEmpty())
{
return null;
}
return node().getNodeName();
}
/**
* Returns the XPath that identifies the first/sole {@link Node}
* represented by this instance.
* @return xpath node path
*/
public String getPath()
{
if (isEmpty())
{
return null;
}
return XmlUtils.nodePath(node());
}
/**
* 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}.
* @param o attribute name
* @return attribute value or null
*/
public String attr(Object o)
{
if (o == null)
{
return null;
}
String key = String.valueOf(o);
Node node = node();
if (node instanceof Element)
{
String value = ((Element)node).getAttribute(key);
if (value.length() > 0)
{
return value;
}
}
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.
* @return attributes map
*/
public Map<String,String> attributes()
{
Node node = node();
if (node instanceof Element)
{
Map<String,String> attrs = new HashMap<String,String>();
NamedNodeMap map = node.getAttributes();
for (int i = 0; i < map.getLength(); ++i)
{
Attr attr = (Attr)map.item(i);
attrs.put(attr.getName(), attr.getValue());
}
return attrs;
}
return null;
}
/**
* Returns {@code true} if there are no {@link Node}s internally held
* by this instance.
* @return whether the set of current nodes is empty
*/
public boolean isEmpty()
{
return (nodes == null || nodes.isEmpty());
}
/**
* Returns the number of {@link Node}s internally held by this instance.
* @return the size of the set of current nodes
*/
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.
* @return an iterator on wrappers of current nodes or null
*/
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.
* @return a wrapper on the first node or null
*/
public XmlTool getFirst()
{
if (size() == 1)
{
return this;
}
else if (isEmpty())
{
return null;
}
return new XmlTool(node());
}
/**
* Returns an {@link XmlTool} that wraps only the
* last {@link Node} from this instance's internal Node list.
* @return a wrapper on the last node or null
*/
public XmlTool getLast()
{
if (size() == 1)
{
return this;
}
else if (isEmpty())
{
return null;
}
return new XmlTool(nodes.get(size() - 1));
}
/**
* Returns an {@link XmlTool} that wraps the specified
* {@link Node} from this instance's internal Node list.
* @param n node index in current nodes list
* @return a wrapper on the nth node or null
*/
public XmlTool get(Number n)
{
if (n == null || isEmpty())
{
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.
* @return the unwrapped first node or null
*/
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.
* @param o xpath to search
* @return a wrapper on found nodes or null
*/
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.
* @param xpath xpath to search
* @return a wrapper on found nodes or null
*/
public XmlTool find(String xpath)
{
if (xpath == null || xpath.length() == 0 || isEmpty())
{
return null;
}
if (xpath.indexOf('/') < 0)
{
xpath = "//"+xpath;
}
List<Node> found = new ArrayList<Node>();
for (Node n : nodes)
{
NodeList lst;
try
{
lst = XmlUtils.search(xpath, n);
}
catch(XPathExpressionException xpee)
{
getLog().error("could not parse XML expression '{}'", xpath, xpee);
return null;
}
if (lst != null)
{
for (int i = 0; i < lst.getLength(); ++i)
{
found.add(lst.item(i));
}
}
}
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.
* @return wrapper on the parent of the first node or null
*/
public XmlTool getParent()
{
if (isEmpty())
{
return null;
}
Node parent = node().getParentNode();
if (parent == null || parent instanceof Document)
{
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.
* @return a wrapper on the set of parents nodes or null
*/
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 = null;
if (n instanceof Element)
{
parent = (Element)n.getParentNode();
}
else if (n instanceof Attr)
{
parent = ((Attr) n).getOwnerElement();
}
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.
* @return a wrapper on the children of current nodes
*/
public XmlTool children()
{
if (isEmpty())
{
return null;
}
List<Node> kids = new ArrayList<Node>();
for (Node n : nodes)
{
if (n instanceof Element)
{
NodeList lst = n.getChildNodes();
for (int i = 0; i < lst.getLength(); ++i)
{
Node child = lst.item(i);
if (child instanceof Text)
{
String value = child.getNodeValue().trim();
if (value.length() == 0)
{
continue;
}
}
kids.add(child);
}
}
}
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.
* @return text of current nodes or null
*/
public String getText()
{
if (isEmpty())
{
return null;
}
StringBuilder out = new StringBuilder();
for (Node n : nodes)
{
String text = n.getTextContent().trim();
if (text != null && text.length() > 0)
{
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 Attr}ibutes. For attributes, only the value
* is used.
* @return XML string of current nodes or null
*/
public String toString()
{
if (isEmpty())
{
return super.toString();
}
StringBuilder out = new StringBuilder();
for (Node n : nodes)
{
if (n instanceof Attr)
{
out.append(((Attr)n).getValue().trim());
}
else
{
out.append(XmlUtils.nodeToString(n));
}
}
return out.toString().trim();
}
/**
* 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();
}
}
}