    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 *     http://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.ArrayList;
    020import java.util.Collections;
    021import java.util.LinkedList;
    022import java.util.List;
    023import java.util.StringTokenizer;
    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
    036 * is able to evaluate XPATH expressions.
    037 * </p>
    038 * <p>
    039 * This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons
    040 * JXPath</a> for handling XPath expressions and mapping them to the nodes of a
    041 * hierarchical configuration. This makes the rich and powerful XPATH syntax
    042 * available for accessing properties from a configuration object.
    043 * </p>
    044 * <p>
    045 * For selecting properties arbitrary XPATH expressions can be used, which
    046 * select single or multiple configuration nodes. The associated
    047 * {@code Configuration} instance will directly pass the specified property keys
    048 * into this engine. If a key is not syntactically correct, an exception will be
    049 * thrown.
    050 * </p>
    051 * <p>
    052 * For adding new properties, this expression engine uses a specific syntax: the
    053 * &quot;key&quot; of a new property must consist of two parts that are
    054 * separated by whitespace:
    055 * </p>
    056 * <ol>
    057 * <li>An XPATH expression selecting a single node, to which the new element(s)
    058 * are to be added. This can be an arbitrary complex expression, but it must
    059 * select exactly one node, otherwise an exception will be thrown.</li>
    060 * <li>The name of the new element(s) to be added below this parent node. Here
    061 * either a single node name or a complete path of nodes (separated by the
    062 * &quot;/&quot; character or &quot;@&quot; for an attribute) can be specified.</li>
    063 * </ol>
    064 * <p>
    065 * Some examples for valid keys that can be passed into the configuration's
    066 * {@code addProperty()} method follow:
    067 * </p>
    068 *
    069 * <pre>
    070 * &quot;/tables/table[1] type&quot;
    071 * </pre>
    072 *
    073 * <p>
    074 * This will add a new {@code type} node as a child of the first {@code table}
    075 * element.
    076 * </p>
    077 *
    078 * <pre>
    079 * &quot;/tables/table[1] @type&quot;
    080 * </pre>
    081 *
    082 * <p>
    083 * Similar to the example above, but this time a new attribute named
    084 * {@code type} will be added to the first {@code table} element.
    085 * </p>
    086 *
    087 * <pre>
    088 * &quot;/tables table/fields/field/name&quot;
    089 * </pre>
    090 *
    091 * <p>
    092 * This example shows how a complex path can be added. Parent node is the
    093 * {@code tables} element. Here a new branch consisting of the nodes
    094 * {@code table}, {@code fields}, {@code field}, and {@code name} will be added.
    095 * </p>
    096 *
    097 * <pre>
    098 * &quot;/tables table/fields/field@type&quot;
    099 * </pre>
    100 *
    101 * <p>
    102 * This is similar to the last example, but in this case a complex path ending
    103 * with an attribute is defined.
    104 * </p>
    105 * <p>
    106 * <strong>Note:</strong> This extended syntax for adding properties only works
    107 * with the {@code addProperty()} method. {@code setProperty()} does not support
    108 * creating new nodes this way.
    109 * </p>
    110 * <p>
    111 * From version 1.7 on, it is possible to use regular keys in calls to
    112 * {@code addProperty()} (i.e. keys that do not have to contain a whitespace as
    113 * delimiter). In this case the key is evaluated, and the biggest part pointing
    114 * to an existing node is determined. The remaining part is then added as new
    115 * path. As an example consider the key
    116 * </p>
    117 *
    118 * <pre>
    119 * &quot;tables/table[last()]/fields/field/name&quot;
    120 * </pre>
    121 *
    122 * <p>
    123 * If the key does not point to an existing node, the engine will check the
    124 * paths {@code "tables/table[last()]/fields/field"},
    125 * {@code "tables/table[last()]/fields"}, {@code "tables/table[last()]"}, and so
    126 * on, until a key is found which points to a node. Let's assume that the last
    127 * key listed above can be resolved in this way. Then from this key the
    128 * following key is derived: {@code "tables/table[last()] fields/field/name"} by
    129 * appending the remaining part after a whitespace. This key can now be
    130 * processed using the original algorithm. Keys of this form can also be used
    131 * with the {@code setProperty()} method. However, it is still recommended to
    132 * use the old format because it makes explicit at which position new nodes
    133 * should be added. For keys without a whitespace delimiter there may be
    134 * ambiguities.
    135 * </p>
    136 *
    137 * @since 1.3
    138 * @version $Id: XPathExpressionEngine.java 1842194 2018-09-27 22:24:23Z ggregory $
    139 */
    140public class XPathExpressionEngine implements ExpressionEngine
    141{
    142    /** Constant for the path delimiter. */
    143    static final String PATH_DELIMITER = "/";
    144
    145    /** Constant for the attribute delimiter. */
    146    static final String ATTR_DELIMITER = "@";
    147
    148    /** Constant for the delimiters for splitting node paths. */
    149    private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER
    150            + ATTR_DELIMITER;
    151
    152    /**
    153     * Constant for a space which is used as delimiter in keys for adding
    154     * properties.
    155     */
    156    private static final String SPACE = " ";
    157
    158    /** Constant for a default size of a key buffer. */
    159    private static final int BUF_SIZE = 128;
    160
    161    /** Constant for the start of an index expression. */
    162    private static final char START_INDEX = '[';
    163
    164    /** Constant for the end of an index expression. */
    165    private static final char END_INDEX = ']';
    166
    167    /** The internally used context factory. */
    168    private final XPathContextFactory contextFactory;
    169
    170    /**
    171     * Creates a new instance of {@code XPathExpressionEngine} with default
    172     * settings.
    173     */
    174    public XPathExpressionEngine()
    175    {
    176        this(new XPathContextFactory());
    177    }
    178
    179    /**
    180     * Creates a new instance of {@code XPathExpressionEngine} and sets the
    181     * context factory. This constructor is mainly used for testing purposes.
    182     *
    183     * @param factory the {@code XPathContextFactory}
    184     */
    185    XPathExpressionEngine(final XPathContextFactory factory)
    186    {
    187        contextFactory = factory;
    188    }
    189
    190    /**
    191     * {@inheritDoc} This implementation interprets the passed in key as an XPATH
    192     * expression.
    193     */
    194    @Override
    195    public <T> List<QueryResult<T>> query(final T root, final String key,
    196            final NodeHandler<T> handler)
    197    {
    198        if (StringUtils.isEmpty(key))
    199        {
    200            final QueryResult<T> result = createResult(root);
    201            return Collections.singletonList(result);
    202        }
    203        final JXPathContext context = createContext(root, handler);
    204        List<?> results = context.selectNodes(key);
    205        if (results == null)
    206        {
    207            results = Collections.emptyList();
    208        }
    209        return convertResults(results);
    210    }
    211
    212    /**
    213     * {@inheritDoc} This implementation creates an XPATH expression that
    214     * selects the given node (under the assumption that the passed in parent
    215     * key is valid). As the {@code nodeKey()} implementation of
    216     * {@link org.apache.commons.configuration2.tree.DefaultExpressionEngine
    217     * DefaultExpressionEngine} this method does not return indices for nodes.
    218     * So all child nodes of a given parent with the same name have the same
    219     * key.
    220     */
    221    @Override
    222    public <T> String nodeKey(final T node, final String parentKey, final NodeHandler<T> handler)
    223    {
    224        if (parentKey == null)
    225        {
    226            // name of the root node
    227            return StringUtils.EMPTY;
    228        }
    229        else if (handler.nodeName(node) == null)
    230        {
    231            // paranoia check for undefined node names
    232            return parentKey;
    233        }
    234
    235        else
    236        {
    237            final StringBuilder buf =
    238                    new StringBuilder(parentKey.length()
    239                            + handler.nodeName(node).length()
    240                            + PATH_DELIMITER.length());
    241            if (parentKey.length() > 0)
    242            {
    243                buf.append(parentKey);
    244                buf.append(PATH_DELIMITER);
    245            }
    246            buf.append(handler.nodeName(node));
    247            return buf.toString();
    248        }
    249    }
    250
    251    @Override
    252    public String attributeKey(final String parentKey, final String attributeName)
    253    {
    254        final StringBuilder buf =
    255                new StringBuilder(StringUtils.length(parentKey)
    256                        + StringUtils.length(attributeName)
    257                        + PATH_DELIMITER.length() + ATTR_DELIMITER.length());
    258        if (StringUtils.isNotEmpty(parentKey))
    259        {
    260            buf.append(parentKey).append(PATH_DELIMITER);
    261        }
    262        buf.append(ATTR_DELIMITER).append(attributeName);
    263        return buf.toString();
    264    }
    265
    266    /**
    267     * {@inheritDoc} This implementation works similar to {@code nodeKey()}, but
    268     * always adds an index expression to the resulting key.
    269     */
    270    @Override
    271    public <T> String canonicalKey(final T node, final String parentKey,
    272            final NodeHandler<T> handler)
    273    {
    274        final T parent = handler.getParent(node);
    275        if (parent == null)
    276        {
    277            // this is the root node
    278            return StringUtils.defaultString(parentKey);
    279        }
    280
    281        final StringBuilder buf = new StringBuilder(BUF_SIZE);
    282        if (StringUtils.isNotEmpty(parentKey))
    283        {
    284            buf.append(parentKey).append(PATH_DELIMITER);
    285        }
    286        buf.append(handler.nodeName(node));
    287        buf.append(START_INDEX);
    288        buf.append(determineIndex(parent, node, handler));
    289        buf.append(END_INDEX);
    290        return buf.toString();
    291    }
    292
    293    /**
    294     * {@inheritDoc} The expected format of the passed in key is explained in
    295     * the class comment.
    296     */
    297    @Override
    298    public <T> NodeAddData<T> prepareAdd(final T root, final String key,
    299            final NodeHandler<T> handler)
    300    {
    301        if (key == null)
    302        {
    303            throw new IllegalArgumentException(
    304                    "prepareAdd: key must not be null!");
    305        }
    306
    307        String addKey = key;
    308        int index = findKeySeparator(addKey);
    309        if (index < 0)
    310        {
    311            addKey = generateKeyForAdd(root, addKey, handler);
    312            index = findKeySeparator(addKey);
    313        }
    314        else if (index >= addKey.length() - 1)
    315        {
    316            invalidPath(addKey, " new node path must not be empty.");
    317        }
    318
    319        final List<QueryResult<T>> nodes =
    320                query(root, addKey.substring(0, index).trim(), handler);
    321        if (nodes.size() != 1)
    322        {
    323            throw new IllegalArgumentException("prepareAdd: key '" + key
    324                    + "' must select exactly one target node!");
    325        }
    326
    327        return createNodeAddData(addKey.substring(index).trim(), nodes.get(0));
    328    }
    329
    330    /**
    331     * Creates the {@code JXPathContext} to be used for executing a query. This
    332     * method delegates to the context factory.
    333     *
    334     * @param root the configuration root node
    335     * @param handler the node handler
    336     * @return the new context
    337     */
    338    private <T> JXPathContext createContext(final T root, final NodeHandler<T> handler)
    339    {
    340        return getContextFactory().createContext(root, handler);
    341    }
    342
    343    /**
    344     * Creates a {@code NodeAddData} object as a result of a
    345     * {@code prepareAdd()} operation. This method interprets the passed in path
    346     * of the new node.
    347     *
    348     * @param path the path of the new node
    349     * @param parentNodeResult the parent node
    350     * @param <T> the type of the nodes involved
    351     */
    352    <T> NodeAddData<T> createNodeAddData(final String path,
    353            final QueryResult<T> parentNodeResult)
    354    {
    355        if (parentNodeResult.isAttributeResult())
    356        {
    357            invalidPath(path, " cannot add properties to an attribute.");
    358        }
    359        final List<String> pathNodes = new LinkedList<>();
    360        String lastComponent = null;
    361        boolean attr = false;
    362        boolean first = true;
    363
    364        final StringTokenizer tok =
    365                new StringTokenizer(path, NODE_PATH_DELIMITERS, true);
    366        while (tok.hasMoreTokens())
    367        {
    368            final String token = tok.nextToken();
    369            if (PATH_DELIMITER.equals(token))
    370            {
    371                if (attr)
    372                {
    373                    invalidPath(path, " contains an attribute"
    374                            + " delimiter at a disallowed position.");
    375                }
    376                if (lastComponent == null)
    377                {
    378                    invalidPath(path,
    379                            " contains a '/' at a disallowed position.");
    380                }
    381                pathNodes.add(lastComponent);
    382                lastComponent = null;
    383            }
    384
    385            else if (ATTR_DELIMITER.equals(token))
    386            {
    387                if (attr)
    388                {
    389                    invalidPath(path,
    390                            " contains multiple attribute delimiters.");
    391                }
    392                if (lastComponent == null && !first)
    393                {
    394                    invalidPath(path,
    395                            " contains an attribute delimiter at a disallowed position.");
    396                }
    397                if (lastComponent != null)
    398                {
    399                    pathNodes.add(lastComponent);
    400                }
    401                attr = true;
    402                lastComponent = null;
    403            }
    404
    405            else
    406            {
    407                lastComponent = token;
    408            }
    409            first = false;
    410        }
    411
    412        if (lastComponent == null)
    413        {
    414            invalidPath(path, "contains no components.");
    415        }
    416
    417        return new NodeAddData<>(parentNodeResult.getNode(), lastComponent,
    418                attr, pathNodes);
    419    }
    420
    421    /**
    422     * Returns the {@code XPathContextFactory} used by this instance.
    423     *
    424     * @return the {@code XPathContextFactory}
    425     */
    426    XPathContextFactory getContextFactory()
    427    {
    428        return contextFactory;
    429    }
    430
    431    /**
    432     * Tries to generate a key for adding a property. This method is called if a
    433     * key was used for adding properties which does not contain a space
    434     * character. It splits the key at its single components and searches for
    435     * the last existing component. Then a key compatible key for adding
    436     * properties is generated.
    437     *
    438     * @param root the root node of the configuration
    439     * @param key the key in question
    440     * @param handler the node handler
    441     * @return the key to be used for adding the property
    442     */
    443    private <T> String generateKeyForAdd(final T root, final String key,
    444            final NodeHandler<T> handler)
    445    {
    446        int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
    447
    448        while (pos >= 0)
    449        {
    450            final String keyExisting = key.substring(0, pos);
    451            if (!query(root, keyExisting, handler).isEmpty())
    452            {
    453                final StringBuilder buf = new StringBuilder(key.length() + 1);
    454                buf.append(keyExisting).append(SPACE);
    455                buf.append(key.substring(pos + 1));
    456                return buf.toString();
    457            }
    458            pos = key.lastIndexOf(PATH_DELIMITER, pos - 1);
    459        }
    460
    461        return SPACE + key;
    462    }
    463
    464    /**
    465     * Determines the index of the given child node in the node list of its
    466     * parent.
    467     *
    468     * @param parent the parent node
    469     * @param child the child node
    470     * @param handler the node handler
    471     * @param <T> the type of the nodes involved
    472     * @return the index of this child node
    473     */
    474    private static <T> int determineIndex(final T parent, final T child,
    475            final NodeHandler<T> handler)
    476    {
    477        return handler.getChildren(parent, handler.nodeName(child)).indexOf(
    478                child) + 1;
    479    }
    480
    481    /**
    482     * Helper method for throwing an exception about an invalid path.
    483     *
    484     * @param path the invalid path
    485     * @param msg the exception message
    486     */
    487    private static void invalidPath(final String path, final String msg)
    488    {
    489        throw new IllegalArgumentException("Invalid node path: \"" + path
    490                + "\" " + msg);
    491    }
    492
    493    /**
    494     * Determines the position of the separator in a key for adding new
    495     * properties. If no delimiter is found, result is -1.
    496     *
    497     * @param key the key
    498     * @return the position of the delimiter
    499     */
    500    private static int findKeySeparator(final String key)
    501    {
    502        int index = key.length() - 1;
    503        while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
    504        {
    505            index--;
    506        }
    507        return index;
    508    }
    509
    510    /**
    511     * Converts the objects returned as query result from the JXPathContext to
    512     * query result objects.
    513     *
    514     * @param results the list with results from the context
    515     * @param <T> the type of results to be produced
    516     * @return the result list
    517     */
    518    private static <T> List<QueryResult<T>> convertResults(final List<?> results)
    519    {
    520        final List<QueryResult<T>> queryResults =
    521                new ArrayList<>(results.size());
    522        for (final Object res : results)
    523        {
    524            final QueryResult<T> queryResult = createResult(res);
    525            queryResults.add(queryResult);
    526        }
    527        return queryResults;
    528    }
    529
    530    /**
    531     * Creates a {@code QueryResult} object from the given result object of a
    532     * query. Because of the node pointers involved result objects can only be
    533     * of two types:
    534     * <ul>
    535     * <li>nodes of type T</li>
    536     * <li>attribute results already wrapped in {@code QueryResult} objects</li>
    537     * </ul>
    538     * This method performs a corresponding cast. Warnings can be suppressed
    539     * because of the implementation of the query functionality.
    540     *
    541     * @param resObj the query result object
    542     * @param <T> the type of the result to be produced
    543     * @return the {@code QueryResult}
    544     */
    545    @SuppressWarnings("unchecked")
    546    private static <T> QueryResult<T> createResult(final Object resObj)
    547    {
    548        if (resObj instanceof QueryResult)
    549        {
    550            return (QueryResult<T>) resObj;
    551        }
    552        return QueryResult.createNodeResult((T) resObj);
    553    }
    554
    555    // static initializer: registers the configuration node pointer factory
    556    static
    557    {
    558        JXPathContextReferenceImpl
    559                .addNodePointerFactory(new ConfigurationNodePointerFactory());
    560    }
    561}