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