XPathExpressionEngine.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.configuration2.tree.xpath;

  18. import java.util.Collections;
  19. import java.util.LinkedList;
  20. import java.util.List;
  21. import java.util.StringTokenizer;
  22. import java.util.stream.Collectors;

  23. import org.apache.commons.configuration2.tree.ExpressionEngine;
  24. import org.apache.commons.configuration2.tree.NodeAddData;
  25. import org.apache.commons.configuration2.tree.NodeHandler;
  26. import org.apache.commons.configuration2.tree.QueryResult;
  27. import org.apache.commons.jxpath.JXPathContext;
  28. import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
  29. import org.apache.commons.lang3.StringUtils;

  30. /**
  31.  * <p>
  32.  * A specialized implementation of the {@code ExpressionEngine} interface that is able to evaluate XPATH expressions.
  33.  * </p>
  34.  * <p>
  35.  * This class makes use of <a href="https://commons.apache.org/jxpath/"> Commons JXPath</a> for handling XPath
  36.  * expressions and mapping them to the nodes of a hierarchical configuration. This makes the rich and powerful XPATH
  37.  * syntax available for accessing properties from a configuration object.
  38.  * </p>
  39.  * <p>
  40.  * For selecting properties arbitrary XPATH expressions can be used, which select single or multiple configuration
  41.  * nodes. The associated {@code Configuration} instance will directly pass the specified property keys into this engine.
  42.  * If a key is not syntactically correct, an exception will be thrown.
  43.  * </p>
  44.  * <p>
  45.  * For adding new properties, this expression engine uses a specific syntax: the &quot;key&quot; of a new property must
  46.  * consist of two parts that are separated by whitespace:
  47.  * </p>
  48.  * <ol>
  49.  * <li>An XPATH expression selecting a single node, to which the new element(s) are to be added. This can be an
  50.  * arbitrary complex expression, but it must select exactly one node, otherwise an exception will be thrown.</li>
  51.  * <li>The name of the new element(s) to be added below this parent node. Here either a single node name or a complete
  52.  * path of nodes (separated by the &quot;/&quot; character or &quot;@&quot; for an attribute) can be specified.</li>
  53.  * </ol>
  54.  * <p>
  55.  * Some examples for valid keys that can be passed into the configuration's {@code addProperty()} method follow:
  56.  * </p>
  57.  *
  58.  * <pre>
  59.  * &quot;/tables/table[1] type&quot;
  60.  * </pre>
  61.  *
  62.  * <p>
  63.  * This will add a new {@code type} node as a child of the first {@code table} element.
  64.  * </p>
  65.  *
  66.  * <pre>
  67.  * &quot;/tables/table[1] @type&quot;
  68.  * </pre>
  69.  *
  70.  * <p>
  71.  * Similar to the example above, but this time a new attribute named {@code type} will be added to the first
  72.  * {@code table} element.
  73.  * </p>
  74.  *
  75.  * <pre>
  76.  * &quot;/tables table/fields/field/name&quot;
  77.  * </pre>
  78.  *
  79.  * <p>
  80.  * This example shows how a complex path can be added. Parent node is the {@code tables} element. Here a new branch
  81.  * consisting of the nodes {@code table}, {@code fields}, {@code field}, and {@code name} will be added.
  82.  * </p>
  83.  *
  84.  * <pre>
  85.  * &quot;/tables table/fields/field@type&quot;
  86.  * </pre>
  87.  *
  88.  * <p>
  89.  * This is similar to the last example, but in this case a complex path ending with an attribute is defined.
  90.  * </p>
  91.  * <p>
  92.  * <strong>Note:</strong> This extended syntax for adding properties only works with the {@code addProperty()} method.
  93.  * {@code setProperty()} does not support creating new nodes this way.
  94.  * </p>
  95.  * <p>
  96.  * From version 1.7 on, it is possible to use regular keys in calls to {@code addProperty()} (i.e. keys that do not have
  97.  * to contain a whitespace as delimiter). In this case the key is evaluated, and the biggest part pointing to an
  98.  * existing node is determined. The remaining part is then added as new path. As an example consider the key
  99.  * </p>
  100.  *
  101.  * <pre>
  102.  * &quot;tables/table[last()]/fields/field/name&quot;
  103.  * </pre>
  104.  *
  105.  * <p>
  106.  * If the key does not point to an existing node, the engine will check the paths
  107.  * {@code "tables/table[last()]/fields/field"}, {@code "tables/table[last()]/fields"}, {@code "tables/table[last()]"},
  108.  * 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
  109.  * in this way. Then from this key the following key is derived: {@code "tables/table[last()] fields/field/name"} by
  110.  * appending the remaining part after a whitespace. This key can now be processed using the original algorithm. Keys of
  111.  * this form can also be used with the {@code setProperty()} method. However, it is still recommended to use the old
  112.  * format because it makes explicit at which position new nodes should be added. For keys without a whitespace delimiter
  113.  * there may be ambiguities.
  114.  * </p>
  115.  *
  116.  * @since 1.3
  117.  */
  118. public class XPathExpressionEngine implements ExpressionEngine {
  119.     /** Constant for the path delimiter. */
  120.     static final String PATH_DELIMITER = "/";

  121.     /** Constant for the attribute delimiter. */
  122.     static final String ATTR_DELIMITER = "@";

  123.     /** Constant for the delimiters for splitting node paths. */
  124.     private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER + ATTR_DELIMITER;

  125.     /**
  126.      * Constant for a space which is used as delimiter in keys for adding properties.
  127.      */
  128.     private static final String SPACE = " ";

  129.     /** Constant for a default size of a key buffer. */
  130.     private static final int BUF_SIZE = 128;

  131.     /** Constant for the start of an index expression. */
  132.     private static final char START_INDEX = '[';

  133.     /** Constant for the end of an index expression. */
  134.     private static final char END_INDEX = ']';

  135.     // static initializer: registers the configuration node pointer factory
  136.     static {
  137.         JXPathContextReferenceImpl.addNodePointerFactory(new ConfigurationNodePointerFactory());
  138.     }

  139.     /**
  140.      * Converts the objects returned as query result from the JXPathContext to query result objects.
  141.      *
  142.      * @param results the list with results from the context
  143.      * @param <T> the type of results to be produced
  144.      * @return the result list
  145.      */
  146.     private static <T> List<QueryResult<T>> convertResults(final List<?> results) {
  147.         return results.stream().map(res -> (QueryResult<T>) createResult(res)).collect(Collectors.toList());
  148.     }

  149.     /**
  150.      * Creates a {@code QueryResult} object from the given result object of a query. Because of the node pointers involved
  151.      * result objects can only be of two types:
  152.      * <ul>
  153.      * <li>nodes of type T</li>
  154.      * <li>attribute results already wrapped in {@code QueryResult} objects</li>
  155.      * </ul>
  156.      * This method performs a corresponding cast. Warnings can be suppressed because of the implementation of the query
  157.      * functionality.
  158.      *
  159.      * @param resObj the query result object
  160.      * @param <T> the type of the result to be produced
  161.      * @return the {@code QueryResult}
  162.      */
  163.     @SuppressWarnings("unchecked")
  164.     private static <T> QueryResult<T> createResult(final Object resObj) {
  165.         if (resObj instanceof QueryResult) {
  166.             return (QueryResult<T>) resObj;
  167.         }
  168.         return QueryResult.createNodeResult((T) resObj);
  169.     }

  170.     /**
  171.      * Determines the index of the given child node in the node list of its parent.
  172.      *
  173.      * @param parent the parent node
  174.      * @param child the child node
  175.      * @param handler the node handler
  176.      * @param <T> the type of the nodes involved
  177.      * @return the index of this child node
  178.      */
  179.     private static <T> int determineIndex(final T parent, final T child, final NodeHandler<T> handler) {
  180.         return handler.getChildren(parent, handler.nodeName(child)).indexOf(child) + 1;
  181.     }

  182.     /**
  183.      * Determines the position of the separator in a key for adding new properties. If no delimiter is found, result is -1.
  184.      *
  185.      * @param key the key
  186.      * @return the position of the delimiter
  187.      */
  188.     private static int findKeySeparator(final String key) {
  189.         int index = key.length() - 1;
  190.         while (index >= 0 && !Character.isWhitespace(key.charAt(index))) {
  191.             index--;
  192.         }
  193.         return index;
  194.     }

  195.     /**
  196.      * Helper method for throwing an exception about an invalid path.
  197.      *
  198.      * @param path the invalid path
  199.      * @param msg the exception message
  200.      */
  201.     private static void invalidPath(final String path, final String msg) {
  202.         throw new IllegalArgumentException("Invalid node path: \"" + path + "\" " + msg);
  203.     }

  204.     /** The internally used context factory. */
  205.     private final XPathContextFactory contextFactory;

  206.     /**
  207.      * Creates a new instance of {@code XPathExpressionEngine} with default settings.
  208.      */
  209.     public XPathExpressionEngine() {
  210.         this(new XPathContextFactory());
  211.     }

  212.     /**
  213.      * Creates a new instance of {@code XPathExpressionEngine} and sets the context factory. This constructor is mainly used
  214.      * for testing purposes.
  215.      *
  216.      * @param factory the {@code XPathContextFactory}
  217.      */
  218.     XPathExpressionEngine(final XPathContextFactory factory) {
  219.         contextFactory = factory;
  220.     }

  221.     @Override
  222.     public String attributeKey(final String parentKey, final String attributeName) {
  223.         final StringBuilder buf = new StringBuilder(
  224.             StringUtils.length(parentKey) + StringUtils.length(attributeName) + PATH_DELIMITER.length() + ATTR_DELIMITER.length());
  225.         if (StringUtils.isNotEmpty(parentKey)) {
  226.             buf.append(parentKey).append(PATH_DELIMITER);
  227.         }
  228.         buf.append(ATTR_DELIMITER).append(attributeName);
  229.         return buf.toString();
  230.     }

  231.     /**
  232.      * {@inheritDoc} This implementation works similar to {@code nodeKey()}, but always adds an index expression to the
  233.      * resulting key.
  234.      */
  235.     @Override
  236.     public <T> String canonicalKey(final T node, final String parentKey, final NodeHandler<T> handler) {
  237.         final T parent = handler.getParent(node);
  238.         if (parent == null) {
  239.             // this is the root node
  240.             return StringUtils.defaultString(parentKey);
  241.         }

  242.         final StringBuilder buf = new StringBuilder(BUF_SIZE);
  243.         if (StringUtils.isNotEmpty(parentKey)) {
  244.             buf.append(parentKey).append(PATH_DELIMITER);
  245.         }
  246.         buf.append(handler.nodeName(node));
  247.         buf.append(START_INDEX);
  248.         buf.append(determineIndex(parent, node, handler));
  249.         buf.append(END_INDEX);
  250.         return buf.toString();
  251.     }

  252.     /**
  253.      * Creates the {@code JXPathContext} to be used for executing a query. This method delegates to the context factory.
  254.      *
  255.      * @param root the configuration root node
  256.      * @param handler the node handler
  257.      * @return the new context
  258.      */
  259.     private <T> JXPathContext createContext(final T root, final NodeHandler<T> handler) {
  260.         return getContextFactory().createContext(root, handler);
  261.     }

  262.     /**
  263.      * Creates a {@code NodeAddData} object as a result of a {@code prepareAdd()} operation. This method interprets the
  264.      * passed in path of the new node.
  265.      *
  266.      * @param path the path of the new node
  267.      * @param parentNodeResult the parent node
  268.      * @param <T> the type of the nodes involved
  269.      */
  270.     <T> NodeAddData<T> createNodeAddData(final String path, final QueryResult<T> parentNodeResult) {
  271.         if (parentNodeResult.isAttributeResult()) {
  272.             invalidPath(path, " cannot add properties to an attribute.");
  273.         }
  274.         final List<String> pathNodes = new LinkedList<>();
  275.         String lastComponent = null;
  276.         boolean attr = false;
  277.         boolean first = true;

  278.         final StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS, true);
  279.         while (tok.hasMoreTokens()) {
  280.             final String token = tok.nextToken();
  281.             if (PATH_DELIMITER.equals(token)) {
  282.                 if (attr) {
  283.                     invalidPath(path, " contains an attribute" + " delimiter at a disallowed position.");
  284.                 }
  285.                 if (lastComponent == null) {
  286.                     invalidPath(path, " contains a '/' at a disallowed position.");
  287.                 }
  288.                 pathNodes.add(lastComponent);
  289.                 lastComponent = null;
  290.             } else if (ATTR_DELIMITER.equals(token)) {
  291.                 if (attr) {
  292.                     invalidPath(path, " contains multiple attribute delimiters.");
  293.                 }
  294.                 if (lastComponent == null && !first) {
  295.                     invalidPath(path, " contains an attribute delimiter at a disallowed position.");
  296.                 }
  297.                 if (lastComponent != null) {
  298.                     pathNodes.add(lastComponent);
  299.                 }
  300.                 attr = true;
  301.                 lastComponent = null;
  302.             } else {
  303.                 lastComponent = token;
  304.             }
  305.             first = false;
  306.         }

  307.         if (lastComponent == null) {
  308.             invalidPath(path, "contains no components.");
  309.         }

  310.         return new NodeAddData<>(parentNodeResult.getNode(), lastComponent, attr, pathNodes);
  311.     }

  312.     /**
  313.      * Tries to generate a key for adding a property. This method is called if a key was used for adding properties which
  314.      * does not contain a space character. It splits the key at its single components and searches for the last existing
  315.      * component. Then a key compatible key for adding properties is generated.
  316.      *
  317.      * @param root the root node of the configuration
  318.      * @param key the key in question
  319.      * @param handler the node handler
  320.      * @return the key to be used for adding the property
  321.      */
  322.     private <T> String generateKeyForAdd(final T root, final String key, final NodeHandler<T> handler) {
  323.         int pos = key.lastIndexOf(PATH_DELIMITER, key.length());

  324.         while (pos >= 0) {
  325.             final String keyExisting = key.substring(0, pos);
  326.             if (!query(root, keyExisting, handler).isEmpty()) {
  327.                 final StringBuilder buf = new StringBuilder(key.length() + 1);
  328.                 buf.append(keyExisting).append(SPACE);
  329.                 buf.append(key.substring(pos + 1));
  330.                 return buf.toString();
  331.             }
  332.             pos = key.lastIndexOf(PATH_DELIMITER, pos - 1);
  333.         }

  334.         return SPACE + key;
  335.     }

  336.     /**
  337.      * Gets the {@code XPathContextFactory} used by this instance.
  338.      *
  339.      * @return the {@code XPathContextFactory}
  340.      */
  341.     XPathContextFactory getContextFactory() {
  342.         return contextFactory;
  343.     }

  344.     /**
  345.      * {@inheritDoc} This implementation creates an XPATH expression that selects the given node (under the assumption that
  346.      * the passed in parent key is valid). As the {@code nodeKey()} implementation of
  347.      * {@link org.apache.commons.configuration2.tree.DefaultExpressionEngine DefaultExpressionEngine} this method does not
  348.      * return indices for nodes. So all child nodes of a given parent with the same name have the same key.
  349.      */
  350.     @Override
  351.     public <T> String nodeKey(final T node, final String parentKey, final NodeHandler<T> handler) {
  352.         if (parentKey == null) {
  353.             // name of the root node
  354.             return StringUtils.EMPTY;
  355.         }
  356.         if (handler.nodeName(node) == null) {
  357.             // paranoia check for undefined node names
  358.             return parentKey;
  359.         }
  360.         final StringBuilder buf = new StringBuilder(parentKey.length() + handler.nodeName(node).length() + PATH_DELIMITER.length());
  361.         if (!parentKey.isEmpty()) {
  362.             buf.append(parentKey);
  363.             buf.append(PATH_DELIMITER);
  364.         }
  365.         buf.append(handler.nodeName(node));
  366.         return buf.toString();
  367.     }

  368.     /**
  369.      * {@inheritDoc} The expected format of the passed in key is explained in the class comment.
  370.      */
  371.     @Override
  372.     public <T> NodeAddData<T> prepareAdd(final T root, final String key, final NodeHandler<T> handler) {
  373.         if (key == null) {
  374.             throw new IllegalArgumentException("prepareAdd: key must not be null!");
  375.         }

  376.         String addKey = key;
  377.         int index = findKeySeparator(addKey);
  378.         if (index < 0) {
  379.             addKey = generateKeyForAdd(root, addKey, handler);
  380.             index = findKeySeparator(addKey);
  381.         } else if (index >= addKey.length() - 1) {
  382.             invalidPath(addKey, " new node path must not be empty.");
  383.         }

  384.         final List<QueryResult<T>> nodes = query(root, addKey.substring(0, index).trim(), handler);
  385.         if (nodes.size() != 1) {
  386.             throw new IllegalArgumentException("prepareAdd: key '" + key + "' must select exactly one target node!");
  387.         }

  388.         return createNodeAddData(addKey.substring(index).trim(), nodes.get(0));
  389.     }

  390.     /**
  391.      * {@inheritDoc} This implementation interprets the passed in key as an XPATH expression.
  392.      */
  393.     @Override
  394.     public <T> List<QueryResult<T>> query(final T root, final String key, final NodeHandler<T> handler) {
  395.         if (StringUtils.isEmpty(key)) {
  396.             final QueryResult<T> result = createResult(root);
  397.             return Collections.singletonList(result);
  398.         }
  399.         final JXPathContext context = createContext(root, handler);
  400.         List<?> results = context.selectNodes(key);
  401.         if (results == null) {
  402.             results = Collections.emptyList();
  403.         }
  404.         return convertResults(results);
  405.     }
  406. }