XPathExpressionEngine.java
- /*
- * 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.Collections;
- import java.util.LinkedList;
- import java.util.List;
- import java.util.StringTokenizer;
- import java.util.stream.Collectors;
- 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;
- /**
- * <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="https://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 "key" of a new property must
- * consist of two parts that are separated by whitespace:
- * </p>
- * <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 "/" character or "@" for an attribute) can be specified.</li>
- * </ol>
- * <p>
- * Some examples for valid keys that can be passed into the configuration's {@code addProperty()} method follow:
- * </p>
- *
- * <pre>
- * "/tables/table[1] type"
- * </pre>
- *
- * <p>
- * This will add a new {@code type} node as a child of the first {@code table} element.
- * </p>
- *
- * <pre>
- * "/tables/table[1] @type"
- * </pre>
- *
- * <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>
- *
- * <pre>
- * "/tables table/fields/field/name"
- * </pre>
- *
- * <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>
- *
- * <pre>
- * "/tables table/fields/field@type"
- * </pre>
- *
- * <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
- * </p>
- *
- * <pre>
- * "tables/table[last()]/fields/field/name"
- * </pre>
- *
- * <p>
- * 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
- */
- 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 = ']';
- // static initializer: registers the configuration node pointer factory
- static {
- JXPathContextReferenceImpl.addNodePointerFactory(new ConfigurationNodePointerFactory());
- }
- /**
- * Converts the objects returned as query result from the JXPathContext to query result objects.
- *
- * @param results the list with results from the context
- * @param <T> the type of results to be produced
- * @return the result list
- */
- private static <T> List<QueryResult<T>> convertResults(final List<?> results) {
- return results.stream().map(res -> (QueryResult<T>) createResult(res)).collect(Collectors.toList());
- }
- /**
- * 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:
- * <ul>
- * <li>nodes of type T</li>
- * <li>attribute results already wrapped in {@code QueryResult} objects</li>
- * </ul>
- * 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 <T> the type of the result to be produced
- * @return the {@code QueryResult}
- */
- @SuppressWarnings("unchecked")
- private static <T> QueryResult<T> createResult(final Object resObj) {
- if (resObj instanceof QueryResult) {
- return (QueryResult<T>) resObj;
- }
- return QueryResult.createNodeResult((T) resObj);
- }
- /**
- * 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 <T> the type of the nodes involved
- * @return the index of this child node
- */
- private static <T> int determineIndex(final T parent, final T child, final NodeHandler<T> handler) {
- return handler.getChildren(parent, handler.nodeName(child)).indexOf(child) + 1;
- }
- /**
- * 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(final String key) {
- int index = key.length() - 1;
- while (index >= 0 && !Character.isWhitespace(key.charAt(index))) {
- index--;
- }
- return index;
- }
- /**
- * Helper method for throwing an exception about an invalid path.
- *
- * @param path the invalid path
- * @param msg the exception message
- */
- private static void invalidPath(final String path, final String msg) {
- throw new IllegalArgumentException("Invalid node path: \"" + path + "\" " + msg);
- }
- /** 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(final XPathContextFactory factory) {
- contextFactory = factory;
- }
- @Override
- public String attributeKey(final String parentKey, final String attributeName) {
- final 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 <T> String canonicalKey(final T node, final String parentKey, final NodeHandler<T> handler) {
- final T parent = handler.getParent(node);
- if (parent == null) {
- // this is the root node
- return StringUtils.defaultString(parentKey);
- }
- final 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();
- }
- /**
- * 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 <T> JXPathContext createContext(final T root, final NodeHandler<T> 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 <T> the type of the nodes involved
- */
- <T> NodeAddData<T> createNodeAddData(final String path, final QueryResult<T> parentNodeResult) {
- if (parentNodeResult.isAttributeResult()) {
- invalidPath(path, " cannot add properties to an attribute.");
- }
- final List<String> pathNodes = new LinkedList<>();
- String lastComponent = null;
- boolean attr = false;
- boolean first = true;
- final StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS, true);
- while (tok.hasMoreTokens()) {
- final 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);
- }
- /**
- * 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 <T> String generateKeyForAdd(final T root, final String key, final NodeHandler<T> handler) {
- int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
- while (pos >= 0) {
- final String keyExisting = key.substring(0, pos);
- if (!query(root, keyExisting, handler).isEmpty()) {
- final 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;
- }
- /**
- * Gets the {@code XPathContextFactory} used by this instance.
- *
- * @return the {@code XPathContextFactory}
- */
- XPathContextFactory getContextFactory() {
- return contextFactory;
- }
- /**
- * {@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 <T> String nodeKey(final T node, final String parentKey, final NodeHandler<T> handler) {
- if (parentKey == null) {
- // name of the root node
- return StringUtils.EMPTY;
- }
- if (handler.nodeName(node) == null) {
- // paranoia check for undefined node names
- return parentKey;
- }
- final StringBuilder buf = new StringBuilder(parentKey.length() + handler.nodeName(node).length() + PATH_DELIMITER.length());
- if (!parentKey.isEmpty()) {
- buf.append(parentKey);
- buf.append(PATH_DELIMITER);
- }
- buf.append(handler.nodeName(node));
- return buf.toString();
- }
- /**
- * {@inheritDoc} The expected format of the passed in key is explained in the class comment.
- */
- @Override
- public <T> NodeAddData<T> prepareAdd(final T root, final String key, final NodeHandler<T> 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.");
- }
- final List<QueryResult<T>> 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));
- }
- /**
- * {@inheritDoc} This implementation interprets the passed in key as an XPATH expression.
- */
- @Override
- public <T> List<QueryResult<T>> query(final T root, final String key, final NodeHandler<T> handler) {
- if (StringUtils.isEmpty(key)) {
- final QueryResult<T> result = createResult(root);
- return Collections.singletonList(result);
- }
- final JXPathContext context = createContext(root, handler);
- List<?> results = context.selectNodes(key);
- if (results == null) {
- results = Collections.emptyList();
- }
- return convertResults(results);
- }
- }