/* * 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. * <p> * JXPathContext allows alternative implementations. This is why instead of * allocating JXPathContext directly, you should call a static * <code>newContext</code> 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. * </p> * * <h2>JXPath Interprets XPath Syntax on Java Object Graphs</h2> * * JXPath uses an intuitive interpretation of the xpath syntax in the context * of Java object graphs. Here are some examples: * * <h3>Example 1: JavaBean Property Access</h3> * * JXPath can be used to access properties of a JavaBean. * * <pre><blockquote> * public class Employee { * public String getFirstName(){ * ... * } * } * * Employee emp = new Employee(); * ... * * JXPathContext context = JXPathContext.newContext(emp); * String fName = (String)context.getValue("firstName"); * </blockquote></pre> * * In this example, we are using JXPath to access a property of the * <code>emp</code> bean. In this simple case the invocation of JXPath is * equivalent to invocation of getFirstName() on the bean. * * <h3>Example 2: Nested Bean Property Access</h3> * JXPath can traverse object graphs: * * <pre><blockquote> * 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"); * </blockquote></pre> * * In this case XPath is used to access a property of a nested bean. * <p> * 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: * * <pre><blockquote> * Address addr = (Address)context.getValue("homeAddress"); * </blockquote></pre> * </p> * * <h3>Example 3: Collection Subscripts</h3> * JXPath can extract elements from arrays and collections. * * <pre><blockquote> * public class Integers { * public int[] getNumbers(){ * ... * } * } * * Integers ints = new Integers(); * ... * * JXPathContext context = JXPathContext.newContext(ints); * Integer thirdInt = (Integer)context.getValue("numbers[3]"); * </blockquote></pre> * A collection can be an arbitrary array or an instance of java.util. * Collection. * <p> * Note: in XPath the first element of a collection has index 1, not 0.<br> * * <h3>Example 4: Map Element Access</h3> * * JXPath supports maps. To get a value use its key. * * <pre><blockquote> * 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"); * </blockquote></pre> * * Often you will need to use the alternative syntax for accessing Map * elements: * * <pre><blockquote> * String homeZipCode = * (String) context.getValue("addresses[@name='home']/zipCode"); * </blockquote></pre> * * In this case, the key can be an expression, e.g. a variable.<br> * * Note: At this point JXPath only supports Maps that use strings for keys.<br> * 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}. * * <h3>Example 5: Retrieving Multiple Results</h3> * * JXPath can retrieve multiple objects from a graph. Note that the method * called in this case is not <code>getValue</code>, but <code>iterate</code>. * * <pre><blockquote> * public class Author { * public Book[] getBooks(){ * ... * } * } * * Author auth = new Author(); * ... * * JXPathContext context = JXPathContext.newContext(auth); * Iterator threeBooks = context.iterate("books[position() < 4]"); * </blockquote></pre> * * This returns a list of at most three books from the array of all books * written by the author. * * <h3>Example 6: Setting Properties</h3> * JXPath can be used to modify property values. * * <pre><blockquote> * 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"); * * </blockquote></pre> * * <h3>Example 7: Creating objects</h3> * 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 <b>null</b>. It will not override * existing nodes. * * <pre><blockquote> * 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"); * </blockquote></pre> * * <h3>Example 8: Using Variables</h3> * JXPath supports the notion of variables. The XPath syntax for accessing * variables is <i>"$varName"</i>. * * <pre><blockquote> * 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]"); * </blockquote></pre> * * You can also set variables using JXPath: * * <pre><blockquote> * context.setValue("$index", new Integer(3)); * </blockquote></pre> * * Note: you can only <i>change</i> the value of an existing variable this * way, you cannot <i>define</i> a new variable. * * <p> * When a variable contains a JavaBean or a collection, you can * traverse the bean or collection as well: * <pre><blockquote> * ... * 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); * </blockquote></pre> * * <h3>Example 9: Using Nested Contexts</h3> * 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. * * <pre><blockquote> * JXPathContext varContext = JXPathContext.newContext(null); * varContext.getVariables().declareVariable("title", "Java"); * * JXPathContext context = JXPathContext.newContext(varContext, auth); * * Iterator javaBooks = context.iterate("books[title = $title]"); * </blockquote></pre> * * <h3>Using Custom Variable Pools</h3> * 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. * * <h3>Example 10: Using Standard Extension Functions</h3> * 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. * <p> * Here's how you can create new objects: * <pre><blockquote> * Book book = * (Book) context.getValue( * "org.apache.commons.jxpath.example.Book.new ('John Updike')"); * </blockquote></pre> * * Here's how you can call static methods: * <pre><blockquote> * Book book = * (Book) context.getValue( * "org. apache.commons.jxpath.example.Book.getBestBook('John Updike')"); * </blockquote></pre> * * Here's how you can call regular methods: * <pre><blockquote> * String firstName = (String)context.getValue("getAuthorsFirstName($book)"); * </blockquote></pre> * As you can see, the target of the method is specified as the first parameter * of the function. * * <h3>Example 11: Using Custom Extension Functions</h3> * Collections of custom extension functions can be implemented * as {@link Functions Functions} objects or as Java classes, whose methods * become extenstion functions. * <p> * Let's say the following class implements various formatting operations: * <pre><blockquote> * public class Formats { * public static String date(Date d, String pattern){ * return new SimpleDateFormat(pattern).format(d); * } * ... * } * </blockquote></pre> * * We can register this class with a JXPathContext: * * <pre><blockquote> * context.setFunctions(new ClassFunctions(Formats.class, "format")); * ... * * context.getVariables().declareVariable("today", new Date()); * String today = (String)context.getValue("format:date($today, 'MM/dd/yyyy')"); * * </blockquote></pre> * You can also register whole packages of Java classes using PackageFunctions. * <p> * Also, see {@link FunctionLibrary FunctionLibrary}, which is a class * that allows you to register multiple sets of extension functions with * the same JXPathContext. * * <h2>Configuring JXPath</h2> * * JXPath uses JavaBeans introspection to discover properties of JavaBeans. * You can provide alternative property lists by supplying * custom JXPathBeanInfo classes (see {@link JXPathBeanInfo JXPathBeanInfo}). * * <h2>Notes</h2> * <ul> * <li> 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']". * </ul> * * See <a href="http://www.w3schools.com/xpath">XPath Tutorial by * W3Schools</a><br>. Also see <a href="http://www.w3.org/TR/xpath">XML Path * Language (XPath) Version 1.0</a><br><br> * * You will also find more information and examples in * <a href="http://commons.apache.org/jxpath/users-guide.html"> * JXPath User's Guide</a> * * * @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 * <code>createPath()</code> and <code>createPathAndSetValue()</code> * 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 * <code>Locale.getDefault()</code> * @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 * <code>format-number (number,format,decimal-format-name)</code> 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 <i>not</i> * thrown. * <p> * 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 <code>compile()</code>. * @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 <code>getPointer(xpath).getNode()</code>. Note that this method * produces the same result as <code>getValue()</code> 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: * <ul> * <li>The xpath does not in fact describe an existing property * <li>The property is not writable (no public, non-static set method) * </ul> * @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}. * <p> * 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}. * <p> * Will throw an exception if one of the following conditions occurs: * <ul> * <li>Elements of the xpath aleady exist, but the path does not in * fact describe an existing property * <li>The AbstractFactory fails to create an instance for an intermediate * element. * <li>The property is not writable (no public, non-static set method) * </ul> * @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<Object> */ public abstract Iterator iterate(String xpath); /** * Traverses the xpath and returns a Pointer. * A Pointer provides easy access to a property. * If the xpath matches no properties * in the graph, the pointer will be null. * @param xpath desired * @return Pointer */ public abstract Pointer getPointer(String xpath); /** * Traverses the xpath and returns an Iterator of Pointers. * A Pointer provides easy access to a property. * If the xpath matches no properties * in the graph, the Iterator be empty, but not null. * @param xpath to iterate * @return Iterator<Pointer> */ public abstract Iterator iteratePointers(String xpath); /** * Install an identity manager that will be used by the context * to look up a node by its ID. * @param idManager IdentityManager to set */ public void setIdentityManager(IdentityManager idManager) { this.idManager = idManager; } /** * Returns this context's identity manager. If none has been installed, * returns the identity manager of the parent context. * @return IdentityManager */ public IdentityManager getIdentityManager() { if (idManager == null && parentContext != null) { return parentContext.getIdentityManager(); } return idManager; } /** * Locates a Node by its ID. * * @param id is the ID of the sought node. * @return Pointer */ public Pointer getPointerByID(String id) { IdentityManager manager = getIdentityManager(); if (manager != null) { return manager.getPointerByID(this, id); } throw new JXPathException( "Cannot find an element by ID - " + "no IdentityManager has been specified"); } /** * Install a key manager that will be used by the context * to look up a node by a key value. * @param keyManager KeyManager */ public void setKeyManager(KeyManager keyManager) { this.keyManager = keyManager; } /** * Returns this context's key manager. If none has been installed, * returns the key manager of the parent context. * @return KeyManager */ public KeyManager getKeyManager() { if (keyManager == null && parentContext != null) { return parentContext.getKeyManager(); } return keyManager; } /** * Locates a Node by a key value. * @param key string * @param value string * @return Pointer found */ public Pointer getPointerByKey(String key, String value) { KeyManager manager = getKeyManager(); if (manager != null) { return manager.getPointerByKey(this, key, value); } throw new JXPathException( "Cannot find an element by key - " + "no KeyManager has been specified"); } /** * Locates a NodeSet by key/value. * @param key string * @param value object * @return NodeSet found */ public NodeSet getNodeSetByKey(String key, Object value) { KeyManager manager = getKeyManager(); if (manager != null) { return KeyManagerUtils.getExtendedKeyManager(manager) .getNodeSetByKey(this, key, value); } throw new JXPathException("Cannot find an element by key - " + "no KeyManager has been specified"); } /** * Registers a namespace prefix. * * @param prefix A namespace prefix * @param namespaceURI A URI for that prefix */ public void registerNamespace(String prefix, String namespaceURI) { throw new UnsupportedOperationException( "Namespace registration is not implemented by " + getClass()); } /** * Given a prefix, returns a registered namespace URI. If the requested * prefix was not defined explicitly using the registerNamespace method, * JXPathContext will then check the context node to see if the prefix is * defined there. See * {@link #setNamespaceContextPointer(Pointer) setNamespaceContextPointer}. * * @param prefix The namespace prefix to look up * @return namespace URI or null if the prefix is undefined. */ public String getNamespaceURI(String prefix) { throw new UnsupportedOperationException( "Namespace registration is not implemented by " + getClass()); } /** * Get the prefix associated with the specifed namespace URI. * @param namespaceURI the ns URI to check. * @return String prefix * @since JXPath 1.3 */ public String getPrefix(String namespaceURI) { throw new UnsupportedOperationException( "Namespace registration is not implemented by " + getClass()); } /** * Namespace prefixes can be defined implicitly by specifying a pointer to a * context where the namespaces are defined. By default, * NamespaceContextPointer is the same as the Context Pointer, see * {@link #getContextPointer() getContextPointer()} * * @param namespaceContextPointer The pointer to the context where prefixes used in * XPath expressions should be resolved. */ public void setNamespaceContextPointer(Pointer namespaceContextPointer) { throw new UnsupportedOperationException( "Namespace registration is not implemented by " + getClass()); } /** * Returns the namespace context pointer set with * {@link #setNamespaceContextPointer(Pointer) setNamespaceContextPointer()} * or, if none has been specified, the context pointer otherwise. * * @return The namespace context pointer. */ public Pointer getNamespaceContextPointer() { throw new UnsupportedOperationException( "Namespace registration is not implemented by " + getClass()); } }