    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements.  See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache License, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License.  You may obtain a copy of the License at
    008     *
    009     *     http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the License for the specific language governing permissions and
    015     * limitations under the License.
    016     */
    017    package org.apache.commons.configuration.tree.xpath;
    018
    019    import java.util.ArrayList;
    020    import java.util.Collections;
    021    import java.util.List;
    022    import java.util.StringTokenizer;
    023
    024    import org.apache.commons.configuration.tree.ConfigurationNode;
    025    import org.apache.commons.configuration.tree.ExpressionEngine;
    026    import org.apache.commons.configuration.tree.NodeAddData;
    027    import org.apache.commons.jxpath.JXPathContext;
    028    import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
    029    import org.apache.commons.lang.StringUtils;
    030
    031    /**
    032     * <p>
    033     * A specialized implementation of the <code>ExpressionEngine</code> interface
    034     * that is able to evaluate XPATH expressions.
    035     * </p>
    036     * <p>
    037     * This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons
    038     * JXPath</a> for handling XPath expressions and mapping them to the nodes of a
    039     * hierarchical configuration. This makes the rich and powerful XPATH syntax
    040     * available for accessing properties from a configuration object.
    041     * </p>
    042     * <p>
    043     * For selecting properties arbitrary XPATH expressions can be used, which
    044     * select single or multiple configuration nodes. The associated
    045     * <code>Configuration</code> instance will directly pass the specified property
    046     * keys into this engine. If a key is not syntactically correct, an exception
    047     * will be thrown.
    048     * </p>
    049     * <p>
    050     * For adding new properties, this expression engine uses a specific syntax: the
    051     * &quot;key&quot; of a new property must consist of two parts that are
    052     * separated by whitespace:
    053     * <ol>
    054     * <li>An XPATH expression selecting a single node, to which the new element(s)
    055     * are to be added. This can be an arbitrary complex expression, but it must
    056     * select exactly one node, otherwise an exception will be thrown.</li>
    057     * <li>The name of the new element(s) to be added below this parent node. Here
    058     * either a single node name or a complete path of nodes (separated by the
    059     * &quot;/&quot; character or &quot;@&quot; for an attribute) can be specified.</li>
    060     * </ol>
    061     * Some examples for valid keys that can be passed into the configuration's
    062     * <code>addProperty()</code> method follow:
    063     * </p>
    064     * <p>
    065     *
    066     * <pre>
    067     * &quot;/tables/table[1] type&quot;
    068     * </pre>
    069     *
    070     * </p>
    071     * <p>
    072     * This will add a new <code>type</code> node as a child of the first
    073     * <code>table</code> element.
    074     * </p>
    075     * <p>
    076     *
    077     * <pre>
    078     * &quot;/tables/table[1] @type&quot;
    079     * </pre>
    080     *
    081     * </p>
    082     * <p>
    083     * Similar to the example above, but this time a new attribute named
    084     * <code>type</code> will be added to the first <code>table</code> element.
    085     * </p>
    086     * <p>
    087     *
    088     * <pre>
    089     * &quot;/tables table/fields/field/name&quot;
    090     * </pre>
    091     *
    092     * </p>
    093     * <p>
    094     * This example shows how a complex path can be added. Parent node is the
    095     * <code>tables</code> element. Here a new branch consisting of the nodes
    096     * <code>table</code>, <code>fields</code>, <code>field</code>, and
    097     * <code>name</code> will be added.
    098     * </p>
    099     * <p>
    100     *
    101     * <pre>
    102     * &quot;/tables table/fields/field@type&quot;
    103     * </pre>
    104     *
    105     * </p>
    106     * <p>
    107     * This is similar to the last example, but in this case a complex path ending
    108     * with an attribute is defined.
    109     * </p>
    110     * <p>
    111     * <strong>Note:</strong> This extended syntax for adding properties only works
    112     * with the <code>addProperty()</code> method. <code>setProperty()</code> does
    113     * not support creating new nodes this way.
    114     * </p>
    115     * <p>
    116     * From version 1.7 on, it is possible to use regular keys in calls to
    117     * <code>addProperty()</code> (i.e. keys that do not have to contain a
    118     * whitespace as delimiter). In this case the key is evaluated, and the biggest
    119     * part pointing to an existing node is determined. The remaining part is then
    120     * added as new path. As an example consider the key
    121     *
    122     * <pre>
    123     * &quot;tables/table[last()]/fields/field/name&quot;
    124     * </pre>
    125     *
    126     * If the key does not point to an existing node, the engine will check the
    127     * paths <code>&quot;tables/table[last()]/fields/field&quot;</code>,
    128     * <code>&quot;tables/table[last()]/fields&quot;</code>,
    129     * <code>&quot;tables/table[last()]&quot;</code>, and so on, until a key is
    130     * found which points to a node. Let's assume that the last key listed above can
    131     * be resolved in this way. Then from this key the following key is derived:
    132     * <code>&quot;tables/table[last()] fields/field/name&quot;</code> by appending
    133     * the remaining part after a whitespace. This key can now be processed using
    134     * the original algorithm. Keys of this form can also be used with the
    135     * <code>setProperty()</code> method. However, it is still recommended to use
    136     * the old format because it makes explicit at which position new nodes should
    137     * be added. For keys without a whitespace delimiter there may be ambiguities.
    138     * </p>
    139     *
    140     * @since 1.3
    141     * @author <a
    142     *         href="http://commons.apache.org/configuration/team-list.html">Commons
    143     *         Configuration team</a>
    144     * @version $Id: XPathExpressionEngine.java 1152357 2011-07-29 19:56:01Z oheger $
    145     */
    146    public class XPathExpressionEngine implements ExpressionEngine
    147    {
    148        /** Constant for the path delimiter. */
    149        static final String PATH_DELIMITER = "/";
    150
    151        /** Constant for the attribute delimiter. */
    152        static final String ATTR_DELIMITER = "@";
    153
    154        /** Constant for the delimiters for splitting node paths. */
    155        private static final String NODE_PATH_DELIMITERS = PATH_DELIMITER
    156                + ATTR_DELIMITER;
    157
    158        /**
    159         * Constant for a space which is used as delimiter in keys for adding
    160         * properties.
    161         */
    162        private static final String SPACE = " ";
    163
    164        /**
    165         * Executes a query. The passed in property key is directly passed to a
    166         * JXPath context.
    167         *
    168         * @param root the configuration root node
    169         * @param key the query to be executed
    170         * @return a list with the nodes that are selected by the query
    171         */
    172        public List query(ConfigurationNode root, String key)
    173        {
    174            if (StringUtils.isEmpty(key))
    175            {
    176                List result = new ArrayList(1);
    177                result.add(root);
    178                return result;
    179            }
    180            else
    181            {
    182                JXPathContext context = createContext(root, key);
    183                List result = context.selectNodes(key);
    184                return (result != null) ? result : Collections.EMPTY_LIST;
    185            }
    186        }
    187
    188        /**
    189         * Returns a (canonical) key for the given node based on the parent's key.
    190         * This implementation will create an XPATH expression that selects the
    191         * given node (under the assumption that the passed in parent key is valid).
    192         * As the <code>nodeKey()</code> implementation of
    193         * <code>{@link org.apache.commons.configuration.tree.DefaultExpressionEngine DefaultExpressionEngine}</code>
    194         * this method will not return indices for nodes. So all child nodes of a
    195         * given parent with the same name will have the same key.
    196         *
    197         * @param node the node for which a key is to be constructed
    198         * @param parentKey the key of the parent node
    199         * @return the key for the given node
    200         */
    201        public String nodeKey(ConfigurationNode node, String parentKey)
    202        {
    203            if (parentKey == null)
    204            {
    205                // name of the root node
    206                return StringUtils.EMPTY;
    207            }
    208            else if (node.getName() == null)
    209            {
    210                // paranoia check for undefined node names
    211                return parentKey;
    212            }
    213
    214            else
    215            {
    216                StringBuffer buf = new StringBuffer(parentKey.length()
    217                        + node.getName().length() + PATH_DELIMITER.length());
    218                if (parentKey.length() > 0)
    219                {
    220                    buf.append(parentKey);
    221                    buf.append(PATH_DELIMITER);
    222                }
    223                if (node.isAttribute())
    224                {
    225                    buf.append(ATTR_DELIMITER);
    226                }
    227                buf.append(node.getName());
    228                return buf.toString();
    229            }
    230        }
    231
    232        /**
    233         * Prepares an add operation for a configuration property. The expected
    234         * format of the passed in key is explained in the class comment.
    235         *
    236         * @param root the configuration's root node
    237         * @param key the key describing the target of the add operation and the
    238         * path of the new node
    239         * @return a data object to be evaluated by the calling configuration object
    240         */
    241        public NodeAddData prepareAdd(ConfigurationNode root, String key)
    242        {
    243            if (key == null)
    244            {
    245                throw new IllegalArgumentException(
    246                        "prepareAdd: key must not be null!");
    247            }
    248
    249            String addKey = key;
    250            int index = findKeySeparator(addKey);
    251            if (index < 0)
    252            {
    253                addKey = generateKeyForAdd(root, addKey);
    254                index = findKeySeparator(addKey);
    255            }
    256
    257            List nodes = query(root, addKey.substring(0, index).trim());
    258            if (nodes.size() != 1)
    259            {
    260                throw new IllegalArgumentException(
    261                        "prepareAdd: key must select exactly one target node!");
    262            }
    263
    264            NodeAddData data = new NodeAddData();
    265            data.setParent((ConfigurationNode) nodes.get(0));
    266            initNodeAddData(data, addKey.substring(index).trim());
    267            return data;
    268        }
    269
    270        /**
    271         * Creates the <code>JXPathContext</code> used for executing a query. This
    272         * method will create a new context and ensure that it is correctly
    273         * initialized.
    274         *
    275         * @param root the configuration root node
    276         * @param key the key to be queried
    277         * @return the new context
    278         */
    279        protected JXPathContext createContext(ConfigurationNode root, String key)
    280        {
    281            JXPathContext context = JXPathContext.newContext(root);
    282            context.setLenient(true);
    283            return context;
    284        }
    285
    286        /**
    287         * Initializes most properties of a <code>NodeAddData</code> object. This
    288         * method is called by <code>prepareAdd()</code> after the parent node has
    289         * been found. Its task is to interpret the passed in path of the new node.
    290         *
    291         * @param data the data object to initialize
    292         * @param path the path of the new node
    293         */
    294        protected void initNodeAddData(NodeAddData data, String path)
    295        {
    296            String lastComponent = null;
    297            boolean attr = false;
    298            boolean first = true;
    299
    300            StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
    301                    true);
    302            while (tok.hasMoreTokens())
    303            {
    304                String token = tok.nextToken();
    305                if (PATH_DELIMITER.equals(token))
    306                {
    307                    if (attr)
    308                    {
    309                        invalidPath(path, " contains an attribute"
    310                                + " delimiter at an unallowed position.");
    311                    }
    312                    if (lastComponent == null)
    313                    {
    314                        invalidPath(path,
    315                                " contains a '/' at an unallowed position.");
    316                    }
    317                    data.addPathNode(lastComponent);
    318                    lastComponent = null;
    319                }
    320
    321                else if (ATTR_DELIMITER.equals(token))
    322                {
    323                    if (attr)
    324                    {
    325                        invalidPath(path,
    326                                " contains multiple attribute delimiters.");
    327                    }
    328                    if (lastComponent == null && !first)
    329                    {
    330                        invalidPath(path,
    331                                " contains an attribute delimiter at an unallowed position.");
    332                    }
    333                    if (lastComponent != null)
    334                    {
    335                        data.addPathNode(lastComponent);
    336                    }
    337                    attr = true;
    338                    lastComponent = null;
    339                }
    340
    341                else
    342                {
    343                    lastComponent = token;
    344                }
    345                first = false;
    346            }
    347
    348            if (lastComponent == null)
    349            {
    350                invalidPath(path, "contains no components.");
    351            }
    352            data.setNewNodeName(lastComponent);
    353            data.setAttribute(attr);
    354        }
    355
    356        /**
    357         * Tries to generate a key for adding a property. This method is called if a
    358         * key was used for adding properties which does not contain a space
    359         * character. It splits the key at its single components and searches for
    360         * the last existing component. Then a key compatible for adding properties
    361         * is generated.
    362         *
    363         * @param root the root node of the configuration
    364         * @param key the key in question
    365         * @return the key to be used for adding the property
    366         */
    367        private String generateKeyForAdd(ConfigurationNode root, String key)
    368        {
    369            int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
    370
    371            while (pos >= 0)
    372            {
    373                String keyExisting = key.substring(0, pos);
    374                if (!query(root, keyExisting).isEmpty())
    375                {
    376                    StringBuffer buf = new StringBuffer(key.length() + 1);
    377                    buf.append(keyExisting).append(SPACE);
    378                    buf.append(key.substring(pos + 1));
    379                    return buf.toString();
    380                }
    381                pos = key.lastIndexOf(PATH_DELIMITER, pos - 1);
    382            }
    383
    384            return SPACE + key;
    385        }
    386
    387        /**
    388         * Helper method for throwing an exception about an invalid path.
    389         *
    390         * @param path the invalid path
    391         * @param msg the exception message
    392         */
    393        private void invalidPath(String path, String msg)
    394        {
    395            throw new IllegalArgumentException("Invalid node path: \"" + path
    396                    + "\" " + msg);
    397        }
    398
    399        /**
    400         * Determines the position of the separator in a key for adding new
    401         * properties. If no delimiter is found, result is -1.
    402         *
    403         * @param key the key
    404         * @return the position of the delimiter
    405         */
    406        private static int findKeySeparator(String key)
    407        {
    408            int index = key.length() - 1;
    409            while (index >= 0 && !Character.isWhitespace(key.charAt(index)))
    410            {
    411                index--;
    412            }
    413            return index;
    414        }
    415
    416        // static initializer: registers the configuration node pointer factory
    417        static
    418        {
    419            JXPathContextReferenceImpl
    420                    .addNodePointerFactory(new ConfigurationNodePointerFactory());
    421        }
    422    }