Coverage Report - org.apache.commons.configuration.tree.xpath.XPathExpressionEngine
 
Classes in this File Line Coverage Branch Coverage Complexity
XPathExpressionEngine
100%
70/70
100%
20/20
5,333
 
 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://jakarta.apache.org/commons/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 439648 2006-09-02 20:42:10Z oheger $
 103  
  */
 104  31
 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  249
         if (StringUtils.isEmpty(key))
 127  
         {
 128  81
             List result = new ArrayList(1);
 129  81
             result.add(root);
 130  81
             return result;
 131  
         }
 132  
         else
 133  
         {
 134  168
             JXPathContext context = createContext(root, key);
 135  168
             List result = context.selectNodes(key);
 136  168
             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  21
         if (parentKey == null)
 156  
         {
 157  
             // name of the root node
 158  2
             return StringUtils.EMPTY;
 159  
         }
 160  19
         else if (node.getName() == null)
 161  
         {
 162  
             // paranoia check for undefined node names
 163  1
             return parentKey;
 164  
         }
 165  
 
 166  
         else
 167  
         {
 168  18
             StringBuffer buf = new StringBuffer(parentKey.length()
 169  
                     + node.getName().length() + PATH_DELIMITER.length());
 170  18
             if (parentKey.length() > 0)
 171  
             {
 172  14
                 buf.append(parentKey);
 173  14
                 if (!node.isAttribute())
 174  
                 {
 175  13
                     buf.append(PATH_DELIMITER);
 176  
                 }
 177  
             }
 178  18
             if (node.isAttribute())
 179  
             {
 180  2
                 buf.append(ATTR_DELIMITER);
 181  
             }
 182  18
             buf.append(node.getName());
 183  18
             return buf.toString();
 184  
         }
 185  
     }
 186  
 
 187  
     /**
 188  
      * Prepares an add operation for a configuration property. The expected
 189  
      * format of the passed in key is explained in the class comment.
 190  
      *
 191  
      * @param root the configuration's root node
 192  
      * @param key the key describing the target of the add operation and the
 193  
      * path of the new node
 194  
      * @return a data object to be evaluated by the calling configuration object
 195  
      */
 196  
     public NodeAddData prepareAdd(ConfigurationNode root, String key)
 197  
     {
 198  28
         if (key == null)
 199  
         {
 200  1
             throw new IllegalArgumentException(
 201  
                     "prepareAdd: key must not be null!");
 202  
         }
 203  
 
 204  27
         int index = key.length() - 1;
 205  438
         while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
 206  
         {
 207  384
             index--;
 208  
         }
 209  27
         if (index < 0)
 210  
         {
 211  2
             throw new IllegalArgumentException(
 212  
                     "prepareAdd: Passed in key must contain a whitespace!");
 213  
         }
 214  
 
 215  25
         List nodes = query(root, key.substring(0, index).trim());
 216  25
         if (nodes.size() != 1)
 217  
         {
 218  1
             throw new IllegalArgumentException(
 219  
                     "prepareAdd: key must select exactly one target node!");
 220  
         }
 221  
 
 222  24
         NodeAddData data = new NodeAddData();
 223  24
         data.setParent((ConfigurationNode) nodes.get(0));
 224  24
         initNodeAddData(data, key.substring(index).trim());
 225  18
         return data;
 226  
     }
 227  
 
 228  
     /**
 229  
      * Creates the <code>JXPathContext</code> used for executing a query. This
 230  
      * method will create a new context and ensure that it is correctly
 231  
      * initialized.
 232  
      *
 233  
      * @param root the configuration root node
 234  
      * @param key the key to be queried
 235  
      * @return the new context
 236  
      */
 237  
     protected JXPathContext createContext(ConfigurationNode root, String key)
 238  
     {
 239  169
         JXPathContext context = JXPathContext.newContext(root);
 240  169
         context.setLenient(true);
 241  169
         return context;
 242  
     }
 243  
 
 244  
     /**
 245  
      * Initializes most properties of a <code>NodeAddData</code> object. This
 246  
      * method is called by <code>prepareAdd()</code> after the parent node has
 247  
      * been found. Its task is to interprete the passed in path of the new node.
 248  
      *
 249  
      * @param data the data object to initialize
 250  
      * @param path the path of the new node
 251  
      */
 252  
     protected void initNodeAddData(NodeAddData data, String path)
 253  
     {
 254  24
         String lastComponent = null;
 255  24
         boolean attr = false;
 256  24
         boolean first = true;
 257  
 
 258  24
         StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
 259  
                 true);
 260  122
         while (tok.hasMoreTokens())
 261  
         {
 262  79
             String token = tok.nextToken();
 263  79
             if (PATH_DELIMITER.equals(token))
 264  
             {
 265  18
                 if (attr)
 266  
                 {
 267  1
                     invalidPath(path, " contains an attribute"
 268  
                             + " delimiter at an unallowed position.");
 269  
                 }
 270  17
                 if (lastComponent == null)
 271  
                 {
 272  2
                     invalidPath(path,
 273  
                             " contains a '/' at an unallowed position.");
 274  
                 }
 275  15
                 data.addPathNode(lastComponent);
 276  15
                 lastComponent = null;
 277  
             }
 278  
 
 279  61
             else if (ATTR_DELIMITER.equals(token))
 280  
             {
 281  18
                 if (attr)
 282  
                 {
 283  1
                     invalidPath(path,
 284  
                             " contains multiple attribute delimiters.");
 285  
                 }
 286  17
                 if (lastComponent == null && !first)
 287  
                 {
 288  1
                     invalidPath(path,
 289  
                             " contains an attribute delimiter at an unallowed position.");
 290  
                 }
 291  16
                 if (lastComponent != null)
 292  
                 {
 293  8
                     data.addPathNode(lastComponent);
 294  
                 }
 295  16
                 attr = true;
 296  16
                 lastComponent = null;
 297  
             }
 298  
 
 299  
             else
 300  
             {
 301  43
                 lastComponent = token;
 302  
             }
 303  74
             first = false;
 304  
         }
 305  
 
 306  19
         if (lastComponent == null)
 307  
         {
 308  1
             invalidPath(path, "contains no components.");
 309  
         }
 310  18
         data.setNewNodeName(lastComponent);
 311  18
         data.setAttribute(attr);
 312  18
     }
 313  
 
 314  
     /**
 315  
      * Helper method for throwing an exception about an invalid path.
 316  
      *
 317  
      * @param path the invalid path
 318  
      * @param msg the exception message
 319  
      */
 320  
     private void invalidPath(String path, String msg)
 321  
     {
 322  6
         throw new IllegalArgumentException("Invalid node path: \"" + path
 323  
                 + "\" " + msg);
 324  
     }
 325  
 
 326  
     // static initializer: registers the configuration node pointer factory
 327  
     static
 328  4
     {
 329  4
         JXPathContextReferenceImpl
 330  
                 .addNodePointerFactory(new ConfigurationNodePointerFactory());
 331  
     }
 332  
 }