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