![JAR search and dependency download from the Maven repository](/logo.png)
net.sf.practicalxml.xpath.XPathWrapper Maven / Gradle / Ivy
// Copyright 2008-2014 severally by the contributors
//
// Licensed 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.
package net.sf.practicalxml.xpath;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.xml.namespace.QName;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPathFunction;
import javax.xml.xpath.XPathVariableResolver;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import net.sf.practicalxml.DomUtil;
import net.sf.practicalxml.XmlException;
/**
* This class simplifies the use of XPath expressions, hiding the factory and
* return types, and providing a simple builder-style interface for adding
* resolvers. It also maintains the expression in a compiled form, improving
* reuse performance.
*
* Warning:
* XPathWrapper
, like the underlying JDK XPath
object,
* is not thread-safe. However, separate instances may be created and evaluated
* on separate threads without explicit synchronization. To make this work, we
* create a new XPathFactory
instance for each wrapper (with the
* Sun JDK, this adds little overhead), synchronizing newInstance()
* on XPathWrapper.class
.
*/
public class XPathWrapper
implements Cloneable
{
private final String _expr;
private final NamespaceResolver _nsResolver = new NamespaceResolver();
private Map _variables = new HashMap();
private FunctionResolver _functions = new FunctionResolver();
private XPathExpression _compiled;
/**
* Creates a new instance, which may then be customized with various
* resolvers, and used to evaluate expressions.
*/
public XPathWrapper(String expr)
{
_expr = expr;
}
//----------------------------------------------------------------------------
// Public methods
//----------------------------------------------------------------------------
/**
* Adds a namespace binding to this expression. All bindings must be
* added prior to the first call to evaluate()
. If you add
* multiple bindings with the same namespace, only the last is retained.
*
* @param prefix The prefix used to reference this namespace in the
* XPath expression. Note that this does not
* need to be the same prefix used by the document.
* @param nsURI The namespace URI to associate with this prefix.
*
* @return The wrapper, so that calls may be chained.
*/
public XPathWrapper bindNamespace(String prefix, String nsURI)
{
_nsResolver.addNamespace(prefix, nsURI);
return this;
}
/**
* Sets the default namespace binding: this will be applied to all
* expressions that do not explicitly specify a prefix (although
* they must use the colon separating the non-existent prefix from
* the element name).
*
* @param nsURI The default namespace for this document.
*
* @return The wrapper, so that calls may be chained.
*
* @deprecated In practice, this method isn't particularly useful:
* if you're going to specify ":" on a term, you might
* as well specify a single-character prefix. It won't
* be removed, but a future implementation might parse
* an expression sans colons, and insert references.
*/
@Deprecated
public XPathWrapper bindDefaultNamespace(String nsURI)
{
_nsResolver.setDefaultNamespace(nsURI);
return this;
}
/**
* Binds a value to a variable, replacing any previous value for that
* variable. Unlike other configuration methods, this may be called
* after calling evaluate()
; the new values will be used
* for subsequent evaluations.
*
* @param name The name of the variable; this is turned into a
* QName
without namespace.
* @param value The value of the variable; the XPath evaluator must
* be able to convert this value into a type usable in
* an XPath expression.
*
* @return The wrapper, so that calls may be chained.
*/
public XPathWrapper bindVariable(String name, Object value)
{
return bindVariable(new QName(name), value);
}
/**
* Binds a value to a variable, replacing any previous value for that
* variable. Unlike other configuration methods, this may be called
* after calling evaluate()
; the new values will be used
* for subsequent evaluations.
*
* @param name The fully-qualified name of the variable.
* @param value The value of the variable; the XPath evaluator must
* be able to convert this value into a type usable in
* an XPath expression.
*
* @return The wrapper, so that calls may be chained.
*/
public XPathWrapper bindVariable(QName name, Object value)
{
_variables.put(name, value);
return this;
}
/**
* Binds a self-describing function to this expression. Subsequent calls
* with the same name/arity will silently replace the binding.
*
* Per the JDK documentation, user-defined functions must occupy their
* own namespace. You must bind a namespace for this function, and use
* the bound prefix to reference it in your expression. Alternatively,
* you can call the variant of bindFunction()
that binds
* a prefix to the function's namespace.
*
* @param func The function.
*
* @return The wrapper, so that calls may be chained.
*/
public XPathWrapper bindFunction(FunctionResolver.SelfDescribingFunction func)
{
_functions.addFunction(func);
return this;
}
/**
* Binds a self-describing function to this expression, along with the
* prefix used to access that function. This also establishes a namespace
* binding for that prefix.
*
* Per the JDK documentation, user-defined functions must occupy their
* own namespace. This method will retrieve the namespace from the
* function, and bind it to the passed prefix.
*
* @param func The function.
* @param prefix The prefix to bind to this function's namespace.
*
* @return The wrapper, so that calls may be chained.
*/
public XPathWrapper bindFunction(FunctionResolver.SelfDescribingFunction func, String prefix)
{
_functions.addFunction(func);
return bindNamespace(prefix, func.getNamespaceUri());
}
/**
* Binds a standard XPathFunction
to this expression,
* handling any number of arguments. Subsequent calls to this method
* with the same name will silently replace the binding.
*
* Per the JDK documentation, user-defined functions must occupy their
* own namespace. If the qualified name that you pass to this method
* includes a prefix, the associated namespace will be bound to that
* prefix. If not, you must bind the namespace explicitly. In either
* case, you must refer to the function in your expression using a
* bound prefix.
*
* @param name The qualified name for this function. Must contain
* a name and namespace, may contain a prefix.
* @param func The function to bind to this name.
*
* @return The wrapper, so that calls may be chained.
*/
public XPathWrapper bindFunction(QName name, XPathFunction func)
{
return bindFunction(name, func, 0, Integer.MAX_VALUE);
}
/**
* Binds a standard XPathFunction
to this expression,
* handling a specific number of arguments. Subsequent calls to this
* method with the same name and arity will silently replace the binding.
*
* Per the JDK documentation, user-defined functions must occupy their
* own namespace. If the qualified name that you pass to this method
* includes a prefix, the associated namespace will be bound to that
* prefix. If not, you must bind the namespace explicitly. In either
* case, you must refer to the function in your expression using a
* bound prefix.
*
* @param name The qualified name for this function. Must contain
* a name and namespace, may contain a prefix.
* @param func The function to bind to this name.
* @param arity The number of arguments accepted by this function.
*
* @return The wrapper, so that calls may be chained.
*/
public XPathWrapper bindFunction(QName name, XPathFunction func, int arity)
{
return bindFunction(name, func, arity, arity);
}
/**
* Binds a standard XPathFunction
to this expression,
* handling a specific range of arguments. Subsequent calls to this
* method with the same name and range of arguments will silently
* replace the binding.
*
* Per the JDK documentation, user-defined functions must occupy their
* own namespace. If the qualified name that you pass to this method
* includes a prefix, the associated namespace will be bound to that
* prefix. If not, you must bind the namespace explicitly. In either
* case, you must refer to the function in your expression using a
* bound prefix.
*
* @param name The qualified name for this function. Must contain
* a name and namespace, may contain a prefix.
* @param func The function to bind to this name.
* @param minArity The minimum number of arguments accepted by this
* function.
* @param maxArity The maximum number of arguments accepted by this
* function.
*
* @return The wrapper, so that calls may be chained.
*/
public XPathWrapper bindFunction(QName name, XPathFunction func,
int minArity, int maxArity)
{
_functions.addFunction(func, name, minArity, maxArity);
if (!"".equals(name.getPrefix()))
bindNamespace(name.getPrefix(), name.getNamespaceURI());
return this;
}
/**
* Attaches a pre-build {@link FunctionResolver} to this wrapper,
* replacing the existing resolver (and removing all bound functions).
* This method is intended for use by {@link XPathWrapperFactory}, but
* is exposed for applications that wish to manage their own XPaths.
*
* @since 1.1.12
*/
public XPathWrapper setFunctionResolver(FunctionResolver resolver)
{
_functions = resolver;
return this;
}
/**
* Applies this expression to the the specified node, converting the
* resulting NodeList
into a java.util.List
.
*/
public List evaluate(Node context)
{
return DomUtil.asList(
evaluate(context, XPathConstants.NODESET, NodeList.class),
Node.class);
}
/**
* Applies this expression to the the specified node, converting the
* resulting NodeList
into a java.util.List
* containing only nodes of the specified type.
*/
public List evaluate(Node context, Class klass)
{
return DomUtil.filter(
evaluate(context, XPathConstants.NODESET, NodeList.class),
klass);
}
/**
* Applies this expression to the the specified node, and returning the
* first Element
from the resulting nodelist. Returns
* null
if there is no element in the list.
*
* @since 1.1.11
*/
public Element evaluateAsElement(Node context)
{
NodeList nodelist = evaluate(context, XPathConstants.NODESET, NodeList.class);
int size = nodelist.getLength();
for (int ii = 0 ; ii < size ; ii++)
{
Node node = nodelist.item(ii);
if (node instanceof Element)
return (Element)node;
}
return null;
}
/**
* Applies this expression to the specified node, requesting the
* STRING
return type.
*/
public String evaluateAsString(Node context)
{
return evaluate(context, XPathConstants.STRING, String.class);
}
/**
* Applies this expression to the specified node, requesting the
* NODESET
return type, and then retrieving the text
* content for each returned node.
*
* Note: this method calls Node.getTextContent()
in
* keeping with normal XPath behavior. If you want to get
* only the immediate child text nodes, evaluate as a list
* of elements and use DomUtil.getText()
.
*
* @since 1.1.3
*/
public List evaluateAsStringList(Node context)
{
List nodes = evaluate(context);
List result = new ArrayList(nodes.size());
for (Node node : nodes)
result.add(node.getTextContent());
return result;
}
/**
* Applies this expression to the specified node, requesting the
* NUMBER
return type.
*/
public Number evaluateAsNumber(Node context)
{
return evaluate(context, XPathConstants.NUMBER, Number.class);
}
/**
* Applies this expression to the the specified node, requesting the
* BOOLEAN
return type.
*/
public Boolean evaluateAsBoolean(Node context)
{
return evaluate(context, XPathConstants.BOOLEAN, Boolean.class);
}
//----------------------------------------------------------------------------
// Overrides of Object
//----------------------------------------------------------------------------
/**
* Two instances are considered equal if they have the same expression,
* namespace mappings, variable mappings, and function mappings. Note
* that instances with function mappings are all but guaranteed to be
* not-equal, unless you override the equals()
method on
* the function implementation class.
*/
@Override
public final boolean equals(Object obj)
{
if (obj instanceof XPathWrapper)
{
XPathWrapper that = (XPathWrapper)obj;
return this._expr.equals(that._expr)
&& this._nsResolver.equals(that._nsResolver)
&& this._variables.equals(that._variables)
&& this._functions.equals(that._functions);
}
return false;
}
/**
* Hash code is driven by the expression, ignoring differences between
* namespaces, variables, and functions.
*/
@Override
public int hashCode()
{
return _expr.hashCode();
}
/**
* The string value is the expression.
*/
@Override
public String toString()
{
return _expr;
}
/**
* Creates a shallow clone of this object, omitting the compiled
* XPath expression. This method exists to support thread-local instances,
* which will recompile the expression.
*/
@Override
protected XPathWrapper clone() throws CloneNotSupportedException
{
XPathWrapper newInstance = (XPathWrapper)super.clone();
newInstance._compiled = null;
return newInstance;
}
//----------------------------------------------------------------------------
// Internals
//----------------------------------------------------------------------------
/**
* Compiles the expression, if it has not already been compiled. This is
* called from the various evaluate
methods, and ensures
* that the caller has completely configured the wrapper prior to use.
*/
private void compileIfNeeded()
{
if (_compiled != null)
return;
try
{
XPathFactory fact = null;
synchronized (XPathFactory.class)
{
fact = XPathFactory.newInstance();
}
XPath xpath = fact.newXPath();
xpath.setNamespaceContext(_nsResolver);
xpath.setXPathVariableResolver(new MyVariableResolver());
xpath.setXPathFunctionResolver(_functions);
_compiled = xpath.compile(_expr);
}
catch (XPathExpressionException ee)
{
throw new XmlException("unable to compile: " + _expr, ee);
}
}
/**
* The base evaluation method: compiles the XPath, applies it as instructed
* by the caller, and casts the result (also as instructed).
*/
private T evaluate(Node context, QName returnType, Class castTo)
{
compileIfNeeded();
try
{
return castTo.cast(_compiled.evaluate(context, returnType));
}
catch (Exception ee)
{
throw new XmlException("unable to evaluate: " + _expr, ee);
}
}
/**
* Resolver for variable references.
*/
private class MyVariableResolver
implements XPathVariableResolver
{
public Object resolveVariable(QName name)
{
return _variables.get(name);
}
}
}