Source code
Description: source: package: org.apache.commons.configuration2.tree.xpath, class: XPathExpressionEngine

    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *     https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017package org.apache.commons.configuration2.tree.xpath;
    018
    019import java.util.Collections;
    020import java.util.LinkedList;
    021import java.util.List;
    022import java.util.StringTokenizer;
    023import java.util.stream.Collectors;
    024
    025import org.apache.commons.configuration2.tree.ExpressionEngine;
    026import org.apache.commons.configuration2.tree.NodeAddData;
    027import org.apache.commons.configuration2.tree.NodeHandler;
    028import org.apache.commons.configuration2.tree.QueryResult;
    029import org.apache.commons.jxpath.JXPathContext;
    030import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
    031import org.apache.commons.lang3.StringUtils;
    032
    033/**
    034 * <p>
    035 * A specialized implementation of the {@code ExpressionEngine} interface that is able to evaluate XPATH expressions.
    036 * </p>
    037 * <p>
    038 * This class makes use of <a href="https://commons.apache.org/jxpath/"> Commons JXPath</a> for handling XPath
    039 * expressions and mapping them to the nodes of a hierarchical configuration. This makes the rich and powerful XPATH
    040 * syntax available for accessing properties from a configuration object.
    041 * </p>
    042 * <p>
    043 * For selecting properties arbitrary XPATH expressions can be used, which select single or multiple configuration
    044 * nodes. The associated {@code Configuration} instance will directly pass the specified property keys into this engine.
    045 * If a key is not syntactically correct, an exception will be thrown.
    046 * </p>
    047 * <p>
    048 * For adding new properties, this expression engine uses a specific syntax: the &quot;key&quot; of a new property must
    049 * consist of two parts that are separated by whitespace:
    050 * </p>
    051 * <ol>
    052 * <li>An XPATH expression selecting a single node, to which the new element(s) are to be added. This can be an
    053 * arbitrary complex expression, but it must select exactly one node, otherwise an exception will be thrown.</li>
    054 * <li>The name of the new element(s) to be added below this parent node. Here either a single node name or a complete
    055 * path of nodes (separated by the &quot;/&quot; character or &quot;@&quot; for an attribute) can be specified.</li>
    056 * </ol>
    057 * <p>
    058 * Some examples for valid keys that can be passed into the configuration's {@code addProperty()} method follow:
    059 * </p>
    060 *
    061 * <pre>
    062 * &quot;/tables/table[1] type&quot;
    063 * </pre>
    064 *
    065 * <p>
    066 * This will add a new {@code type} node as a child of the first {@code table} element.
    067 * </p>
    068 *
    069 * <pre>
    070 * &quot;/tables/table[1] @type&quot;
    071 * </pre>
    072 *
    073 * <p>
    074 * Similar to the example above, but this time a new attribute named {@code type} will be added to the first
    075 * {@code table} element.
    076 * </p>
    077 *
    078 * <pre>
    079 * &quot;/tables table/fields/field/name&quot;
    080 * </pre>
    081 *
    082 * <p>
    083 * This example shows how a complex path can be added. Parent node is the {@code tables} element. Here a new branch
    084 * consisting of the nodes {@code table}, {@code fields}, {@code field}, and {@code name} will be added.
    085 * </p>
    086 *
    087 * <pre>
    088 * &quot;/tables table/fields/field@type&quot;
    089 * </pre>
    090 *
    091 * <p>
    092 * This is similar to the last example, but in this case a complex path ending with an attribute is defined.
    093 * </p>
    094 * <p>
    095 * <strong>Note:</strong> This extended syntax for adding properties only works with the {@code addProperty()} method.
    096 * {@code setProperty()} does not support creating new nodes this way.
    097 * </p>
    098 * <p>
    099 * From version 1.7 on, it is possible to use regular keys in calls to {@code addProperty()} (i.e. keys that do not have
    100 * to contain a whitespace as delimiter). In this case the key is evaluated, and the biggest part pointing to an
    101 * existing node is determined. The remaining part is then added as new path. As an example consider the key
    102 * </p>
    103 *
    104 * <pre>
    105 * &quot;tables/table[last()]/fields/field/name&quot;
    106 * </pre>
    107 *
    108 * <p>
    109 * If the key does not point to an existing node, the engine will check the paths
    110 * {@code "tables/table[last()]/fields/field"}, {@code "tables/table[last()]/fields"}, {@code "tables/table[last()]"},
    111 * 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
    112 * in this way. Then from this key the following key is derived: {@code "tables/table[last()] fields/field/name"} by
    113 * appending the remaining part after a whitespace. This key can now be processed using the original algorithm. Keys of
    114 * this form can also be used with the {@code setProperty()} method. However, it is still recommended to use the old
    115 * format because it makes explicit at which position new nodes should be added. For keys without a whitespace delimiter
    116 * there may be ambiguities.
    117 * </p>
    118 *
    119 * @since 1.3
    120 */
    121public class XPathExpressionEngine implements ExpressionEngine {
    122
    123    /** Constant for the path delimiter. */
    124    static final String PATH_DELIMITER = "/";
    125
    126    /** Constant for the attribute delimiter. */
    127    static final String ATTR_DELIMITER = "@";
    128
    129    /** Constant for the delimiters for splitting node paths. */
    130    private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER + ATTR_DELIMITER;
    131
    132    /**
    133     * Constant for a space which is used as delimiter in keys for adding properties.
    134     */
    135    private static final String SPACE = " ";
    136
    137    /** Constant for a default size of a key buffer. */
    138    private static final int BUF_SIZE = 128;
    139
    140    /** Constant for the start of an index expression. */
    141    private static final char START_INDEX = '[';
    142
    143    /** Constant for the end of an index expression. */
    144    private static final char END_INDEX = ']';
    145
    146    // static initializer: registers the configuration node pointer factory
    147    static {
    148        JXPathContextReferenceImpl.addNodePointerFactory(new ConfigurationNodePointerFactory());
    149    }
    150
    151    /**
    152     * Converts the objects returned as query result from the JXPathContext to query result objects.
    153     *
    154     * @param results the list with results from the context
    155     * @param <T> the type of results to be produced
    156     * @return the result list
    157     */
    158    private static <T> List<QueryResult<T>> convertResults(final List<?> results) {
    159        return results.stream().map(res -> (QueryResult<T>) createResult(res)).collect(Collectors.toList());
    160    }
    161
    162    /**
    163     * Creates a {@code QueryResult} object from the given result object of a query. Because of the node pointers involved
    164     * result objects can only be of two types:
    165     * <ul>
    166     * <li>nodes of type T</li>
    167     * <li>attribute results already wrapped in {@code QueryResult} objects</li>
    168     * </ul>
    169     * This method performs a corresponding cast. Warnings can be suppressed because of the implementation of the query
    170     * functionality.
    171     *
    172     * @param resObj the query result object
    173     * @param <T> the type of the result to be produced
    174     * @return the {@code QueryResult}
    175     */
    176    @SuppressWarnings("unchecked")
    177    private static <T> QueryResult<T> createResult(final Object resObj) {
    178        if (resObj instanceof QueryResult) {
    179            return (QueryResult<T>) resObj;
    180        }
    181        return QueryResult.createNodeResult((T) resObj);
    182    }
    183
    184    /**
    185     * Determines the index of the given child node in the node list of its parent.
    186     *
    187     * @param parent the parent node
    188     * @param child the child node
    189     * @param handler the node handler
    190     * @param <T> the type of the nodes involved
    191     * @return the index of this child node
    192     */
    193    private static <T> int determineIndex(final T parent, final T child, final NodeHandler<T> handler) {
    194        return handler.getChildren(parent, handler.nodeName(child)).indexOf(child) + 1;
    195    }
    196
    197    /**
    198     * Determines the position of the separator in a key for adding new properties. If no delimiter is found, result is -1.
    199     *
    200     * @param key the key
    201     * @return the position of the delimiter
    202     */
    203    private static int findKeySeparator(final String key) {
    204        int index = key.length() - 1;
    205        while (index >= 0 && !Character.isWhitespace(key.charAt(index))) {
    206            index--;
    207        }
    208        return index;
    209    }
    210
    211    /**
    212     * Helper method for throwing an exception about an invalid path.
    213     *
    214     * @param path the invalid path
    215     * @param msg the exception message
    216     */
    217    private static void invalidPath(final String path, final String msg) {
    218        throw new IllegalArgumentException("Invalid node path: \"" + path + "\" " + msg);
    219    }
    220
    221    /** The internally used context factory. */
    222    private final XPathContextFactory contextFactory;
    223
    224    /**
    225     * Creates a new instance of {@code XPathExpressionEngine} with default settings.
    226     */
    227    public XPathExpressionEngine() {
    228        this(new XPathContextFactory());
    229    }
    230
    231    /**
    232     * Creates a new instance of {@code XPathExpressionEngine} and sets the context factory. This constructor is mainly used
    233     * for testing purposes.
    234     *
    235     * @param factory the {@code XPathContextFactory}
    236     */
    237    XPathExpressionEngine(final XPathContextFactory factory) {
    238        contextFactory = factory;
    239    }
    240
    241    @Override
    242    public String attributeKey(final String parentKey, final String attributeName) {
    243        final StringBuilder buf = new StringBuilder(
    244            StringUtils.length(parentKey) + StringUtils.length(attributeName) + PATH_DELIMITER.length() + ATTR_DELIMITER.length());
    245        if (StringUtils.isNotEmpty(parentKey)) {
    246            buf.append(parentKey).append(PATH_DELIMITER);
    247        }
    248        buf.append(ATTR_DELIMITER).append(attributeName);
    249        return buf.toString();
    250    }
    251
    252    /**
    253     * {@inheritDoc} This implementation works similar to {@code nodeKey()}, but always adds an index expression to the
    254     * resulting key.
    255     */
    256    @Override
    257    public <T> String canonicalKey(final T node, final String parentKey, final NodeHandler<T> handler) {
    258        final T parent = handler.getParent(node);
    259        if (parent == null) {
    260            // this is the root node
    261            return StringUtils.defaultString(parentKey);
    262        }
    263
    264        final StringBuilder buf = new StringBuilder(BUF_SIZE);
    265        if (StringUtils.isNotEmpty(parentKey)) {
    266            buf.append(parentKey).append(PATH_DELIMITER);
    267        }
    268        buf.append(handler.nodeName(node));
    269        buf.append(START_INDEX);
    270        buf.append(determineIndex(parent, node, handler));
    271        buf.append(END_INDEX);
    272        return buf.toString();
    273    }
    274
    275    /**
    276     * Creates the {@code JXPathContext} to be used for executing a query. This method delegates to the context factory.
    277     *
    278     * @param root the configuration root node
    279     * @param handler the node handler
    280     * @return the new context
    281     */
    282    private <T> JXPathContext createContext(final T root, final NodeHandler<T> handler) {
    283        return getContextFactory().createContext(root, handler);
    284    }
    285
    286    /**
    287     * Creates a {@code NodeAddData} object as a result of a {@code prepareAdd()} operation. This method interprets the
    288     * passed in path of the new node.
    289     *
    290     * @param path the path of the new node
    291     * @param parentNodeResult the parent node
    292     * @param <T> the type of the nodes involved
    293     */
    294    <T> NodeAddData<T> createNodeAddData(final String path, final QueryResult<T> parentNodeResult) {
    295        if (parentNodeResult.isAttributeResult()) {
    296            invalidPath(path, " cannot add properties to an attribute.");
    297        }
    298        final List<String> pathNodes = new LinkedList<>();
    299        String lastComponent = null;
    300        boolean attr = false;
    301        boolean first = true;
    302
    303        final StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS, true);
    304        while (tok.hasMoreTokens()) {
    305            final String token = tok.nextToken();
    306            if (PATH_DELIMITER.equals(token)) {
    307                if (attr) {
    308                    invalidPath(path, " contains an attribute delimiter at a disallowed position.");
    309                }
    310                if (lastComponent == null) {
    311                    invalidPath(path, " contains a '/' at a disallowed position.");
    312                }
    313                pathNodes.add(lastComponent);
    314                lastComponent = null;
    315            } else if (ATTR_DELIMITER.equals(token)) {
    316                if (attr) {
    317                    invalidPath(path, " contains multiple attribute delimiters.");
    318                }
    319                if (lastComponent == null && !first) {
    320                    invalidPath(path, " contains an attribute delimiter at a disallowed position.");
    321                }
    322                if (lastComponent != null) {
    323                    pathNodes.add(lastComponent);
    324                }
    325                attr = true;
    326                lastComponent = null;
    327            } else {
    328                lastComponent = token;
    329            }
    330            first = false;
    331        }
    332
    333        if (lastComponent == null) {
    334            invalidPath(path, "contains no components.");
    335        }
    336
    337        return new NodeAddData<>(parentNodeResult.getNode(), lastComponent, attr, pathNodes);
    338    }
    339
    340    /**
    341     * Tries to generate a key for adding a property. This method is called if a key was used for adding properties which
    342     * does not contain a space character. It splits the key at its single components and searches for the last existing
    343     * component. Then a key compatible key for adding properties is generated.
    344     *
    345     * @param root the root node of the configuration
    346     * @param key the key in question
    347     * @param handler the node handler
    348     * @return the key to be used for adding the property
    349     */
    350    private <T> String generateKeyForAdd(final T root, final String key, final NodeHandler<T> handler) {
    351        int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
    352
    353        while (pos >= 0) {
    354            final String keyExisting = key.substring(0, pos);
    355            if (!query(root, keyExisting, handler).isEmpty()) {
    356                final StringBuilder buf = new StringBuilder(key.length() + 1);
    357                buf.append(keyExisting).append(SPACE);
    358                buf.append(key.substring(pos + 1));
    359                return buf.toString();
    360            }
    361            pos = key.lastIndexOf(PATH_DELIMITER, pos - 1);
    362        }
    363
    364        return SPACE + key;
    365    }
    366
    367    /**
    368     * Gets the {@code XPathContextFactory} used by this instance.
    369     *
    370     * @return the {@code XPathContextFactory}
    371     */
    372    XPathContextFactory getContextFactory() {
    373        return contextFactory;
    374    }
    375
    376    /**
    377     * {@inheritDoc} This implementation creates an XPATH expression that selects the given node (under the assumption that
    378     * the passed in parent key is valid). As the {@code nodeKey()} implementation of
    379     * {@link org.apache.commons.configuration2.tree.DefaultExpressionEngine DefaultExpressionEngine} this method does not
    380     * return indices for nodes. So all child nodes of a given parent with the same name have the same key.
    381     */
    382    @Override
    383    public <T> String nodeKey(final T node, final String parentKey, final NodeHandler<T> handler) {
    384        if (parentKey == null) {
    385            // name of the root node
    386            return StringUtils.EMPTY;
    387        }
    388        if (handler.nodeName(node) == null) {
    389            // paranoia check for undefined node names
    390            return parentKey;
    391        }
    392        final StringBuilder buf = new StringBuilder(parentKey.length() + handler.nodeName(node).length() + PATH_DELIMITER.length());
    393        if (!parentKey.isEmpty()) {
    394            buf.append(parentKey);
    395            buf.append(PATH_DELIMITER);
    396        }
    397        buf.append(handler.nodeName(node));
    398        return buf.toString();
    399    }
    400
    401    /**
    402     * {@inheritDoc} The expected format of the passed in key is explained in the class comment.
    403     */
    404    @Override
    405    public <T> NodeAddData<T> prepareAdd(final T root, final String key, final NodeHandler<T> handler) {
    406        if (key == null) {
    407            throw new IllegalArgumentException("prepareAdd: key must not be null.");
    408        }
    409
    410        String addKey = key;
    411        int index = findKeySeparator(addKey);
    412        if (index < 0) {
    413            addKey = generateKeyForAdd(root, addKey, handler);
    414            index = findKeySeparator(addKey);
    415        } else if (index >= addKey.length() - 1) {
    416            invalidPath(addKey, " new node path must not be empty.");
    417        }
    418
    419        final List<QueryResult<T>> nodes = query(root, addKey.substring(0, index).trim(), handler);
    420        if (nodes.size() != 1) {
    421            throw new IllegalArgumentException("prepareAdd: key '" + key + "' must select exactly one target node.");
    422        }
    423
    424        return createNodeAddData(addKey.substring(index).trim(), nodes.get(0));
    425    }
    426
    427    /**
    428     * {@inheritDoc} This implementation interprets the passed in key as an XPATH expression.
    429     */
    430    @Override
    431    public <T> List<QueryResult<T>> query(final T root, final String key, final NodeHandler<T> handler) {
    432        if (StringUtils.isEmpty(key)) {
    433            final QueryResult<T> result = createResult(root);
    434            return Collections.singletonList(result);
    435        }
    436        final JXPathContext context = createContext(root, handler);
    437        List<?> results = context.selectNodes(key);
    438        if (results == null) {
    439            results = Collections.emptyList();
    440        }
    441        return convertResults(results);
    442    }
    443}