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