All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine Maven / Gradle / Ivy

Go to download

Tools to assist in the reading of configuration/preferences files in various formats

The newest version!
/*
 * 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;

/**
 * 

* 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: *

*
    *
  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. *
  3. 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.
  4. *
*

* 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 */ 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 the type of results to be produced * @return the result list */ private static List> convertResults(final List results) { return results.stream().map(res -> (QueryResult) 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: *
    *
  • 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(final Object resObj) { if (resObj instanceof QueryResult) { return (QueryResult) 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 the type of the nodes involved * @return the index of this child node */ private static int determineIndex(final T parent, final T child, final NodeHandler 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 String canonicalKey(final T node, final String parentKey, final NodeHandler 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 JXPathContext createContext(final T root, final 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(final String path, final QueryResult parentNodeResult) { if (parentNodeResult.isAttributeResult()) { invalidPath(path, " cannot add properties to an attribute."); } final List 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 String generateKeyForAdd(final T root, final String key, final NodeHandler 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 String nodeKey(final T node, final String parentKey, final NodeHandler 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 NodeAddData prepareAdd(final T root, final String key, final 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."); } final 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)); } /** * {@inheritDoc} This implementation interprets the passed in key as an XPATH expression. */ @Override public List> query(final T root, final String key, final NodeHandler handler) { if (StringUtils.isEmpty(key)) { final QueryResult 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy