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.configuration.tree.xpath;
018
019import java.util.Collections;
020import java.util.List;
021import java.util.StringTokenizer;
022
023import org.apache.commons.configuration.tree.ConfigurationNode;
024import org.apache.commons.configuration.tree.ExpressionEngine;
025import org.apache.commons.configuration.tree.NodeAddData;
026import org.apache.commons.jxpath.JXPathContext;
027import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
028import org.apache.commons.lang.StringUtils;
029
030/**
031 * <p>
032 * A specialized implementation of the {@code ExpressionEngine} interface
033 * that is able to evaluate XPATH expressions.
034 * </p>
035 * <p>
036 * This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons
037 * JXPath</a> for handling XPath expressions and mapping them to the nodes of a
038 * hierarchical configuration. This makes the rich and powerful XPATH syntax
039 * available for accessing properties from a configuration object.
040 * </p>
041 * <p>
042 * For selecting properties arbitrary XPATH expressions can be used, which
043 * select single or multiple configuration nodes. The associated
044 * {@code Configuration} instance will directly pass the specified property
045 * keys into this engine. If a key is not syntactically correct, an exception
046 * will be thrown.
047 * </p>
048 * <p>
049 * For adding new properties, this expression engine uses a specific syntax: the
050 * &quot;key&quot; of a new property must consist of two parts that are
051 * separated by whitespace:
052 * <ol>
053 * <li>An XPATH expression selecting a single node, to which the new element(s)
054 * are to be added. This can be an arbitrary complex expression, but it must
055 * select exactly one node, otherwise an exception will be thrown.</li>
056 * <li>The name of the new element(s) to be added below this parent node. Here
057 * either a single node name or a complete path of nodes (separated by the
058 * &quot;/&quot; character or &quot;@&quot; for an attribute) can be specified.</li>
059 * </ol>
060 * Some examples for valid keys that can be passed into the configuration's
061 * {@code addProperty()} method follow:
062 * </p>
063 * <p>
064 *
065 * <pre>
066 * &quot;/tables/table[1] type&quot;
067 * </pre>
068 *
069 * </p>
070 * <p>
071 * This will add a new {@code type} node as a child of the first
072 * {@code table} element.
073 * </p>
074 * <p>
075 *
076 * <pre>
077 * &quot;/tables/table[1] @type&quot;
078 * </pre>
079 *
080 * </p>
081 * <p>
082 * Similar to the example above, but this time a new attribute named
083 * {@code type} will be added to the first {@code table} element.
084 * </p>
085 * <p>
086 *
087 * <pre>
088 * &quot;/tables table/fields/field/name&quot;
089 * </pre>
090 *
091 * </p>
092 * <p>
093 * This example shows how a complex path can be added. Parent node is the
094 * {@code tables} element. Here a new branch consisting of the nodes
095 * {@code table}, {@code fields}, {@code field}, and
096 * {@code name} will be added.
097 * </p>
098 * <p>
099 *
100 * <pre>
101 * &quot;/tables table/fields/field@type&quot;
102 * </pre>
103 *
104 * </p>
105 * <p>
106 * This is similar to the last example, but in this case a complex path ending
107 * with an attribute is defined.
108 * </p>
109 * <p>
110 * <strong>Note:</strong> This extended syntax for adding properties only works
111 * with the {@code addProperty()} method. {@code setProperty()} does
112 * not support creating new nodes this way.
113 * </p>
114 * <p>
115 * From version 1.7 on, it is possible to use regular keys in calls to
116 * {@code addProperty()} (i.e. keys that do not have to contain a
117 * whitespace as delimiter). In this case the key is evaluated, and the biggest
118 * part pointing to an existing node is determined. The remaining part is then
119 * added as new path. As an example consider the key
120 *
121 * <pre>
122 * &quot;tables/table[last()]/fields/field/name&quot;
123 * </pre>
124 *
125 * If the key does not point to an existing node, the engine will check the
126 * paths {@code "tables/table[last()]/fields/field"},
127 * {@code "tables/table[last()]/fields"},
128 * {@code "tables/table[last()]"}, and so on, until a key is
129 * found which points to a node. Let's assume that the last key listed above can
130 * be resolved in this way. Then from this key the following key is derived:
131 * {@code "tables/table[last()] fields/field/name"} by appending
132 * the remaining part after a whitespace. This key can now be processed using
133 * the original algorithm. Keys of this form can also be used with the
134 * {@code setProperty()} method. However, it is still recommended to use
135 * the old format because it makes explicit at which position new nodes should
136 * be added. For keys without a whitespace delimiter there may be ambiguities.
137 * </p>
138 *
139 * @since 1.3
140 * @author <a
141 *         href="http://commons.apache.org/configuration/team-list.html">Commons
142 *         Configuration team</a>
143 * @version $Id: XPathExpressionEngine.java 1206563 2011-11-26 19:47:26Z oheger $
144 */
145public class XPathExpressionEngine implements ExpressionEngine
146{
147    /** Constant for the path delimiter. */
148    static final String PATH_DELIMITER = "/";
149
150    /** Constant for the attribute delimiter. */
151    static final String ATTR_DELIMITER = "@";
152
153    /** Constant for the delimiters for splitting node paths. */
154    private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER
155            + ATTR_DELIMITER;
156
157    /**
158     * Constant for a space which is used as delimiter in keys for adding
159     * properties.
160     */
161    private static final String SPACE = " ";
162
163    /**
164     * Executes a query. The passed in property key is directly passed to a
165     * JXPath context.
166     *
167     * @param root the configuration root node
168     * @param key the query to be executed
169     * @return a list with the nodes that are selected by the query
170     */
171    public List<ConfigurationNode> query(ConfigurationNode root, String key)
172    {
173        if (StringUtils.isEmpty(key))
174        {
175            return Collections.singletonList(root);
176        }
177        else
178        {
179            JXPathContext context = createContext(root, key);
180            // This is safe because our node pointer implementations will return
181            // a list of configuration nodes.
182            @SuppressWarnings("unchecked")
183            List<ConfigurationNode> result = context.selectNodes(key);
184            if (result == null)
185            {
186                result = Collections.emptyList();
187            }
188            return result;
189        }
190    }
191
192    /**
193     * Returns a (canonical) key for the given node based on the parent's key.
194     * This implementation will create an XPATH expression that selects the
195     * given node (under the assumption that the passed in parent key is valid).
196     * As the {@code nodeKey()} implementation of
197     * {@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine}
198     * this method will not return indices for nodes. So all child nodes of a
199     * given parent with the same name will have the same key.
200     *
201     * @param node the node for which a key is to be constructed
202     * @param parentKey the key of the parent node
203     * @return the key for the given node
204     */
205    public String nodeKey(ConfigurationNode node, String parentKey)
206    {
207        if (parentKey == null)
208        {
209            // name of the root node
210            return StringUtils.EMPTY;
211        }
212        else if (node.getName() == null)
213        {
214            // paranoia check for undefined node names
215            return parentKey;
216        }
217
218        else
219        {
220            StringBuilder buf = new StringBuilder(parentKey.length()
221                    + node.getName().length() + PATH_DELIMITER.length());
222            if (parentKey.length() > 0)
223            {
224                buf.append(parentKey);
225                buf.append(PATH_DELIMITER);
226            }
227            if (node.isAttribute())
228            {
229                buf.append(ATTR_DELIMITER);
230            }
231            buf.append(node.getName());
232            return buf.toString();
233        }
234    }
235
236    /**
237     * Prepares an add operation for a configuration property. The expected
238     * format of the passed in key is explained in the class comment.
239     *
240     * @param root the configuration's root node
241     * @param key the key describing the target of the add operation and the
242     * path of the new node
243     * @return a data object to be evaluated by the calling configuration object
244     */
245    public NodeAddData prepareAdd(ConfigurationNode root, String key)
246    {
247        if (key == null)
248        {
249            throw new IllegalArgumentException(
250                    "prepareAdd: key must not be null!");
251        }
252
253        String addKey = key;
254        int index = findKeySeparator(addKey);
255        if (index < 0)
256        {
257            addKey = generateKeyForAdd(root, addKey);
258            index = findKeySeparator(addKey);
259        }
260
261        List<ConfigurationNode> nodes = query(root, addKey.substring(0, index).trim());
262        if (nodes.size() != 1)
263        {
264            throw new IllegalArgumentException(
265                    "prepareAdd: key must select exactly one target node!");
266        }
267
268        NodeAddData data = new NodeAddData();
269        data.setParent(nodes.get(0));
270        initNodeAddData(data, addKey.substring(index).trim());
271        return data;
272    }
273
274    /**
275     * Creates the {@code JXPathContext} used for executing a query. This
276     * method will create a new context and ensure that it is correctly
277     * initialized.
278     *
279     * @param root the configuration root node
280     * @param key the key to be queried
281     * @return the new context
282     */
283    protected JXPathContext createContext(ConfigurationNode root, String key)
284    {
285        JXPathContext context = JXPathContext.newContext(root);
286        context.setLenient(true);
287        return context;
288    }
289
290    /**
291     * Initializes most properties of a {@code NodeAddData} object. This
292     * method is called by {@code prepareAdd()} after the parent node has
293     * been found. Its task is to interpret the passed in path of the new node.
294     *
295     * @param data the data object to initialize
296     * @param path the path of the new node
297     */
298    protected void initNodeAddData(NodeAddData data, String path)
299    {
300        String lastComponent = null;
301        boolean attr = false;
302        boolean first = true;
303
304        StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
305                true);
306        while (tok.hasMoreTokens())
307        {
308            String token = tok.nextToken();
309            if (PATH_DELIMITER.equals(token))
310            {
311                if (attr)
312                {
313                    invalidPath(path, " contains an attribute"
314                            + " delimiter at an unallowed position.");
315                }
316                if (lastComponent == null)
317                {
318                    invalidPath(path,
319                            " contains a '/' at an unallowed position.");
320                }
321                data.addPathNode(lastComponent);
322                lastComponent = null;
323            }
324
325            else if (ATTR_DELIMITER.equals(token))
326            {
327                if (attr)
328                {
329                    invalidPath(path,
330                            " contains multiple attribute delimiters.");
331                }
332                if (lastComponent == null && !first)
333                {
334                    invalidPath(path,
335                            " contains an attribute delimiter at an unallowed position.");
336                }
337                if (lastComponent != null)
338                {
339                    data.addPathNode(lastComponent);
340                }
341                attr = true;
342                lastComponent = null;
343            }
344
345            else
346            {
347                lastComponent = token;
348            }
349            first = false;
350        }
351
352        if (lastComponent == null)
353        {
354            invalidPath(path, "contains no components.");
355        }
356        data.setNewNodeName(lastComponent);
357        data.setAttribute(attr);
358    }
359
360    /**
361     * Tries to generate a key for adding a property. This method is called if a
362     * key was used for adding properties which does not contain a space
363     * character. It splits the key at its single components and searches for
364     * the last existing component. Then a key compatible for adding properties
365     * is generated.
366     *
367     * @param root the root node of the configuration
368     * @param key the key in question
369     * @return the key to be used for adding the property
370     */
371    private String generateKeyForAdd(ConfigurationNode root, String key)
372    {
373        int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
374
375        while (pos >= 0)
376        {
377            String keyExisting = key.substring(0, pos);
378            if (!query(root, keyExisting).isEmpty())
379            {
380                StringBuilder buf = new StringBuilder(key.length() + 1);
381                buf.append(keyExisting).append(SPACE);
382                buf.append(key.substring(pos + 1));
383                return buf.toString();
384            }
385            pos = key.lastIndexOf(PATH_DELIMITER, pos - 1);
386        }
387
388        return SPACE + key;
389    }
390
391    /**
392     * Helper method for throwing an exception about an invalid path.
393     *
394     * @param path the invalid path
395     * @param msg the exception message
396     */
397    private void invalidPath(String path, String msg)
398    {
399        throw new IllegalArgumentException("Invalid node path: \"" + path
400                + "\" " + msg);
401    }
402
403    /**
404     * Determines the position of the separator in a key for adding new
405     * properties. If no delimiter is found, result is -1.
406     *
407     * @param key the key
408     * @return the position of the delimiter
409     */
410    private static int findKeySeparator(String key)
411    {
412        int index = key.length() - 1;
413        while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
414        {
415            index--;
416        }
417        return index;
418    }
419
420    // static initializer: registers the configuration node pointer factory
421    static
422    {
423        JXPathContextReferenceImpl
424                .addNodePointerFactory(new ConfigurationNodePointerFactory());
425    }
426}