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 static org.junit.Assert.assertEquals;
020    import static org.junit.Assert.assertNotNull;
021    import static org.junit.Assert.assertSame;
022    import static org.junit.Assert.assertTrue;
023    
024    import java.util.ArrayList;
025    import java.util.Iterator;
026    import java.util.List;
027    
028    import org.apache.commons.configuration.tree.ConfigurationNode;
029    import org.apache.commons.configuration.tree.DefaultConfigurationNode;
030    import org.apache.commons.configuration.tree.NodeAddData;
031    import org.apache.commons.jxpath.JXPathContext;
032    import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
033    import org.apache.commons.jxpath.ri.model.NodePointerFactory;
034    import org.junit.Before;
035    import org.junit.Test;
036    
037    /**
038     * Test class for XPathExpressionEngine.
039     *
040     * @author <a
041     * href="http://commons.apache.org/configuration/team-list.html">Commons
042     * Configuration team</a>
043     * @version $Id: TestXPathExpressionEngine.java 1226111 2011-12-31 15:44:50Z oheger $
044     */
045    public class TestXPathExpressionEngine
046    {
047        /** Constant for the test root node. */
048        static final ConfigurationNode ROOT = new DefaultConfigurationNode(
049                "testRoot");
050    
051        /** Constant for the valid test key. */
052        static final String TEST_KEY = "TESTKEY";
053    
054        /** The expression engine to be tested. */
055        XPathExpressionEngine engine;
056    
057        @Before
058        public void setUp() throws Exception
059        {
060            engine = new MockJXPathContextExpressionEngine();
061        }
062    
063        /**
064         * Tests the query() method with a normal expression.
065         */
066        @Test
067        public void testQueryExpression()
068        {
069            List<ConfigurationNode> nodes = engine.query(ROOT, TEST_KEY);
070            assertEquals("Incorrect number of results", 1, nodes.size());
071            assertSame("Wrong result node", ROOT, nodes.get(0));
072            checkSelectCalls(1);
073        }
074    
075        /**
076         * Tests a query that has no results. This should return an empty list.
077         */
078        @Test
079        public void testQueryWithoutResult()
080        {
081            List<ConfigurationNode> nodes = engine.query(ROOT, "a non existing key");
082            assertTrue("Result list is not empty", nodes.isEmpty());
083            checkSelectCalls(1);
084        }
085    
086        /**
087         * Tests a query with an empty key. This should directly return the root
088         * node without invoking the JXPathContext.
089         */
090        @Test
091        public void testQueryWithEmptyKey()
092        {
093            checkEmptyKey("");
094        }
095    
096        /**
097         * Tests a query with a null key. Same as an empty key.
098         */
099        @Test
100        public void testQueryWithNullKey()
101        {
102            checkEmptyKey(null);
103        }
104    
105        /**
106         * Helper method for testing undefined keys.
107         *
108         * @param key the key
109         */
110        private void checkEmptyKey(String key)
111        {
112            List<ConfigurationNode> nodes = engine.query(ROOT, key);
113            assertEquals("Incorrect number of results", 1, nodes.size());
114            assertSame("Wrong result node", ROOT, nodes.get(0));
115            checkSelectCalls(0);
116        }
117    
118        /**
119         * Tests if the used JXPathContext is correctly initialized.
120         */
121        @Test
122        public void testCreateContext()
123        {
124            JXPathContext ctx = new XPathExpressionEngine().createContext(ROOT,
125                    TEST_KEY);
126            assertNotNull("Context is null", ctx);
127            assertTrue("Lenient mode is not set", ctx.isLenient());
128            assertSame("Incorrect context bean set", ROOT, ctx.getContextBean());
129    
130            NodePointerFactory[] factories = JXPathContextReferenceImpl
131                    .getNodePointerFactories();
132            boolean found = false;
133            for (int i = 0; i < factories.length; i++)
134            {
135                if (factories[i] instanceof ConfigurationNodePointerFactory)
136                {
137                    found = true;
138                }
139            }
140            assertTrue("No configuration pointer factory found", found);
141        }
142    
143        /**
144         * Tests a normal call of nodeKey().
145         */
146        @Test
147        public void testNodeKeyNormal()
148        {
149            assertEquals("Wrong node key", "parent/child", engine.nodeKey(
150                    new DefaultConfigurationNode("child"), "parent"));
151        }
152    
153        /**
154         * Tests nodeKey() for an attribute node.
155         */
156        @Test
157        public void testNodeKeyAttribute()
158        {
159            ConfigurationNode node = new DefaultConfigurationNode("attr");
160            node.setAttribute(true);
161            assertEquals("Wrong attribute key", "node/@attr", engine.nodeKey(node,
162                    "node"));
163        }
164    
165        /**
166         * Tests nodeKey() for the root node.
167         */
168        @Test
169        public void testNodeKeyForRootNode()
170        {
171            assertEquals("Wrong key for root node", "", engine.nodeKey(ROOT, null));
172            assertEquals("Null name not detected", "test", engine.nodeKey(
173                    new DefaultConfigurationNode(), "test"));
174        }
175    
176        /**
177         * Tests node key() for direct children of the root node.
178         */
179        @Test
180        public void testNodeKeyForRootChild()
181        {
182            ConfigurationNode node = new DefaultConfigurationNode("child");
183            assertEquals("Wrong key for root child node", "child", engine.nodeKey(
184                    node, ""));
185            node.setAttribute(true);
186            assertEquals("Wrong key for root attribute", "@child", engine.nodeKey(
187                    node, ""));
188        }
189    
190        /**
191         * Tests adding a single child node.
192         */
193        @Test
194        public void testPrepareAddNode()
195        {
196            NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + "  newNode");
197            checkAddPath(data, new String[]
198            { "newNode" }, false);
199            checkSelectCalls(1);
200        }
201    
202        /**
203         * Tests adding a new attribute node.
204         */
205        @Test
206        public void testPrepareAddAttribute()
207        {
208            NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + "\t@newAttr");
209            checkAddPath(data, new String[]
210            { "newAttr" }, true);
211            checkSelectCalls(1);
212        }
213    
214        /**
215         * Tests adding a complete path.
216         */
217        @Test
218        public void testPrepareAddPath()
219        {
220            NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY
221                    + " \t a/full/path/node");
222            checkAddPath(data, new String[]
223            { "a", "full", "path", "node" }, false);
224            checkSelectCalls(1);
225        }
226    
227        /**
228         * Tests adding a complete path whose final node is an attribute.
229         */
230        @Test
231        public void testPrepareAddAttributePath()
232        {
233            NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY
234                    + " a/full/path@attr");
235            checkAddPath(data, new String[]
236            { "a", "full", "path", "attr" }, true);
237            checkSelectCalls(1);
238        }
239    
240        /**
241         * Tests adding a new node to the root.
242         */
243        @Test
244        public void testPrepareAddRootChild()
245        {
246            NodeAddData data = engine.prepareAdd(ROOT, " newNode");
247            checkAddPath(data, new String[]
248            { "newNode" }, false);
249            checkSelectCalls(0);
250        }
251    
252        /**
253         * Tests adding a new attribute to the root.
254         */
255        @Test
256        public void testPrepareAddRootAttribute()
257        {
258            NodeAddData data = engine.prepareAdd(ROOT, " @attr");
259            checkAddPath(data, new String[]
260            { "attr" }, true);
261            checkSelectCalls(0);
262        }
263    
264        /**
265         * Tests an add operation with a query that does not return a single node.
266         */
267        @Test(expected = IllegalArgumentException.class)
268        public void testPrepareAddInvalidParent()
269        {
270            engine.prepareAdd(ROOT, "invalidKey newNode");
271        }
272    
273        /**
274         * Tests an add operation with an empty path for the new node.
275         */
276        @Test(expected = IllegalArgumentException.class)
277        public void testPrepareAddEmptyPath()
278        {
279            engine.prepareAdd(ROOT, TEST_KEY + " ");
280        }
281    
282        /**
283         * Tests an add operation where the key is null.
284         */
285        @Test(expected = IllegalArgumentException.class)
286        public void testPrepareAddNullKey()
287        {
288            engine.prepareAdd(ROOT, null);
289        }
290    
291        /**
292         * Tests an add operation where the key is null.
293         */
294        @Test(expected = IllegalArgumentException.class)
295        public void testPrepareAddEmptyKey()
296        {
297            engine.prepareAdd(ROOT, "");
298        }
299    
300        /**
301         * Tests an add operation with an invalid path.
302         */
303        @Test(expected = IllegalArgumentException.class)
304        public void testPrepareAddInvalidPath()
305        {
306            engine.prepareAdd(ROOT, TEST_KEY + " an/invalid//path");
307        }
308    
309        /**
310         * Tests an add operation with an invalid path: the path contains an
311         * attribute in the middle part.
312         */
313        @Test(expected = IllegalArgumentException.class)
314        public void testPrepareAddInvalidAttributePath()
315        {
316            engine.prepareAdd(ROOT, TEST_KEY + " a/path/with@an/attribute");
317        }
318    
319        /**
320         * Tests an add operation with an invalid path: the path contains an
321         * attribute after a slash.
322         */
323        @Test(expected = IllegalArgumentException.class)
324        public void testPrepareAddInvalidAttributePath2()
325        {
326            engine.prepareAdd(ROOT, TEST_KEY + " a/path/with/@attribute");
327        }
328    
329        /**
330         * Tests an add operation with an invalid path that starts with a slash.
331         */
332        @Test(expected = IllegalArgumentException.class)
333        public void testPrepareAddInvalidPathWithSlash()
334        {
335            engine.prepareAdd(ROOT, TEST_KEY + " /a/path/node");
336        }
337    
338        /**
339         * Tests an add operation with an invalid path that contains multiple
340         * attribute components.
341         */
342        @Test(expected = IllegalArgumentException.class)
343        public void testPrepareAddInvalidPathMultipleAttributes()
344        {
345            engine.prepareAdd(ROOT, TEST_KEY + " an@attribute@path");
346        }
347    
348        /**
349         * Helper method for testing the path nodes in the given add data object.
350         *
351         * @param data the data object to check
352         * @param expected an array with the expected path elements
353         * @param attr a flag if the new node is an attribute
354         */
355        private void checkAddPath(NodeAddData data, String[] expected, boolean attr)
356        {
357            assertSame("Wrong parent node", ROOT, data.getParent());
358            List<String> path = data.getPathNodes();
359            assertEquals("Incorrect number of path nodes", expected.length - 1,
360                    path.size());
361            Iterator<String> it = path.iterator();
362            for (int idx = 0; idx < expected.length - 1; idx++)
363            {
364                assertEquals("Wrong node at position " + idx, expected[idx], it
365                        .next());
366            }
367            assertEquals("Wrong name of new node", expected[expected.length - 1],
368                    data.getNewNodeName());
369            assertEquals("Incorrect attribute flag", attr, data.isAttribute());
370        }
371    
372        /**
373         * Checks if the JXPath context's selectNodes() method was called as often
374         * as expected.
375         *
376         * @param expected the number of expected calls
377         */
378        protected void checkSelectCalls(int expected)
379        {
380            MockJXPathContext ctx = ((MockJXPathContextExpressionEngine) engine).getContext();
381            int calls = (ctx == null) ? 0 : ctx.selectInvocations;
382            assertEquals("Incorrect number of select calls", expected, calls);
383        }
384    
385        /**
386         * A mock implementation of the JXPathContext class. This implementation
387         * will overwrite the <code>selectNodes()</code> method that is used by
388         * <code>XPathExpressionEngine</code> to count the invocations of this
389         * method.
390         */
391        static class MockJXPathContext extends JXPathContextReferenceImpl
392        {
393            int selectInvocations;
394    
395            public MockJXPathContext(Object bean)
396            {
397                super(null, bean);
398            }
399    
400            /**
401             * Dummy implementation of this method. If the passed in string is the
402             * test key, the root node will be returned in the list. Otherwise the
403             * return value is <b>null</b>.
404             */
405            @Override
406            public List<?> selectNodes(String xpath)
407            {
408                selectInvocations++;
409                if (TEST_KEY.equals(xpath))
410                {
411                    List<ConfigurationNode> result = new ArrayList<ConfigurationNode>(1);
412                    result.add(ROOT);
413                    return result;
414                }
415                else
416                {
417                    return null;
418                }
419            }
420        }
421    
422        /**
423         * A special implementation of XPathExpressionEngine that overrides
424         * createContext() to return a mock context object.
425         */
426        static class MockJXPathContextExpressionEngine extends
427                XPathExpressionEngine
428        {
429            /** Stores the context instance. */
430            private MockJXPathContext context;
431    
432            @Override
433            protected JXPathContext createContext(ConfigurationNode root, String key)
434            {
435                context = new MockJXPathContext(root);
436                return context;
437            }
438    
439            /**
440             * Returns the context created by the last newContext() call.
441             *
442             * @return the current context
443             */
444            public MockJXPathContext getContext()
445            {
446                return context;
447            }
448        }
449    }