| 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: |
| * <foo><bar>woogie</bar><a name="test"/></foo> |
| * |
| * Template: |
| * $foo.bar.text |
| * $foo.find('a') |
| * $foo.a.name |
| * |
| * Output: |
| * woogie |
| * <a name="test"/> |
| * test |
| * |
| * Configuration: |
| * <tools> |
| * <toolbox scope="application"> |
| * <tool class="org.apache.velocity.tools.generic.XmlTool" |
| * key="foo" source="doc.xml"/> |
| * </toolbox> |
| * </tools> |
| * </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(); |
| } |
| } |
| } |