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