View Javadoc

    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 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         if (StringUtils.isEmpty(key))
    127         {
    128             List result = new ArrayList(1);
    129             result.add(root);
    130             return result;
    131         }
    132         else
    133         {
    134             JXPathContext context = createContext(root, key);
    135             List result = context.selectNodes(key);
    136             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         if (parentKey == null)
    156         {
    157             // name of the root node
    158             return StringUtils.EMPTY;
    159         }
    160         else if (node.getName() == null)
    161         {
    162             // paranoia check for undefined node names
    163             return parentKey;
    164         }
    165
    166         else
    167         {
    168             StringBuffer buf = new StringBuffer(parentKey.length()
    169                     + node.getName().length() + PATH_DELIMITER.length());
    170             if (parentKey.length() > 0)
    171             {
    172                 buf.append(parentKey);
    173                 buf.append(PATH_DELIMITER);
    174             }
    175             if (node.isAttribute())
    176             {
    177                 buf.append(ATTR_DELIMITER);
    178             }
    179             buf.append(node.getName());
    180             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         if (key == null)
    196         {
    197             throw new IllegalArgumentException(
    198                     "prepareAdd: key must not be null!");
    199         }
    200
    201         int index = key.length() - 1;
    202         while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
    203         {
    204             index--;
    205         }
    206         if (index < 0)
    207         {
    208             throw new IllegalArgumentException(
    209                     "prepareAdd: Passed in key must contain a whitespace!");
    210         }
    211
    212         List nodes = query(root, key.substring(0, index).trim());
    213         if (nodes.size() != 1)
    214         {
    215             throw new IllegalArgumentException(
    216                     "prepareAdd: key must select exactly one target node!");
    217         }
    218
    219         NodeAddData data = new NodeAddData();
    220         data.setParent((ConfigurationNode) nodes.get(0));
    221         initNodeAddData(data, key.substring(index).trim());
    222         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         JXPathContext context = JXPathContext.newContext(root);
    237         context.setLenient(true);
    238         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         String lastComponent = null;
    252         boolean attr = false;
    253         boolean first = true;
    254
    255         StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
    256                 true);
    257         while (tok.hasMoreTokens())
    258         {
    259             String token = tok.nextToken();
    260             if (PATH_DELIMITER.equals(token))
    261             {
    262                 if (attr)
    263                 {
    264                     invalidPath(path, " contains an attribute"
    265                             + " delimiter at an unallowed position.");
    266                 }
    267                 if (lastComponent == null)
    268                 {
    269                     invalidPath(path,
    270                             " contains a '/' at an unallowed position.");
    271                 }
    272                 data.addPathNode(lastComponent);
    273                 lastComponent = null;
    274             }
    275
    276             else if (ATTR_DELIMITER.equals(token))
    277             {
    278                 if (attr)
    279                 {
    280                     invalidPath(path,
    281                             " contains multiple attribute delimiters.");
    282                 }
    283                 if (lastComponent == null && !first)
    284                 {
    285                     invalidPath(path,
    286                             " contains an attribute delimiter at an unallowed position.");
    287                 }
    288                 if (lastComponent != null)
    289                 {
    290                     data.addPathNode(lastComponent);
    291                 }
    292                 attr = true;
    293                 lastComponent = null;
    294             }
    295
    296             else
    297             {
    298                 lastComponent = token;
    299             }
    300             first = false;
    301         }
    302
    303         if (lastComponent == null)
    304         {
    305             invalidPath(path, "contains no components.");
    306         }
    307         data.setNewNodeName(lastComponent);
    308         data.setAttribute(attr);
    309     }
    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         throw new IllegalArgumentException("Invalid node path: \"" + path
    320                 + "\" " + msg);
    321     }
    322
    323     // static initializer: registers the configuration node pointer factory
    324     static
    325     {
    326         JXPathContextReferenceImpl
    327                 .addNodePointerFactory(new ConfigurationNodePointerFactory());
    328     }
    329 }