1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.configuration2.tree.xpath;
18
19 import java.util.Collections;
20 import java.util.LinkedList;
21 import java.util.List;
22 import java.util.StringTokenizer;
23 import java.util.stream.Collectors;
24
25 import org.apache.commons.configuration2.tree.ExpressionEngine;
26 import org.apache.commons.configuration2.tree.NodeAddData;
27 import org.apache.commons.configuration2.tree.NodeHandler;
28 import org.apache.commons.configuration2.tree.QueryResult;
29 import org.apache.commons.jxpath.JXPathContext;
30 import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
31 import org.apache.commons.lang3.StringUtils;
32
33 /**
34 * <p>
35 * A specialized implementation of the {@code ExpressionEngine} interface that is able to evaluate XPATH expressions.
36 * </p>
37 * <p>
38 * This class makes use of <a href="https://commons.apache.org/jxpath/"> Commons JXPath</a> for handling XPath
39 * expressions and mapping them to the nodes of a hierarchical configuration. This makes the rich and powerful XPATH
40 * syntax available for accessing properties from a configuration object.
41 * </p>
42 * <p>
43 * For selecting properties arbitrary XPATH expressions can be used, which select single or multiple configuration
44 * nodes. The associated {@code Configuration} instance will directly pass the specified property keys into this engine.
45 * If a key is not syntactically correct, an exception will be thrown.
46 * </p>
47 * <p>
48 * For adding new properties, this expression engine uses a specific syntax: the "key" of a new property must
49 * consist of two parts that are separated by whitespace:
50 * </p>
51 * <ol>
52 * <li>An XPATH expression selecting a single node, to which the new element(s) are to be added. This can be an
53 * arbitrary complex expression, but it must select exactly one node, otherwise an exception will be thrown.</li>
54 * <li>The name of the new element(s) to be added below this parent node. Here either a single node name or a complete
55 * path of nodes (separated by the "/" character or "@" for an attribute) can be specified.</li>
56 * </ol>
57 * <p>
58 * Some examples for valid keys that can be passed into the configuration's {@code addProperty()} method follow:
59 * </p>
60 *
61 * <pre>
62 * "/tables/table[1] type"
63 * </pre>
64 *
65 * <p>
66 * This will add a new {@code type} node as a child of the first {@code table} element.
67 * </p>
68 *
69 * <pre>
70 * "/tables/table[1] @type"
71 * </pre>
72 *
73 * <p>
74 * Similar to the example above, but this time a new attribute named {@code type} will be added to the first
75 * {@code table} element.
76 * </p>
77 *
78 * <pre>
79 * "/tables table/fields/field/name"
80 * </pre>
81 *
82 * <p>
83 * This example shows how a complex path can be added. Parent node is the {@code tables} element. Here a new branch
84 * consisting of the nodes {@code table}, {@code fields}, {@code field}, and {@code name} will be added.
85 * </p>
86 *
87 * <pre>
88 * "/tables table/fields/field@type"
89 * </pre>
90 *
91 * <p>
92 * This is similar to the last example, but in this case a complex path ending with an attribute is defined.
93 * </p>
94 * <p>
95 * <strong>Note:</strong> This extended syntax for adding properties only works with the {@code addProperty()} method.
96 * {@code setProperty()} does not support creating new nodes this way.
97 * </p>
98 * <p>
99 * From version 1.7 on, it is possible to use regular keys in calls to {@code addProperty()} (i.e. keys that do not have
100 * to contain a whitespace as delimiter). In this case the key is evaluated, and the biggest part pointing to an
101 * existing node is determined. The remaining part is then added as new path. As an example consider the key
102 * </p>
103 *
104 * <pre>
105 * "tables/table[last()]/fields/field/name"
106 * </pre>
107 *
108 * <p>
109 * If the key does not point to an existing node, the engine will check the paths
110 * {@code "tables/table[last()]/fields/field"}, {@code "tables/table[last()]/fields"}, {@code "tables/table[last()]"},
111 * and so on, until a key is found which points to a node. Let's assume that the last key listed above can be resolved
112 * in this way. Then from this key the following key is derived: {@code "tables/table[last()] fields/field/name"} by
113 * appending the remaining part after a whitespace. This key can now be processed using the original algorithm. Keys of
114 * this form can also be used with the {@code setProperty()} method. However, it is still recommended to use the old
115 * format because it makes explicit at which position new nodes should be added. For keys without a whitespace delimiter
116 * there may be ambiguities.
117 * </p>
118 *
119 * @since 1.3
120 */
121 public class XPathExpressionEngine implements ExpressionEngine {
122
123 /** Constant for the path delimiter. */
124 static final String PATH_DELIMITER = "/";
125
126 /** Constant for the attribute delimiter. */
127 static final String ATTR_DELIMITER = "@";
128
129 /** Constant for the delimiters for splitting node paths. */
130 private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER + ATTR_DELIMITER;
131
132 /**
133 * Constant for a space which is used as delimiter in keys for adding properties.
134 */
135 private static final String SPACE = " ";
136
137 /** Constant for a default size of a key buffer. */
138 private static final int BUF_SIZE = 128;
139
140 /** Constant for the start of an index expression. */
141 private static final char START_INDEX = '[';
142
143 /** Constant for the end of an index expression. */
144 private static final char END_INDEX = ']';
145
146 // static initializer: registers the configuration node pointer factory
147 static {
148 JXPathContextReferenceImpl.addNodePointerFactory(new ConfigurationNodePointerFactory());
149 }
150
151 /**
152 * Converts the objects returned as query result from the JXPathContext to query result objects.
153 *
154 * @param results the list with results from the context
155 * @param <T> the type of results to be produced
156 * @return the result list
157 */
158 private static <T> List<QueryResult<T>> convertResults(final List<?> results) {
159 return results.stream().map(res -> (QueryResult<T>) createResult(res)).collect(Collectors.toList());
160 }
161
162 /**
163 * Creates a {@code QueryResult} object from the given result object of a query. Because of the node pointers involved
164 * result objects can only be of two types:
165 * <ul>
166 * <li>nodes of type T</li>
167 * <li>attribute results already wrapped in {@code QueryResult} objects</li>
168 * </ul>
169 * This method performs a corresponding cast. Warnings can be suppressed because of the implementation of the query
170 * functionality.
171 *
172 * @param resObj the query result object
173 * @param <T> the type of the result to be produced
174 * @return the {@code QueryResult}
175 */
176 @SuppressWarnings("unchecked")
177 private static <T> QueryResult<T> createResult(final Object resObj) {
178 if (resObj instanceof QueryResult) {
179 return (QueryResult<T>) resObj;
180 }
181 return QueryResult.createNodeResult((T) resObj);
182 }
183
184 /**
185 * Determines the index of the given child node in the node list of its parent.
186 *
187 * @param parent the parent node
188 * @param child the child node
189 * @param handler the node handler
190 * @param <T> the type of the nodes involved
191 * @return the index of this child node
192 */
193 private static <T> int determineIndex(final T parent, final T child, final NodeHandler<T> handler) {
194 return handler.getChildren(parent, handler.nodeName(child)).indexOf(child) + 1;
195 }
196
197 /**
198 * Determines the position of the separator in a key for adding new properties. If no delimiter is found, result is -1.
199 *
200 * @param key the key
201 * @return the position of the delimiter
202 */
203 private static int findKeySeparator(final String key) {
204 int index = key.length() - 1;
205 while (index >= 0 && !Character.isWhitespace(key.charAt(index))) {
206 index--;
207 }
208 return index;
209 }
210
211 /**
212 * Helper method for throwing an exception about an invalid path.
213 *
214 * @param path the invalid path
215 * @param msg the exception message
216 */
217 private static void invalidPath(final String path, final String msg) {
218 throw new IllegalArgumentException("Invalid node path: \"" + path + "\" " + msg);
219 }
220
221 /** The internally used context factory. */
222 private final XPathContextFactory contextFactory;
223
224 /**
225 * Creates a new instance of {@code XPathExpressionEngine} with default settings.
226 */
227 public XPathExpressionEngine() {
228 this(new XPathContextFactory());
229 }
230
231 /**
232 * Creates a new instance of {@code XPathExpressionEngine} and sets the context factory. This constructor is mainly used
233 * for testing purposes.
234 *
235 * @param factory the {@code XPathContextFactory}
236 */
237 XPathExpressionEngine(final XPathContextFactory factory) {
238 contextFactory = factory;
239 }
240
241 @Override
242 public String attributeKey(final String parentKey, final String attributeName) {
243 final StringBuilder buf = new StringBuilder(
244 StringUtils.length(parentKey) + StringUtils.length(attributeName) + PATH_DELIMITER.length() + ATTR_DELIMITER.length());
245 if (StringUtils.isNotEmpty(parentKey)) {
246 buf.append(parentKey).append(PATH_DELIMITER);
247 }
248 buf.append(ATTR_DELIMITER).append(attributeName);
249 return buf.toString();
250 }
251
252 /**
253 * {@inheritDoc} This implementation works similar to {@code nodeKey()}, but always adds an index expression to the
254 * resulting key.
255 */
256 @Override
257 public <T> String canonicalKey(final T node, final String parentKey, final NodeHandler<T> handler) {
258 final T parent = handler.getParent(node);
259 if (parent == null) {
260 // this is the root node
261 return StringUtils.defaultString(parentKey);
262 }
263
264 final StringBuilder buf = new StringBuilder(BUF_SIZE);
265 if (StringUtils.isNotEmpty(parentKey)) {
266 buf.append(parentKey).append(PATH_DELIMITER);
267 }
268 buf.append(handler.nodeName(node));
269 buf.append(START_INDEX);
270 buf.append(determineIndex(parent, node, handler));
271 buf.append(END_INDEX);
272 return buf.toString();
273 }
274
275 /**
276 * Creates the {@code JXPathContext} to be used for executing a query. This method delegates to the context factory.
277 *
278 * @param root the configuration root node
279 * @param handler the node handler
280 * @return the new context
281 */
282 private <T> JXPathContext createContext(final T root, final NodeHandler<T> handler) {
283 return getContextFactory().createContext(root, handler);
284 }
285
286 /**
287 * Creates a {@code NodeAddData} object as a result of a {@code prepareAdd()} operation. This method interprets the
288 * passed in path of the new node.
289 *
290 * @param path the path of the new node
291 * @param parentNodeResult the parent node
292 * @param <T> the type of the nodes involved
293 */
294 <T> NodeAddData<T> createNodeAddData(final String path, final QueryResult<T> parentNodeResult) {
295 if (parentNodeResult.isAttributeResult()) {
296 invalidPath(path, " cannot add properties to an attribute.");
297 }
298 final List<String> pathNodes = new LinkedList<>();
299 String lastComponent = null;
300 boolean attr = false;
301 boolean first = true;
302
303 final StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS, true);
304 while (tok.hasMoreTokens()) {
305 final String token = tok.nextToken();
306 if (PATH_DELIMITER.equals(token)) {
307 if (attr) {
308 invalidPath(path, " contains an attribute delimiter at a disallowed position.");
309 }
310 if (lastComponent == null) {
311 invalidPath(path, " contains a '/' at a disallowed position.");
312 }
313 pathNodes.add(lastComponent);
314 lastComponent = null;
315 } else if (ATTR_DELIMITER.equals(token)) {
316 if (attr) {
317 invalidPath(path, " contains multiple attribute delimiters.");
318 }
319 if (lastComponent == null && !first) {
320 invalidPath(path, " contains an attribute delimiter at a disallowed position.");
321 }
322 if (lastComponent != null) {
323 pathNodes.add(lastComponent);
324 }
325 attr = true;
326 lastComponent = null;
327 } else {
328 lastComponent = token;
329 }
330 first = false;
331 }
332
333 if (lastComponent == null) {
334 invalidPath(path, "contains no components.");
335 }
336
337 return new NodeAddData<>(parentNodeResult.getNode(), lastComponent, attr, pathNodes);
338 }
339
340 /**
341 * Tries to generate a key for adding a property. This method is called if a key was used for adding properties which
342 * does not contain a space character. It splits the key at its single components and searches for the last existing
343 * component. Then a key compatible key for adding properties is generated.
344 *
345 * @param root the root node of the configuration
346 * @param key the key in question
347 * @param handler the node handler
348 * @return the key to be used for adding the property
349 */
350 private <T> String generateKeyForAdd(final T root, final String key, final NodeHandler<T> handler) {
351 int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
352
353 while (pos >= 0) {
354 final String keyExisting = key.substring(0, pos);
355 if (!query(root, keyExisting, handler).isEmpty()) {
356 final StringBuilder buf = new StringBuilder(key.length() + 1);
357 buf.append(keyExisting).append(SPACE);
358 buf.append(key.substring(pos + 1));
359 return buf.toString();
360 }
361 pos = key.lastIndexOf(PATH_DELIMITER, pos - 1);
362 }
363
364 return SPACE + key;
365 }
366
367 /**
368 * Gets the {@code XPathContextFactory} used by this instance.
369 *
370 * @return the {@code XPathContextFactory}
371 */
372 XPathContextFactory getContextFactory() {
373 return contextFactory;
374 }
375
376 /**
377 * {@inheritDoc} This implementation creates an XPATH expression that selects the given node (under the assumption that
378 * the passed in parent key is valid). As the {@code nodeKey()} implementation of
379 * {@link org.apache.commons.configuration2.tree.DefaultExpressionEngine DefaultExpressionEngine} this method does not
380 * return indices for nodes. So all child nodes of a given parent with the same name have the same key.
381 */
382 @Override
383 public <T> String nodeKey(final T node, final String parentKey, final NodeHandler<T> handler) {
384 if (parentKey == null) {
385 // name of the root node
386 return StringUtils.EMPTY;
387 }
388 if (handler.nodeName(node) == null) {
389 // paranoia check for undefined node names
390 return parentKey;
391 }
392 final StringBuilder buf = new StringBuilder(parentKey.length() + handler.nodeName(node).length() + PATH_DELIMITER.length());
393 if (!parentKey.isEmpty()) {
394 buf.append(parentKey);
395 buf.append(PATH_DELIMITER);
396 }
397 buf.append(handler.nodeName(node));
398 return buf.toString();
399 }
400
401 /**
402 * {@inheritDoc} The expected format of the passed in key is explained in the class comment.
403 */
404 @Override
405 public <T> NodeAddData<T> prepareAdd(final T root, final String key, final NodeHandler<T> handler) {
406 if (key == null) {
407 throw new IllegalArgumentException("prepareAdd: key must not be null.");
408 }
409
410 String addKey = key;
411 int index = findKeySeparator(addKey);
412 if (index < 0) {
413 addKey = generateKeyForAdd(root, addKey, handler);
414 index = findKeySeparator(addKey);
415 } else if (index >= addKey.length() - 1) {
416 invalidPath(addKey, " new node path must not be empty.");
417 }
418
419 final List<QueryResult<T>> nodes = query(root, addKey.substring(0, index).trim(), handler);
420 if (nodes.size() != 1) {
421 throw new IllegalArgumentException("prepareAdd: key '" + key + "' must select exactly one target node.");
422 }
423
424 return createNodeAddData(addKey.substring(index).trim(), nodes.get(0));
425 }
426
427 /**
428 * {@inheritDoc} This implementation interprets the passed in key as an XPATH expression.
429 */
430 @Override
431 public <T> List<QueryResult<T>> query(final T root, final String key, final NodeHandler<T> handler) {
432 if (StringUtils.isEmpty(key)) {
433 final QueryResult<T> result = createResult(root);
434 return Collections.singletonList(result);
435 }
436 final JXPathContext context = createContext(root, handler);
437 List<?> results = context.selectNodes(key);
438 if (results == null) {
439 results = Collections.emptyList();
440 }
441 return convertResults(results);
442 }
443 }