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 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 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         if (StringUtils.isEmpty(key))
    142         {
    143             List result = new ArrayList(1);
    144             result.add(root);
    145             return result;
    146         }
    147         else
    148         {
    149             JXPathContext context = createContext(root, key);
    150             List result = context.selectNodes(key);
    151             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         if (parentKey == null)
    171         {
    172             // name of the root node
    173             return StringUtils.EMPTY;
    174         }
    175         else if (node.getName() == null)
    176         {
    177             // paranoia check for undefined node names
    178             return parentKey;
    179         }
    180
    181         else
    182         {
    183             StringBuffer buf = new StringBuffer(parentKey.length()
    184                     + node.getName().length() + PATH_DELIMITER.length());
    185             if (parentKey.length() > 0)
    186             {
    187                 buf.append(parentKey);
    188                 buf.append(PATH_DELIMITER);
    189             }
    190             if (node.isAttribute())
    191             {
    192                 buf.append(ATTR_DELIMITER);
    193             }
    194             buf.append(node.getName());
    195             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         if (key == null)
    211         {
    212             throw new IllegalArgumentException(
    213                     "prepareAdd: key must not be null!");
    214         }
    215
    216         int index = key.length() - 1;
    217         while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
    218         {
    219             index--;
    220         }
    221         if (index < 0)
    222         {
    223             throw new IllegalArgumentException(
    224                     "prepareAdd: Passed in key must contain a whitespace!");
    225         }
    226
    227         List nodes = query(root, key.substring(0, index).trim());
    228         if (nodes.size() != 1)
    229         {
    230             throw new IllegalArgumentException(
    231                     "prepareAdd: key must select exactly one target node!");
    232         }
    233
    234         NodeAddData data = new NodeAddData();
    235         data.setParent((ConfigurationNode) nodes.get(0));
    236         initNodeAddData(data, key.substring(index).trim());
    237         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         JXPathContext context = JXPathContext.newContext(root);
    252         context.setLenient(true);
    253         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         String lastComponent = null;
    267         boolean attr = false;
    268         boolean first = true;
    269
    270         StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
    271                 true);
    272         while (tok.hasMoreTokens())
    273         {
    274             String token = tok.nextToken();
    275             if (PATH_DELIMITER.equals(token))
    276             {
    277                 if (attr)
    278                 {
    279                     invalidPath(path, " contains an attribute"
    280                             + " delimiter at an unallowed position.");
    281                 }
    282                 if (lastComponent == null)
    283                 {
    284                     invalidPath(path,
    285                             " contains a '/' at an unallowed position.");
    286                 }
    287                 data.addPathNode(lastComponent);
    288                 lastComponent = null;
    289             }
    290
    291             else if (ATTR_DELIMITER.equals(token))
    292             {
    293                 if (attr)
    294                 {
    295                     invalidPath(path,
    296                             " contains multiple attribute delimiters.");
    297                 }
    298                 if (lastComponent == null && !first)
    299                 {
    300                     invalidPath(path,
    301                             " contains an attribute delimiter at an unallowed position.");
    302                 }
    303                 if (lastComponent != null)
    304                 {
    305                     data.addPathNode(lastComponent);
    306                 }
    307                 attr = true;
    308                 lastComponent = null;
    309             }
    310
    311             else
    312             {
    313                 lastComponent = token;
    314             }
    315             first = false;
    316         }
    317
    318         if (lastComponent == null)
    319         {
    320             invalidPath(path, "contains no components.");
    321         }
    322         data.setNewNodeName(lastComponent);
    323         data.setAttribute(attr);
    324     }
    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         throw new IllegalArgumentException("Invalid node path: \"" + path
    335                 + "\" " + msg);
    336     }
    337
    338     // static initializer: registers the configuration node pointer factory
    339     static
    340     {
    341         JXPathContextReferenceImpl
    342                 .addNodePointerFactory(new ConfigurationNodePointerFactory());
    343     }
    344 }