/*
 * 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.configuration.tree.xpath;

import java.util.Collections;
import java.util.List;
import java.util.StringTokenizer;

import org.apache.commons.configuration.tree.ConfigurationNode;
import org.apache.commons.configuration.tree.ExpressionEngine;
import org.apache.commons.configuration.tree.NodeAddData;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
import org.apache.commons.lang.StringUtils;

A specialized implementation of the ExpressionEngine interface that is able to evaluate XPATH expressions.

This class makes use of Commons JXPath for handling XPath expressions and mapping them to the nodes of a hierarchical configuration. This makes the rich and powerful XPATH syntax available for accessing properties from a configuration object.

For selecting properties arbitrary XPATH expressions can be used, which select single or multiple configuration nodes. The associated Configuration instance will directly pass the specified property keys into this engine. If a key is not syntactically correct, an exception will be thrown.

For adding new properties, this expression engine uses a specific syntax: the "key" of a new property must consist of two parts that are separated by whitespace:

  1. An XPATH expression selecting a single node, to which the new element(s) are to be added. This can be an arbitrary complex expression, but it must select exactly one node, otherwise an exception will be thrown.
  2. The name of the new element(s) to be added below this parent node. Here either a single node name or a complete path of nodes (separated by the "/" character or "@" for an attribute) can be specified.
Some examples for valid keys that can be passed into the configuration's addProperty() method follow:

"/tables/table[1] type"

This will add a new type node as a child of the first table element.

"/tables/table[1] @type"

Similar to the example above, but this time a new attribute named type will be added to the first table element.

"/tables table/fields/field/name"

This example shows how a complex path can be added. Parent node is the tables element. Here a new branch consisting of the nodes table, fields, field, and name will be added.

"/tables table/fields/field@type"

This is similar to the last example, but in this case a complex path ending with an attribute is defined.

Note: This extended syntax for adding properties only works with the addProperty() method. setProperty() does not support creating new nodes this way.

From version 1.7 on, it is possible to use regular keys in calls to addProperty() (i.e. keys that do not have to contain a whitespace as delimiter). In this case the key is evaluated, and the biggest part pointing to an existing node is determined. The remaining part is then added as new path. As an example consider the key

"tables/table[last()]/fields/field/name"
If the key does not point to an existing node, the engine will check the paths "tables/table[last()]/fields/field", "tables/table[last()]/fields", "tables/table[last()]", and so on, until a key is found which points to a node. Let's assume that the last key listed above can be resolved in this way. Then from this key the following key is derived: "tables/table[last()] fields/field/name" by appending the remaining part after a whitespace. This key can now be processed using the original algorithm. Keys of this form can also be used with the setProperty() method. However, it is still recommended to use the old format because it makes explicit at which position new nodes should be added. For keys without a whitespace delimiter there may be ambiguities.

Author:Commons Configuration team
Since:1.3
Version:$Id: XPathExpressionEngine.java 1206563 2011-11-26 19:47:26Z oheger $
/** * <p> * A specialized implementation of the {@code ExpressionEngine} interface * that is able to evaluate XPATH expressions. * </p> * <p> * This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons * JXPath</a> for handling XPath expressions and mapping them to the nodes of a * hierarchical configuration. This makes the rich and powerful XPATH syntax * available for accessing properties from a configuration object. * </p> * <p> * For selecting properties arbitrary XPATH expressions can be used, which * select single or multiple configuration nodes. The associated * {@code Configuration} instance will directly pass the specified property * keys into this engine. If a key is not syntactically correct, an exception * will be thrown. * </p> * <p> * For adding new properties, this expression engine uses a specific syntax: the * &quot;key&quot; of a new property must consist of two parts that are * separated by whitespace: * <ol> * <li>An XPATH expression selecting a single node, to which the new element(s) * are to be added. This can be an arbitrary complex expression, but it must * select exactly one node, otherwise an exception will be thrown.</li> * <li>The name of the new element(s) to be added below this parent node. Here * either a single node name or a complete path of nodes (separated by the * &quot;/&quot; character or &quot;@&quot; for an attribute) can be specified.</li> * </ol> * Some examples for valid keys that can be passed into the configuration's * {@code addProperty()} method follow: * </p> * <p> * * <pre> * &quot;/tables/table[1] type&quot; * </pre> * * </p> * <p> * This will add a new {@code type} node as a child of the first * {@code table} element. * </p> * <p> * * <pre> * &quot;/tables/table[1] @type&quot; * </pre> * * </p> * <p> * Similar to the example above, but this time a new attribute named * {@code type} will be added to the first {@code table} element. * </p> * <p> * * <pre> * &quot;/tables table/fields/field/name&quot; * </pre> * * </p> * <p> * This example shows how a complex path can be added. Parent node is the * {@code tables} element. Here a new branch consisting of the nodes * {@code table}, {@code fields}, {@code field}, and * {@code name} will be added. * </p> * <p> * * <pre> * &quot;/tables table/fields/field@type&quot; * </pre> * * </p> * <p> * This is similar to the last example, but in this case a complex path ending * with an attribute is defined. * </p> * <p> * <strong>Note:</strong> This extended syntax for adding properties only works * with the {@code addProperty()} method. {@code setProperty()} does * not support creating new nodes this way. * </p> * <p> * From version 1.7 on, it is possible to use regular keys in calls to * {@code addProperty()} (i.e. keys that do not have to contain a * whitespace as delimiter). In this case the key is evaluated, and the biggest * part pointing to an existing node is determined. The remaining part is then * added as new path. As an example consider the key * * <pre> * &quot;tables/table[last()]/fields/field/name&quot; * </pre> * * If the key does not point to an existing node, the engine will check the * paths {@code "tables/table[last()]/fields/field"}, * {@code "tables/table[last()]/fields"}, * {@code "tables/table[last()]"}, and so on, until a key is * found which points to a node. Let's assume that the last key listed above can * be resolved in this way. Then from this key the following key is derived: * {@code "tables/table[last()] fields/field/name"} by appending * the remaining part after a whitespace. This key can now be processed using * the original algorithm. Keys of this form can also be used with the * {@code setProperty()} method. However, it is still recommended to use * the old format because it makes explicit at which position new nodes should * be added. For keys without a whitespace delimiter there may be ambiguities. * </p> * * @since 1.3 * @author <a * href="http://commons.apache.org/configuration/team-list.html">Commons * Configuration team</a> * @version $Id: XPathExpressionEngine.java 1206563 2011-11-26 19:47:26Z oheger $ */
public class XPathExpressionEngine implements ExpressionEngine {
Constant for the path delimiter.
/** Constant for the path delimiter. */
static final String PATH_DELIMITER = "/";
Constant for the attribute delimiter.
/** Constant for the attribute delimiter. */
static final String ATTR_DELIMITER = "@";
Constant for the delimiters for splitting node paths.
/** Constant for the delimiters for splitting node paths. */
private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER + ATTR_DELIMITER;
Constant for a space which is used as delimiter in keys for adding properties.
/** * Constant for a space which is used as delimiter in keys for adding * properties. */
private static final String SPACE = " ";
Executes a query. The passed in property key is directly passed to a JXPath context.
Params:
  • root – the configuration root node
  • key – the query to be executed
Returns:a list with the nodes that are selected by the query
/** * Executes a query. The passed in property key is directly passed to a * JXPath context. * * @param root the configuration root node * @param key the query to be executed * @return a list with the nodes that are selected by the query */
public List<ConfigurationNode> query(ConfigurationNode root, String key) { if (StringUtils.isEmpty(key)) { return Collections.singletonList(root); } else { JXPathContext context = createContext(root, key); // This is safe because our node pointer implementations will return // a list of configuration nodes. @SuppressWarnings("unchecked") List<ConfigurationNode> result = context.selectNodes(key); if (result == null) { result = Collections.emptyList(); } return result; } }
Returns a (canonical) key for the given node based on the parent's key. This implementation will create an XPATH expression that selects the given node (under the assumption that the passed in parent key is valid). As the nodeKey() implementation of DefaultExpressionEngine this method will not return indices for nodes. So all child nodes of a given parent with the same name will have the same key.
Params:
  • node – the node for which a key is to be constructed
  • parentKey – the key of the parent node
Returns:the key for the given node
/** * Returns a (canonical) key for the given node based on the parent's key. * This implementation will create an XPATH expression that selects the * given node (under the assumption that the passed in parent key is valid). * As the {@code nodeKey()} implementation of * {@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine} * this method will not return indices for nodes. So all child nodes of a * given parent with the same name will have the same key. * * @param node the node for which a key is to be constructed * @param parentKey the key of the parent node * @return the key for the given node */
public String nodeKey(ConfigurationNode node, String parentKey) { if (parentKey == null) { // name of the root node return StringUtils.EMPTY; } else if (node.getName() == null) { // paranoia check for undefined node names return parentKey; } else { StringBuilder buf = new StringBuilder(parentKey.length() + node.getName().length() + PATH_DELIMITER.length()); if (parentKey.length() > 0) { buf.append(parentKey); buf.append(PATH_DELIMITER); } if (node.isAttribute()) { buf.append(ATTR_DELIMITER); } buf.append(node.getName()); return buf.toString(); } }
Prepares an add operation for a configuration property. The expected format of the passed in key is explained in the class comment.
Params:
  • root – the configuration's root node
  • key – the key describing the target of the add operation and the path of the new node
Returns:a data object to be evaluated by the calling configuration object
/** * Prepares an add operation for a configuration property. The expected * format of the passed in key is explained in the class comment. * * @param root the configuration's root node * @param key the key describing the target of the add operation and the * path of the new node * @return a data object to be evaluated by the calling configuration object */
public NodeAddData prepareAdd(ConfigurationNode root, String key) { if (key == null) { throw new IllegalArgumentException( "prepareAdd: key must not be null!"); } String addKey = key; int index = findKeySeparator(addKey); if (index < 0) { addKey = generateKeyForAdd(root, addKey); index = findKeySeparator(addKey); } List<ConfigurationNode> nodes = query(root, addKey.substring(0, index).trim()); if (nodes.size() != 1) { throw new IllegalArgumentException( "prepareAdd: key must select exactly one target node!"); } NodeAddData data = new NodeAddData(); data.setParent(nodes.get(0)); initNodeAddData(data, addKey.substring(index).trim()); return data; }
Creates the JXPathContext used for executing a query. This method will create a new context and ensure that it is correctly initialized.
Params:
  • root – the configuration root node
  • key – the key to be queried
Returns:the new context
/** * Creates the {@code JXPathContext} used for executing a query. This * method will create a new context and ensure that it is correctly * initialized. * * @param root the configuration root node * @param key the key to be queried * @return the new context */
protected JXPathContext createContext(ConfigurationNode root, String key) { JXPathContext context = JXPathContext.newContext(root); context.setLenient(true); return context; }
Initializes most properties of a NodeAddData object. This method is called by prepareAdd() after the parent node has been found. Its task is to interpret the passed in path of the new node.
Params:
  • data – the data object to initialize
  • path – the path of the new node
/** * Initializes most properties of a {@code NodeAddData} object. This * method is called by {@code prepareAdd()} after the parent node has * been found. Its task is to interpret the passed in path of the new node. * * @param data the data object to initialize * @param path the path of the new node */
protected void initNodeAddData(NodeAddData data, String path) { String lastComponent = null; boolean attr = false; boolean first = true; StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS, true); while (tok.hasMoreTokens()) { String token = tok.nextToken(); if (PATH_DELIMITER.equals(token)) { if (attr) { invalidPath(path, " contains an attribute" + " delimiter at an unallowed position."); } if (lastComponent == null) { invalidPath(path, " contains a '/' at an unallowed position."); } data.addPathNode(lastComponent); lastComponent = null; } else if (ATTR_DELIMITER.equals(token)) { if (attr) { invalidPath(path, " contains multiple attribute delimiters."); } if (lastComponent == null && !first) { invalidPath(path, " contains an attribute delimiter at an unallowed position."); } if (lastComponent != null) { data.addPathNode(lastComponent); } attr = true; lastComponent = null; } else { lastComponent = token; } first = false; } if (lastComponent == null) { invalidPath(path, "contains no components."); } data.setNewNodeName(lastComponent); data.setAttribute(attr); }
Tries to generate a key for adding a property. This method is called if a key was used for adding properties which does not contain a space character. It splits the key at its single components and searches for the last existing component. Then a key compatible for adding properties is generated.
Params:
  • root – the root node of the configuration
  • key – the key in question
Returns:the key to be used for adding the property
/** * Tries to generate a key for adding a property. This method is called if a * key was used for adding properties which does not contain a space * character. It splits the key at its single components and searches for * the last existing component. Then a key compatible for adding properties * is generated. * * @param root the root node of the configuration * @param key the key in question * @return the key to be used for adding the property */
private String generateKeyForAdd(ConfigurationNode root, String key) { int pos = key.lastIndexOf(PATH_DELIMITER, key.length()); while (pos >= 0) { String keyExisting = key.substring(0, pos); if (!query(root, keyExisting).isEmpty()) { StringBuilder buf = new StringBuilder(key.length() + 1); buf.append(keyExisting).append(SPACE); buf.append(key.substring(pos + 1)); return buf.toString(); } pos = key.lastIndexOf(PATH_DELIMITER, pos - 1); } return SPACE + key; }
Helper method for throwing an exception about an invalid path.
Params:
  • path – the invalid path
  • msg – the exception message
/** * Helper method for throwing an exception about an invalid path. * * @param path the invalid path * @param msg the exception message */
private void invalidPath(String path, String msg) { throw new IllegalArgumentException("Invalid node path: \"" + path + "\" " + msg); }
Determines the position of the separator in a key for adding new properties. If no delimiter is found, result is -1.
Params:
  • key – the key
Returns:the position of the delimiter
/** * Determines the position of the separator in a key for adding new * properties. If no delimiter is found, result is -1. * * @param key the key * @return the position of the delimiter */
private static int findKeySeparator(String key) { int index = key.length() - 1; while (index >= 0 && !Character.isWhitespace(key.charAt(index))) { index--; } return index; } // static initializer: registers the configuration node pointer factory static { JXPathContextReferenceImpl .addNodePointerFactory(new ConfigurationNodePointerFactory()); } }