org.apache.commons.jxpath.JXPathContext Maven / Gradle / Ivy
/*
* 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.
*/
package org.apache.commons.jxpath;
import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import org.apache.commons.jxpath.util.KeyManagerUtils;
/**
* JXPathContext provides APIs for the traversal of graphs of JavaBeans using
* the XPath syntax. Using JXPathContext, you can read and write properties of
* JavaBeans, arrays, collections and maps. JXPathContext uses JavaBeans
* introspection to enumerate and access JavaBeans properties.
*
* JXPathContext allows alternative implementations. This is why instead of
* allocating JXPathContext directly, you should call a static
* newContext
method. This method will utilize the
* {@link JXPathContextFactory} API to locate a suitable implementation of
* JXPath. Bundled with JXPath comes a default implementation called Reference
* Implementation.
*
*
* JXPath Interprets XPath Syntax on Java Object Graphs
*
* JXPath uses an intuitive interpretation of the xpath syntax in the context
* of Java object graphs. Here are some examples:
*
* Example 1: JavaBean Property Access
*
* JXPath can be used to access properties of a JavaBean.
*
*
* public class Employee {
* public String getFirstName(){
* ...
* }
* }
*
* Employee emp = new Employee();
* ...
*
* JXPathContext context = JXPathContext.newContext(emp);
* String fName = (String)context.getValue("firstName");
*
*
* In this example, we are using JXPath to access a property of the
* emp
bean. In this simple case the invocation of JXPath is
* equivalent to invocation of getFirstName() on the bean.
*
* Example 2: Nested Bean Property Access
* JXPath can traverse object graphs:
*
*
* public class Employee {
* public Address getHomeAddress(){
* ...
* }
* }
* public class Address {
* public String getStreetNumber(){
* ...
* }
* }
*
* Employee emp = new Employee();
* ...
*
* JXPathContext context = JXPathContext.newContext(emp);
* String sNumber = (String)context.getValue("homeAddress/streetNumber");
*
*
* In this case XPath is used to access a property of a nested bean.
*
* A property identified by the xpath does not have to be a "leaf" property.
* For instance, we can extract the whole Address object in above example:
*
*
* Address addr = (Address)context.getValue("homeAddress");
*
*
*
* Example 3: Collection Subscripts
* JXPath can extract elements from arrays and collections.
*
*
* public class Integers {
* public int[] getNumbers(){
* ...
* }
* }
*
* Integers ints = new Integers();
* ...
*
* JXPathContext context = JXPathContext.newContext(ints);
* Integer thirdInt = (Integer)context.getValue("numbers[3]");
*
* A collection can be an arbitrary array or an instance of java.util.
* Collection.
*
* Note: in XPath the first element of a collection has index 1, not 0.
*
*
Example 4: Map Element Access
*
* JXPath supports maps. To get a value use its key.
*
*
* public class Employee {
* public Map getAddresses(){
* return addressMap;
* }
*
* public void addAddress(String key, Address address){
* addressMap.put(key, address);
* }
* ...
* }
*
* Employee emp = new Employee();
* emp.addAddress("home", new Address(...));
* emp.addAddress("office", new Address(...));
* ...
*
* JXPathContext context = JXPathContext.newContext(emp);
* String homeZipCode = (String)context.getValue("addresses/home/zipCode");
*
*
* Often you will need to use the alternative syntax for accessing Map
* elements:
*
*
* String homeZipCode =
* (String) context.getValue("addresses[@name='home']/zipCode");
*
*
* In this case, the key can be an expression, e.g. a variable.
*
* Note: At this point JXPath only supports Maps that use strings for keys.
* Note: JXPath supports the extended notion of Map: any object with
* dynamic properties can be handled by JXPath provided that its
* class is registered with the {@link JXPathIntrospector}.
*
* Example 5: Retrieving Multiple Results
*
* JXPath can retrieve multiple objects from a graph. Note that the method
* called in this case is not getValue
, but iterate
.
*
*
* public class Author {
* public Book[] getBooks(){
* ...
* }
* }
*
* Author auth = new Author();
* ...
*
* JXPathContext context = JXPathContext.newContext(auth);
* Iterator threeBooks = context.iterate("books[position() < 4]");
*
*
* This returns a list of at most three books from the array of all books
* written by the author.
*
* Example 6: Setting Properties
* JXPath can be used to modify property values.
*
*
* public class Employee {
* public Address getAddress() {
* ...
* }
*
* public void setAddress(Address address) {
* ...
* }
* }
*
* Employee emp = new Employee();
* Address addr = new Address();
* ...
*
* JXPathContext context = JXPathContext.newContext(emp);
* context.setValue("address", addr);
* context.setValue("address/zipCode", "90190");
*
*
*
* Example 7: Creating objects
* JXPath can be used to create new objects. First, create a subclass of {@link
* AbstractFactory AbstractFactory} and install it on the JXPathContext. Then
* call {@link JXPathContext#createPath createPathAndSetValue()} instead of
* "setValue". JXPathContext will invoke your AbstractFactory when it discovers
* that an intermediate node of the path is null. It will not override
* existing nodes.
*
*
* public class AddressFactory extends AbstractFactory {
* public boolean createObject(JXPathContext context,
* Pointer pointer, Object parent, String name, int index){
* if ((parent instanceof Employee) && name.equals("address"){
* ((Employee)parent).setAddress(new Address());
* return true;
* }
* return false;
* }
* }
*
* JXPathContext context = JXPathContext.newContext(emp);
* context.setFactory(new AddressFactory());
* context.createPathAndSetValue("address/zipCode", "90190");
*
*
* Example 8: Using Variables
* JXPath supports the notion of variables. The XPath syntax for accessing
* variables is "$varName".
*
*
* public class Author {
* public Book[] getBooks(){
* ...
* }
* }
*
* Author auth = new Author();
* ...
*
* JXPathContext context = JXPathContext.newContext(auth);
* context.getVariables().declareVariable("index", new Integer(2));
*
* Book secondBook = (Book)context.getValue("books[$index]");
*
*
* You can also set variables using JXPath:
*
*
* context.setValue("$index", new Integer(3));
*
*
* Note: you can only change the value of an existing variable this
* way, you cannot define a new variable.
*
*
* When a variable contains a JavaBean or a collection, you can
* traverse the bean or collection as well:
*
* ...
* context.getVariables().declareVariable("book", myBook);
* String title = (String)context.getValue("$book/title);
*
* Book array[] = new Book[]{...};
*
* context.getVariables().declareVariable("books", array);
*
* String title = (String)context.getValue("$books[2]/title);
*
*
* Example 9: Using Nested Contexts
* If you need to use the same set of variable while interpreting XPaths with
* different beans, it makes sense to put the variables in a separate context
* and specify that context as a parent context every time you allocate a new
* JXPathContext for a JavaBean.
*
*
* JXPathContext varContext = JXPathContext.newContext(null);
* varContext.getVariables().declareVariable("title", "Java");
*
* JXPathContext context = JXPathContext.newContext(varContext, auth);
*
* Iterator javaBooks = context.iterate("books[title = $title]");
*
*
* Using Custom Variable Pools
* By default, JXPathContext creates a HashMap of variables. However,
* you can substitute a custom implementation of the Variables
* interface to make JXPath work with an alternative source of variables.
* For example, you can define implementations of Variables that
* cover a servlet context, HTTP request or any similar structure.
*
* Example 10: Using Standard Extension Functions
* Using the standard extension functions, you can call methods on objects,
* static methods on classes and create objects using any constructor.
* The class names should be fully qualified.
*
* Here's how you can create new objects:
*
* Book book =
* (Book) context.getValue(
* "org.apache.commons.jxpath.example.Book.new ('John Updike')");
*
*
* Here's how you can call static methods:
*
* Book book =
* (Book) context.getValue(
* "org. apache.commons.jxpath.example.Book.getBestBook('John Updike')");
*
*
* Here's how you can call regular methods:
*
* String firstName = (String)context.getValue("getAuthorsFirstName($book)");
*
* As you can see, the target of the method is specified as the first parameter
* of the function.
*
* Example 11: Using Custom Extension Functions
* Collections of custom extension functions can be implemented
* as {@link Functions Functions} objects or as Java classes, whose methods
* become extenstion functions.
*
* Let's say the following class implements various formatting operations:
*
* public class Formats {
* public static String date(Date d, String pattern){
* return new SimpleDateFormat(pattern).format(d);
* }
* ...
* }
*
*
* We can register this class with a JXPathContext:
*
*
* context.setFunctions(new ClassFunctions(Formats.class, "format"));
* ...
*
* context.getVariables().declareVariable("today", new Date());
* String today = (String)context.getValue("format:date($today, 'MM/dd/yyyy')");
*
*
* You can also register whole packages of Java classes using PackageFunctions.
*
* Also, see {@link FunctionLibrary FunctionLibrary}, which is a class
* that allows you to register multiple sets of extension functions with
* the same JXPathContext.
*
*
Configuring JXPath
*
* JXPath uses JavaBeans introspection to discover properties of JavaBeans.
* You can provide alternative property lists by supplying
* custom JXPathBeanInfo classes (see {@link JXPathBeanInfo JXPathBeanInfo}).
*
* Notes
*
* - JXPath does not support DOM attributes for non-DOM objects. Even though
* XPaths like "para[@type='warning']" are legitimate, they will always produce
* empty results. The only attribute supported for JavaBeans is "name". The
* XPath "foo/bar" is equivalent to "foo[@name='bar']".
*
*
* See XPath Tutorial by
* W3Schools
. Also see XML Path
* Language (XPath) Version 1.0
*
* You will also find more information and examples in
*
* JXPath User's Guide
*
*
* @author Dmitri Plotnikov
* @version $Revision: 652845 $ $Date: 2008-05-02 12:46:46 -0500 (Fri, 02 May 2008) $
*/
public abstract class JXPathContext {
private static JXPathContextFactory contextFactory;
private static JXPathContext compilationContext;
private static final PackageFunctions GENERIC_FUNCTIONS =
new PackageFunctions("", null);
/** parent context */
protected JXPathContext parentContext;
/** context bean */
protected Object contextBean;
/** variables */
protected Variables vars;
/** functions */
protected Functions functions;
/** AbstractFactory */
protected AbstractFactory factory;
/** IdentityManager */
protected IdentityManager idManager;
/** KeyManager */
protected KeyManager keyManager;
/** decimal format map */
protected HashMap decimalFormats;
private Locale locale;
private boolean lenientSet = false;
private boolean lenient = false;
/**
* Creates a new JXPathContext with the specified object as the root node.
* @param contextBean Object
* @return JXPathContext
*/
public static JXPathContext newContext(Object contextBean) {
return getContextFactory().newContext(null, contextBean);
}
/**
* Creates a new JXPathContext with the specified bean as the root node and
* the specified parent context. Variables defined in a parent context can
* be referenced in XPaths passed to the child context.
* @param parentContext parent context
* @param contextBean Object
* @return JXPathContext
*/
public static JXPathContext newContext(
JXPathContext parentContext,
Object contextBean) {
return getContextFactory().newContext(parentContext, contextBean);
}
/**
* Acquires a context factory and caches it.
* @return JXPathContextFactory
*/
private static JXPathContextFactory getContextFactory () {
if (contextFactory == null) {
contextFactory = JXPathContextFactory.newInstance();
}
return contextFactory;
}
/**
* This constructor should remain protected - it is to be overridden by
* subclasses, but never explicitly invoked by clients.
* @param parentContext parent context
* @param contextBean Object
*/
protected JXPathContext(JXPathContext parentContext, Object contextBean) {
this.parentContext = parentContext;
this.contextBean = contextBean;
}
/**
* Returns the parent context of this context or null.
* @return JXPathContext
*/
public JXPathContext getParentContext() {
return parentContext;
}
/**
* Returns the JavaBean associated with this context.
* @return Object
*/
public Object getContextBean() {
return contextBean;
}
/**
* Returns a Pointer for the context bean.
* @return Pointer
*/
public abstract Pointer getContextPointer();
/**
* Returns a JXPathContext that is relative to the current JXPathContext.
* The supplied pointer becomes the context pointer of the new context.
* The relative context inherits variables, extension functions, locale etc
* from the parent context.
* @param pointer Pointer
* @return JXPathContext
*/
public abstract JXPathContext getRelativeContext(Pointer pointer);
/**
* Installs a custom implementation of the Variables interface.
* @param vars Variables
*/
public void setVariables(Variables vars) {
this.vars = vars;
}
/**
* Returns the variable pool associated with the context. If no such
* pool was specified with the {@link #setVariables} method,
* returns the default implementation of Variables,
* {@link BasicVariables BasicVariables}.
* @return Variables
*/
public Variables getVariables() {
if (vars == null) {
vars = new BasicVariables();
}
return vars;
}
/**
* Install a library of extension functions.
* @param functions Functions
* @see FunctionLibrary
*/
public void setFunctions(Functions functions) {
this.functions = functions;
}
/**
* Returns the set of functions installed on the context.
* @return Functions
*/
public Functions getFunctions() {
if (functions != null) {
return functions;
}
if (parentContext == null) {
return GENERIC_FUNCTIONS;
}
return null;
}
/**
* Install an abstract factory that should be used by the
* createPath()
and createPathAndSetValue()
* methods.
* @param factory AbstractFactory
*/
public void setFactory(AbstractFactory factory) {
this.factory = factory;
}
/**
* Returns the AbstractFactory installed on this context.
* If none has been installed and this context has a parent context,
* returns the parent's factory. Otherwise returns null.
* @return AbstractFactory
*/
public AbstractFactory getFactory() {
if (factory == null && parentContext != null) {
return parentContext.getFactory();
}
return factory;
}
/**
* Set the locale for this context. The value of the "lang"
* attribute as well as the the lang() function will be
* affected by the locale. By default, JXPath uses
* Locale.getDefault()
* @param locale Locale
*/
public synchronized void setLocale(Locale locale) {
this.locale = locale;
}
/**
* Returns the locale set with setLocale. If none was set and
* the context has a parent, returns the parent's locale.
* Otherwise, returns Locale.getDefault().
* @return Locale
*/
public synchronized Locale getLocale() {
if (locale == null) {
if (parentContext != null) {
return parentContext.getLocale();
}
locale = Locale.getDefault();
}
return locale;
}
/**
* Sets {@link DecimalFormatSymbols} for a given name. The DecimalFormatSymbols
* can be referenced as the third, optional argument in the invocation of
* format-number (number,format,decimal-format-name)
function.
* By default, JXPath uses the symbols for the current locale.
*
* @param name the format name or null for default format.
* @param symbols DecimalFormatSymbols
*/
public synchronized void setDecimalFormatSymbols(String name,
DecimalFormatSymbols symbols) {
if (decimalFormats == null) {
decimalFormats = new HashMap();
}
decimalFormats.put(name, symbols);
}
/**
* Get the named DecimalFormatSymbols.
* @param name key
* @return DecimalFormatSymbols
* @see #setDecimalFormatSymbols(String, DecimalFormatSymbols)
*/
public synchronized DecimalFormatSymbols getDecimalFormatSymbols(String name) {
if (decimalFormats == null) {
return parentContext == null ? null : parentContext.getDecimalFormatSymbols(name);
}
return (DecimalFormatSymbols) decimalFormats.get(name);
}
/**
* If the context is in the lenient mode, then getValue() returns null
* for inexistent paths. Otherwise, a path that does not map to
* an existing property will throw an exception. Note that if the
* property exists, but its value is null, the exception is not
* thrown.
*
* By default, lenient = false
* @param lenient flag
*/
public synchronized void setLenient(boolean lenient) {
this.lenient = lenient;
lenientSet = true;
}
/**
* Learn whether this JXPathContext is lenient.
* @return boolean
* @see #setLenient(boolean)
*/
public synchronized boolean isLenient() {
if (!lenientSet && parentContext != null) {
return parentContext.isLenient();
}
return lenient;
}
/**
* Compiles the supplied XPath and returns an internal representation
* of the path that can then be evaluated. Use CompiledExpressions
* when you need to evaluate the same expression multiple times
* and there is a convenient place to cache CompiledExpression
* between invocations.
* @param xpath to compile
* @return CompiledExpression
*/
public static CompiledExpression compile(String xpath) {
if (compilationContext == null) {
compilationContext = JXPathContext.newContext(null);
}
return compilationContext.compilePath(xpath);
}
/**
* Overridden by each concrete implementation of JXPathContext
* to perform compilation. Is called by compile()
.
* @param xpath to compile
* @return CompiledExpression
*/
protected abstract CompiledExpression compilePath(String xpath);
/**
* Finds the first object that matches the specified XPath. It is equivalent
* to getPointer(xpath).getNode()
. Note that this method
* produces the same result as getValue()
on object models
* like JavaBeans, but a different result for DOM/JDOM etc., because it
* returns the Node itself, rather than its textual contents.
*
* @param xpath the xpath to be evaluated
* @return the found object
*/
public Object selectSingleNode(String xpath) {
Pointer pointer = getPointer(xpath);
return pointer == null ? null : pointer.getNode();
}
/**
* Finds all nodes that match the specified XPath.
*
* @param xpath the xpath to be evaluated
* @return a list of found objects
*/
public List selectNodes(String xpath) {
ArrayList list = new ArrayList();
Iterator iterator = iteratePointers(xpath);
while (iterator.hasNext()) {
Pointer pointer = (Pointer) iterator.next();
list.add(pointer.getNode());
}
return list;
}
/**
* Evaluates the xpath and returns the resulting object. Primitive
* types are wrapped into objects.
* @param xpath to evaluate
* @return Object found
*/
public abstract Object getValue(String xpath);
/**
* Evaluates the xpath, converts the result to the specified class and
* returns the resulting object.
* @param xpath to evaluate
* @param requiredType required type
* @return Object found
*/
public abstract Object getValue(String xpath, Class requiredType);
/**
* Modifies the value of the property described by the supplied xpath.
* Will throw an exception if one of the following conditions occurs:
*
* - The xpath does not in fact describe an existing property
*
- The property is not writable (no public, non-static set method)
*
* @param xpath indicating position
* @param value to set
*/
public abstract void setValue(String xpath, Object value);
/**
* Creates missing elements of the path by invoking an {@link AbstractFactory},
* which should first be installed on the context by calling {@link #setFactory}.
*
* Will throw an exception if the AbstractFactory fails to create
* an instance for a path element.
* @param xpath indicating destination to create
* @return pointer to new location
*/
public abstract Pointer createPath(String xpath);
/**
* The same as setValue, except it creates intermediate elements of
* the path by invoking an {@link AbstractFactory}, which should first be
* installed on the context by calling {@link #setFactory}.
*
* Will throw an exception if one of the following conditions occurs:
*
* - Elements of the xpath aleady exist, but the path does not in
* fact describe an existing property
*
- The AbstractFactory fails to create an instance for an intermediate
* element.
*
- The property is not writable (no public, non-static set method)
*
* @param xpath indicating position to create
* @param value to set
* @return pointer to new location
*/
public abstract Pointer createPathAndSetValue(String xpath, Object value);
/**
* Removes the element of the object graph described by the xpath.
* @param xpath indicating position to remove
*/
public abstract void removePath(String xpath);
/**
* Removes all elements of the object graph described by the xpath.
* @param xpath indicating positions to remove
*/
public abstract void removeAll(String xpath);
/**
* Traverses the xpath and returns an Iterator of all results found
* for the path. If the xpath matches no properties
* in the graph, the Iterator will be empty, but not null.
* @param xpath to iterate
* @return Iterator