/******************************************************************************* * Copyright (c) 2008, 2009 Bug Labs, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * - Neither the name of Bug Labs, Inc. nor the names of its contributors may be * used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. *******************************************************************************/ package com.buglabs.util.xml; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * Reduced XPath query engine against simplified Xml DOM. * * @author ken * */ public class XpathQuery { /** * Return a list of nodes. */ public static final int NODE_LIST = 1; public static final int NODE = 2; /** * Convenience method. Calls evaluate with NODE_LIST. * * @param expression * @param node * @return */ public static List<XmlNode> getNodes(String expression, XmlNode node) { return (List<XmlNode>) evaluate(expression, node, NODE_LIST); } /** * Convenience method. Calls evaluate with NODE. * * @param expression * @param node * @return */ public static XmlNode getNode(String expression, XmlNode node) { return (XmlNode) evaluate(expression, node, NODE); } /** * Execute a XPath query against a node. * * @param expression * @param element * @param returnType * The type of data expected to be returned. * @return */ public static Object evaluate(String expression, XmlNode element, int returnType) { List<XmlNode> elems; switch (returnType) { case NODE: elems = new ArrayList<XmlNode>(); findElements(element, expression.split("/"), 1, elems, true); if (elems.size() > 1) { throw new RuntimeException("Search for first returned multiple matches!"); } if (elems.size() == 1) { return elems.get(0); } return null; case NODE_LIST: elems = new ArrayList<XmlNode>(); if (expression.startsWith("//")) { findAllElements(element, expression.split("/"), 2, elems); } else { findElements(element, expression.split("/"), 1, elems, false); } return elems; default: return null; } } /** * Search through a node tree, applying xpath string to each node. * * @param node * @param xpath * @param xpathIndex * xpath string split on '/'. This value is index of the array. * @param matches * List of matched XmlNodes. * @param firstOnly * If true, return first match only, else search entire tree. */ private static void findElements(XmlNode node, String[] xpath, int xpathIndex, List<XmlNode> matches, boolean firstOnly) { if (nodeMatch(node, xpath[xpathIndex])) { if (xpathIndex + 1 == xpath.length) { matches.add(node); } else { for (Iterator<XmlNode> i = node.getChildren().iterator(); i.hasNext() && (!searchComplete(firstOnly, matches));) { XmlNode child = (XmlNode) i.next(); findElements(child, xpath, xpathIndex + 1, matches, firstOnly); } } } } /** * Apply full XPath query to each tree in node. * * @param node * @param xpath * @param xpathIndex * @param matches */ private static void findAllElements(XmlNode node, String[] xpath, int xpathIndex, List<XmlNode> matches) { // First see if this node matches, if so add it to the list. if (deepNodeMatch(node, xpath[xpathIndex])) { matches.add(node); } // Second iterate through the children. for (Iterator<XmlNode> i = node.getChildren().iterator(); i.hasNext();) { XmlNode child = (XmlNode) i.next(); findAllElements(child, xpath, xpathIndex, matches); } } /** * check if search is first node only and node has been found. * * @param firstOnly * @param matches * @return */ private static boolean searchComplete(boolean firstOnly, List<XmlNode> matches) { if (!firstOnly) { return false; } if (matches.size() > 0) { return true; } return false; } /** * Evaluate if given node matches XPath term. * * @param node * @param exprStr * @return */ private static boolean nodeMatch(XmlNode node, String exprStr) { AttribExpression ae = new AttribExpression(exprStr); if (!ae.exists) { return tagNameMatch(node, exprStr); } if (ae.getTagName().equalsIgnoreCase(node.getName())) { if (node.getAttributes().containsKey(ae.getName())) { String attrVal = node.getAttribute(ae.getName()); switch (ae.getOperator()) { case AttribExpression.EQUAL_OPERATOR: if (attrVal.equals(ae.getValue())) { return true; } return false; case AttribExpression.NOT_EQUAL_OPERATOR: if (attrVal.equals(ae.getValue())) { return true; } return false; default: throw new RuntimeException("Invalid operator for attribute."); } } } return false; } /** * Evaluate simple xpath term that matches tag name only. * * @param node * @param exprStr * @return */ private static boolean tagNameMatch(XmlNode node, String exprStr) { if (node.getName().equalsIgnoreCase(exprStr)) { return true; } return false; } /** * Perform deep search, applying full xpath expression to current node. * * @param node * @param exprStr * @return */ private static boolean deepNodeMatch(XmlNode node, String exprStr) { List<XmlNode> l = new ArrayList<XmlNode>(); findElements(node, exprStr.split("/"), 0, l, true); if (l.size() > 0) { return true; } return false; } }