View Javadoc
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.configuration2.tree.xpath;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertFalse;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertSame;
23  import static org.junit.jupiter.api.Assertions.assertThrows;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  import static org.mockito.Mockito.mock;
26  import static org.mockito.Mockito.when;
27  
28  import java.util.Arrays;
29  import java.util.List;
30  
31  import org.apache.commons.configuration2.tree.ImmutableNode;
32  import org.apache.commons.configuration2.tree.InMemoryNodeModel;
33  import org.apache.commons.configuration2.tree.NodeAddData;
34  import org.apache.commons.configuration2.tree.NodeHandler;
35  import org.apache.commons.configuration2.tree.QueryResult;
36  import org.apache.commons.jxpath.JXPathContext;
37  import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
38  import org.apache.commons.jxpath.ri.model.NodePointerFactory;
39  import org.junit.jupiter.api.BeforeAll;
40  import org.junit.jupiter.api.Test;
41  
42  /**
43   * Test class for XPathExpressionEngine.
44   */
45  public class TestXPathExpressionEngine {
46      /** Constant for the valid test key. */
47      private static final String TEST_KEY = "TESTKEY";
48  
49      /** Constant for the name of the root node. */
50      private static final String ROOT_NAME = "testRoot";
51  
52      /** The test root node. */
53      private static ImmutableNode root;
54  
55      /** A test node handler. */
56      private static NodeHandler<ImmutableNode> handler;
57  
58      /**
59       * Helper method for testing the path nodes in the given add data object.
60       *
61       * @param data the data object to check
62       * @param attr a flag if the new node is an attribute
63       * @param expected an array with the expected path elements
64       */
65      private static void checkAddPath(final NodeAddData<ImmutableNode> data, final boolean attr, final String... expected) {
66          assertSame(root, data.getParent());
67          final List<String> path = data.getPathNodes();
68          assertEquals(Arrays.asList(expected).subList(0, expected.length - 1), path);
69          assertEquals(expected[expected.length - 1], data.getNewNodeName());
70          assertEquals(attr, data.isAttribute());
71      }
72  
73      @BeforeAll
74      public static void setUpBeforeClass() throws Exception {
75          root = new ImmutableNode.Builder().name(ROOT_NAME).create();
76          handler = new InMemoryNodeModel(root).getNodeHandler();
77      }
78  
79      /**
80       * Helper method for testing queries with undefined keys.
81       *
82       * @param key the key
83       */
84      private void checkEmptyKey(final String key) {
85          final XPathContextFactory factory = mock(XPathContextFactory.class);
86          final XPathExpressionEngine engine = new XPathExpressionEngine(factory);
87          final List<QueryResult<ImmutableNode>> results = engine.query(root, key, handler);
88          assertEquals(1, results.size());
89          assertSame(root, results.get(0).getNode());
90      }
91  
92      /**
93       * Helper method for checking whether an exception is thrown for an invalid path passed to prepareAdd().
94       *
95       * @param path the path to be tested
96       * @throws IllegalArgumentException if the test is successful
97       */
98      private void checkInvalidAddPath(final String path) {
99          final XPathExpressionEngine engine = new XPathExpressionEngine();
100         final QueryResult<ImmutableNode> res = QueryResult.createNodeResult(root);
101         assertThrows(IllegalArgumentException.class, () -> engine.createNodeAddData(path, res));
102     }
103 
104     /**
105      * Creates a mock for a context and prepares it to expect a select invocation yielding the provided results.
106      *
107      * @param results the results
108      * @return the mock context
109      */
110     private JXPathContext expectSelect(final Object... results) {
111         final JXPathContext ctx = mock(JXPathContext.class);
112 
113         when(ctx.selectNodes(TEST_KEY)).thenReturn(Arrays.asList(results));
114 
115         return ctx;
116     }
117 
118     /**
119      * Creates a test engine instance configured with a context factory which returns the given test context.
120      *
121      * @param ctx the context mock
122      * @return the test engine instance
123      */
124     private XPathExpressionEngine setUpEngine(final JXPathContext ctx) {
125         final XPathContextFactory factory = mock(XPathContextFactory.class);
126 
127         when(factory.createContext(root, handler)).thenReturn(ctx);
128 
129         return new XPathExpressionEngine(factory);
130     }
131 
132     /**
133      * Tests the key of an attribute which belongs to the root node.
134      */
135     @Test
136     public void testAttributeKeyOfRootNode() {
137         final XPathExpressionEngine engine = new XPathExpressionEngine();
138         assertEquals("@child", engine.attributeKey(null, "child"));
139     }
140 
141     /**
142      * Tests whether a canonical key can be queried if all child nodes have different names.
143      */
144     @Test
145     public void testCanonicalKeyNoDuplicates() {
146         final ImmutableNode.Builder parentBuilder = new ImmutableNode.Builder(2);
147         final ImmutableNode c1 = new ImmutableNode.Builder().name("child").create();
148         final ImmutableNode c2 = new ImmutableNode.Builder().name("child_other").create();
149         parentBuilder.addChildren(Arrays.asList(c2, c1));
150         final ImmutableNode parent = parentBuilder.create();
151         final NodeHandler<ImmutableNode> testHandler = new InMemoryNodeModel(parent).getNodeHandler();
152         final XPathExpressionEngine engine = new XPathExpressionEngine();
153         assertEquals("parent/child[1]", engine.canonicalKey(c1, "parent", testHandler));
154     }
155 
156     /**
157      * Tests whether the parent key can be undefined when querying a canonical key.
158      */
159     @Test
160     public void testCanonicalKeyNoParentKey() {
161         final ImmutableNode.Builder parentBuilder = new ImmutableNode.Builder(1);
162         final ImmutableNode c1 = new ImmutableNode.Builder().name("child").create();
163         final ImmutableNode parent = parentBuilder.addChild(c1).create();
164         final NodeHandler<ImmutableNode> testHandler = new InMemoryNodeModel(parent).getNodeHandler();
165         final XPathExpressionEngine engine = new XPathExpressionEngine();
166         assertEquals("child[1]", engine.canonicalKey(c1, null, testHandler));
167     }
168 
169     /**
170      * Tests whether a canonical key for the parent node can be queried if no parent key was passed in.
171      */
172     @Test
173     public void testCanonicalKeyRootNoParentKey() {
174         final XPathExpressionEngine engine = new XPathExpressionEngine();
175         assertEquals("", engine.canonicalKey(root, null, handler));
176     }
177 
178     /**
179      * Tests whether a parent key is evaluated when determining the canonical key of the root node.
180      */
181     @Test
182     public void testCanonicalKeyRootWithParentKey() {
183         final XPathExpressionEngine engine = new XPathExpressionEngine();
184         assertEquals("parent", engine.canonicalKey(root, "parent", handler));
185     }
186 
187     /**
188      * Tests whether duplicates are correctly resolved when querying for canonical keys.
189      */
190     @Test
191     public void testCanonicalKeyWithDuplicates() {
192         final ImmutableNode.Builder parentBuilder = new ImmutableNode.Builder(3);
193         final ImmutableNode c1 = new ImmutableNode.Builder().name("child").create();
194         final ImmutableNode c2 = new ImmutableNode.Builder().name("child").create();
195         final ImmutableNode c3 = new ImmutableNode.Builder().name("child_other").create();
196         parentBuilder.addChildren(Arrays.asList(c1, c2, c3));
197         final ImmutableNode parent = parentBuilder.create();
198         final NodeHandler<ImmutableNode> testHandler = new InMemoryNodeModel(parent).getNodeHandler();
199         final XPathExpressionEngine engine = new XPathExpressionEngine();
200         assertEquals("parent/child[1]", engine.canonicalKey(c1, "parent", testHandler));
201         assertEquals("parent/child[2]", engine.canonicalKey(c2, "parent", testHandler));
202     }
203 
204     /**
205      * Tests whether a correct default context factory is created.
206      */
207     @Test
208     public void testDefaultContextFactory() {
209         final XPathExpressionEngine engine = new XPathExpressionEngine();
210         assertNotNull(engine.getContextFactory());
211     }
212 
213     /**
214      * Tests whether the key of an attribute can be generated..
215      */
216     @Test
217     public void testNodeKeyAttribute() {
218         final XPathExpressionEngine engine = new XPathExpressionEngine();
219         assertEquals("node/@attr", engine.attributeKey("node", "attr"));
220     }
221 
222     /**
223      * Tests node key() for direct children of the root node.
224      */
225     @Test
226     public void testNodeKeyForRootChild() {
227         final XPathExpressionEngine engine = new XPathExpressionEngine();
228         assertEquals(ROOT_NAME, engine.nodeKey(root, "", handler));
229     }
230 
231     /**
232      * Tests nodeKey() for the root node.
233      */
234     @Test
235     public void testNodeKeyForRootNode() {
236         final XPathExpressionEngine engine = new XPathExpressionEngine();
237         assertEquals("", engine.nodeKey(root, null, handler));
238     }
239 
240     /**
241      * Tests a node key if the node does not have a name.
242      */
243     @Test
244     public void testNodeKeyNoNodeName() {
245         final XPathExpressionEngine engine = new XPathExpressionEngine();
246         assertEquals("test", engine.nodeKey(new ImmutableNode.Builder().create(), "test", handler));
247     }
248 
249     /**
250      * Tests a normal call of nodeKey().
251      */
252     @Test
253     public void testNodeKeyNormal() {
254         final XPathExpressionEngine engine = new XPathExpressionEngine();
255         assertEquals("parent/" + ROOT_NAME, engine.nodeKey(root, "parent", handler));
256     }
257 
258     /**
259      * Tests if the JXPathContext is correctly initialized with the node pointer factory.
260      */
261     @Test
262     public void testNodePointerFactory() {
263         JXPathContext.newContext(this);
264         final NodePointerFactory[] factories = JXPathContextReferenceImpl.getNodePointerFactories();
265         boolean found = false;
266         for (final NodePointerFactory factory : factories) {
267             if (factory instanceof ConfigurationNodePointerFactory) {
268                 found = true;
269             }
270         }
271         assertTrue(found);
272     }
273 
274     /**
275      * Tests adding a new attribute node.
276      */
277     @Test
278     public void testPrepareAddAttribute() {
279         final JXPathContext ctx = expectSelect(root);
280         final XPathExpressionEngine engine = setUpEngine(ctx);
281         final NodeAddData<ImmutableNode> data = engine.prepareAdd(root, TEST_KEY + "\t@newAttr", handler);
282         checkAddPath(data, true, "newAttr");
283     }
284 
285     /**
286      * Tests adding a complete path whose final node is an attribute.
287      */
288     @Test
289     public void testPrepareAddAttributePath() {
290         final JXPathContext ctx = expectSelect(root);
291         final XPathExpressionEngine engine = setUpEngine(ctx);
292         final NodeAddData<ImmutableNode> data = engine.prepareAdd(root, TEST_KEY + " a/full/path@attr", handler);
293         checkAddPath(data, true, "a", "full", "path", "attr");
294     }
295 
296     /**
297      * Tests an add operation where the key is empty.
298      */
299     @Test
300     public void testPrepareAddEmptyKey() {
301         final XPathExpressionEngine engine = new XPathExpressionEngine();
302         assertThrows(IllegalArgumentException.class, () -> engine.prepareAdd(root, "", handler));
303     }
304 
305     /**
306      * Tests an add operation with an empty path for the new node.
307      */
308     @Test
309     public void testPrepareAddEmptyPath() {
310         final XPathExpressionEngine engine = new XPathExpressionEngine();
311         assertThrows(IllegalArgumentException.class, () -> engine.prepareAdd(root, TEST_KEY + " ", handler));
312     }
313 
314     /**
315      * Tests an add operation with an invalid path: the path contains an attribute in the middle part.
316      */
317     @Test
318     public void testPrepareAddInvalidAttributePath() {
319         checkInvalidAddPath("a/path/with@an/attribute");
320     }
321 
322     /**
323      * Tests an add operation with an invalid path: the path contains an attribute after a slash.
324      */
325     @Test
326     public void testPrepareAddInvalidAttributePath2() {
327         checkInvalidAddPath("a/path/with/@attribute");
328     }
329 
330     /**
331      * Tests an add operation with a query that does not return a single node.
332      */
333     @Test
334     public void testPrepareAddInvalidParent() {
335         final JXPathContext ctx = expectSelect();
336         final XPathExpressionEngine engine = setUpEngine(ctx);
337         assertThrows(IllegalArgumentException.class, () -> engine.prepareAdd(root, TEST_KEY + " test", handler));
338     }
339 
340     /**
341      * Tests an add operation with an invalid path.
342      */
343     @Test
344     public void testPrepareAddInvalidPath() {
345         checkInvalidAddPath("an/invalid//path");
346     }
347 
348     /**
349      * Tests an add operation with an invalid path that contains multiple attribute components.
350      */
351     @Test
352     public void testPrepareAddInvalidPathMultipleAttributes() {
353         checkInvalidAddPath("an@attribute@path");
354     }
355 
356     /**
357      * Tests an add operation with an invalid path that starts with a slash.
358      */
359     @Test
360     public void testPrepareAddInvalidPathWithSlash() {
361         checkInvalidAddPath("/a/path/node");
362     }
363 
364     /**
365      * Tests adding a single child node.
366      */
367     @Test
368     public void testPrepareAddNode() {
369         final JXPathContext ctx = expectSelect(root);
370         final XPathExpressionEngine engine = setUpEngine(ctx);
371         final NodeAddData<ImmutableNode> data = engine.prepareAdd(root, TEST_KEY + "  newNode", handler);
372         checkAddPath(data, false, "newNode");
373     }
374 
375     /**
376      * Tests an add operation where the key is null.
377      */
378     @Test
379     public void testPrepareAddNullKey() {
380         final XPathExpressionEngine engine = new XPathExpressionEngine();
381         assertThrows(IllegalArgumentException.class, () -> engine.prepareAdd(root, null, handler));
382     }
383 
384     /**
385      * Tests adding a complete path.
386      */
387     @Test
388     public void testPrepareAddPath() {
389         final JXPathContext ctx = expectSelect(root);
390         final XPathExpressionEngine engine = setUpEngine(ctx);
391         final NodeAddData<ImmutableNode> data = engine.prepareAdd(root, TEST_KEY + " \t a/full/path/node", handler);
392         checkAddPath(data, false, "a", "full", "path", "node");
393     }
394 
395     /**
396      * Tests adding a new attribute to the root.
397      */
398     @Test
399     public void testPrepareAddRootAttribute() {
400         final JXPathContext ctx = expectSelect(root);
401         final XPathExpressionEngine engine = setUpEngine(ctx);
402         final NodeAddData<ImmutableNode> data = engine.prepareAdd(root, " @attr", handler);
403         checkAddPath(data, true, "attr");
404     }
405 
406     /**
407      * Tests adding a new node to the root.
408      */
409     @Test
410     public void testPrepareAddRootChild() {
411         final JXPathContext ctx = expectSelect(root);
412         final XPathExpressionEngine engine = setUpEngine(ctx);
413         final NodeAddData<ImmutableNode> data = engine.prepareAdd(root, " newNode", handler);
414         checkAddPath(data, false, "newNode");
415     }
416 
417     /**
418      * Tests that it is not possible to add nodes to an attribute.
419      */
420     @Test
421     public void testPrepareAddToAttributeResult() {
422         final XPathExpressionEngine engine = new XPathExpressionEngine();
423         final QueryResult<ImmutableNode> result = QueryResult.createAttributeResult(root, TEST_KEY);
424         assertThrows(IllegalArgumentException.class, () -> engine.createNodeAddData("path", result));
425     }
426 
427     /**
428      * Tests a query which yields an attribute result.
429      */
430     @Test
431     public void testQueryAttributeExpression() {
432         final QueryResult<ImmutableNode> attrResult = QueryResult.createAttributeResult(root, "attr");
433         final JXPathContext ctx = expectSelect(attrResult);
434         final XPathExpressionEngine engine = setUpEngine(ctx);
435         final List<QueryResult<ImmutableNode>> result = engine.query(root, TEST_KEY, handler);
436         assertEquals(1, result.size());
437         assertSame(attrResult, result.get(0));
438     }
439 
440     /**
441      * Tests the query() method with an expression yielding a node.
442      */
443     @Test
444     public void testQueryNodeExpression() {
445         final JXPathContext ctx = expectSelect(root);
446         final XPathExpressionEngine engine = setUpEngine(ctx);
447         final List<QueryResult<ImmutableNode>> result = engine.query(root, TEST_KEY, handler);
448         assertEquals(1, result.size());
449         assertSame(root, result.get(0).getNode());
450         assertFalse(result.get(0).isAttributeResult());
451     }
452 
453     /**
454      * Tests a query with an empty key. This should directly return the root node without invoking the JXPathContext.
455      */
456     @Test
457     public void testQueryWithEmptyKey() {
458         checkEmptyKey("");
459     }
460 
461     /**
462      * Tests a query with a null key. Same as an empty key.
463      */
464     @Test
465     public void testQueryWithNullKey() {
466         checkEmptyKey(null);
467     }
468 
469     /**
470      * Tests a query that has no results. This should return an empty list.
471      */
472     @Test
473     public void testQueryWithoutResult() {
474         final JXPathContext ctx = expectSelect();
475         final XPathExpressionEngine engine = setUpEngine(ctx);
476         assertTrue(engine.query(root, TEST_KEY, handler).isEmpty());
477     }
478 }