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.ArrayList; 020import java.util.Collections; 021import java.util.LinkedList; 022import java.util.List; 023import java.util.StringTokenizer; 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 036 * is able to evaluate XPATH expressions. 037 * </p> 038 * <p> 039 * This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons 040 * JXPath</a> for handling XPath expressions and mapping them to the nodes of a 041 * hierarchical configuration. This makes the rich and powerful XPATH syntax 042 * available for accessing properties from a configuration object. 043 * </p> 044 * <p> 045 * For selecting properties arbitrary XPATH expressions can be used, which 046 * select single or multiple configuration nodes. The associated 047 * {@code Configuration} instance will directly pass the specified property keys 048 * into this engine. If a key is not syntactically correct, an exception will be 049 * thrown. 050 * </p> 051 * <p> 052 * For adding new properties, this expression engine uses a specific syntax: the 053 * "key" of a new property must consist of two parts that are 054 * separated by whitespace: 055 * </p> 056 * <ol> 057 * <li>An XPATH expression selecting a single node, to which the new element(s) 058 * are to be added. This can be an arbitrary complex expression, but it must 059 * select exactly one node, otherwise an exception will be thrown.</li> 060 * <li>The name of the new element(s) to be added below this parent node. Here 061 * either a single node name or a complete path of nodes (separated by the 062 * "/" character or "@" for an attribute) can be specified.</li> 063 * </ol> 064 * <p> 065 * Some examples for valid keys that can be passed into the configuration's 066 * {@code addProperty()} method follow: 067 * </p> 068 * 069 * <pre> 070 * "/tables/table[1] type" 071 * </pre> 072 * 073 * <p> 074 * This will add a new {@code type} node as a child of the first {@code table} 075 * element. 076 * </p> 077 * 078 * <pre> 079 * "/tables/table[1] @type" 080 * </pre> 081 * 082 * <p> 083 * Similar to the example above, but this time a new attribute named 084 * {@code type} will be added to the first {@code table} element. 085 * </p> 086 * 087 * <pre> 088 * "/tables table/fields/field/name" 089 * </pre> 090 * 091 * <p> 092 * This example shows how a complex path can be added. Parent node is the 093 * {@code tables} element. Here a new branch consisting of the nodes 094 * {@code table}, {@code fields}, {@code field}, and {@code name} will be added. 095 * </p> 096 * 097 * <pre> 098 * "/tables table/fields/field@type" 099 * </pre> 100 * 101 * <p> 102 * This is similar to the last example, but in this case a complex path ending 103 * with an attribute is defined. 104 * </p> 105 * <p> 106 * <strong>Note:</strong> This extended syntax for adding properties only works 107 * with the {@code addProperty()} method. {@code setProperty()} does not support 108 * creating new nodes this way. 109 * </p> 110 * <p> 111 * From version 1.7 on, it is possible to use regular keys in calls to 112 * {@code addProperty()} (i.e. keys that do not have to contain a whitespace as 113 * delimiter). In this case the key is evaluated, and the biggest part pointing 114 * to an existing node is determined. The remaining part is then added as new 115 * path. As an example consider the key 116 * </p> 117 * 118 * <pre> 119 * "tables/table[last()]/fields/field/name" 120 * </pre> 121 * 122 * <p> 123 * If the key does not point to an existing node, the engine will check the 124 * paths {@code "tables/table[last()]/fields/field"}, 125 * {@code "tables/table[last()]/fields"}, {@code "tables/table[last()]"}, and so 126 * on, until a key is found which points to a node. Let's assume that the last 127 * key listed above can be resolved in this way. Then from this key the 128 * following key is derived: {@code "tables/table[last()] fields/field/name"} by 129 * appending the remaining part after a whitespace. This key can now be 130 * processed using the original algorithm. Keys of this form can also be used 131 * with the {@code setProperty()} method. However, it is still recommended to 132 * use the old format because it makes explicit at which position new nodes 133 * should be added. For keys without a whitespace delimiter there may be 134 * ambiguities. 135 * </p> 136 * 137 * @since 1.3 138 * @version $Id: XPathExpressionEngine.java 1842194 2018-09-27 22:24:23Z ggregory $ 139 */ 140public class XPathExpressionEngine implements ExpressionEngine 141{ 142 /** Constant for the path delimiter. */ 143 static final String PATH_DELIMITER = "/"; 144 145 /** Constant for the attribute delimiter. */ 146 static final String ATTR_DELIMITER = "@"; 147 148 /** Constant for the delimiters for splitting node paths. */ 149 private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER 150 + ATTR_DELIMITER; 151 152 /** 153 * Constant for a space which is used as delimiter in keys for adding 154 * properties. 155 */ 156 private static final String SPACE = " "; 157 158 /** Constant for a default size of a key buffer. */ 159 private static final int BUF_SIZE = 128; 160 161 /** Constant for the start of an index expression. */ 162 private static final char START_INDEX = '['; 163 164 /** Constant for the end of an index expression. */ 165 private static final char END_INDEX = ']'; 166 167 /** The internally used context factory. */ 168 private final XPathContextFactory contextFactory; 169 170 /** 171 * Creates a new instance of {@code XPathExpressionEngine} with default 172 * settings. 173 */ 174 public XPathExpressionEngine() 175 { 176 this(new XPathContextFactory()); 177 } 178 179 /** 180 * Creates a new instance of {@code XPathExpressionEngine} and sets the 181 * context factory. This constructor is mainly used for testing purposes. 182 * 183 * @param factory the {@code XPathContextFactory} 184 */ 185 XPathExpressionEngine(final XPathContextFactory factory) 186 { 187 contextFactory = factory; 188 } 189 190 /** 191 * {@inheritDoc} This implementation interprets the passed in key as an XPATH 192 * expression. 193 */ 194 @Override 195 public <T> List<QueryResult<T>> query(final T root, final String key, 196 final NodeHandler<T> handler) 197 { 198 if (StringUtils.isEmpty(key)) 199 { 200 final QueryResult<T> result = createResult(root); 201 return Collections.singletonList(result); 202 } 203 final JXPathContext context = createContext(root, handler); 204 List<?> results = context.selectNodes(key); 205 if (results == null) 206 { 207 results = Collections.emptyList(); 208 } 209 return convertResults(results); 210 } 211 212 /** 213 * {@inheritDoc} This implementation creates an XPATH expression that 214 * selects the given node (under the assumption that the passed in parent 215 * key is valid). As the {@code nodeKey()} implementation of 216 * {@link org.apache.commons.configuration2.tree.DefaultExpressionEngine 217 * DefaultExpressionEngine} this method does not return indices for nodes. 218 * So all child nodes of a given parent with the same name have the same 219 * key. 220 */ 221 @Override 222 public <T> String nodeKey(final T node, final String parentKey, final NodeHandler<T> handler) 223 { 224 if (parentKey == null) 225 { 226 // name of the root node 227 return StringUtils.EMPTY; 228 } 229 else if (handler.nodeName(node) == null) 230 { 231 // paranoia check for undefined node names 232 return parentKey; 233 } 234 235 else 236 { 237 final StringBuilder buf = 238 new StringBuilder(parentKey.length() 239 + handler.nodeName(node).length() 240 + PATH_DELIMITER.length()); 241 if (parentKey.length() > 0) 242 { 243 buf.append(parentKey); 244 buf.append(PATH_DELIMITER); 245 } 246 buf.append(handler.nodeName(node)); 247 return buf.toString(); 248 } 249 } 250 251 @Override 252 public String attributeKey(final String parentKey, final String attributeName) 253 { 254 final StringBuilder buf = 255 new StringBuilder(StringUtils.length(parentKey) 256 + StringUtils.length(attributeName) 257 + PATH_DELIMITER.length() + ATTR_DELIMITER.length()); 258 if (StringUtils.isNotEmpty(parentKey)) 259 { 260 buf.append(parentKey).append(PATH_DELIMITER); 261 } 262 buf.append(ATTR_DELIMITER).append(attributeName); 263 return buf.toString(); 264 } 265 266 /** 267 * {@inheritDoc} This implementation works similar to {@code nodeKey()}, but 268 * always adds an index expression to the resulting key. 269 */ 270 @Override 271 public <T> String canonicalKey(final T node, final String parentKey, 272 final NodeHandler<T> handler) 273 { 274 final T parent = handler.getParent(node); 275 if (parent == null) 276 { 277 // this is the root node 278 return StringUtils.defaultString(parentKey); 279 } 280 281 final StringBuilder buf = new StringBuilder(BUF_SIZE); 282 if (StringUtils.isNotEmpty(parentKey)) 283 { 284 buf.append(parentKey).append(PATH_DELIMITER); 285 } 286 buf.append(handler.nodeName(node)); 287 buf.append(START_INDEX); 288 buf.append(determineIndex(parent, node, handler)); 289 buf.append(END_INDEX); 290 return buf.toString(); 291 } 292 293 /** 294 * {@inheritDoc} The expected format of the passed in key is explained in 295 * the class comment. 296 */ 297 @Override 298 public <T> NodeAddData<T> prepareAdd(final T root, final String key, 299 final NodeHandler<T> handler) 300 { 301 if (key == null) 302 { 303 throw new IllegalArgumentException( 304 "prepareAdd: key must not be null!"); 305 } 306 307 String addKey = key; 308 int index = findKeySeparator(addKey); 309 if (index < 0) 310 { 311 addKey = generateKeyForAdd(root, addKey, handler); 312 index = findKeySeparator(addKey); 313 } 314 else if (index >= addKey.length() - 1) 315 { 316 invalidPath(addKey, " new node path must not be empty."); 317 } 318 319 final List<QueryResult<T>> nodes = 320 query(root, addKey.substring(0, index).trim(), handler); 321 if (nodes.size() != 1) 322 { 323 throw new IllegalArgumentException("prepareAdd: key '" + key 324 + "' must select exactly one target node!"); 325 } 326 327 return createNodeAddData(addKey.substring(index).trim(), nodes.get(0)); 328 } 329 330 /** 331 * Creates the {@code JXPathContext} to be used for executing a query. This 332 * method delegates to the context factory. 333 * 334 * @param root the configuration root node 335 * @param handler the node handler 336 * @return the new context 337 */ 338 private <T> JXPathContext createContext(final T root, final NodeHandler<T> handler) 339 { 340 return getContextFactory().createContext(root, handler); 341 } 342 343 /** 344 * Creates a {@code NodeAddData} object as a result of a 345 * {@code prepareAdd()} operation. This method interprets the passed in path 346 * of the new node. 347 * 348 * @param path the path of the new node 349 * @param parentNodeResult the parent node 350 * @param <T> the type of the nodes involved 351 */ 352 <T> NodeAddData<T> createNodeAddData(final String path, 353 final QueryResult<T> parentNodeResult) 354 { 355 if (parentNodeResult.isAttributeResult()) 356 { 357 invalidPath(path, " cannot add properties to an attribute."); 358 } 359 final List<String> pathNodes = new LinkedList<>(); 360 String lastComponent = null; 361 boolean attr = false; 362 boolean first = true; 363 364 final StringTokenizer tok = 365 new StringTokenizer(path, NODE_PATH_DELIMITERS, true); 366 while (tok.hasMoreTokens()) 367 { 368 final String token = tok.nextToken(); 369 if (PATH_DELIMITER.equals(token)) 370 { 371 if (attr) 372 { 373 invalidPath(path, " contains an attribute" 374 + " delimiter at a disallowed position."); 375 } 376 if (lastComponent == null) 377 { 378 invalidPath(path, 379 " contains a '/' at a disallowed position."); 380 } 381 pathNodes.add(lastComponent); 382 lastComponent = null; 383 } 384 385 else if (ATTR_DELIMITER.equals(token)) 386 { 387 if (attr) 388 { 389 invalidPath(path, 390 " contains multiple attribute delimiters."); 391 } 392 if (lastComponent == null && !first) 393 { 394 invalidPath(path, 395 " contains an attribute delimiter at a disallowed position."); 396 } 397 if (lastComponent != null) 398 { 399 pathNodes.add(lastComponent); 400 } 401 attr = true; 402 lastComponent = null; 403 } 404 405 else 406 { 407 lastComponent = token; 408 } 409 first = false; 410 } 411 412 if (lastComponent == null) 413 { 414 invalidPath(path, "contains no components."); 415 } 416 417 return new NodeAddData<>(parentNodeResult.getNode(), lastComponent, 418 attr, pathNodes); 419 } 420 421 /** 422 * Returns the {@code XPathContextFactory} used by this instance. 423 * 424 * @return the {@code XPathContextFactory} 425 */ 426 XPathContextFactory getContextFactory() 427 { 428 return contextFactory; 429 } 430 431 /** 432 * Tries to generate a key for adding a property. This method is called if a 433 * key was used for adding properties which does not contain a space 434 * character. It splits the key at its single components and searches for 435 * the last existing component. Then a key compatible key for adding 436 * properties is generated. 437 * 438 * @param root the root node of the configuration 439 * @param key the key in question 440 * @param handler the node handler 441 * @return the key to be used for adding the property 442 */ 443 private <T> String generateKeyForAdd(final T root, final String key, 444 final NodeHandler<T> handler) 445 { 446 int pos = key.lastIndexOf(PATH_DELIMITER, key.length()); 447 448 while (pos >= 0) 449 { 450 final String keyExisting = key.substring(0, pos); 451 if (!query(root, keyExisting, handler).isEmpty()) 452 { 453 final StringBuilder buf = new StringBuilder(key.length() + 1); 454 buf.append(keyExisting).append(SPACE); 455 buf.append(key.substring(pos + 1)); 456 return buf.toString(); 457 } 458 pos = key.lastIndexOf(PATH_DELIMITER, pos - 1); 459 } 460 461 return SPACE + key; 462 } 463 464 /** 465 * Determines the index of the given child node in the node list of its 466 * parent. 467 * 468 * @param parent the parent node 469 * @param child the child node 470 * @param handler the node handler 471 * @param <T> the type of the nodes involved 472 * @return the index of this child node 473 */ 474 private static <T> int determineIndex(final T parent, final T child, 475 final NodeHandler<T> handler) 476 { 477 return handler.getChildren(parent, handler.nodeName(child)).indexOf( 478 child) + 1; 479 } 480 481 /** 482 * Helper method for throwing an exception about an invalid path. 483 * 484 * @param path the invalid path 485 * @param msg the exception message 486 */ 487 private static void invalidPath(final String path, final String msg) 488 { 489 throw new IllegalArgumentException("Invalid node path: \"" + path 490 + "\" " + msg); 491 } 492 493 /** 494 * Determines the position of the separator in a key for adding new 495 * properties. If no delimiter is found, result is -1. 496 * 497 * @param key the key 498 * @return the position of the delimiter 499 */ 500 private static int findKeySeparator(final String key) 501 { 502 int index = key.length() - 1; 503 while (index >= 0 && !Character.isWhitespace(key.charAt(index))) 504 { 505 index--; 506 } 507 return index; 508 } 509 510 /** 511 * Converts the objects returned as query result from the JXPathContext to 512 * query result objects. 513 * 514 * @param results the list with results from the context 515 * @param <T> the type of results to be produced 516 * @return the result list 517 */ 518 private static <T> List<QueryResult<T>> convertResults(final List<?> results) 519 { 520 final List<QueryResult<T>> queryResults = 521 new ArrayList<>(results.size()); 522 for (final Object res : results) 523 { 524 final QueryResult<T> queryResult = createResult(res); 525 queryResults.add(queryResult); 526 } 527 return queryResults; 528 } 529 530 /** 531 * Creates a {@code QueryResult} object from the given result object of a 532 * query. Because of the node pointers involved result objects can only be 533 * of two types: 534 * <ul> 535 * <li>nodes of type T</li> 536 * <li>attribute results already wrapped in {@code QueryResult} objects</li> 537 * </ul> 538 * This method performs a corresponding cast. Warnings can be suppressed 539 * because of the implementation of the query functionality. 540 * 541 * @param resObj the query result object 542 * @param <T> the type of the result to be produced 543 * @return the {@code QueryResult} 544 */ 545 @SuppressWarnings("unchecked") 546 private static <T> QueryResult<T> createResult(final Object resObj) 547 { 548 if (resObj instanceof QueryResult) 549 { 550 return (QueryResult<T>) resObj; 551 } 552 return QueryResult.createNodeResult((T) resObj); 553 } 554 555 // static initializer: registers the configuration node pointer factory 556 static 557 { 558 JXPathContextReferenceImpl 559 .addNodePointerFactory(new ConfigurationNodePointerFactory()); 560 } 561}