View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.configuration.tree.xpath;
18  
19  import java.util.Collections;
20  import java.util.List;
21  import java.util.StringTokenizer;
22  
23  import org.apache.commons.configuration.tree.ConfigurationNode;
24  import org.apache.commons.configuration.tree.ExpressionEngine;
25  import org.apache.commons.configuration.tree.NodeAddData;
26  import org.apache.commons.jxpath.JXPathContext;
27  import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
28  import org.apache.commons.lang.StringUtils;
29  
30  /**
31   * <p>
32   * A specialized implementation of the {@code ExpressionEngine} interface
33   * that is able to evaluate XPATH expressions.
34   * </p>
35   * <p>
36   * This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons
37   * JXPath</a> for handling XPath expressions and mapping them to the nodes of a
38   * hierarchical configuration. This makes the rich and powerful XPATH syntax
39   * available for accessing properties from a configuration object.
40   * </p>
41   * <p>
42   * For selecting properties arbitrary XPATH expressions can be used, which
43   * select single or multiple configuration nodes. The associated
44   * {@code Configuration} instance will directly pass the specified property
45   * keys into this engine. If a key is not syntactically correct, an exception
46   * will be thrown.
47   * </p>
48   * <p>
49   * For adding new properties, this expression engine uses a specific syntax: the
50   * &quot;key&quot; of a new property must consist of two parts that are
51   * separated by whitespace:
52   * <ol>
53   * <li>An XPATH expression selecting a single node, to which the new element(s)
54   * are to be added. This can be an arbitrary complex expression, but it must
55   * select exactly one node, otherwise an exception will be thrown.</li>
56   * <li>The name of the new element(s) to be added below this parent node. Here
57   * either a single node name or a complete path of nodes (separated by the
58   * &quot;/&quot; character or &quot;@&quot; for an attribute) can be specified.</li>
59   * </ol>
60   * Some examples for valid keys that can be passed into the configuration's
61   * {@code addProperty()} method follow:
62   * </p>
63   * <p>
64   *
65   * <pre>
66   * &quot;/tables/table[1] type&quot;
67   * </pre>
68   *
69   * </p>
70   * <p>
71   * This will add a new {@code type} node as a child of the first
72   * {@code table} element.
73   * </p>
74   * <p>
75   *
76   * <pre>
77   * &quot;/tables/table[1] @type&quot;
78   * </pre>
79   *
80   * </p>
81   * <p>
82   * Similar to the example above, but this time a new attribute named
83   * {@code type} will be added to the first {@code table} element.
84   * </p>
85   * <p>
86   *
87   * <pre>
88   * &quot;/tables table/fields/field/name&quot;
89   * </pre>
90   *
91   * </p>
92   * <p>
93   * This example shows how a complex path can be added. Parent node is the
94   * {@code tables} element. Here a new branch consisting of the nodes
95   * {@code table}, {@code fields}, {@code field}, and
96   * {@code name} will be added.
97   * </p>
98   * <p>
99   *
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 }