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 1790899 2017-04-10 21:56:46Z 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(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(T root, String key, 196 NodeHandler<T> handler) 197 { 198 if (StringUtils.isEmpty(key)) 199 { 200 QueryResult<T> result = createResult(root); 201 return Collections.singletonList(result); 202 } 203 else 204 { 205 JXPathContext context = createContext(root, handler); 206 List<?> results = context.selectNodes(key); 207 if (results == null) 208 { 209 results = Collections.emptyList(); 210 } 211 return convertResults(results); 212 } 213 } 214 215 /** 216 * {@inheritDoc} This implementation creates an XPATH expression that 217 * selects the given node (under the assumption that the passed in parent 218 * key is valid). As the {@code nodeKey()} implementation of 219 * {@link org.apache.commons.configuration2.tree.DefaultExpressionEngine 220 * DefaultExpressionEngine} this method does not return indices for nodes. 221 * So all child nodes of a given parent with the same name have the same 222 * key. 223 */ 224 @Override 225 public <T> String nodeKey(T node, String parentKey, NodeHandler<T> handler) 226 { 227 if (parentKey == null) 228 { 229 // name of the root node 230 return StringUtils.EMPTY; 231 } 232 else if (handler.nodeName(node) == null) 233 { 234 // paranoia check for undefined node names 235 return parentKey; 236 } 237 238 else 239 { 240 StringBuilder buf = 241 new StringBuilder(parentKey.length() 242 + handler.nodeName(node).length() 243 + PATH_DELIMITER.length()); 244 if (parentKey.length() > 0) 245 { 246 buf.append(parentKey); 247 buf.append(PATH_DELIMITER); 248 } 249 buf.append(handler.nodeName(node)); 250 return buf.toString(); 251 } 252 } 253 254 @Override 255 public String attributeKey(String parentKey, String attributeName) 256 { 257 StringBuilder buf = 258 new StringBuilder(StringUtils.length(parentKey) 259 + StringUtils.length(attributeName) 260 + PATH_DELIMITER.length() + ATTR_DELIMITER.length()); 261 if (StringUtils.isNotEmpty(parentKey)) 262 { 263 buf.append(parentKey).append(PATH_DELIMITER); 264 } 265 buf.append(ATTR_DELIMITER).append(attributeName); 266 return buf.toString(); 267 } 268 269 /** 270 * {@inheritDoc} This implementation works similar to {@code nodeKey()}, but 271 * always adds an index expression to the resulting key. 272 */ 273 @Override 274 public <T> String canonicalKey(T node, String parentKey, 275 NodeHandler<T> handler) 276 { 277 T parent = handler.getParent(node); 278 if (parent == null) 279 { 280 // this is the root node 281 return StringUtils.defaultString(parentKey); 282 } 283 284 StringBuilder buf = new StringBuilder(BUF_SIZE); 285 if (StringUtils.isNotEmpty(parentKey)) 286 { 287 buf.append(parentKey).append(PATH_DELIMITER); 288 } 289 buf.append(handler.nodeName(node)); 290 buf.append(START_INDEX); 291 buf.append(determineIndex(parent, node, handler)); 292 buf.append(END_INDEX); 293 return buf.toString(); 294 } 295 296 /** 297 * {@inheritDoc} The expected format of the passed in key is explained in 298 * the class comment. 299 */ 300 @Override 301 public <T> NodeAddData<T> prepareAdd(T root, String key, 302 NodeHandler<T> handler) 303 { 304 if (key == null) 305 { 306 throw new IllegalArgumentException( 307 "prepareAdd: key must not be null!"); 308 } 309 310 String addKey = key; 311 int index = findKeySeparator(addKey); 312 if (index < 0) 313 { 314 addKey = generateKeyForAdd(root, addKey, handler); 315 index = findKeySeparator(addKey); 316 } 317 else if (index >= addKey.length() - 1) 318 { 319 invalidPath(addKey, " new node path must not be empty."); 320 } 321 322 List<QueryResult<T>> nodes = 323 query(root, addKey.substring(0, index).trim(), handler); 324 if (nodes.size() != 1) 325 { 326 throw new IllegalArgumentException("prepareAdd: key '" + key 327 + "' must select exactly one target node!"); 328 } 329 330 return createNodeAddData(addKey.substring(index).trim(), nodes.get(0)); 331 } 332 333 /** 334 * Creates the {@code JXPathContext} to be used for executing a query. This 335 * method delegates to the context factory. 336 * 337 * @param root the configuration root node 338 * @param handler the node handler 339 * @return the new context 340 */ 341 private <T> JXPathContext createContext(T root, NodeHandler<T> handler) 342 { 343 return getContextFactory().createContext(root, handler); 344 } 345 346 /** 347 * Creates a {@code NodeAddData} object as a result of a 348 * {@code prepareAdd()} operation. This method interprets the passed in path 349 * of the new node. 350 * 351 * @param path the path of the new node 352 * @param parentNodeResult the parent node 353 * @param <T> the type of the nodes involved 354 */ 355 <T> NodeAddData<T> createNodeAddData(String path, 356 QueryResult<T> parentNodeResult) 357 { 358 if (parentNodeResult.isAttributeResult()) 359 { 360 invalidPath(path, " cannot add properties to an attribute."); 361 } 362 List<String> pathNodes = new LinkedList<>(); 363 String lastComponent = null; 364 boolean attr = false; 365 boolean first = true; 366 367 StringTokenizer tok = 368 new StringTokenizer(path, NODE_PATH_DELIMITERS, true); 369 while (tok.hasMoreTokens()) 370 { 371 String token = tok.nextToken(); 372 if (PATH_DELIMITER.equals(token)) 373 { 374 if (attr) 375 { 376 invalidPath(path, " contains an attribute" 377 + " delimiter at a disallowed position."); 378 } 379 if (lastComponent == null) 380 { 381 invalidPath(path, 382 " contains a '/' at a disallowed position."); 383 } 384 pathNodes.add(lastComponent); 385 lastComponent = null; 386 } 387 388 else if (ATTR_DELIMITER.equals(token)) 389 { 390 if (attr) 391 { 392 invalidPath(path, 393 " contains multiple attribute delimiters."); 394 } 395 if (lastComponent == null && !first) 396 { 397 invalidPath(path, 398 " contains an attribute delimiter at a disallowed position."); 399 } 400 if (lastComponent != null) 401 { 402 pathNodes.add(lastComponent); 403 } 404 attr = true; 405 lastComponent = null; 406 } 407 408 else 409 { 410 lastComponent = token; 411 } 412 first = false; 413 } 414 415 if (lastComponent == null) 416 { 417 invalidPath(path, "contains no components."); 418 } 419 420 return new NodeAddData<>(parentNodeResult.getNode(), lastComponent, 421 attr, pathNodes); 422 } 423 424 /** 425 * Returns the {@code XPathContextFactory} used by this instance. 426 * 427 * @return the {@code XPathContextFactory} 428 */ 429 XPathContextFactory getContextFactory() 430 { 431 return contextFactory; 432 } 433 434 /** 435 * Tries to generate a key for adding a property. This method is called if a 436 * key was used for adding properties which does not contain a space 437 * character. It splits the key at its single components and searches for 438 * the last existing component. Then a key compatible key for adding 439 * properties is generated. 440 * 441 * @param root the root node of the configuration 442 * @param key the key in question 443 * @param handler the node handler 444 * @return the key to be used for adding the property 445 */ 446 private <T> String generateKeyForAdd(T root, String key, 447 NodeHandler<T> handler) 448 { 449 int pos = key.lastIndexOf(PATH_DELIMITER, key.length()); 450 451 while (pos >= 0) 452 { 453 String keyExisting = key.substring(0, pos); 454 if (!query(root, keyExisting, handler).isEmpty()) 455 { 456 StringBuilder buf = new StringBuilder(key.length() + 1); 457 buf.append(keyExisting).append(SPACE); 458 buf.append(key.substring(pos + 1)); 459 return buf.toString(); 460 } 461 pos = key.lastIndexOf(PATH_DELIMITER, pos - 1); 462 } 463 464 return SPACE + key; 465 } 466 467 /** 468 * Determines the index of the given child node in the node list of its 469 * parent. 470 * 471 * @param parent the parent node 472 * @param child the child node 473 * @param handler the node handler 474 * @param <T> the type of the nodes involved 475 * @return the index of this child node 476 */ 477 private static <T> int determineIndex(T parent, T child, 478 NodeHandler<T> handler) 479 { 480 return handler.getChildren(parent, handler.nodeName(child)).indexOf( 481 child) + 1; 482 } 483 484 /** 485 * Helper method for throwing an exception about an invalid path. 486 * 487 * @param path the invalid path 488 * @param msg the exception message 489 */ 490 private static void invalidPath(String path, String msg) 491 { 492 throw new IllegalArgumentException("Invalid node path: \"" + path 493 + "\" " + msg); 494 } 495 496 /** 497 * Determines the position of the separator in a key for adding new 498 * properties. If no delimiter is found, result is -1. 499 * 500 * @param key the key 501 * @return the position of the delimiter 502 */ 503 private static int findKeySeparator(String key) 504 { 505 int index = key.length() - 1; 506 while (index >= 0 && !Character.isWhitespace(key.charAt(index))) 507 { 508 index--; 509 } 510 return index; 511 } 512 513 /** 514 * Converts the objects returned as query result from the JXPathContext to 515 * query result objects. 516 * 517 * @param results the list with results from the context 518 * @param <T> the type of results to be produced 519 * @return the result list 520 */ 521 private static <T> List<QueryResult<T>> convertResults(List<?> results) 522 { 523 List<QueryResult<T>> queryResults = 524 new ArrayList<>(results.size()); 525 for (Object res : results) 526 { 527 QueryResult<T> queryResult = createResult(res); 528 queryResults.add(queryResult); 529 } 530 return queryResults; 531 } 532 533 /** 534 * Creates a {@code QueryResult} object from the given result object of a 535 * query. Because of the node pointers involved result objects can only be 536 * of two types: 537 * <ul> 538 * <li>nodes of type T</li> 539 * <li>attribute results already wrapped in {@code QueryResult} objects</li> 540 * </ul> 541 * This method performs a corresponding cast. Warnings can be suppressed 542 * because of the implementation of the query functionality. 543 * 544 * @param resObj the query result object 545 * @param <T> the type of the result to be produced 546 * @return the {@code QueryResult} 547 */ 548 @SuppressWarnings("unchecked") 549 private static <T> QueryResult<T> createResult(Object resObj) 550 { 551 if (resObj instanceof QueryResult) 552 { 553 return (QueryResult<T>) resObj; 554 } 555 else 556 { 557 return QueryResult.createNodeResult((T) resObj); 558 } 559 } 560 561 // static initializer: registers the configuration node pointer factory 562 static 563 { 564 JXPathContextReferenceImpl 565 .addNodePointerFactory(new ConfigurationNodePointerFactory()); 566 } 567}