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