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}