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