1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 * "key" 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 * "/" character or "@" 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 * "/tables/table[1] type"
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 * "/tables/table[1] @type"
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 * "/tables table/fields/field/name"
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 * "/tables table/fields/field@type"
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
173 return StringUtils.EMPTY;
174 }
175 else if (node.getName() == null)
176 {
177
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
339 static
340 {
341 JXPathContextReferenceImpl
342 .addNodePointerFactory(new ConfigurationNodePointerFactory());
343 }
344 }