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.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  import java.util.StringTokenizer;
23  
24  import org.apache.commons.configuration.tree.ConfigurationNode;
25  import org.apache.commons.configuration.tree.ExpressionEngine;
26  import org.apache.commons.configuration.tree.NodeAddData;
27  import org.apache.commons.jxpath.JXPathContext;
28  import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
29  import org.apache.commons.lang.StringUtils;
30  
31  /***
32   * <p>
33   * A specialized implementation of the <code>ExpressionEngine</code> interface
34   * that is able to evaluate XPATH expressions.
35   * </p>
36   * <p>
37   * This class makes use of <a href="http://commons.apache.org/jxpath/">
38   * Commons JXPath</a> for handling XPath expressions and mapping them to the
39   * nodes of a hierarchical configuration. This makes the rich and powerful
40   * XPATH syntax available for accessing properties from a configuration object.
41   * </p>
42   * <p>
43   * For selecting properties arbitrary XPATH expressions can be used, which
44   * select single or multiple configuration nodes. The associated
45   * <code>Configuration</code> instance will directly pass the specified
46   * property keys into this engine. If a key is not syntactically correct, an
47   * exception will be thrown.
48   * </p>
49   * <p>
50   * For adding new properties, this expression engine uses a specific syntax: the
51   * &quot;key&quot; of a new property must consist of two parts that are
52   * separated by whitespace:
53   * <ol>
54   * <li>An XPATH expression selecting a single node, to which the new element(s)
55   * are to be added. This can be an arbitrary complex expression, but it must
56   * select exactly one node, otherwise an exception will be thrown.</li>
57   * <li>The name of the new element(s) to be added below this parent node. Here
58   * either a single node name or a complete path of nodes (separated by the
59   * &quot;/&quot; character or &quot;@&quot; for an attribute) can be specified.</li>
60   * </ol>
61   * Some examples for valid keys that can be passed into the configuration's
62   * <code>addProperty()</code> method follow:
63   * </p>
64   * <p>
65   *
66   * <pre>
67   * &quot;/tables/table[1] type&quot;
68   * </pre>
69   *
70   * </p>
71   * <p>
72   * This will add a new <code>type</code> node as a child of the first
73   * <code>table</code> element.
74   * </p>
75   * <p>
76   *
77   * <pre>
78   * &quot;/tables/table[1] @type&quot;
79   * </pre>
80   *
81   * </p>
82   * <p>
83   * Similar to the example above, but this time a new attribute named
84   * <code>type</code> will be added to the first <code>table</code> element.
85   * </p>
86   * <p>
87   *
88   * <pre>
89   * &quot;/tables table/fields/field/name&quot;
90   * </pre>
91   *
92   * </p>
93   * <p>
94   * This example shows how a complex path can be added. Parent node is the
95   * <code>tables</code> element. Here a new branch consisting of the nodes
96   * <code>table</code>, <code>fields</code>, <code>field</code>, and
97   * <code>name</code> will be added.
98   * </p>
99   *
100  * <p>
101  * <pre>
102  * &quot;/tables table/fields/field@type&quot;
103  * </pre>
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()</code> method. <code>setProperty()</code> does
112  * not support creating new nodes this way.
113  * </p>
114  *
115  * @since 1.3
116  * @author Oliver Heger
117  * @version $Id: XPathExpressionEngine.java 656402 2008-05-14 20:15:23Z oheger $
118  */
119 public class XPathExpressionEngine implements ExpressionEngine
120 {
121     /*** Constant for the path delimiter. */
122     static final String PATH_DELIMITER = "/";
123 
124     /*** Constant for the attribute delimiter. */
125     static final String ATTR_DELIMITER = "@";
126 
127     /*** Constant for the delimiters for splitting node paths. */
128     private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER
129             + ATTR_DELIMITER;
130 
131     /***
132      * Executes a query. The passed in property key is directly passed to a
133      * JXPath context.
134      *
135      * @param root the configuration root node
136      * @param key the query to be executed
137      * @return a list with the nodes that are selected by the query
138      */
139     public List query(ConfigurationNode root, String key)
140     {
141         if (StringUtils.isEmpty(key))
142         {
143             List result = new ArrayList(1);
144             result.add(root);
145             return result;
146         }
147         else
148         {
149             JXPathContext context = createContext(root, key);
150             List result = context.selectNodes(key);
151             return (result != null) ? result : Collections.EMPTY_LIST;
152         }
153     }
154 
155     /***
156      * Returns a (canonic) key for the given node based on the parent's key.
157      * This implementation will create an XPATH expression that selects the
158      * given node (under the assumption that the passed in parent key is valid).
159      * As the <code>nodeKey()</code> implementation of
160      * <code>{@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine}</code>
161      * this method will not return indices for nodes. So all child nodes of a
162      * given parent whith the same name will have the same key.
163      *
164      * @param node the node for which a key is to be constructed
165      * @param parentKey the key of the parent node
166      * @return the key for the given node
167      */
168     public String nodeKey(ConfigurationNode node, String parentKey)
169     {
170         if (parentKey == null)
171         {
172             // name of the root node
173             return StringUtils.EMPTY;
174         }
175         else if (node.getName() == null)
176         {
177             // paranoia check for undefined node names
178             return parentKey;
179         }
180 
181         else
182         {
183             StringBuffer buf = new StringBuffer(parentKey.length()
184                     + node.getName().length() + PATH_DELIMITER.length());
185             if (parentKey.length() > 0)
186             {
187                 buf.append(parentKey);
188                 buf.append(PATH_DELIMITER);
189             }
190             if (node.isAttribute())
191             {
192                 buf.append(ATTR_DELIMITER);
193             }
194             buf.append(node.getName());
195             return buf.toString();
196         }
197     }
198 
199     /***
200      * Prepares an add operation for a configuration property. The expected
201      * format of the passed in key is explained in the class comment.
202      *
203      * @param root the configuration's root node
204      * @param key the key describing the target of the add operation and the
205      * path of the new node
206      * @return a data object to be evaluated by the calling configuration object
207      */
208     public NodeAddData prepareAdd(ConfigurationNode root, String key)
209     {
210         if (key == null)
211         {
212             throw new IllegalArgumentException(
213                     "prepareAdd: key must not be null!");
214         }
215 
216         int index = key.length() - 1;
217         while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
218         {
219             index--;
220         }
221         if (index < 0)
222         {
223             throw new IllegalArgumentException(
224                     "prepareAdd: Passed in key must contain a whitespace!");
225         }
226 
227         List nodes = query(root, key.substring(0, index).trim());
228         if (nodes.size() != 1)
229         {
230             throw new IllegalArgumentException(
231                     "prepareAdd: key must select exactly one target node!");
232         }
233 
234         NodeAddData data = new NodeAddData();
235         data.setParent((ConfigurationNode) nodes.get(0));
236         initNodeAddData(data, key.substring(index).trim());
237         return data;
238     }
239 
240     /***
241      * Creates the <code>JXPathContext</code> used for executing a query. This
242      * method will create a new context and ensure that it is correctly
243      * initialized.
244      *
245      * @param root the configuration root node
246      * @param key the key to be queried
247      * @return the new context
248      */
249     protected JXPathContext createContext(ConfigurationNode root, String key)
250     {
251         JXPathContext context = JXPathContext.newContext(root);
252         context.setLenient(true);
253         return context;
254     }
255 
256     /***
257      * Initializes most properties of a <code>NodeAddData</code> object. This
258      * method is called by <code>prepareAdd()</code> after the parent node has
259      * been found. Its task is to interpret the passed in path of the new node.
260      *
261      * @param data the data object to initialize
262      * @param path the path of the new node
263      */
264     protected void initNodeAddData(NodeAddData data, String path)
265     {
266         String lastComponent = null;
267         boolean attr = false;
268         boolean first = true;
269 
270         StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
271                 true);
272         while (tok.hasMoreTokens())
273         {
274             String token = tok.nextToken();
275             if (PATH_DELIMITER.equals(token))
276             {
277                 if (attr)
278                 {
279                     invalidPath(path, " contains an attribute"
280                             + " delimiter at an unallowed position.");
281                 }
282                 if (lastComponent == null)
283                 {
284                     invalidPath(path,
285                             " contains a '/' at an unallowed position.");
286                 }
287                 data.addPathNode(lastComponent);
288                 lastComponent = null;
289             }
290 
291             else if (ATTR_DELIMITER.equals(token))
292             {
293                 if (attr)
294                 {
295                     invalidPath(path,
296                             " contains multiple attribute delimiters.");
297                 }
298                 if (lastComponent == null && !first)
299                 {
300                     invalidPath(path,
301                             " contains an attribute delimiter at an unallowed position.");
302                 }
303                 if (lastComponent != null)
304                 {
305                     data.addPathNode(lastComponent);
306                 }
307                 attr = true;
308                 lastComponent = null;
309             }
310 
311             else
312             {
313                 lastComponent = token;
314             }
315             first = false;
316         }
317 
318         if (lastComponent == null)
319         {
320             invalidPath(path, "contains no components.");
321         }
322         data.setNewNodeName(lastComponent);
323         data.setAttribute(attr);
324     }
325 
326     /***
327      * Helper method for throwing an exception about an invalid path.
328      *
329      * @param path the invalid path
330      * @param msg the exception message
331      */
332     private void invalidPath(String path, String msg)
333     {
334         throw new IllegalArgumentException("Invalid node path: \"" + path
335                 + "\" " + msg);
336     }
337 
338     // static initializer: registers the configuration node pointer factory
339     static
340     {
341         JXPathContextReferenceImpl
342                 .addNodePointerFactory(new ConfigurationNodePointerFactory());
343     }
344 }