org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine Maven / Gradle / Ivy
Show all versions of commons-configuration2 Show documentation
/*
* 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.configuration2.tree.xpath;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import org.apache.commons.configuration2.tree.ExpressionEngine;
import org.apache.commons.configuration2.tree.NodeAddData;
import org.apache.commons.configuration2.tree.NodeHandler;
import org.apache.commons.configuration2.tree.QueryResult;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
import org.apache.commons.lang3.StringUtils;
/**
*
* A specialized implementation of the {@code 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
* {@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.
*
*
* 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:
*
*
* - 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.
* - 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
* {@code addProperty()} method follow:
*
*
*
* "/tables/table[1] type"
*
*
*
* This will add a new {@code type} node as a child of the first {@code table}
* element.
*
*
*
* "/tables/table[1] @type"
*
*
*
* Similar to the example above, but this time a new attribute named
* {@code type} will be added to the first {@code table} element.
*
*
*
* "/tables table/fields/field/name"
*
*
*
* 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.
*
*
*
* "/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 {@code addProperty()} method. {@code setProperty()} does not support
* creating new nodes this way.
*
*
* 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
*
*
*
* "tables/table[last()]/fields/field/name"
*
*
*
* 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.
*
*
* @since 1.3
* @version $Id: XPathExpressionEngine.java 1679792 2015-05-16 17:49:31Z oheger $
*/
public class XPathExpressionEngine implements ExpressionEngine
{
/** Constant for the path delimiter. */
static final String PATH_DELIMITER = "/";
/** Constant for the attribute delimiter. */
static final String ATTR_DELIMITER = "@";
/** 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.
*/
private static final String SPACE = " ";
/** Constant for a default size of a key buffer. */
private static final int BUF_SIZE = 128;
/** Constant for the start of an index expression. */
private static final char START_INDEX = '[';
/** Constant for the end of an index expression. */
private static final char END_INDEX = ']';
/** The internally used context factory. */
private final XPathContextFactory contextFactory;
/**
* Creates a new instance of {@code XPathExpressionEngine} with default
* settings.
*/
public XPathExpressionEngine()
{
this(new XPathContextFactory());
}
/**
* Creates a new instance of {@code XPathExpressionEngine} and sets the
* context factory. This constructor is mainly used for testing purposes.
*
* @param factory the {@code XPathContextFactory}
*/
XPathExpressionEngine(XPathContextFactory factory)
{
contextFactory = factory;
}
/**
* {@inheritDoc} This implementation interprets the passed in key as an XPATH
* expression.
*/
@Override
public List> query(T root, String key,
NodeHandler handler)
{
if (StringUtils.isEmpty(key))
{
QueryResult result = createResult(root);
return Collections.singletonList(result);
}
else
{
JXPathContext context = createContext(root, handler);
List results = context.selectNodes(key);
if (results == null)
{
results = Collections.emptyList();
}
return convertResults(results);
}
}
/**
* {@inheritDoc} This implementation creates 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.configuration2.tree.DefaultExpressionEngine
* DefaultExpressionEngine} this method does not return indices for nodes.
* So all child nodes of a given parent with the same name have the same
* key.
*/
@Override
public String nodeKey(T node, String parentKey, NodeHandler handler)
{
if (parentKey == null)
{
// name of the root node
return StringUtils.EMPTY;
}
else if (handler.nodeName(node) == null)
{
// paranoia check for undefined node names
return parentKey;
}
else
{
StringBuilder buf =
new StringBuilder(parentKey.length()
+ handler.nodeName(node).length()
+ PATH_DELIMITER.length());
if (parentKey.length() > 0)
{
buf.append(parentKey);
buf.append(PATH_DELIMITER);
}
buf.append(handler.nodeName(node));
return buf.toString();
}
}
@Override
public String attributeKey(String parentKey, String attributeName)
{
StringBuilder buf =
new StringBuilder(StringUtils.length(parentKey)
+ StringUtils.length(attributeName)
+ PATH_DELIMITER.length() + ATTR_DELIMITER.length());
if (StringUtils.isNotEmpty(parentKey))
{
buf.append(parentKey).append(PATH_DELIMITER);
}
buf.append(ATTR_DELIMITER).append(attributeName);
return buf.toString();
}
/**
* {@inheritDoc} This implementation works similar to {@code nodeKey()}, but
* always adds an index expression to the resulting key.
*/
@Override
public String canonicalKey(T node, String parentKey,
NodeHandler handler)
{
T parent = handler.getParent(node);
if (parent == null)
{
// this is the root node
return StringUtils.defaultString(parentKey);
}
StringBuilder buf = new StringBuilder(BUF_SIZE);
if (StringUtils.isNotEmpty(parentKey))
{
buf.append(parentKey).append(PATH_DELIMITER);
}
buf.append(handler.nodeName(node));
buf.append(START_INDEX);
buf.append(determineIndex(parent, node, handler));
buf.append(END_INDEX);
return buf.toString();
}
/**
* {@inheritDoc} The expected format of the passed in key is explained in
* the class comment.
*/
@Override
public NodeAddData prepareAdd(T root, String key,
NodeHandler handler)
{
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, handler);
index = findKeySeparator(addKey);
}
else if (index >= addKey.length() - 1)
{
invalidPath(addKey, " new node path must not be empty.");
}
List> nodes =
query(root, addKey.substring(0, index).trim(), handler);
if (nodes.size() != 1)
{
throw new IllegalArgumentException("prepareAdd: key '" + key
+ "' must select exactly one target node!");
}
return createNodeAddData(addKey.substring(index).trim(), nodes.get(0));
}
/**
* Creates the {@code JXPathContext} to be used for executing a query. This
* method delegates to the context factory.
*
* @param root the configuration root node
* @param handler the node handler
* @return the new context
*/
private JXPathContext createContext(T root, NodeHandler handler)
{
return getContextFactory().createContext(root, handler);
}
/**
* Creates a {@code NodeAddData} object as a result of a
* {@code prepareAdd()} operation. This method interprets the passed in path
* of the new node.
*
* @param path the path of the new node
* @param parentNodeResult the parent node
* @param the type of the nodes involved
*/
NodeAddData createNodeAddData(String path,
QueryResult parentNodeResult)
{
if (parentNodeResult.isAttributeResult())
{
invalidPath(path, " cannot add properties to an attribute.");
}
List pathNodes = new LinkedList();
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 a disallowed position.");
}
if (lastComponent == null)
{
invalidPath(path,
" contains a '/' at a disallowed position.");
}
pathNodes.add(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 a disallowed position.");
}
if (lastComponent != null)
{
pathNodes.add(lastComponent);
}
attr = true;
lastComponent = null;
}
else
{
lastComponent = token;
}
first = false;
}
if (lastComponent == null)
{
invalidPath(path, "contains no components.");
}
return new NodeAddData(parentNodeResult.getNode(), lastComponent,
attr, pathNodes);
}
/**
* Returns the {@code XPathContextFactory} used by this instance.
*
* @return the {@code XPathContextFactory}
*/
XPathContextFactory getContextFactory()
{
return contextFactory;
}
/**
* 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 key for adding
* properties is generated.
*
* @param root the root node of the configuration
* @param key the key in question
* @param handler the node handler
* @return the key to be used for adding the property
*/
private String generateKeyForAdd(T root, String key,
NodeHandler handler)
{
int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
while (pos >= 0)
{
String keyExisting = key.substring(0, pos);
if (!query(root, keyExisting, handler).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;
}
/**
* Determines the index of the given child node in the node list of its
* parent.
*
* @param parent the parent node
* @param child the child node
* @param handler the node handler
* @param the type of the nodes involved
* @return the index of this child node
*/
private static int determineIndex(T parent, T child,
NodeHandler handler)
{
return handler.getChildren(parent, handler.nodeName(child)).indexOf(
child) + 1;
}
/**
* Helper method for throwing an exception about an invalid path.
*
* @param path the invalid path
* @param msg the exception message
*/
private static 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.
*
* @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;
}
/**
* Converts the objects returned as query result from the JXPathContext to
* query result objects.
*
* @param results the list with results from the context
* @param the type of results to be produced
* @return the result list
*/
private static List> convertResults(List results)
{
List> queryResults =
new ArrayList>(results.size());
for (Object res : results)
{
QueryResult queryResult = createResult(res);
queryResults.add(queryResult);
}
return queryResults;
}
/**
* Creates a {@code QueryResult} object from the given result object of a
* query. Because of the node pointers involved result objects can only be
* of two types:
*
* - nodes of type T
* - attribute results already wrapped in {@code QueryResult} objects
*
* This method performs a corresponding cast. Warnings can be suppressed
* because of the implementation of the query functionality.
*
* @param resObj the query result object
* @param the type of the result to be produced
* @return the {@code QueryResult}
*/
@SuppressWarnings("unchecked")
private static QueryResult createResult(Object resObj)
{
if (resObj instanceof QueryResult)
{
return (QueryResult) resObj;
}
else
{
return QueryResult.createNodeResult((T) resObj);
}
}
// static initializer: registers the configuration node pointer factory
static
{
JXPathContextReferenceImpl
.addNodePointerFactory(new ConfigurationNodePointerFactory());
}
}