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