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 466413 2006-10-21 15:23:45Z 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 buf.append(PATH_DELIMITER);
174 }
175 if (node.isAttribute())
176 {
177 buf.append(ATTR_DELIMITER);
178 }
179 buf.append(node.getName());
180 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 if (key == null)
196 {
197 throw new IllegalArgumentException(
198 "prepareAdd: key must not be null!");
199 }
200
201 int index = key.length() - 1;
202 while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
203 {
204 index--;
205 }
206 if (index < 0)
207 {
208 throw new IllegalArgumentException(
209 "prepareAdd: Passed in key must contain a whitespace!");
210 }
211
212 List nodes = query(root, key.substring(0, index).trim());
213 if (nodes.size() != 1)
214 {
215 throw new IllegalArgumentException(
216 "prepareAdd: key must select exactly one target node!");
217 }
218
219 NodeAddData data = new NodeAddData();
220 data.setParent((ConfigurationNode) nodes.get(0));
221 initNodeAddData(data, key.substring(index).trim());
222 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 JXPathContext context = JXPathContext.newContext(root);
237 context.setLenient(true);
238 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 String lastComponent = null;
252 boolean attr = false;
253 boolean first = true;
254
255 StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
256 true);
257 while (tok.hasMoreTokens())
258 {
259 String token = tok.nextToken();
260 if (PATH_DELIMITER.equals(token))
261 {
262 if (attr)
263 {
264 invalidPath(path, " contains an attribute"
265 + " delimiter at an unallowed position.");
266 }
267 if (lastComponent == null)
268 {
269 invalidPath(path,
270 " contains a '/' at an unallowed position.");
271 }
272 data.addPathNode(lastComponent);
273 lastComponent = null;
274 }
275
276 else if (ATTR_DELIMITER.equals(token))
277 {
278 if (attr)
279 {
280 invalidPath(path,
281 " contains multiple attribute delimiters.");
282 }
283 if (lastComponent == null && !first)
284 {
285 invalidPath(path,
286 " contains an attribute delimiter at an unallowed position.");
287 }
288 if (lastComponent != null)
289 {
290 data.addPathNode(lastComponent);
291 }
292 attr = true;
293 lastComponent = null;
294 }
295
296 else
297 {
298 lastComponent = token;
299 }
300 first = false;
301 }
302
303 if (lastComponent == null)
304 {
305 invalidPath(path, "contains no components.");
306 }
307 data.setNewNodeName(lastComponent);
308 data.setAttribute(attr);
309 }
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 throw new IllegalArgumentException("Invalid node path: \"" + path
320 + "\" " + msg);
321 }
322
323
324 static
325 {
326 JXPathContextReferenceImpl
327 .addNodePointerFactory(new ConfigurationNodePointerFactory());
328 }
329 }