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 */ 017 package org.apache.commons.configuration.tree.xpath; 018 019 import java.util.Collections; 020 import java.util.List; 021 import java.util.StringTokenizer; 022 023 import org.apache.commons.configuration.tree.ConfigurationNode; 024 import org.apache.commons.configuration.tree.ExpressionEngine; 025 import org.apache.commons.configuration.tree.NodeAddData; 026 import org.apache.commons.jxpath.JXPathContext; 027 import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl; 028 import org.apache.commons.lang.StringUtils; 029 030 /** 031 * <p> 032 * A specialized implementation of the {@code ExpressionEngine} interface 033 * that is able to evaluate XPATH expressions. 034 * </p> 035 * <p> 036 * This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons 037 * JXPath</a> for handling XPath expressions and mapping them to the nodes of a 038 * hierarchical configuration. This makes the rich and powerful XPATH syntax 039 * available for accessing properties from a configuration object. 040 * </p> 041 * <p> 042 * For selecting properties arbitrary XPATH expressions can be used, which 043 * select single or multiple configuration nodes. The associated 044 * {@code Configuration} instance will directly pass the specified property 045 * keys into this engine. If a key is not syntactically correct, an exception 046 * will be thrown. 047 * </p> 048 * <p> 049 * For adding new properties, this expression engine uses a specific syntax: the 050 * "key" of a new property must consist of two parts that are 051 * separated by whitespace: 052 * <ol> 053 * <li>An XPATH expression selecting a single node, to which the new element(s) 054 * are to be added. This can be an arbitrary complex expression, but it must 055 * select exactly one node, otherwise an exception will be thrown.</li> 056 * <li>The name of the new element(s) to be added below this parent node. Here 057 * either a single node name or a complete path of nodes (separated by the 058 * "/" character or "@" for an attribute) can be specified.</li> 059 * </ol> 060 * Some examples for valid keys that can be passed into the configuration's 061 * {@code addProperty()} method follow: 062 * </p> 063 * <p> 064 * 065 * <pre> 066 * "/tables/table[1] type" 067 * </pre> 068 * 069 * </p> 070 * <p> 071 * This will add a new {@code type} node as a child of the first 072 * {@code table} element. 073 * </p> 074 * <p> 075 * 076 * <pre> 077 * "/tables/table[1] @type" 078 * </pre> 079 * 080 * </p> 081 * <p> 082 * Similar to the example above, but this time a new attribute named 083 * {@code type} will be added to the first {@code table} element. 084 * </p> 085 * <p> 086 * 087 * <pre> 088 * "/tables table/fields/field/name" 089 * </pre> 090 * 091 * </p> 092 * <p> 093 * This example shows how a complex path can be added. Parent node is the 094 * {@code tables} element. Here a new branch consisting of the nodes 095 * {@code table}, {@code fields}, {@code field}, and 096 * {@code name} will be added. 097 * </p> 098 * <p> 099 * 100 * <pre> 101 * "/tables table/fields/field@type" 102 * </pre> 103 * 104 * </p> 105 * <p> 106 * This is similar to the last example, but in this case a complex path ending 107 * with an attribute is defined. 108 * </p> 109 * <p> 110 * <strong>Note:</strong> This extended syntax for adding properties only works 111 * with the {@code addProperty()} method. {@code setProperty()} does 112 * not support creating new nodes this way. 113 * </p> 114 * <p> 115 * From version 1.7 on, it is possible to use regular keys in calls to 116 * {@code addProperty()} (i.e. keys that do not have to contain a 117 * whitespace as delimiter). In this case the key is evaluated, and the biggest 118 * part pointing to an existing node is determined. The remaining part is then 119 * added as new path. As an example consider the key 120 * 121 * <pre> 122 * "tables/table[last()]/fields/field/name" 123 * </pre> 124 * 125 * If the key does not point to an existing node, the engine will check the 126 * paths {@code "tables/table[last()]/fields/field"}, 127 * {@code "tables/table[last()]/fields"}, 128 * {@code "tables/table[last()]"}, and so on, until a key is 129 * found which points to a node. Let's assume that the last key listed above can 130 * be resolved in this way. Then from this key the following key is derived: 131 * {@code "tables/table[last()] fields/field/name"} by appending 132 * the remaining part after a whitespace. This key can now be processed using 133 * the original algorithm. Keys of this form can also be used with the 134 * {@code setProperty()} method. However, it is still recommended to use 135 * the old format because it makes explicit at which position new nodes should 136 * be added. For keys without a whitespace delimiter there may be ambiguities. 137 * </p> 138 * 139 * @since 1.3 140 * @author <a 141 * href="http://commons.apache.org/configuration/team-list.html">Commons 142 * Configuration team</a> 143 * @version $Id: XPathExpressionEngine.java 1206563 2011-11-26 19:47:26Z oheger $ 144 */ 145 public class XPathExpressionEngine implements ExpressionEngine 146 { 147 /** Constant for the path delimiter. */ 148 static final String PATH_DELIMITER = "/"; 149 150 /** Constant for the attribute delimiter. */ 151 static final String ATTR_DELIMITER = "@"; 152 153 /** Constant for the delimiters for splitting node paths. */ 154 private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER 155 + ATTR_DELIMITER; 156 157 /** 158 * Constant for a space which is used as delimiter in keys for adding 159 * properties. 160 */ 161 private static final String SPACE = " "; 162 163 /** 164 * Executes a query. The passed in property key is directly passed to a 165 * JXPath context. 166 * 167 * @param root the configuration root node 168 * @param key the query to be executed 169 * @return a list with the nodes that are selected by the query 170 */ 171 public List<ConfigurationNode> query(ConfigurationNode root, String key) 172 { 173 if (StringUtils.isEmpty(key)) 174 { 175 return Collections.singletonList(root); 176 } 177 else 178 { 179 JXPathContext context = createContext(root, key); 180 // This is safe because our node pointer implementations will return 181 // a list of configuration nodes. 182 @SuppressWarnings("unchecked") 183 List<ConfigurationNode> result = context.selectNodes(key); 184 if (result == null) 185 { 186 result = Collections.emptyList(); 187 } 188 return result; 189 } 190 } 191 192 /** 193 * Returns a (canonical) key for the given node based on the parent's key. 194 * This implementation will create an XPATH expression that selects the 195 * given node (under the assumption that the passed in parent key is valid). 196 * As the {@code nodeKey()} implementation of 197 * {@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine} 198 * this method will not return indices for nodes. So all child nodes of a 199 * given parent with the same name will have the same key. 200 * 201 * @param node the node for which a key is to be constructed 202 * @param parentKey the key of the parent node 203 * @return the key for the given node 204 */ 205 public String nodeKey(ConfigurationNode node, String parentKey) 206 { 207 if (parentKey == null) 208 { 209 // name of the root node 210 return StringUtils.EMPTY; 211 } 212 else if (node.getName() == null) 213 { 214 // paranoia check for undefined node names 215 return parentKey; 216 } 217 218 else 219 { 220 StringBuilder buf = new StringBuilder(parentKey.length() 221 + node.getName().length() + PATH_DELIMITER.length()); 222 if (parentKey.length() > 0) 223 { 224 buf.append(parentKey); 225 buf.append(PATH_DELIMITER); 226 } 227 if (node.isAttribute()) 228 { 229 buf.append(ATTR_DELIMITER); 230 } 231 buf.append(node.getName()); 232 return buf.toString(); 233 } 234 } 235 236 /** 237 * Prepares an add operation for a configuration property. The expected 238 * format of the passed in key is explained in the class comment. 239 * 240 * @param root the configuration's root node 241 * @param key the key describing the target of the add operation and the 242 * path of the new node 243 * @return a data object to be evaluated by the calling configuration object 244 */ 245 public NodeAddData prepareAdd(ConfigurationNode root, String key) 246 { 247 if (key == null) 248 { 249 throw new IllegalArgumentException( 250 "prepareAdd: key must not be null!"); 251 } 252 253 String addKey = key; 254 int index = findKeySeparator(addKey); 255 if (index < 0) 256 { 257 addKey = generateKeyForAdd(root, addKey); 258 index = findKeySeparator(addKey); 259 } 260 261 List<ConfigurationNode> nodes = query(root, addKey.substring(0, index).trim()); 262 if (nodes.size() != 1) 263 { 264 throw new IllegalArgumentException( 265 "prepareAdd: key must select exactly one target node!"); 266 } 267 268 NodeAddData data = new NodeAddData(); 269 data.setParent(nodes.get(0)); 270 initNodeAddData(data, addKey.substring(index).trim()); 271 return data; 272 } 273 274 /** 275 * Creates the {@code JXPathContext} used for executing a query. This 276 * method will create a new context and ensure that it is correctly 277 * initialized. 278 * 279 * @param root the configuration root node 280 * @param key the key to be queried 281 * @return the new context 282 */ 283 protected JXPathContext createContext(ConfigurationNode root, String key) 284 { 285 JXPathContext context = JXPathContext.newContext(root); 286 context.setLenient(true); 287 return context; 288 } 289 290 /** 291 * Initializes most properties of a {@code NodeAddData} object. This 292 * method is called by {@code prepareAdd()} after the parent node has 293 * been found. Its task is to interpret the passed in path of the new node. 294 * 295 * @param data the data object to initialize 296 * @param path the path of the new node 297 */ 298 protected void initNodeAddData(NodeAddData data, String path) 299 { 300 String lastComponent = null; 301 boolean attr = false; 302 boolean first = true; 303 304 StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS, 305 true); 306 while (tok.hasMoreTokens()) 307 { 308 String token = tok.nextToken(); 309 if (PATH_DELIMITER.equals(token)) 310 { 311 if (attr) 312 { 313 invalidPath(path, " contains an attribute" 314 + " delimiter at an unallowed position."); 315 } 316 if (lastComponent == null) 317 { 318 invalidPath(path, 319 " contains a '/' at an unallowed position."); 320 } 321 data.addPathNode(lastComponent); 322 lastComponent = null; 323 } 324 325 else if (ATTR_DELIMITER.equals(token)) 326 { 327 if (attr) 328 { 329 invalidPath(path, 330 " contains multiple attribute delimiters."); 331 } 332 if (lastComponent == null && !first) 333 { 334 invalidPath(path, 335 " contains an attribute delimiter at an unallowed position."); 336 } 337 if (lastComponent != null) 338 { 339 data.addPathNode(lastComponent); 340 } 341 attr = true; 342 lastComponent = null; 343 } 344 345 else 346 { 347 lastComponent = token; 348 } 349 first = false; 350 } 351 352 if (lastComponent == null) 353 { 354 invalidPath(path, "contains no components."); 355 } 356 data.setNewNodeName(lastComponent); 357 data.setAttribute(attr); 358 } 359 360 /** 361 * Tries to generate a key for adding a property. This method is called if a 362 * key was used for adding properties which does not contain a space 363 * character. It splits the key at its single components and searches for 364 * the last existing component. Then a key compatible for adding properties 365 * is generated. 366 * 367 * @param root the root node of the configuration 368 * @param key the key in question 369 * @return the key to be used for adding the property 370 */ 371 private String generateKeyForAdd(ConfigurationNode root, String key) 372 { 373 int pos = key.lastIndexOf(PATH_DELIMITER, key.length()); 374 375 while (pos >= 0) 376 { 377 String keyExisting = key.substring(0, pos); 378 if (!query(root, keyExisting).isEmpty()) 379 { 380 StringBuilder buf = new StringBuilder(key.length() + 1); 381 buf.append(keyExisting).append(SPACE); 382 buf.append(key.substring(pos + 1)); 383 return buf.toString(); 384 } 385 pos = key.lastIndexOf(PATH_DELIMITER, pos - 1); 386 } 387 388 return SPACE + key; 389 } 390 391 /** 392 * Helper method for throwing an exception about an invalid path. 393 * 394 * @param path the invalid path 395 * @param msg the exception message 396 */ 397 private void invalidPath(String path, String msg) 398 { 399 throw new IllegalArgumentException("Invalid node path: \"" + path 400 + "\" " + msg); 401 } 402 403 /** 404 * Determines the position of the separator in a key for adding new 405 * properties. If no delimiter is found, result is -1. 406 * 407 * @param key the key 408 * @return the position of the delimiter 409 */ 410 private static int findKeySeparator(String key) 411 { 412 int index = key.length() - 1; 413 while (index >= 0 && !Character.isWhitespace(key.charAt(index))) 414 { 415 index--; 416 } 417 return index; 418 } 419 420 // static initializer: registers the configuration node pointer factory 421 static 422 { 423 JXPathContextReferenceImpl 424 .addNodePointerFactory(new ConfigurationNodePointerFactory()); 425 } 426 }