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 */ 017package org.apache.commons.configuration2.tree.xpath; 018 019import java.util.Collections; 020import java.util.LinkedList; 021import java.util.List; 022import java.util.StringTokenizer; 023import java.util.stream.Collectors; 024 025import org.apache.commons.configuration2.tree.ExpressionEngine; 026import org.apache.commons.configuration2.tree.NodeAddData; 027import org.apache.commons.configuration2.tree.NodeHandler; 028import org.apache.commons.configuration2.tree.QueryResult; 029import org.apache.commons.jxpath.JXPathContext; 030import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl; 031import org.apache.commons.lang3.StringUtils; 032 033/** 034 * <p> 035 * A specialized implementation of the {@code ExpressionEngine} interface that is able to evaluate XPATH expressions. 036 * </p> 037 * <p> 038 * This class makes use of <a href="https://commons.apache.org/jxpath/"> Commons JXPath</a> for handling XPath 039 * expressions and mapping them to the nodes of a hierarchical configuration. This makes the rich and powerful XPATH 040 * syntax available for accessing properties from a configuration object. 041 * </p> 042 * <p> 043 * For selecting properties arbitrary XPATH expressions can be used, which select single or multiple configuration 044 * nodes. The associated {@code Configuration} instance will directly pass the specified property keys into this engine. 045 * If a key is not syntactically correct, an exception will be thrown. 046 * </p> 047 * <p> 048 * For adding new properties, this expression engine uses a specific syntax: the "key" of a new property must 049 * consist of two parts that are separated by whitespace: 050 * </p> 051 * <ol> 052 * <li>An XPATH expression selecting a single node, to which the new element(s) are to be added. This can be an 053 * arbitrary complex expression, but it must select exactly one node, otherwise an exception will be thrown.</li> 054 * <li>The name of the new element(s) to be added below this parent node. Here either a single node name or a complete 055 * path of nodes (separated by the "/" character or "@" for an attribute) can be specified.</li> 056 * </ol> 057 * <p> 058 * Some examples for valid keys that can be passed into the configuration's {@code addProperty()} method follow: 059 * </p> 060 * 061 * <pre> 062 * "/tables/table[1] type" 063 * </pre> 064 * 065 * <p> 066 * This will add a new {@code type} node as a child of the first {@code table} element. 067 * </p> 068 * 069 * <pre> 070 * "/tables/table[1] @type" 071 * </pre> 072 * 073 * <p> 074 * Similar to the example above, but this time a new attribute named {@code type} will be added to the first 075 * {@code table} element. 076 * </p> 077 * 078 * <pre> 079 * "/tables table/fields/field/name" 080 * </pre> 081 * 082 * <p> 083 * This example shows how a complex path can be added. Parent node is the {@code tables} element. Here a new branch 084 * consisting of the nodes {@code table}, {@code fields}, {@code field}, and {@code name} will be added. 085 * </p> 086 * 087 * <pre> 088 * "/tables table/fields/field@type" 089 * </pre> 090 * 091 * <p> 092 * This is similar to the last example, but in this case a complex path ending with an attribute is defined. 093 * </p> 094 * <p> 095 * <strong>Note:</strong> This extended syntax for adding properties only works with the {@code addProperty()} method. 096 * {@code setProperty()} does not support creating new nodes this way. 097 * </p> 098 * <p> 099 * 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 */ 121public class XPathExpressionEngine implements ExpressionEngine { 122 /** Constant for the path delimiter. */ 123 static final String PATH_DELIMITER = "/"; 124 125 /** Constant for the attribute delimiter. */ 126 static final String ATTR_DELIMITER = "@"; 127 128 /** Constant for the delimiters for splitting node paths. */ 129 private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER + ATTR_DELIMITER; 130 131 /** 132 * Constant for a space which is used as delimiter in keys for adding properties. 133 */ 134 private static final String SPACE = " "; 135 136 /** Constant for a default size of a key buffer. */ 137 private static final int BUF_SIZE = 128; 138 139 /** Constant for the start of an index expression. */ 140 private static final char START_INDEX = '['; 141 142 /** Constant for the end of an index expression. */ 143 private static final char END_INDEX = ']'; 144 145 // static initializer: registers the configuration node pointer factory 146 static { 147 JXPathContextReferenceImpl.addNodePointerFactory(new ConfigurationNodePointerFactory()); 148 } 149 150 /** 151 * Converts the objects returned as query result from the JXPathContext to query result objects. 152 * 153 * @param results the list with results from the context 154 * @param <T> the type of results to be produced 155 * @return the result list 156 */ 157 private static <T> List<QueryResult<T>> convertResults(final List<?> results) { 158 return results.stream().map(res -> (QueryResult<T>) createResult(res)).collect(Collectors.toList()); 159 } 160 161 /** 162 * Creates a {@code QueryResult} object from the given result object of a query. Because of the node pointers involved 163 * result objects can only be of two types: 164 * <ul> 165 * <li>nodes of type T</li> 166 * <li>attribute results already wrapped in {@code QueryResult} objects</li> 167 * </ul> 168 * This method performs a corresponding cast. Warnings can be suppressed because of the implementation of the query 169 * functionality. 170 * 171 * @param resObj the query result object 172 * @param <T> the type of the result to be produced 173 * @return the {@code QueryResult} 174 */ 175 @SuppressWarnings("unchecked") 176 private static <T> QueryResult<T> createResult(final Object resObj) { 177 if (resObj instanceof QueryResult) { 178 return (QueryResult<T>) resObj; 179 } 180 return QueryResult.createNodeResult((T) resObj); 181 } 182 183 /** 184 * Determines the index of the given child node in the node list of its parent. 185 * 186 * @param parent the parent node 187 * @param child the child node 188 * @param handler the node handler 189 * @param <T> the type of the nodes involved 190 * @return the index of this child node 191 */ 192 private static <T> int determineIndex(final T parent, final T child, final NodeHandler<T> handler) { 193 return handler.getChildren(parent, handler.nodeName(child)).indexOf(child) + 1; 194 } 195 196 /** 197 * Determines the position of the separator in a key for adding new properties. If no delimiter is found, result is -1. 198 * 199 * @param key the key 200 * @return the position of the delimiter 201 */ 202 private static int findKeySeparator(final String key) { 203 int index = key.length() - 1; 204 while (index >= 0 && !Character.isWhitespace(key.charAt(index))) { 205 index--; 206 } 207 return index; 208 } 209 210 /** 211 * Helper method for throwing an exception about an invalid path. 212 * 213 * @param path the invalid path 214 * @param msg the exception message 215 */ 216 private static void invalidPath(final String path, final String msg) { 217 throw new IllegalArgumentException("Invalid node path: \"" + path + "\" " + msg); 218 } 219 220 /** The internally used context factory. */ 221 private final XPathContextFactory contextFactory; 222 223 /** 224 * Creates a new instance of {@code XPathExpressionEngine} with default settings. 225 */ 226 public XPathExpressionEngine() { 227 this(new XPathContextFactory()); 228 } 229 230 /** 231 * Creates a new instance of {@code XPathExpressionEngine} and sets the context factory. This constructor is mainly used 232 * for testing purposes. 233 * 234 * @param factory the {@code XPathContextFactory} 235 */ 236 XPathExpressionEngine(final XPathContextFactory factory) { 237 contextFactory = factory; 238 } 239 240 @Override 241 public String attributeKey(final String parentKey, final String attributeName) { 242 final StringBuilder buf = new StringBuilder( 243 StringUtils.length(parentKey) + StringUtils.length(attributeName) + PATH_DELIMITER.length() + ATTR_DELIMITER.length()); 244 if (StringUtils.isNotEmpty(parentKey)) { 245 buf.append(parentKey).append(PATH_DELIMITER); 246 } 247 buf.append(ATTR_DELIMITER).append(attributeName); 248 return buf.toString(); 249 } 250 251 /** 252 * {@inheritDoc} This implementation works similar to {@code nodeKey()}, but always adds an index expression to the 253 * resulting key. 254 */ 255 @Override 256 public <T> String canonicalKey(final T node, final String parentKey, final NodeHandler<T> handler) { 257 final T parent = handler.getParent(node); 258 if (parent == null) { 259 // this is the root node 260 return StringUtils.defaultString(parentKey); 261 } 262 263 final StringBuilder buf = new StringBuilder(BUF_SIZE); 264 if (StringUtils.isNotEmpty(parentKey)) { 265 buf.append(parentKey).append(PATH_DELIMITER); 266 } 267 buf.append(handler.nodeName(node)); 268 buf.append(START_INDEX); 269 buf.append(determineIndex(parent, node, handler)); 270 buf.append(END_INDEX); 271 return buf.toString(); 272 } 273 274 /** 275 * Creates the {@code JXPathContext} to be used for executing a query. This method delegates to the context factory. 276 * 277 * @param root the configuration root node 278 * @param handler the node handler 279 * @return the new context 280 */ 281 private <T> JXPathContext createContext(final T root, final NodeHandler<T> handler) { 282 return getContextFactory().createContext(root, handler); 283 } 284 285 /** 286 * Creates a {@code NodeAddData} object as a result of a {@code prepareAdd()} operation. This method interprets the 287 * passed in path of the new node. 288 * 289 * @param path the path of the new node 290 * @param parentNodeResult the parent node 291 * @param <T> the type of the nodes involved 292 */ 293 <T> NodeAddData<T> createNodeAddData(final String path, final QueryResult<T> parentNodeResult) { 294 if (parentNodeResult.isAttributeResult()) { 295 invalidPath(path, " cannot add properties to an attribute."); 296 } 297 final List<String> pathNodes = new LinkedList<>(); 298 String lastComponent = null; 299 boolean attr = false; 300 boolean first = true; 301 302 final StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS, true); 303 while (tok.hasMoreTokens()) { 304 final String token = tok.nextToken(); 305 if (PATH_DELIMITER.equals(token)) { 306 if (attr) { 307 invalidPath(path, " contains an attribute" + " delimiter at a disallowed position."); 308 } 309 if (lastComponent == null) { 310 invalidPath(path, " contains a '/' at a disallowed position."); 311 } 312 pathNodes.add(lastComponent); 313 lastComponent = null; 314 } else if (ATTR_DELIMITER.equals(token)) { 315 if (attr) { 316 invalidPath(path, " contains multiple attribute delimiters."); 317 } 318 if (lastComponent == null && !first) { 319 invalidPath(path, " contains an attribute delimiter at a disallowed position."); 320 } 321 if (lastComponent != null) { 322 pathNodes.add(lastComponent); 323 } 324 attr = true; 325 lastComponent = null; 326 } else { 327 lastComponent = token; 328 } 329 first = false; 330 } 331 332 if (lastComponent == null) { 333 invalidPath(path, "contains no components."); 334 } 335 336 return new NodeAddData<>(parentNodeResult.getNode(), lastComponent, attr, pathNodes); 337 } 338 339 /** 340 * Tries to generate a key for adding a property. This method is called if a key was used for adding properties which 341 * does not contain a space character. It splits the key at its single components and searches for the last existing 342 * component. Then a key compatible key for adding properties is generated. 343 * 344 * @param root the root node of the configuration 345 * @param key the key in question 346 * @param handler the node handler 347 * @return the key to be used for adding the property 348 */ 349 private <T> String generateKeyForAdd(final T root, final String key, final NodeHandler<T> handler) { 350 int pos = key.lastIndexOf(PATH_DELIMITER, key.length()); 351 352 while (pos >= 0) { 353 final String keyExisting = key.substring(0, pos); 354 if (!query(root, keyExisting, handler).isEmpty()) { 355 final StringBuilder buf = new StringBuilder(key.length() + 1); 356 buf.append(keyExisting).append(SPACE); 357 buf.append(key.substring(pos + 1)); 358 return buf.toString(); 359 } 360 pos = key.lastIndexOf(PATH_DELIMITER, pos - 1); 361 } 362 363 return SPACE + key; 364 } 365 366 /** 367 * Gets the {@code XPathContextFactory} used by this instance. 368 * 369 * @return the {@code XPathContextFactory} 370 */ 371 XPathContextFactory getContextFactory() { 372 return contextFactory; 373 } 374 375 /** 376 * {@inheritDoc} This implementation creates an XPATH expression that selects the given node (under the assumption that 377 * the passed in parent key is valid). As the {@code nodeKey()} implementation of 378 * {@link org.apache.commons.configuration2.tree.DefaultExpressionEngine DefaultExpressionEngine} this method does not 379 * return indices for nodes. So all child nodes of a given parent with the same name have the same key. 380 */ 381 @Override 382 public <T> String nodeKey(final T node, final String parentKey, final NodeHandler<T> handler) { 383 if (parentKey == null) { 384 // name of the root node 385 return StringUtils.EMPTY; 386 } 387 if (handler.nodeName(node) == null) { 388 // paranoia check for undefined node names 389 return parentKey; 390 } 391 final StringBuilder buf = new StringBuilder(parentKey.length() + handler.nodeName(node).length() + PATH_DELIMITER.length()); 392 if (!parentKey.isEmpty()) { 393 buf.append(parentKey); 394 buf.append(PATH_DELIMITER); 395 } 396 buf.append(handler.nodeName(node)); 397 return buf.toString(); 398 } 399 400 /** 401 * {@inheritDoc} The expected format of the passed in key is explained in the class comment. 402 */ 403 @Override 404 public <T> NodeAddData<T> prepareAdd(final T root, final String key, final NodeHandler<T> handler) { 405 if (key == null) { 406 throw new IllegalArgumentException("prepareAdd: key must not be null!"); 407 } 408 409 String addKey = key; 410 int index = findKeySeparator(addKey); 411 if (index < 0) { 412 addKey = generateKeyForAdd(root, addKey, handler); 413 index = findKeySeparator(addKey); 414 } else if (index >= addKey.length() - 1) { 415 invalidPath(addKey, " new node path must not be empty."); 416 } 417 418 final List<QueryResult<T>> nodes = query(root, addKey.substring(0, index).trim(), handler); 419 if (nodes.size() != 1) { 420 throw new IllegalArgumentException("prepareAdd: key '" + key + "' must select exactly one target node!"); 421 } 422 423 return createNodeAddData(addKey.substring(index).trim(), nodes.get(0)); 424 } 425 426 /** 427 * {@inheritDoc} This implementation interprets the passed in key as an XPATH expression. 428 */ 429 @Override 430 public <T> List<QueryResult<T>> query(final T root, final String key, final NodeHandler<T> handler) { 431 if (StringUtils.isEmpty(key)) { 432 final QueryResult<T> result = createResult(root); 433 return Collections.singletonList(result); 434 } 435 final JXPathContext context = createContext(root, handler); 436 List<?> results = context.selectNodes(key); 437 if (results == null) { 438 results = Collections.emptyList(); 439 } 440 return convertResults(results); 441 } 442}