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://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 * "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) 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 * @since 1.3
101 * @author Oliver Heger
102 * @version $Id: XPathExpressionEngine.java 439648 2006-09-02 20:42:10Z oheger $
103 */
104 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 if (StringUtils.isEmpty(key))
127 {
128 List result = new ArrayList(1);
129 result.add(root);
130 return result;
131 }
132 else
133 {
134 JXPathContext context = createContext(root, key);
135 List result = context.selectNodes(key);
136 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 if (parentKey == null)
156 {
157
158 return StringUtils.EMPTY;
159 }
160 else if (node.getName() == null)
161 {
162
163 return parentKey;
164 }
165
166 else
167 {
168 StringBuffer buf = new StringBuffer(parentKey.length()
169 + node.getName().length() + PATH_DELIMITER.length());
170 if (parentKey.length() > 0)
171 {
172 buf.append(parentKey);
173 if (!node.isAttribute())
174 {
175 buf.append(PATH_DELIMITER);
176 }
177 }
178 if (node.isAttribute())
179 {
180 buf.append(ATTR_DELIMITER);
181 }
182 buf.append(node.getName());
183 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 if (key == null)
199 {
200 throw new IllegalArgumentException(
201 "prepareAdd: key must not be null!");
202 }
203
204 int index = key.length() - 1;
205 while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
206 {
207 index--;
208 }
209 if (index < 0)
210 {
211 throw new IllegalArgumentException(
212 "prepareAdd: Passed in key must contain a whitespace!");
213 }
214
215 List nodes = query(root, key.substring(0, index).trim());
216 if (nodes.size() != 1)
217 {
218 throw new IllegalArgumentException(
219 "prepareAdd: key must select exactly one target node!");
220 }
221
222 NodeAddData data = new NodeAddData();
223 data.setParent((ConfigurationNode) nodes.get(0));
224 initNodeAddData(data, key.substring(index).trim());
225 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 JXPathContext context = JXPathContext.newContext(root);
240 context.setLenient(true);
241 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 String lastComponent = null;
255 boolean attr = false;
256 boolean first = true;
257
258 StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
259 true);
260 while (tok.hasMoreTokens())
261 {
262 String token = tok.nextToken();
263 if (PATH_DELIMITER.equals(token))
264 {
265 if (attr)
266 {
267 invalidPath(path, " contains an attribute"
268 + " delimiter at an unallowed position.");
269 }
270 if (lastComponent == null)
271 {
272 invalidPath(path,
273 " contains a '/' at an unallowed position.");
274 }
275 data.addPathNode(lastComponent);
276 lastComponent = null;
277 }
278
279 else if (ATTR_DELIMITER.equals(token))
280 {
281 if (attr)
282 {
283 invalidPath(path,
284 " contains multiple attribute delimiters.");
285 }
286 if (lastComponent == null && !first)
287 {
288 invalidPath(path,
289 " contains an attribute delimiter at an unallowed position.");
290 }
291 if (lastComponent != null)
292 {
293 data.addPathNode(lastComponent);
294 }
295 attr = true;
296 lastComponent = null;
297 }
298
299 else
300 {
301 lastComponent = token;
302 }
303 first = false;
304 }
305
306 if (lastComponent == null)
307 {
308 invalidPath(path, "contains no components.");
309 }
310 data.setNewNodeName(lastComponent);
311 data.setAttribute(attr);
312 }
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 throw new IllegalArgumentException("Invalid node path: \"" + path
323 + "\" " + msg);
324 }
325
326
327 static
328 {
329 JXPathContextReferenceImpl
330 .addNodePointerFactory(new ConfigurationNodePointerFactory());
331 }
332 }