001 /*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.commons.configuration.tree.xpath;
018
019 import java.util.ArrayList;
020 import java.util.Collections;
021 import java.util.List;
022 import java.util.StringTokenizer;
023
024 import org.apache.commons.configuration.tree.ConfigurationNode;
025 import org.apache.commons.configuration.tree.ExpressionEngine;
026 import org.apache.commons.configuration.tree.NodeAddData;
027 import org.apache.commons.jxpath.JXPathContext;
028 import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
029 import org.apache.commons.lang.StringUtils;
030
031 /**
032 * <p>
033 * A specialized implementation of the <code>ExpressionEngine</code> interface
034 * that is able to evaluate XPATH expressions.
035 * </p>
036 * <p>
037 * This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons
038 * JXPath</a> for handling XPath expressions and mapping them to the nodes of a
039 * hierarchical configuration. This makes the rich and powerful XPATH syntax
040 * available for accessing properties from a configuration object.
041 * </p>
042 * <p>
043 * For selecting properties arbitrary XPATH expressions can be used, which
044 * select single or multiple configuration nodes. The associated
045 * <code>Configuration</code> instance will directly pass the specified property
046 * keys into this engine. If a key is not syntactically correct, an exception
047 * will be thrown.
048 * </p>
049 * <p>
050 * For adding new properties, this expression engine uses a specific syntax: the
051 * "key" of a new property must consist of two parts that are
052 * separated by whitespace:
053 * <ol>
054 * <li>An XPATH expression selecting a single node, to which the new element(s)
055 * are to be added. This can be an arbitrary complex expression, but it must
056 * select exactly one node, otherwise an exception will be thrown.</li>
057 * <li>The name of the new element(s) to be added below this parent node. Here
058 * either a single node name or a complete path of nodes (separated by the
059 * "/" character or "@" for an attribute) can be specified.</li>
060 * </ol>
061 * Some examples for valid keys that can be passed into the configuration's
062 * <code>addProperty()</code> method follow:
063 * </p>
064 * <p>
065 *
066 * <pre>
067 * "/tables/table[1] type"
068 * </pre>
069 *
070 * </p>
071 * <p>
072 * This will add a new <code>type</code> node as a child of the first
073 * <code>table</code> element.
074 * </p>
075 * <p>
076 *
077 * <pre>
078 * "/tables/table[1] @type"
079 * </pre>
080 *
081 * </p>
082 * <p>
083 * Similar to the example above, but this time a new attribute named
084 * <code>type</code> will be added to the first <code>table</code> element.
085 * </p>
086 * <p>
087 *
088 * <pre>
089 * "/tables table/fields/field/name"
090 * </pre>
091 *
092 * </p>
093 * <p>
094 * This example shows how a complex path can be added. Parent node is the
095 * <code>tables</code> element. Here a new branch consisting of the nodes
096 * <code>table</code>, <code>fields</code>, <code>field</code>, and
097 * <code>name</code> will be added.
098 * </p>
099 * <p>
100 *
101 * <pre>
102 * "/tables table/fields/field@type"
103 * </pre>
104 *
105 * </p>
106 * <p>
107 * This is similar to the last example, but in this case a complex path ending
108 * with an attribute is defined.
109 * </p>
110 * <p>
111 * <strong>Note:</strong> This extended syntax for adding properties only works
112 * with the <code>addProperty()</code> method. <code>setProperty()</code> does
113 * not support creating new nodes this way.
114 * </p>
115 * <p>
116 * From version 1.7 on, it is possible to use regular keys in calls to
117 * <code>addProperty()</code> (i.e. keys that do not have to contain a
118 * whitespace as delimiter). In this case the key is evaluated, and the biggest
119 * part pointing to an existing node is determined. The remaining part is then
120 * added as new path. As an example consider the key
121 *
122 * <pre>
123 * "tables/table[last()]/fields/field/name"
124 * </pre>
125 *
126 * If the key does not point to an existing node, the engine will check the
127 * paths <code>"tables/table[last()]/fields/field"</code>,
128 * <code>"tables/table[last()]/fields"</code>,
129 * <code>"tables/table[last()]"</code>, and so on, until a key is
130 * found which points to a node. Let's assume that the last key listed above can
131 * be resolved in this way. Then from this key the following key is derived:
132 * <code>"tables/table[last()] fields/field/name"</code> by appending
133 * the remaining part after a whitespace. This key can now be processed using
134 * the original algorithm. Keys of this form can also be used with the
135 * <code>setProperty()</code> method. However, it is still recommended to use
136 * the old format because it makes explicit at which position new nodes should
137 * be added. For keys without a whitespace delimiter there may be ambiguities.
138 * </p>
139 *
140 * @since 1.3
141 * @author <a
142 * href="http://commons.apache.org/configuration/team-list.html">Commons
143 * Configuration team</a>
144 * @version $Id: XPathExpressionEngine.java 1152357 2011-07-29 19:56:01Z oheger $
145 */
146 public class XPathExpressionEngine implements ExpressionEngine
147 {
148 /** Constant for the path delimiter. */
149 static final String PATH_DELIMITER = "/";
150
151 /** Constant for the attribute delimiter. */
152 static final String ATTR_DELIMITER = "@";
153
154 /** Constant for the delimiters for splitting node paths. */
155 private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER
156 + ATTR_DELIMITER;
157
158 /**
159 * Constant for a space which is used as delimiter in keys for adding
160 * properties.
161 */
162 private static final String SPACE = " ";
163
164 /**
165 * Executes a query. The passed in property key is directly passed to a
166 * JXPath context.
167 *
168 * @param root the configuration root node
169 * @param key the query to be executed
170 * @return a list with the nodes that are selected by the query
171 */
172 public List query(ConfigurationNode root, String key)
173 {
174 if (StringUtils.isEmpty(key))
175 {
176 List result = new ArrayList(1);
177 result.add(root);
178 return result;
179 }
180 else
181 {
182 JXPathContext context = createContext(root, key);
183 List result = context.selectNodes(key);
184 return (result != null) ? result : Collections.EMPTY_LIST;
185 }
186 }
187
188 /**
189 * Returns a (canonical) key for the given node based on the parent's key.
190 * This implementation will create an XPATH expression that selects the
191 * given node (under the assumption that the passed in parent key is valid).
192 * As the <code>nodeKey()</code> implementation of
193 * <code>{@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine}</code>
194 * this method will not return indices for nodes. So all child nodes of a
195 * given parent with the same name will have the same key.
196 *
197 * @param node the node for which a key is to be constructed
198 * @param parentKey the key of the parent node
199 * @return the key for the given node
200 */
201 public String nodeKey(ConfigurationNode node, String parentKey)
202 {
203 if (parentKey == null)
204 {
205 // name of the root node
206 return StringUtils.EMPTY;
207 }
208 else if (node.getName() == null)
209 {
210 // paranoia check for undefined node names
211 return parentKey;
212 }
213
214 else
215 {
216 StringBuffer buf = new StringBuffer(parentKey.length()
217 + node.getName().length() + PATH_DELIMITER.length());
218 if (parentKey.length() > 0)
219 {
220 buf.append(parentKey);
221 buf.append(PATH_DELIMITER);
222 }
223 if (node.isAttribute())
224 {
225 buf.append(ATTR_DELIMITER);
226 }
227 buf.append(node.getName());
228 return buf.toString();
229 }
230 }
231
232 /**
233 * Prepares an add operation for a configuration property. The expected
234 * format of the passed in key is explained in the class comment.
235 *
236 * @param root the configuration's root node
237 * @param key the key describing the target of the add operation and the
238 * path of the new node
239 * @return a data object to be evaluated by the calling configuration object
240 */
241 public NodeAddData prepareAdd(ConfigurationNode root, String key)
242 {
243 if (key == null)
244 {
245 throw new IllegalArgumentException(
246 "prepareAdd: key must not be null!");
247 }
248
249 String addKey = key;
250 int index = findKeySeparator(addKey);
251 if (index < 0)
252 {
253 addKey = generateKeyForAdd(root, addKey);
254 index = findKeySeparator(addKey);
255 }
256
257 List nodes = query(root, addKey.substring(0, index).trim());
258 if (nodes.size() != 1)
259 {
260 throw new IllegalArgumentException(
261 "prepareAdd: key must select exactly one target node!");
262 }
263
264 NodeAddData data = new NodeAddData();
265 data.setParent((ConfigurationNode) nodes.get(0));
266 initNodeAddData(data, addKey.substring(index).trim());
267 return data;
268 }
269
270 /**
271 * Creates the <code>JXPathContext</code> used for executing a query. This
272 * method will create a new context and ensure that it is correctly
273 * initialized.
274 *
275 * @param root the configuration root node
276 * @param key the key to be queried
277 * @return the new context
278 */
279 protected JXPathContext createContext(ConfigurationNode root, String key)
280 {
281 JXPathContext context = JXPathContext.newContext(root);
282 context.setLenient(true);
283 return context;
284 }
285
286 /**
287 * Initializes most properties of a <code>NodeAddData</code> object. This
288 * method is called by <code>prepareAdd()</code> after the parent node has
289 * been found. Its task is to interpret the passed in path of the new node.
290 *
291 * @param data the data object to initialize
292 * @param path the path of the new node
293 */
294 protected void initNodeAddData(NodeAddData data, String path)
295 {
296 String lastComponent = null;
297 boolean attr = false;
298 boolean first = true;
299
300 StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
301 true);
302 while (tok.hasMoreTokens())
303 {
304 String token = tok.nextToken();
305 if (PATH_DELIMITER.equals(token))
306 {
307 if (attr)
308 {
309 invalidPath(path, " contains an attribute"
310 + " delimiter at an unallowed position.");
311 }
312 if (lastComponent == null)
313 {
314 invalidPath(path,
315 " contains a '/' at an unallowed position.");
316 }
317 data.addPathNode(lastComponent);
318 lastComponent = null;
319 }
320
321 else if (ATTR_DELIMITER.equals(token))
322 {
323 if (attr)
324 {
325 invalidPath(path,
326 " contains multiple attribute delimiters.");
327 }
328 if (lastComponent == null && !first)
329 {
330 invalidPath(path,
331 " contains an attribute delimiter at an unallowed position.");
332 }
333 if (lastComponent != null)
334 {
335 data.addPathNode(lastComponent);
336 }
337 attr = true;
338 lastComponent = null;
339 }
340
341 else
342 {
343 lastComponent = token;
344 }
345 first = false;
346 }
347
348 if (lastComponent == null)
349 {
350 invalidPath(path, "contains no components.");
351 }
352 data.setNewNodeName(lastComponent);
353 data.setAttribute(attr);
354 }
355
356 /**
357 * Tries to generate a key for adding a property. This method is called if a
358 * key was used for adding properties which does not contain a space
359 * character. It splits the key at its single components and searches for
360 * the last existing component. Then a key compatible for adding properties
361 * is generated.
362 *
363 * @param root the root node of the configuration
364 * @param key the key in question
365 * @return the key to be used for adding the property
366 */
367 private String generateKeyForAdd(ConfigurationNode root, String key)
368 {
369 int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
370
371 while (pos >= 0)
372 {
373 String keyExisting = key.substring(0, pos);
374 if (!query(root, keyExisting).isEmpty())
375 {
376 StringBuffer buf = new StringBuffer(key.length() + 1);
377 buf.append(keyExisting).append(SPACE);
378 buf.append(key.substring(pos + 1));
379 return buf.toString();
380 }
381 pos = key.lastIndexOf(PATH_DELIMITER, pos - 1);
382 }
383
384 return SPACE + key;
385 }
386
387 /**
388 * Helper method for throwing an exception about an invalid path.
389 *
390 * @param path the invalid path
391 * @param msg the exception message
392 */
393 private void invalidPath(String path, String msg)
394 {
395 throw new IllegalArgumentException("Invalid node path: \"" + path
396 + "\" " + msg);
397 }
398
399 /**
400 * Determines the position of the separator in a key for adding new
401 * properties. If no delimiter is found, result is -1.
402 *
403 * @param key the key
404 * @return the position of the delimiter
405 */
406 private static int findKeySeparator(String key)
407 {
408 int index = key.length() - 1;
409 while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
410 {
411 index--;
412 }
413 return index;
414 }
415
416 // static initializer: registers the configuration node pointer factory
417 static
418 {
419 JXPathContextReferenceImpl
420 .addNodePointerFactory(new ConfigurationNodePointerFactory());
421 }
422 }