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     */
017    package org.apache.commons.configuration.tree.xpath;
018    
019    import java.util.Collections;
020    import java.util.List;
021    import java.util.StringTokenizer;
022    
023    import org.apache.commons.configuration.tree.ConfigurationNode;
024    import org.apache.commons.configuration.tree.ExpressionEngine;
025    import org.apache.commons.configuration.tree.NodeAddData;
026    import org.apache.commons.jxpath.JXPathContext;
027    import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
028    import 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     */
145    public 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    }