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://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 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                 if (!node.isAttribute())
    174                 {
    175                     buf.append(PATH_DELIMITER);
    176                 }
    177             }
    178             if (node.isAttribute())
    179             {
    180                 buf.append(ATTR_DELIMITER);
    181             }
    182             buf.append(node.getName());
    183             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         if (key == null)
    199         {
    200             throw new IllegalArgumentException(
    201                     "prepareAdd: key must not be null!");
    202         }
    203
    204         int index = key.length() - 1;
    205         while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
    206         {
    207             index--;
    208         }
    209         if (index < 0)
    210         {
    211             throw new IllegalArgumentException(
    212                     "prepareAdd: Passed in key must contain a whitespace!");
    213         }
    214
    215         List nodes = query(root, key.substring(0, index).trim());
    216         if (nodes.size() != 1)
    217         {
    218             throw new IllegalArgumentException(
    219                     "prepareAdd: key must select exactly one target node!");
    220         }
    221
    222         NodeAddData data = new NodeAddData();
    223         data.setParent((ConfigurationNode) nodes.get(0));
    224         initNodeAddData(data, key.substring(index).trim());
    225         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         JXPathContext context = JXPathContext.newContext(root);
    240         context.setLenient(true);
    241         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         String lastComponent = null;
    255         boolean attr = false;
    256         boolean first = true;
    257
    258         StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
    259                 true);
    260         while (tok.hasMoreTokens())
    261         {
    262             String token = tok.nextToken();
    263             if (PATH_DELIMITER.equals(token))
    264             {
    265                 if (attr)
    266                 {
    267                     invalidPath(path, " contains an attribute"
    268                             + " delimiter at an unallowed position.");
    269                 }
    270                 if (lastComponent == null)
    271                 {
    272                     invalidPath(path,
    273                             " contains a '/' at an unallowed position.");
    274                 }
    275                 data.addPathNode(lastComponent);
    276                 lastComponent = null;
    277             }
    278
    279             else if (ATTR_DELIMITER.equals(token))
    280             {
    281                 if (attr)
    282                 {
    283                     invalidPath(path,
    284                             " contains multiple attribute delimiters.");
    285                 }
    286                 if (lastComponent == null && !first)
    287                 {
    288                     invalidPath(path,
    289                             " contains an attribute delimiter at an unallowed position.");
    290                 }
    291                 if (lastComponent != null)
    292                 {
    293                     data.addPathNode(lastComponent);
    294                 }
    295                 attr = true;
    296                 lastComponent = null;
    297             }
    298
    299             else
    300             {
    301                 lastComponent = token;
    302             }
    303             first = false;
    304         }
    305
    306         if (lastComponent == null)
    307         {
    308             invalidPath(path, "contains no components.");
    309         }
    310         data.setNewNodeName(lastComponent);
    311         data.setAttribute(attr);
    312     }
    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         throw new IllegalArgumentException("Invalid node path: \"" + path
    323                 + "\" " + msg);
    324     }
    325
    326     // static initializer: registers the configuration node pointer factory
    327     static
    328     {
    329         JXPathContextReferenceImpl
    330                 .addNodePointerFactory(new ConfigurationNodePointerFactory());
    331     }
    332 }