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.Iterator;
21  import java.util.List;
22  
23  import org.apache.commons.configuration.tree.ConfigurationNode;
24  import org.apache.commons.configuration.tree.DefaultConfigurationNode;
25  import org.apache.commons.configuration.tree.NodeAddData;
26  import org.apache.commons.jxpath.JXPathContext;
27  import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
28  import org.apache.commons.jxpath.ri.model.NodePointerFactory;
29  
30  import junit.framework.TestCase;
31  
32  /***
33   * Test class for XPathExpressionEngine.
34   *
35   * @author Oliver Heger
36   * @version $Id: TestXPathExpressionEngine.java 502705 2007-02-02 19:55:37Z oheger $
37   */
38  public class TestXPathExpressionEngine extends TestCase
39  {
40      /*** Constant for the test root node. */
41      static final ConfigurationNode ROOT = new DefaultConfigurationNode(
42              "testRoot");
43  
44      /*** Constant for the valid test key. */
45      static final String TEST_KEY = "TESTKEY";
46  
47      /*** The expression engine to be tested. */
48      XPathExpressionEngine engine;
49  
50      protected void setUp() throws Exception
51      {
52          super.setUp();
53          engine = new MockJXPathContextExpressionEngine();
54      }
55  
56      /***
57       * Tests the query() method with a normal expression.
58       */
59      public void testQueryExpression()
60      {
61          List nodes = engine.query(ROOT, TEST_KEY);
62          assertEquals("Incorrect number of results", 1, nodes.size());
63          assertSame("Wrong result node", ROOT, nodes.get(0));
64          checkSelectCalls(1);
65      }
66  
67      /***
68       * Tests a query that has no results. This should return an empty list.
69       */
70      public void testQueryWithoutResult()
71      {
72          List nodes = engine.query(ROOT, "a non existing key");
73          assertTrue("Result list is not empty", nodes.isEmpty());
74          checkSelectCalls(1);
75      }
76  
77      /***
78       * Tests a query with an empty key. This should directly return the root
79       * node without invoking the JXPathContext.
80       */
81      public void testQueryWithEmptyKey()
82      {
83          checkEmptyKey("");
84      }
85  
86      /***
87       * Tests a query with a null key. Same as an empty key.
88       */
89      public void testQueryWithNullKey()
90      {
91          checkEmptyKey(null);
92      }
93  
94      /***
95       * Helper method for testing undefined keys.
96       *
97       * @param key the key
98       */
99      private void checkEmptyKey(String key)
100     {
101         List nodes = engine.query(ROOT, key);
102         assertEquals("Incorrect number of results", 1, nodes.size());
103         assertSame("Wrong result node", ROOT, nodes.get(0));
104         checkSelectCalls(0);
105     }
106 
107     /***
108      * Tests if the used JXPathContext is correctly initialized.
109      */
110     public void testCreateContext()
111     {
112         JXPathContext ctx = new XPathExpressionEngine().createContext(ROOT,
113                 TEST_KEY);
114         assertNotNull("Context is null", ctx);
115         assertTrue("Lenient mode is not set", ctx.isLenient());
116         assertSame("Incorrect context bean set", ROOT, ctx.getContextBean());
117 
118         NodePointerFactory[] factories = JXPathContextReferenceImpl
119                 .getNodePointerFactories();
120         boolean found = false;
121         for (int i = 0; i < factories.length; i++)
122         {
123             if (factories[i] instanceof ConfigurationNodePointerFactory)
124             {
125                 found = true;
126             }
127         }
128         assertTrue("No configuration pointer factory found", found);
129     }
130 
131     /***
132      * Tests a normal call of nodeKey().
133      */
134     public void testNodeKeyNormal()
135     {
136         assertEquals("Wrong node key", "parent/child", engine.nodeKey(
137                 new DefaultConfigurationNode("child"), "parent"));
138     }
139 
140     /***
141      * Tests nodeKey() for an attribute node.
142      */
143     public void testNodeKeyAttribute()
144     {
145         ConfigurationNode node = new DefaultConfigurationNode("attr");
146         node.setAttribute(true);
147         assertEquals("Wrong attribute key", "node/@attr", engine.nodeKey(node,
148                 "node"));
149     }
150 
151     /***
152      * Tests nodeKey() for the root node.
153      */
154     public void testNodeKeyForRootNode()
155     {
156         assertEquals("Wrong key for root node", "", engine.nodeKey(ROOT, null));
157         assertEquals("Null name not detected", "test", engine.nodeKey(
158                 new DefaultConfigurationNode(), "test"));
159     }
160 
161     /***
162      * Tests node key() for direct children of the root node.
163      */
164     public void testNodeKeyForRootChild()
165     {
166         ConfigurationNode node = new DefaultConfigurationNode("child");
167         assertEquals("Wrong key for root child node", "child", engine.nodeKey(
168                 node, ""));
169         node.setAttribute(true);
170         assertEquals("Wrong key for root attribute", "@child", engine.nodeKey(
171                 node, ""));
172     }
173 
174     /***
175      * Tests adding a single child node.
176      */
177     public void testPrepareAddNode()
178     {
179         NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + "  newNode");
180         checkAddPath(data, new String[]
181         { "newNode" }, false);
182         checkSelectCalls(1);
183     }
184 
185     /***
186      * Tests adding a new attribute node.
187      */
188     public void testPrepareAddAttribute()
189     {
190         NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + "\t@newAttr");
191         checkAddPath(data, new String[]
192         { "newAttr" }, true);
193         checkSelectCalls(1);
194     }
195 
196     /***
197      * Tests adding a complete path.
198      */
199     public void testPrepareAddPath()
200     {
201         NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY
202                 + " \t a/full/path/node");
203         checkAddPath(data, new String[]
204         { "a", "full", "path", "node" }, false);
205         checkSelectCalls(1);
206     }
207 
208     /***
209      * Tests adding a complete path whose final node is an attribute.
210      */
211     public void testPrepareAddAttributePath()
212     {
213         NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY
214                 + " a/full/path@attr");
215         checkAddPath(data, new String[]
216         { "a", "full", "path", "attr" }, true);
217         checkSelectCalls(1);
218     }
219 
220     /***
221      * Tests adding a new node to the root.
222      */
223     public void testPrepareAddRootChild()
224     {
225         NodeAddData data = engine.prepareAdd(ROOT, " newNode");
226         checkAddPath(data, new String[]
227         { "newNode" }, false);
228         checkSelectCalls(0);
229     }
230 
231     /***
232      * Tests adding a new attribute to the root.
233      */
234     public void testPrepareAddRootAttribute()
235     {
236         NodeAddData data = engine.prepareAdd(ROOT, " @attr");
237         checkAddPath(data, new String[]
238         { "attr" }, true);
239         checkSelectCalls(0);
240     }
241 
242     /***
243      * Tests an add operation with a query that does not return a single node.
244      */
245     public void testPrepareAddInvalidParent()
246     {
247         try
248         {
249             engine.prepareAdd(ROOT, "invalidKey newNode");
250             fail("Could add to invalid parent!");
251         }
252         catch (IllegalArgumentException iex)
253         {
254             // ok
255         }
256     }
257 
258     /***
259      * Tests an add operation where the passed in key has an invalid format: it
260      * does not contain a whitspace. This will cause an error.
261      */
262     public void testPrepareAddInvalidFormat()
263     {
264         try
265         {
266             engine.prepareAdd(ROOT, "anInvalidKey");
267             fail("Could add an invalid key!");
268         }
269         catch (IllegalArgumentException iex)
270         {
271             // ok
272         }
273     }
274 
275     /***
276      * Tests an add operation with an empty path for the new node.
277      */
278     public void testPrepareAddEmptyPath()
279     {
280         try
281         {
282             engine.prepareAdd(ROOT, TEST_KEY + " ");
283             fail("Could add empty path!");
284         }
285         catch (IllegalArgumentException iex)
286         {
287             // ok
288         }
289     }
290 
291     /***
292      * Tests an add operation where the key is null.
293      */
294     public void testPrepareAddNullKey()
295     {
296         try
297         {
298             engine.prepareAdd(ROOT, null);
299             fail("Could add null path!");
300         }
301         catch (IllegalArgumentException iex)
302         {
303             // ok
304         }
305     }
306 
307     /***
308      * Tests an add operation where the key is null.
309      */
310     public void testPrepareAddEmptyKey()
311     {
312         try
313         {
314             engine.prepareAdd(ROOT, "");
315             fail("Could add empty path!");
316         }
317         catch (IllegalArgumentException iex)
318         {
319             // ok
320         }
321     }
322 
323     /***
324      * Tests an add operation with an invalid path.
325      */
326     public void testPrepareAddInvalidPath()
327     {
328         try
329         {
330             engine.prepareAdd(ROOT, TEST_KEY + " an/invalid//path");
331             fail("Could add invalid path!");
332         }
333         catch (IllegalArgumentException iex)
334         {
335             // ok
336         }
337     }
338 
339     /***
340      * Tests an add operation with an invalid path: the path contains an
341      * attribute in the middle part.
342      */
343     public void testPrepareAddInvalidAttributePath()
344     {
345         try
346         {
347             engine.prepareAdd(ROOT, TEST_KEY + " a/path/with@an/attribute");
348             fail("Could add invalid attribute path!");
349         }
350         catch (IllegalArgumentException iex)
351         {
352             // ok
353         }
354     }
355 
356     /***
357      * Tests an add operation with an invalid path: the path contains an
358      * attribute after a slash.
359      */
360     public void testPrepareAddInvalidAttributePath2()
361     {
362         try
363         {
364             engine.prepareAdd(ROOT, TEST_KEY + " a/path/with/@attribute");
365             fail("Could add invalid attribute path!");
366         }
367         catch (IllegalArgumentException iex)
368         {
369             // ok
370         }
371     }
372 
373     /***
374      * Tests an add operation with an invalid path that starts with a slash.
375      */
376     public void testPrepareAddInvalidPathWithSlash()
377     {
378         try
379         {
380             engine.prepareAdd(ROOT, TEST_KEY + " /a/path/node");
381             fail("Could add path starting with a slash!");
382         }
383         catch (IllegalArgumentException iex)
384         {
385             // ok
386         }
387     }
388 
389     /***
390      * Tests an add operation with an invalid path that contains multiple
391      * attribute components.
392      */
393     public void testPrepareAddInvalidPathMultipleAttributes()
394     {
395         try
396         {
397             engine.prepareAdd(ROOT, TEST_KEY + " an@attribute@path");
398             fail("Could add path with multiple attributes!");
399         }
400         catch (IllegalArgumentException iex)
401         {
402             // ok
403         }
404     }
405 
406     /***
407      * Helper method for testing the path nodes in the given add data object.
408      *
409      * @param data the data object to check
410      * @param expected an array with the expected path elements
411      * @param attr a flag if the new node is an attribute
412      */
413     private void checkAddPath(NodeAddData data, String[] expected, boolean attr)
414     {
415         assertSame("Wrong parent node", ROOT, data.getParent());
416         List path = data.getPathNodes();
417         assertEquals("Incorrect number of path nodes", expected.length - 1,
418                 path.size());
419         Iterator it = path.iterator();
420         for (int idx = 0; idx < expected.length - 1; idx++)
421         {
422             assertEquals("Wrong node at position " + idx, expected[idx], it
423                     .next());
424         }
425         assertEquals("Wrong name of new node", expected[expected.length - 1],
426                 data.getNewNodeName());
427         assertEquals("Incorrect attribute flag", attr, data.isAttribute());
428     }
429 
430     /***
431      * Checks if the JXPath context's selectNodes() method was called as often
432      * as expected.
433      *
434      * @param expected the number of expected calls
435      */
436     protected void checkSelectCalls(int expected)
437     {
438         MockJXPathContext ctx = ((MockJXPathContextExpressionEngine) engine).getContext();
439         int calls = (ctx == null) ? 0 : ctx.selectInvocations;
440         assertEquals("Incorrect number of select calls", expected, calls);
441     }
442 
443     /***
444      * A mock implementation of the JXPathContext class. This implementation
445      * will overwrite the <code>selectNodes()</code> method that is used by
446      * <code>XPathExpressionEngine</code> to count the invocations of this
447      * method.
448      */
449     static class MockJXPathContext extends JXPathContextReferenceImpl
450     {
451         int selectInvocations;
452 
453         public MockJXPathContext(Object bean)
454         {
455             super(null, bean);
456         }
457 
458         /***
459          * Dummy implementation of this method. If the passed in string is the
460          * test key, the root node will be returned in the list. Otherwise the
461          * return value is <b>null</b>.
462          */
463         public List selectNodes(String xpath)
464         {
465             selectInvocations++;
466             if (TEST_KEY.equals(xpath))
467             {
468                 List result = new ArrayList(1);
469                 result.add(ROOT);
470                 return result;
471             }
472             else
473             {
474                 return null;
475             }
476         }
477     }
478 
479     /***
480      * A special implementation of XPathExpressionEngine that overrides
481      * createContext() to return a mock context object.
482      */
483     static class MockJXPathContextExpressionEngine extends
484             XPathExpressionEngine
485     {
486         /*** Stores the context instance. */
487         private MockJXPathContext context;
488 
489         protected JXPathContext createContext(ConfigurationNode root, String key)
490         {
491             context = new MockJXPathContext(root);
492             return context;
493         }
494 
495         /***
496          * Returns the context created by the last newContext() call.
497          *
498          * @return the current context
499          */
500         public MockJXPathContext getContext()
501         {
502             return context;
503         }
504     }
505 }