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