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/">
038     * Commons JXPath</a> for handling XPath expressions and mapping them to the
039     * nodes of a hierarchical configuration. This makes the rich and powerful
040     * XPATH syntax 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
046     * property keys into this engine. If a key is not syntactically correct, an
047     * exception 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     *
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    }