Coverage Report - org.apache.commons.configuration.tree.xpath.XPathExpressionEngine
 
Classes in this File Line Coverage Branch Coverage Complexity
XPathExpressionEngine
100%
69/69
100%
19/19
5,167
 
 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.ArrayList;
 20  
 import java.util.Collections;
 21  
 import java.util.List;
 22  
 import java.util.StringTokenizer;
 23  
 
 24  
 import org.apache.commons.configuration.tree.ConfigurationNode;
 25  
 import org.apache.commons.configuration.tree.ExpressionEngine;
 26  
 import org.apache.commons.configuration.tree.NodeAddData;
 27  
 import org.apache.commons.jxpath.JXPathContext;
 28  
 import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
 29  
 import org.apache.commons.lang.StringUtils;
 30  
 
 31  
 /**
 32  
  * <p>
 33  
  * A specialized implementation of the <code>ExpressionEngine</code> interface
 34  
  * that is able to evaluate XPATH expressions.
 35  
  * </p>
 36  
  * <p>
 37  
  * This class makes use of <a href="http://commons.apache.org/jxpath/">
 38  
  * Commons JXPath</a> for handling XPath expressions and mapping them to the
 39  
  * nodes of a hierarchical configuration. This makes the rich and powerful
 40  
  * XPATH syntax available for accessing properties from a configuration object.
 41  
  * </p>
 42  
  * <p>
 43  
  * For selecting properties arbitrary XPATH expressions can be used, which
 44  
  * select single or multiple configuration nodes. The associated
 45  
  * <code>Configuration</code> instance will directly pass the specified
 46  
  * property keys into this engine. If a key is not syntactically correct, an
 47  
  * exception will be thrown.
 48  
  * </p>
 49  
  * <p>
 50  
  * For adding new properties, this expression engine uses a specific syntax: the
 51  
  * &quot;key&quot; of a new property must consist of two parts that are
 52  
  * separated by whitespace:
 53  
  * <ol>
 54  
  * <li>An XPATH expression selecting a single node, to which the new element(s)
 55  
  * are to be added. This can be an arbitrary complex expression, but it must
 56  
  * select exactly one node, otherwise an exception will be thrown.</li>
 57  
  * <li>The name of the new element(s) to be added below this parent node. Here
 58  
  * either a single node name or a complete path of nodes (separated by the
 59  
  * &quot;/&quot; character or &quot;@&quot; for an attribute) can be specified.</li>
 60  
  * </ol>
 61  
  * Some examples for valid keys that can be passed into the configuration's
 62  
  * <code>addProperty()</code> method follow:
 63  
  * </p>
 64  
  * <p>
 65  
  *
 66  
  * <pre>
 67  
  * &quot;/tables/table[1] type&quot;
 68  
  * </pre>
 69  
  *
 70  
  * </p>
 71  
  * <p>
 72  
  * This will add a new <code>type</code> node as a child of the first
 73  
  * <code>table</code> element.
 74  
  * </p>
 75  
  * <p>
 76  
  *
 77  
  * <pre>
 78  
  * &quot;/tables/table[1] @type&quot;
 79  
  * </pre>
 80  
  *
 81  
  * </p>
 82  
  * <p>
 83  
  * Similar to the example above, but this time a new attribute named
 84  
  * <code>type</code> will be added to the first <code>table</code> element.
 85  
  * </p>
 86  
  * <p>
 87  
  *
 88  
  * <pre>
 89  
  * &quot;/tables table/fields/field/name&quot;
 90  
  * </pre>
 91  
  *
 92  
  * </p>
 93  
  * <p>
 94  
  * This example shows how a complex path can be added. Parent node is the
 95  
  * <code>tables</code> element. Here a new branch consisting of the nodes
 96  
  * <code>table</code>, <code>fields</code>, <code>field</code>, and
 97  
  * <code>name</code> will be added.
 98  
  * </p>
 99  
  *
 100  
  * <p>
 101  
  * <pre>
 102  
  * &quot;/tables table/fields/field@type&quot;
 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  38
 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  80
         if (StringUtils.isEmpty(key))
 142  
         {
 143  4
             List result = new ArrayList(1);
 144  4
             result.add(root);
 145  4
             return result;
 146  
         }
 147  
         else
 148  
         {
 149  76
             JXPathContext context = createContext(root, key);
 150  76
             List result = context.selectNodes(key);
 151  75
             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  108
         if (parentKey == null)
 171  
         {
 172  
             // name of the root node
 173  3
             return StringUtils.EMPTY;
 174  
         }
 175  105
         else if (node.getName() == null)
 176  
         {
 177  
             // paranoia check for undefined node names
 178  1
             return parentKey;
 179  
         }
 180  
 
 181  
         else
 182  
         {
 183  104
             StringBuffer buf = new StringBuffer(parentKey.length()
 184  
                     + node.getName().length() + PATH_DELIMITER.length());
 185  104
             if (parentKey.length() > 0)
 186  
             {
 187  85
                 buf.append(parentKey);
 188  85
                 buf.append(PATH_DELIMITER);
 189  
             }
 190  104
             if (node.isAttribute())
 191  
             {
 192  27
                 buf.append(ATTR_DELIMITER);
 193  
             }
 194  104
             buf.append(node.getName());
 195  104
             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  16
         if (key == null)
 211  
         {
 212  1
             throw new IllegalArgumentException(
 213  
                     "prepareAdd: key must not be null!");
 214  
         }
 215  
 
 216  15
         int index = key.length() - 1;
 217  184
         while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
 218  
         {
 219  169
             index--;
 220  
         }
 221  15
         if (index < 0)
 222  
         {
 223  2
             throw new IllegalArgumentException(
 224  
                     "prepareAdd: Passed in key must contain a whitespace!");
 225  
         }
 226  
 
 227  13
         List nodes = query(root, key.substring(0, index).trim());
 228  13
         if (nodes.size() != 1)
 229  
         {
 230  1
             throw new IllegalArgumentException(
 231  
                     "prepareAdd: key must select exactly one target node!");
 232  
         }
 233  
 
 234  12
         NodeAddData data = new NodeAddData();
 235  12
         data.setParent((ConfigurationNode) nodes.get(0));
 236  12
         initNodeAddData(data, key.substring(index).trim());
 237  6
         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  64
         JXPathContext context = JXPathContext.newContext(root);
 252  64
         context.setLenient(true);
 253  64
         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  12
         String lastComponent = null;
 267  12
         boolean attr = false;
 268  12
         boolean first = true;
 269  
 
 270  12
         StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
 271  
                 true);
 272  52
         while (tok.hasMoreTokens())
 273  
         {
 274  45
             String token = tok.nextToken();
 275  45
             if (PATH_DELIMITER.equals(token))
 276  
             {
 277  15
                 if (attr)
 278  
                 {
 279  1
                     invalidPath(path, " contains an attribute"
 280  
                             + " delimiter at an unallowed position.");
 281  
                 }
 282  14
                 if (lastComponent == null)
 283  
                 {
 284  2
                     invalidPath(path,
 285  
                             " contains a '/' at an unallowed position.");
 286  
                 }
 287  12
                 data.addPathNode(lastComponent);
 288  12
                 lastComponent = null;
 289  
             }
 290  
 
 291  30
             else if (ATTR_DELIMITER.equals(token))
 292  
             {
 293  7
                 if (attr)
 294  
                 {
 295  1
                     invalidPath(path,
 296  
                             " contains multiple attribute delimiters.");
 297  
                 }
 298  6
                 if (lastComponent == null && !first)
 299  
                 {
 300  1
                     invalidPath(path,
 301  
                             " contains an attribute delimiter at an unallowed position.");
 302  
                 }
 303  5
                 if (lastComponent != null)
 304  
                 {
 305  3
                     data.addPathNode(lastComponent);
 306  
                 }
 307  5
                 attr = true;
 308  5
                 lastComponent = null;
 309  
             }
 310  
 
 311  
             else
 312  
             {
 313  23
                 lastComponent = token;
 314  
             }
 315  40
             first = false;
 316  
         }
 317  
 
 318  7
         if (lastComponent == null)
 319  
         {
 320  1
             invalidPath(path, "contains no components.");
 321  
         }
 322  6
         data.setNewNodeName(lastComponent);
 323  6
         data.setAttribute(attr);
 324  6
     }
 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  6
         throw new IllegalArgumentException("Invalid node path: \"" + path
 335  
                 + "\" " + msg);
 336  
     }
 337  
 
 338  
     // static initializer: registers the configuration node pointer factory
 339  
     static
 340  
     {
 341  5
         JXPathContextReferenceImpl
 342  
                 .addNodePointerFactory(new ConfigurationNodePointerFactory());
 343  5
     }
 344  
 }