Coverage Report - org.apache.commons.configuration.tree.xpath.XPathExpressionEngine
 
Classes in this File Line Coverage Branch Coverage Complexity
XPathExpressionEngine
100%
85/85
100%
46/46
4,75
 
 1  
 /*
 2  
  * Licensed to the Apache Software Foundation (ASF) under one or more
 3  
  * contributor license agreements.  See the NOTICE file distributed with
 4  
  * this work for additional information regarding copyright ownership.
 5  
  * The ASF licenses this file to You under the Apache License, Version 2.0
 6  
  * (the "License"); you may not use this file except in compliance with
 7  
  * the License.  You may obtain a copy of the License at
 8  
  *
 9  
  *     http://www.apache.org/licenses/LICENSE-2.0
 10  
  *
 11  
  * Unless required by applicable law or agreed to in writing, software
 12  
  * distributed under the License is distributed on an "AS IS" BASIS,
 13  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  
  * See the License for the specific language governing permissions and
 15  
  * limitations under the License.
 16  
  */
 17  
 package org.apache.commons.configuration.tree.xpath;
 18  
 
 19  
 import java.util.Collections;
 20  
 import java.util.List;
 21  
 import java.util.StringTokenizer;
 22  
 
 23  
 import org.apache.commons.configuration.tree.ConfigurationNode;
 24  
 import org.apache.commons.configuration.tree.ExpressionEngine;
 25  
 import org.apache.commons.configuration.tree.NodeAddData;
 26  
 import org.apache.commons.jxpath.JXPathContext;
 27  
 import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
 28  
 import org.apache.commons.lang.StringUtils;
 29  
 
 30  
 /**
 31  
  * <p>
 32  
  * A specialized implementation of the {@code ExpressionEngine} interface
 33  
  * that is able to evaluate XPATH expressions.
 34  
  * </p>
 35  
  * <p>
 36  
  * This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons
 37  
  * JXPath</a> for handling XPath expressions and mapping them to the nodes of a
 38  
  * hierarchical configuration. This makes the rich and powerful XPATH syntax
 39  
  * available for accessing properties from a configuration object.
 40  
  * </p>
 41  
  * <p>
 42  
  * For selecting properties arbitrary XPATH expressions can be used, which
 43  
  * select single or multiple configuration nodes. The associated
 44  
  * {@code Configuration} instance will directly pass the specified property
 45  
  * keys into this engine. If a key is not syntactically correct, an exception
 46  
  * will be thrown.
 47  
  * </p>
 48  
  * <p>
 49  
  * For adding new properties, this expression engine uses a specific syntax: the
 50  
  * &quot;key&quot; of a new property must consist of two parts that are
 51  
  * separated by whitespace:
 52  
  * <ol>
 53  
  * <li>An XPATH expression selecting a single node, to which the new element(s)
 54  
  * are to be added. This can be an arbitrary complex expression, but it must
 55  
  * select exactly one node, otherwise an exception will be thrown.</li>
 56  
  * <li>The name of the new element(s) to be added below this parent node. Here
 57  
  * either a single node name or a complete path of nodes (separated by the
 58  
  * &quot;/&quot; character or &quot;@&quot; for an attribute) can be specified.</li>
 59  
  * </ol>
 60  
  * Some examples for valid keys that can be passed into the configuration's
 61  
  * {@code addProperty()} method follow:
 62  
  * </p>
 63  
  * <p>
 64  
  *
 65  
  * <pre>
 66  
  * &quot;/tables/table[1] type&quot;
 67  
  * </pre>
 68  
  *
 69  
  * </p>
 70  
  * <p>
 71  
  * This will add a new {@code type} node as a child of the first
 72  
  * {@code table} element.
 73  
  * </p>
 74  
  * <p>
 75  
  *
 76  
  * <pre>
 77  
  * &quot;/tables/table[1] @type&quot;
 78  
  * </pre>
 79  
  *
 80  
  * </p>
 81  
  * <p>
 82  
  * Similar to the example above, but this time a new attribute named
 83  
  * {@code type} will be added to the first {@code table} element.
 84  
  * </p>
 85  
  * <p>
 86  
  *
 87  
  * <pre>
 88  
  * &quot;/tables table/fields/field/name&quot;
 89  
  * </pre>
 90  
  *
 91  
  * </p>
 92  
  * <p>
 93  
  * This example shows how a complex path can be added. Parent node is the
 94  
  * {@code tables} element. Here a new branch consisting of the nodes
 95  
  * {@code table}, {@code fields}, {@code field}, and
 96  
  * {@code name} will be added.
 97  
  * </p>
 98  
  * <p>
 99  
  *
 100  
  * <pre>
 101  
  * &quot;/tables table/fields/field@type&quot;
 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  
  * &quot;tables/table[last()]/fields/field/name&quot;
 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  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  417235
         if (StringUtils.isEmpty(key))
 174  
         {
 175  10
             return Collections.singletonList(root);
 176  
         }
 177  
         else
 178  
         {
 179  417229
             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  417179
             List<ConfigurationNode> result = context.selectNodes(key);
 184  417160
             if (result == null)
 185  
             {
 186  2
                 result = Collections.emptyList();
 187  
             }
 188  417197
             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  21
         if (parentKey == null)
 208  
         {
 209  
             // name of the root node
 210  2
             return StringUtils.EMPTY;
 211  
         }
 212  19
         else if (node.getName() == null)
 213  
         {
 214  
             // paranoia check for undefined node names
 215  1
             return parentKey;
 216  
         }
 217  
 
 218  
         else
 219  
         {
 220  18
             StringBuilder buf = new StringBuilder(parentKey.length()
 221  
                     + node.getName().length() + PATH_DELIMITER.length());
 222  18
             if (parentKey.length() > 0)
 223  
             {
 224  14
                 buf.append(parentKey);
 225  14
                 buf.append(PATH_DELIMITER);
 226  
             }
 227  18
             if (node.isAttribute())
 228  
             {
 229  2
                 buf.append(ATTR_DELIMITER);
 230  
             }
 231  18
             buf.append(node.getName());
 232  18
             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  28
         if (key == null)
 248  
         {
 249  1
             throw new IllegalArgumentException(
 250  
                     "prepareAdd: key must not be null!");
 251  
         }
 252  
 
 253  27
         String addKey = key;
 254  27
         int index = findKeySeparator(addKey);
 255  27
         if (index < 0)
 256  
         {
 257  8
             addKey = generateKeyForAdd(root, addKey);
 258  8
             index = findKeySeparator(addKey);
 259  
         }
 260  
 
 261  27
         List<ConfigurationNode> nodes = query(root, addKey.substring(0, index).trim());
 262  27
         if (nodes.size() != 1)
 263  
         {
 264  1
             throw new IllegalArgumentException(
 265  
                     "prepareAdd: key must select exactly one target node!");
 266  
         }
 267  
 
 268  26
         NodeAddData data = new NodeAddData();
 269  26
         data.setParent(nodes.get(0));
 270  26
         initNodeAddData(data, addKey.substring(index).trim());
 271  19
         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  417180
         JXPathContext context = JXPathContext.newContext(root);
 286  417152
         context.setLenient(true);
 287  417114
         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  26
         String lastComponent = null;
 301  26
         boolean attr = false;
 302  26
         boolean first = true;
 303  
 
 304  26
         StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
 305  
                 true);
 306  108
         while (tok.hasMoreTokens())
 307  
         {
 308  87
             String token = tok.nextToken();
 309  87
             if (PATH_DELIMITER.equals(token))
 310  
             {
 311  28
                 if (attr)
 312  
                 {
 313  1
                     invalidPath(path, " contains an attribute"
 314  
                             + " delimiter at an unallowed position.");
 315  
                 }
 316  27
                 if (lastComponent == null)
 317  
                 {
 318  2
                     invalidPath(path,
 319  
                             " contains a '/' at an unallowed position.");
 320  
                 }
 321  25
                 data.addPathNode(lastComponent);
 322  25
                 lastComponent = null;
 323  
             }
 324  
 
 325  59
             else if (ATTR_DELIMITER.equals(token))
 326  
             {
 327  10
                 if (attr)
 328  
                 {
 329  1
                     invalidPath(path,
 330  
                             " contains multiple attribute delimiters.");
 331  
                 }
 332  9
                 if (lastComponent == null && !first)
 333  
                 {
 334  1
                     invalidPath(path,
 335  
                             " contains an attribute delimiter at an unallowed position.");
 336  
                 }
 337  8
                 if (lastComponent != null)
 338  
                 {
 339  3
                     data.addPathNode(lastComponent);
 340  
                 }
 341  8
                 attr = true;
 342  8
                 lastComponent = null;
 343  
             }
 344  
 
 345  
             else
 346  
             {
 347  49
                 lastComponent = token;
 348  
             }
 349  82
             first = false;
 350  82
         }
 351  
 
 352  21
         if (lastComponent == null)
 353  
         {
 354  2
             invalidPath(path, "contains no components.");
 355  
         }
 356  19
         data.setNewNodeName(lastComponent);
 357  19
         data.setAttribute(attr);
 358  19
     }
 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  8
         int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
 374  
 
 375  14
         while (pos >= 0)
 376  
         {
 377  11
             String keyExisting = key.substring(0, pos);
 378  11
             if (!query(root, keyExisting).isEmpty())
 379  
             {
 380  5
                 StringBuilder buf = new StringBuilder(key.length() + 1);
 381  5
                 buf.append(keyExisting).append(SPACE);
 382  5
                 buf.append(key.substring(pos + 1));
 383  5
                 return buf.toString();
 384  
             }
 385  6
             pos = key.lastIndexOf(PATH_DELIMITER, pos - 1);
 386  6
         }
 387  
 
 388  3
         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  7
         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  35
         int index = key.length() - 1;
 413  555
         while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
 414  
         {
 415  520
             index--;
 416  
         }
 417  35
         return index;
 418  
     }
 419  
 
 420  
     // static initializer: registers the configuration node pointer factory
 421  
     static
 422  
     {
 423  1
         JXPathContextReferenceImpl
 424  
                 .addNodePointerFactory(new ConfigurationNodePointerFactory());
 425  1
     }
 426  
 }