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

Classes in this File | Line Coverage | Branch Coverage | Complexity
XPathExpressionEngine |
100% | 70/70
100% | 20/20
| 5.333333333333333;5,333

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://jakarta.apache.org/commons/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 439648 2006-09-02 20:42:10Z oheger $
103 |  |
      */
104 | 31 |
     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 | 249 |
             if (StringUtils.isEmpty(key))
127 |  |
             {
128 | 81 |
                 List result = new ArrayList(1);
129 | 81 |
                 result.add(root);
130 | 81 |
                 return result;
131 |  |
             }
132 |  |
             else
133 |  |
             {
134 | 168 |
                 JXPathContext context = createContext(root, key);
135 | 168 |
                 List result = context.selectNodes(key);
136 | 168 |
                 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 | 21 |
             if (parentKey == null)
156 |  |
             {
157 |  |
                 // name of the root node
158 | 2 |
                 return StringUtils.EMPTY;
159 |  |
             }
160 | 19 |
             else if (node.getName() == null)
161 |  |
             {
162 |  |
                 // paranoia check for undefined node names
163 | 1 |
                 return parentKey;
164 |  |
             }
165 |  |
166 |  |
             else
167 |  |
             {
168 | 18 |
                 StringBuffer buf = new StringBuffer(parentKey.length()
169 |  |
                         + node.getName().length() + PATH_DELIMITER.length());
170 | 18 |
                 if (parentKey.length() > 0)
171 |  |
                 {
172 | 14 |
                     buf.append(parentKey);
173 | 14 |
                     if (!node.isAttribute())
174 |  |
                     {
175 | 13 |
                         buf.append(PATH_DELIMITER);
176 |  |
                     }
177 |  |
                 }
178 | 18 |
                 if (node.isAttribute())
179 |  |
                 {
180 | 2 |
                     buf.append(ATTR_DELIMITER);
181 |  |
                 }
182 | 18 |
                 buf.append(node.getName());
183 | 18 |
                 return buf.toString();
184 |  |
             }
185 |  |
         }
186 |  |
187 |  |
         /**
188 |  |
          * Prepares an add operation for a configuration property. The expected
189 |  |
          * format of the passed in key is explained in the class comment.
190 |  |
          *
191 |  |
          * @param root the configuration's root node
192 |  |
          * @param key the key describing the target of the add operation and the
193 |  |
          * path of the new node
194 |  |
          * @return a data object to be evaluated by the calling configuration object
195 |  |
          */
196 |  |
         public NodeAddData prepareAdd(ConfigurationNode root, String key)
197 |  |
         {
198 | 28 |
             if (key == null)
199 |  |
             {
200 | 1 |
                 throw new IllegalArgumentException(
201 |  |
                         "prepareAdd: key must not be null!");
202 |  |
             }
203 |  |
204 | 27 |
             int index = key.length() - 1;
205 | 438 |
             while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
206 |  |
             {
207 | 384 |
                 index--;
208 |  |
             }
209 | 27 |
             if (index < 0)
210 |  |
             {
211 | 2 |
                 throw new IllegalArgumentException(
212 |  |
                         "prepareAdd: Passed in key must contain a whitespace!");
213 |  |
             }
214 |  |
215 | 25 |
             List nodes = query(root, key.substring(0, index).trim());
216 | 25 |
             if (nodes.size() != 1)
217 |  |
             {
218 | 1 |
                 throw new IllegalArgumentException(
219 |  |
                         "prepareAdd: key must select exactly one target node!");
220 |  |
             }
221 |  |
222 | 24 |
             NodeAddData data = new NodeAddData();
223 | 24 |
             data.setParent((ConfigurationNode) nodes.get(0));
224 | 24 |
             initNodeAddData(data, key.substring(index).trim());
225 | 18 |
             return data;
226 |  |
         }
227 |  |
228 |  |
         /**
229 |  |
          * Creates the <code>JXPathContext</code> used for executing a query. This
230 |  |
          * method will create a new context and ensure that it is correctly
231 |  |
          * initialized.
232 |  |
          *
233 |  |
          * @param root the configuration root node
234 |  |
          * @param key the key to be queried
235 |  |
          * @return the new context
236 |  |
          */
237 |  |
         protected JXPathContext createContext(ConfigurationNode root, String key)
238 |  |
         {
239 | 169 |
             JXPathContext context = JXPathContext.newContext(root);
240 | 169 |
             context.setLenient(true);
241 | 169 |
             return context;
242 |  |
         }
243 |  |
244 |  |
         /**
245 |  |
          * Initializes most properties of a <code>NodeAddData</code> object. This
246 |  |
          * method is called by <code>prepareAdd()</code> after the parent node has
247 |  |
          * been found. Its task is to interprete the passed in path of the new node.
248 |  |
          *
249 |  |
          * @param data the data object to initialize
250 |  |
          * @param path the path of the new node
251 |  |
          */
252 |  |
         protected void initNodeAddData(NodeAddData data, String path)
253 |  |
         {
254 | 24 |
             String lastComponent = null;
255 | 24 |
             boolean attr = false;
256 | 24 |
             boolean first = true;
257 |  |
258 | 24 |
             StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
259 |  |
                     true);
260 | 122 |
             while (tok.hasMoreTokens())
261 |  |
             {
262 | 79 |
                 String token = tok.nextToken();
263 | 79 |
                 if (PATH_DELIMITER.equals(token))
264 |  |
                 {
265 | 18 |
                     if (attr)
266 |  |
                     {
267 | 1 |
                         invalidPath(path, " contains an attribute"
268 |  |
                                 + " delimiter at an unallowed position.");
269 |  |
                     }
270 | 17 |
                     if (lastComponent == null)
271 |  |
                     {
272 | 2 |
                         invalidPath(path,
273 |  |
                                 " contains a '/' at an unallowed position.");
274 |  |
                     }
275 | 15 |
                     data.addPathNode(lastComponent);
276 | 15 |
                     lastComponent = null;
277 |  |
                 }
278 |  |
279 | 61 |
                 else if (ATTR_DELIMITER.equals(token))
280 |  |
                 {
281 | 18 |
                     if (attr)
282 |  |
                     {
283 | 1 |
                         invalidPath(path,
284 |  |
                                 " contains multiple attribute delimiters.");
285 |  |
                     }
286 | 17 |
                     if (lastComponent == null && !first)
287 |  |
                     {
288 | 1 |
                         invalidPath(path,
289 |  |
                                 " contains an attribute delimiter at an unallowed position.");
290 |  |
                     }
291 | 16 |
                     if (lastComponent != null)
292 |  |
                     {
293 | 8 |
                         data.addPathNode(lastComponent);
294 |  |
                     }
295 | 16 |
                     attr = true;
296 | 16 |
                     lastComponent = null;
297 |  |
                 }
298 |  |
299 |  |
                 else
300 |  |
                 {
301 | 43 |
                     lastComponent = token;
302 |  |
                 }
303 | 74 |
                 first = false;
304 |  |
             }
305 |  |
306 | 19 |
             if (lastComponent == null)
307 |  |
             {
308 | 1 |
                 invalidPath(path, "contains no components.");
309 |  |
             }
310 | 18 |
             data.setNewNodeName(lastComponent);
311 | 18 |
             data.setAttribute(attr);
312 | 18 |
         }
313 |  |
314 |  |
         /**
315 |  |
          * Helper method for throwing an exception about an invalid path.
316 |  |
          *
317 |  |
          * @param path the invalid path
318 |  |
          * @param msg the exception message
319 |  |
          */
320 |  |
         private void invalidPath(String path, String msg)
321 |  |
         {
322 | 6 |
             throw new IllegalArgumentException("Invalid node path: \"" + path
323 |  |
                     + "\" " + msg);
324 |  |
         }
325 |  |
326 |  |
         // static initializer: registers the configuration node pointer factory
327 |  |
         static
328 | 4 |
         {
329 | 4 |
             JXPathContextReferenceImpl
330 |  |
                     .addNodePointerFactory(new ConfigurationNodePointerFactory());
331 |  |
         }
332 |  |
     }