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/"> 038 * Commons JXPath</a> for handling XPath expressions and mapping them to the 039 * nodes of a hierarchical configuration. This makes the rich and powerful 040 * XPATH syntax 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 046 * property keys into this engine. If a key is not syntactically correct, an 047 * exception 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 * 100 * <p> 101 * <pre> 102 * "/tables table/fields/field@type" 103 * </pre> 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()</code> method. <code>setProperty()</code> does 112 * not support creating new nodes this way. 113 * </p> 114 * 115 * @since 1.3 116 * @author Oliver Heger 117 * @version $Id: XPathExpressionEngine.java 656402 2008-05-14 20:15:23Z oheger $ 118 */ 119 public class XPathExpressionEngine implements ExpressionEngine 120 { 121 /** Constant for the path delimiter. */ 122 static final String PATH_DELIMITER = "/"; 123 124 /** Constant for the attribute delimiter. */ 125 static final String ATTR_DELIMITER = "@"; 126 127 /** Constant for the delimiters for splitting node paths. */ 128 private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER 129 + ATTR_DELIMITER; 130 131 /** 132 * Executes a query. The passed in property key is directly passed to a 133 * JXPath context. 134 * 135 * @param root the configuration root node 136 * @param key the query to be executed 137 * @return a list with the nodes that are selected by the query 138 */ 139 public List query(ConfigurationNode root, String key) 140 { 141 if (StringUtils.isEmpty(key)) 142 { 143 List result = new ArrayList(1); 144 result.add(root); 145 return result; 146 } 147 else 148 { 149 JXPathContext context = createContext(root, key); 150 List result = context.selectNodes(key); 151 return (result != null) ? result : Collections.EMPTY_LIST; 152 } 153 } 154 155 /** 156 * Returns a (canonic) key for the given node based on the parent's key. 157 * This implementation will create an XPATH expression that selects the 158 * given node (under the assumption that the passed in parent key is valid). 159 * As the <code>nodeKey()</code> implementation of 160 * <code>{@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine}</code> 161 * this method will not return indices for nodes. So all child nodes of a 162 * given parent whith the same name will have the same key. 163 * 164 * @param node the node for which a key is to be constructed 165 * @param parentKey the key of the parent node 166 * @return the key for the given node 167 */ 168 public String nodeKey(ConfigurationNode node, String parentKey) 169 { 170 if (parentKey == null) 171 { 172 // name of the root node 173 return StringUtils.EMPTY; 174 } 175 else if (node.getName() == null) 176 { 177 // paranoia check for undefined node names 178 return parentKey; 179 } 180 181 else 182 { 183 StringBuffer buf = new StringBuffer(parentKey.length() 184 + node.getName().length() + PATH_DELIMITER.length()); 185 if (parentKey.length() > 0) 186 { 187 buf.append(parentKey); 188 buf.append(PATH_DELIMITER); 189 } 190 if (node.isAttribute()) 191 { 192 buf.append(ATTR_DELIMITER); 193 } 194 buf.append(node.getName()); 195 return buf.toString(); 196 } 197 } 198 199 /** 200 * Prepares an add operation for a configuration property. The expected 201 * format of the passed in key is explained in the class comment. 202 * 203 * @param root the configuration's root node 204 * @param key the key describing the target of the add operation and the 205 * path of the new node 206 * @return a data object to be evaluated by the calling configuration object 207 */ 208 public NodeAddData prepareAdd(ConfigurationNode root, String key) 209 { 210 if (key == null) 211 { 212 throw new IllegalArgumentException( 213 "prepareAdd: key must not be null!"); 214 } 215 216 int index = key.length() - 1; 217 while (index >= 0 && !Character.isWhitespace(key.charAt(index))) 218 { 219 index--; 220 } 221 if (index < 0) 222 { 223 throw new IllegalArgumentException( 224 "prepareAdd: Passed in key must contain a whitespace!"); 225 } 226 227 List nodes = query(root, key.substring(0, index).trim()); 228 if (nodes.size() != 1) 229 { 230 throw new IllegalArgumentException( 231 "prepareAdd: key must select exactly one target node!"); 232 } 233 234 NodeAddData data = new NodeAddData(); 235 data.setParent((ConfigurationNode) nodes.get(0)); 236 initNodeAddData(data, key.substring(index).trim()); 237 return data; 238 } 239 240 /** 241 * Creates the <code>JXPathContext</code> used for executing a query. This 242 * method will create a new context and ensure that it is correctly 243 * initialized. 244 * 245 * @param root the configuration root node 246 * @param key the key to be queried 247 * @return the new context 248 */ 249 protected JXPathContext createContext(ConfigurationNode root, String key) 250 { 251 JXPathContext context = JXPathContext.newContext(root); 252 context.setLenient(true); 253 return context; 254 } 255 256 /** 257 * Initializes most properties of a <code>NodeAddData</code> object. This 258 * method is called by <code>prepareAdd()</code> after the parent node has 259 * been found. Its task is to interpret the passed in path of the new node. 260 * 261 * @param data the data object to initialize 262 * @param path the path of the new node 263 */ 264 protected void initNodeAddData(NodeAddData data, String path) 265 { 266 String lastComponent = null; 267 boolean attr = false; 268 boolean first = true; 269 270 StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS, 271 true); 272 while (tok.hasMoreTokens()) 273 { 274 String token = tok.nextToken(); 275 if (PATH_DELIMITER.equals(token)) 276 { 277 if (attr) 278 { 279 invalidPath(path, " contains an attribute" 280 + " delimiter at an unallowed position."); 281 } 282 if (lastComponent == null) 283 { 284 invalidPath(path, 285 " contains a '/' at an unallowed position."); 286 } 287 data.addPathNode(lastComponent); 288 lastComponent = null; 289 } 290 291 else if (ATTR_DELIMITER.equals(token)) 292 { 293 if (attr) 294 { 295 invalidPath(path, 296 " contains multiple attribute delimiters."); 297 } 298 if (lastComponent == null && !first) 299 { 300 invalidPath(path, 301 " contains an attribute delimiter at an unallowed position."); 302 } 303 if (lastComponent != null) 304 { 305 data.addPathNode(lastComponent); 306 } 307 attr = true; 308 lastComponent = null; 309 } 310 311 else 312 { 313 lastComponent = token; 314 } 315 first = false; 316 } 317 318 if (lastComponent == null) 319 { 320 invalidPath(path, "contains no components."); 321 } 322 data.setNewNodeName(lastComponent); 323 data.setAttribute(attr); 324 } 325 326 /** 327 * Helper method for throwing an exception about an invalid path. 328 * 329 * @param path the invalid path 330 * @param msg the exception message 331 */ 332 private void invalidPath(String path, String msg) 333 { 334 throw new IllegalArgumentException("Invalid node path: \"" + path 335 + "\" " + msg); 336 } 337 338 // static initializer: registers the configuration node pointer factory 339 static 340 { 341 JXPathContextReferenceImpl 342 .addNodePointerFactory(new ConfigurationNodePointerFactory()); 343 } 344 }