##### Coverage Report - org.apache.commons.configuration.tree.xpath.XPathExpressionEngine

Classes in this File | Line Coverage | Branch Coverage | Complexity
XPathExpressionEngine |
100% | 73/73
100% | 19/19
| 5.166666666666667;5,167

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 powerfull
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) 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 |  |
      * @since 1.3
101 |  |
      * @author Oliver Heger
102 |  |
      * @version $Id: XPathExpressionEngine.java 561230 2007-07-31 04:17:09Z rahul $
103 |  |
      */
104 | 32 |
     public class XPathExpressionEngine implements ExpressionEngine
105 |  |
     {
106 |  |
         /** Constant for the path delimiter. */
107 |  |
         static final String PATH_DELIMITER = "/";
108 |  |
109 |  |
         /** Constant for the attribute delimiter. */
110 |  |
         static final String ATTR_DELIMITER = "@";
111 |  |
112 |  |
         /** Constant for the delimiters for splitting node paths. */
113 |  |
         private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER
114 |  |
                 + ATTR_DELIMITER;
115 |  |
116 |  |
         /**
117 |  |
          * Executes a query. The passed in property key is directly passed to a
118 |  |
          * JXPath context.
119 |  |
          *
120 |  |
          * @param root the configuration root node
121 |  |
          * @param key the query to be executed
122 |  |
          * @return a list with the nodes that are selected by the query
123 |  |
          */
124 |  |
         public List query(ConfigurationNode root, String key)
125 |  |
         {
126 | 62 |
             if (StringUtils.isEmpty(key))
127 |  |
             {
128 | 4 |
                 List result = new ArrayList(1);
129 | 4 |
                 result.add(root);
130 | 4 |
                 return result;
131 |  |
             }
132 |  |
             else
133 |  |
             {
134 | 58 |
                 JXPathContext context = createContext(root, key);
135 | 58 |
                 List result = context.selectNodes(key);
136 | 57 |
                 return (result != null) ? result : Collections.EMPTY_LIST;
137 |  |
             }
138 |  |
         }
139 |  |
140 |  |
         /**
141 |  |
          * Returns a (canonic) key for the given node based on the parent's key.
142 |  |
          * This implementation will create an XPATH expression that selects the
143 |  |
          * given node (under the assumption that the passed in parent key is valid).
144 |  |
          * As the <code>nodeKey()</code> implementation of
145 |  |
          * <code>{@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine}</code>
146 |  |
          * this method will not return indices for nodes. So all child nodes of a
147 |  |
          * given parent whith the same name will have the same key.
148 |  |
          *
149 |  |
          * @param node the node for which a key is to be constructed
150 |  |
          * @param parentKey the key of the parent node
151 |  |
          * @return the key for the given node
152 |  |
          */
153 |  |
         public String nodeKey(ConfigurationNode node, String parentKey)
154 |  |
         {
155 | 98 |
             if (parentKey == null)
156 |  |
             {
157 |  |
                 // name of the root node
158 | 3 |
                 return StringUtils.EMPTY;
159 |  |
             }
160 | 95 |
             else if (node.getName() == null)
161 |  |
             {
162 |  |
                 // paranoia check for undefined node names
163 | 1 |
                 return parentKey;
164 |  |
             }
165 |  |
166 |  |
             else
167 |  |
             {
168 | 94 |
                 StringBuffer buf = new StringBuffer(parentKey.length()
169 |  |
                         + node.getName().length() + PATH_DELIMITER.length());
170 | 94 |
                 if (parentKey.length() > 0)
171 |  |
                 {
172 | 76 |
                     buf.append(parentKey);
173 | 76 |
                     buf.append(PATH_DELIMITER);
174 |  |
                 }
175 | 94 |
                 if (node.isAttribute())
176 |  |
                 {
177 | 22 |
                     buf.append(ATTR_DELIMITER);
178 |  |
                 }
179 | 94 |
                 buf.append(node.getName());
180 | 94 |
                 return buf.toString();
181 |  |
             }
182 |  |
         }
183 |  |
184 |  |
         /**
185 |  |
          * Prepares an add operation for a configuration property. The expected
186 |  |
          * format of the passed in key is explained in the class comment.
187 |  |
          *
188 |  |
          * @param root the configuration's root node
189 |  |
          * @param key the key describing the target of the add operation and the
190 |  |
          * path of the new node
191 |  |
          * @return a data object to be evaluated by the calling configuration object
192 |  |
          */
193 |  |
         public NodeAddData prepareAdd(ConfigurationNode root, String key)
194 |  |
         {
195 | 16 |
             if (key == null)
196 |  |
             {
197 | 1 |
                 throw new IllegalArgumentException(
198 |  |
                         "prepareAdd: key must not be null!");
199 |  |
             }
200 |  |
201 | 15 |
             int index = key.length() - 1;
202 | 184 |
             while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
203 |  |
             {
204 | 169 |
                 index--;
205 | 169 |
             }
206 | 15 |
             if (index < 0)
207 |  |
             {
208 | 2 |
                 throw new IllegalArgumentException(
209 |  |
                         "prepareAdd: Passed in key must contain a whitespace!");
210 |  |
             }
211 |  |
212 | 13 |
             List nodes = query(root, key.substring(0, index).trim());
213 | 13 |
             if (nodes.size() != 1)
214 |  |
             {
215 | 1 |
                 throw new IllegalArgumentException(
216 |  |
                         "prepareAdd: key must select exactly one target node!");
217 |  |
             }
218 |  |
219 | 12 |
             NodeAddData data = new NodeAddData();
220 | 12 |
             data.setParent((ConfigurationNode) nodes.get(0));
221 | 12 |
             initNodeAddData(data, key.substring(index).trim());
222 | 6 |
             return data;
223 |  |
         }
224 |  |
225 |  |
         /**
226 |  |
          * Creates the <code>JXPathContext</code> used for executing a query. This
227 |  |
          * method will create a new context and ensure that it is correctly
228 |  |
          * initialized.
229 |  |
          *
230 |  |
          * @param root the configuration root node
231 |  |
          * @param key the key to be queried
232 |  |
          * @return the new context
233 |  |
          */
234 |  |
         protected JXPathContext createContext(ConfigurationNode root, String key)
235 |  |
         {
236 | 46 |
             JXPathContext context = JXPathContext.newContext(root);
237 | 46 |
             context.setLenient(true);
238 | 46 |
             return context;
239 |  |
         }
240 |  |
241 |  |
         /**
242 |  |
          * Initializes most properties of a <code>NodeAddData</code> object. This
243 |  |
          * method is called by <code>prepareAdd()</code> after the parent node has
244 |  |
          * been found. Its task is to interprete the passed in path of the new node.
245 |  |
          *
246 |  |
          * @param data the data object to initialize
247 |  |
          * @param path the path of the new node
248 |  |
          */
249 |  |
         protected void initNodeAddData(NodeAddData data, String path)
250 |  |
         {
251 | 12 |
             String lastComponent = null;
252 | 12 |
             boolean attr = false;
253 | 12 |
             boolean first = true;
254 |  |
255 | 12 |
             StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
256 |  |
                     true);
257 | 52 |
             while (tok.hasMoreTokens())
258 |  |
             {
259 | 45 |
                 String token = tok.nextToken();
260 | 45 |
                 if (PATH_DELIMITER.equals(token))
261 |  |
                 {
262 | 15 |
                     if (attr)
263 |  |
                     {
264 | 1 |
                         invalidPath(path, " contains an attribute"
265 |  |
                                 + " delimiter at an unallowed position.");
266 |  |
                     }
267 | 14 |
                     if (lastComponent == null)
268 |  |
                     {
269 | 2 |
                         invalidPath(path,
270 |  |
                                 " contains a '/' at an unallowed position.");
271 |  |
                     }
272 | 12 |
                     data.addPathNode(lastComponent);
273 | 12 |
                     lastComponent = null;
274 | 12 |
                 }
275 |  |
276 | 30 |
                 else if (ATTR_DELIMITER.equals(token))
277 |  |
                 {
278 | 7 |
                     if (attr)
279 |  |
                     {
280 | 1 |
                         invalidPath(path,
281 |  |
                                 " contains multiple attribute delimiters.");
282 |  |
                     }
283 | 6 |
                     if (lastComponent == null && !first)
284 |  |
                     {
285 | 1 |
                         invalidPath(path,
286 |  |
                                 " contains an attribute delimiter at an unallowed position.");
287 |  |
                     }
288 | 5 |
                     if (lastComponent != null)
289 |  |
                     {
290 | 3 |
                         data.addPathNode(lastComponent);
291 |  |
                     }
292 | 5 |
                     attr = true;
293 | 5 |
                     lastComponent = null;
294 | 5 |
                 }
295 |  |
296 |  |
                 else
297 |  |
                 {
298 | 23 |
                     lastComponent = token;
299 |  |
                 }
300 | 40 |
                 first = false;
301 | 40 |
             }
302 |  |
303 | 7 |
             if (lastComponent == null)
304 |  |
             {
305 | 1 |
                 invalidPath(path, "contains no components.");
306 |  |
             }
307 | 6 |
             data.setNewNodeName(lastComponent);
308 | 6 |
             data.setAttribute(attr);
309 | 6 |
         }
310 |  |
311 |  |
         /**
312 |  |
          * Helper method for throwing an exception about an invalid path.
313 |  |
          *
314 |  |
          * @param path the invalid path
315 |  |
          * @param msg the exception message
316 |  |
          */
317 |  |
         private void invalidPath(String path, String msg)
318 |  |
         {
319 | 6 |
             throw new IllegalArgumentException("Invalid node path: \"" + path
320 |  |
                     + "\" " + msg);
321 |  |
         }
322 |  |
323 |  |
         // static initializer: registers the configuration node pointer factory
324 |  |
         static
325 |  |
         {
326 | 4 |
             JXPathContextReferenceImpl
327 |  |
                     .addNodePointerFactory(new ConfigurationNodePointerFactory());
328 | 4 |
         }
329 |  |
     }