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;
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.assertInstanceOf;
22  import static org.junit.jupiter.api.Assertions.assertNotNull;
23  import static org.junit.jupiter.api.Assertions.assertNotSame;
24  import static org.junit.jupiter.api.Assertions.assertNull;
25  import static org.junit.jupiter.api.Assertions.assertSame;
26  import static org.junit.jupiter.api.Assertions.assertThrows;
27  import static org.junit.jupiter.api.Assertions.assertTrue;
28  import static org.mockito.ArgumentMatchers.any;
29  import static org.mockito.ArgumentMatchers.eq;
30  import static org.mockito.Mockito.when;
31  
32  import java.util.ArrayList;
33  import java.util.Arrays;
34  import java.util.Collection;
35  import java.util.Collections;
36  import java.util.HashMap;
37  import java.util.HashSet;
38  import java.util.Iterator;
39  import java.util.List;
40  import java.util.Map;
41  import java.util.Set;
42  
43  import org.apache.commons.configuration2.ex.ConfigurationRuntimeException;
44  import org.junit.jupiter.api.BeforeAll;
45  import org.junit.jupiter.api.BeforeEach;
46  import org.junit.jupiter.api.Test;
47  
48  /**
49   * A special test class for {@code InMemoryNodeModel} which tests the facilities for tracking nodes.
50   */
51  public class TestInMemoryNodeModelTrackedNodes {
52      /** Constant for the name of a new table field. */
53      private static final String NEW_FIELD = "newTableField";
54  
55      /** Constant for a test key. */
56      private static final String TEST_KEY = "someTestKey";
57  
58      /** Constant for the key used by the test selector. */
59      private static final String SELECTOR_KEY = "tables.table(1)";
60  
61      /** The root node for the test hierarchy. */
62      private static ImmutableNode root;
63  
64      /** A default node selector initialized with a test key. */
65      private static NodeSelector selector;
66  
67      /**
68       * Checks whether a fields node was correctly changed by an update operation.
69       *
70       * @param nodeFields the fields node
71       * @param idx the index of the changed node
72       */
73      private static void checkedForChangedField(final ImmutableNode nodeFields, final int idx) {
74          assertEquals(NodeStructureHelper.fieldsLength(1), nodeFields.getChildren().size());
75          int childIndex = 0;
76          for (final ImmutableNode field : nodeFields) {
77              final String expName = childIndex == idx ? NEW_FIELD : NodeStructureHelper.field(1, childIndex);
78              checkFieldNode(field, expName);
79              childIndex++;
80          }
81      }
82  
83      /**
84       * Checks whether a field node has the expected content.
85       *
86       * @param nodeField the field node to be checked
87       * @param name the expected name of this field
88       */
89      private static void checkFieldNode(final ImmutableNode nodeField, final String name) {
90          assertEquals("field", nodeField.getNodeName());
91          assertEquals(1, nodeField.getChildren().size());
92          final ImmutableNode nodeName = nodeField.getChildren().get(0);
93          assertEquals("name", nodeName.getNodeName());
94          assertEquals(name, nodeName.getValue());
95      }
96  
97      /**
98       * Tests whether a field node was added.
99       *
100      * @param nodeFields the fields node
101      */
102     private static void checkForAddedField(final ImmutableNode nodeFields) {
103         assertEquals(NodeStructureHelper.fieldsLength(1) + 1, nodeFields.getChildren().size());
104         final ImmutableNode nodeField = nodeFields.getChildren().get(NodeStructureHelper.fieldsLength(1));
105         checkFieldNode(nodeField, NEW_FIELD);
106     }
107 
108     /**
109      * Helper method for checking whether the expected field node was removed.
110      *
111      * @param nodeFields the fields node
112      * @param idx the index of the removed field
113      */
114     private static void checkForRemovedField(final ImmutableNode nodeFields, final int idx) {
115         assertEquals(NodeStructureHelper.fieldsLength(1) - 1, nodeFields.getChildren().size());
116         final Set<String> expectedNames = new HashSet<>();
117         final Set<String> actualNames = new HashSet<>();
118         for (int i = 0; i < NodeStructureHelper.fieldsLength(1); i++) {
119             if (idx != i) {
120                 expectedNames.add(NodeStructureHelper.field(1, i));
121             }
122         }
123         for (final ImmutableNode field : nodeFields) {
124             final ImmutableNode nodeName = field.getChildren().get(0);
125             actualNames.add(String.valueOf(nodeName.getValue()));
126         }
127         assertEquals(expectedNames, actualNames);
128     }
129 
130     /**
131      * Creates a default resolver which supports arbitrary queries on a target node.
132      *
133      * @return the resolver
134      */
135     private static NodeKeyResolver<ImmutableNode> createResolver() {
136         final NodeKeyResolver<ImmutableNode> resolver = NodeStructureHelper.createResolverMock();
137         NodeStructureHelper.prepareResolveKeyForQueries(resolver);
138         return resolver;
139     }
140 
141     /**
142      * Prepares a mock for a resolver to handle keys for update operations. Support is limited. It is expected that only a
143      * single value is changed.
144      *
145      * @param resolver the {@code NodeKeyResolver} mock
146      */
147     private static void prepareResolverForUpdateKeys(final NodeKeyResolver<ImmutableNode> resolver) {
148         when(resolver.resolveUpdateKey(any(), any(), any(), any())).thenAnswer(invocation -> {
149             final ImmutableNode root = invocation.getArgument(0, ImmutableNode.class);
150             final String key = invocation.getArgument(1, String.class);
151             final TreeData handler = invocation.getArgument(3, TreeData.class);
152             final List<QueryResult<ImmutableNode>> results = DefaultExpressionEngine.INSTANCE.query(root, key, handler);
153             assertEquals(1, results.size());
154             return new NodeUpdateData<>(Collections.singletonMap(results.get(0), invocation.getArgument(2)), null, null, null);
155         });
156     }
157 
158     @BeforeAll
159     public static void setUpBeforeClass() throws Exception {
160         root = new ImmutableNode.Builder(1).addChild(NodeStructureHelper.ROOT_TABLES_TREE).create();
161         selector = new NodeSelector(SELECTOR_KEY);
162     }
163 
164     /** The model to be tested. */
165     private InMemoryNodeModel model;
166 
167     /**
168      * Helper method for testing whether a tracked node can be replaced.
169      */
170     private void checkReplaceTrackedNode() {
171         final ImmutableNode newNode = new ImmutableNode.Builder().name("newNode").create();
172         model.replaceTrackedNode(selector, newNode);
173         assertSame(newNode, model.getTrackedNode(selector));
174         assertTrue(model.isTrackedNodeDetached(selector));
175     }
176 
177     /**
178      * Checks trackChildNodes() if the passed in key has a result set which causes the operation to be aborted.
179      *
180      * @param queryResult the result set of the key
181      */
182     private void checkTrackChildNodesNoResult(final List<ImmutableNode> queryResult) {
183         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
184         when(resolver.resolveNodeKey(root, TEST_KEY, model.getNodeHandler())).thenReturn(queryResult);
185 
186         final TreeData oldData = model.getTreeData();
187 
188         assertTrue(model.trackChildNodes(TEST_KEY, resolver).isEmpty());
189         assertSame(oldData, model.getTreeData());
190     }
191 
192     /**
193      * Helper method for testing trackChildNodeWithCreation() if invalid query results are generated.
194      *
195      * @param queryResult the result set of the key
196      */
197     private void checkTrackChildNodeWithCreationInvalidKey(final List<ImmutableNode> queryResult) {
198         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
199         when(resolver.resolveNodeKey(model.getRootNode(), TEST_KEY, model.getNodeHandler())).thenReturn(queryResult);
200 
201         assertThrows(ConfigurationRuntimeException.class, () -> model.trackChildNodeWithCreation(TEST_KEY, "someChild", resolver));
202     }
203 
204     /**
205      * Prepares the resolver mock to expect a nodeKey() request.
206      *
207      * @param resolver the {@code NodeKeyResolver}
208      * @param node the node whose name is to be resolved
209      * @param key the key to be returned for this node
210      */
211     private void prepareNodeKey(final NodeKeyResolver<ImmutableNode> resolver, final ImmutableNode node, final String key) {
212         final Map<ImmutableNode, String> cache = new HashMap<>();
213         when(resolver.nodeKey(node, cache, model.getNodeHandler())).thenReturn(key);
214     }
215 
216     /**
217      * Returns the fields node from the model.
218      *
219      * @return the fields node
220      */
221     private ImmutableNode fieldsNodeFromModel() {
222         return NodeStructureHelper.nodeForKey(model, "tables/table(1)/fields");
223     }
224 
225     /**
226      * Returns the fields node from a tracked node.
227      *
228      * @return the fields node
229      */
230     private ImmutableNode fieldsNodeFromTrackedNode() {
231         return NodeStructureHelper.nodeForKey(model.getTrackedNode(selector), "fields");
232     }
233 
234     /**
235      * Produces a tracked node with the default selector and executes an operation which detaches this node.
236      *
237      * @param resolver the {@code NodeKeyResolver}
238      */
239     private void initDetachedNode(final NodeKeyResolver<ImmutableNode> resolver) {
240         model.trackNode(selector, resolver);
241         model.clearTree("tables.table(0)", resolver);
242     }
243 
244     @BeforeEach
245     public void setUp() throws Exception {
246         model = new InMemoryNodeModel(root);
247     }
248 
249     /**
250      * Tests an addNodes() operation on a tracked node that is detached.
251      */
252     @Test
253     public void testAddNodesOnDetachedNode() {
254         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
255         NodeStructureHelper.prepareResolveAddKeys(resolver);
256         model.trackNode(selector, resolver);
257         initDetachedNode(resolver);
258         final ImmutableNode rootNode = model.getRootNode();
259         model.addNodes("fields", selector, Collections.singleton(NodeStructureHelper.createFieldNode(NEW_FIELD)), resolver);
260         assertSame(rootNode, model.getRootNode());
261         checkForAddedField(fieldsNodeFromTrackedNode());
262     }
263 
264     /**
265      * Tests whether an addNodes() operation works on a tracked node.
266      */
267     @Test
268     public void testAddNodesOnTrackedNode() {
269         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
270         NodeStructureHelper.prepareResolveAddKeys(resolver);
271         model.trackNode(selector, resolver);
272         model.addNodes("fields", selector, Collections.singleton(NodeStructureHelper.createFieldNode(NEW_FIELD)), resolver);
273         checkForAddedField(fieldsNodeFromModel());
274         checkForAddedField(fieldsNodeFromTrackedNode());
275     }
276 
277     /**
278      * Tests an addProperty() operation on a tracked node that is detached.
279      */
280     @Test
281     public void testAddPropertyOnDetachedNode() {
282         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
283         NodeStructureHelper.prepareResolveAddKeys(resolver);
284         model.trackNode(selector, resolver);
285         initDetachedNode(resolver);
286         final ImmutableNode rootNode = model.getRootNode();
287         model.addProperty("fields.field(-1).name", selector, Collections.singleton(NEW_FIELD), resolver);
288         assertSame(rootNode, model.getRootNode());
289         checkForAddedField(fieldsNodeFromTrackedNode());
290     }
291 
292     /**
293      * Tests whether an addProperty() operation works on a tracked node.
294      */
295     @Test
296     public void testAddPropertyOnTrackedNode() {
297         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
298         NodeStructureHelper.prepareResolveAddKeys(resolver);
299         model.trackNode(selector, resolver);
300         model.addProperty("fields.field(-1).name", selector, Collections.singleton(NEW_FIELD), resolver);
301         checkForAddedField(fieldsNodeFromModel());
302         checkForAddedField(fieldsNodeFromTrackedNode());
303     }
304 
305     /**
306      * Tests a clearProperty() operation on a tracked node which is detached.
307      */
308     @Test
309     public void testClearPropertyOnDetachedNode() {
310         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
311         initDetachedNode(resolver);
312         final ImmutableNode rootNode = model.getRootNode();
313         model.clearProperty("fields.field(0).name", selector, resolver);
314         assertSame(rootNode, model.getRootNode());
315         final ImmutableNode nodeFields = fieldsNodeFromTrackedNode();
316         checkForRemovedField(nodeFields, 0);
317     }
318 
319     /**
320      * Tests whether clearProperty() can operate on a tracked node.
321      */
322     @Test
323     public void testClearPropertyOnTrackedNode() {
324         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
325         model.trackNode(selector, resolver);
326         model.clearProperty("fields.field(0).name", selector, resolver);
327         final ImmutableNode nodeFields = fieldsNodeFromModel();
328         checkForRemovedField(nodeFields, 0);
329     }
330 
331     /**
332      * Tests a clearTree() operation on a tracked node which is detached.
333      */
334     @Test
335     public void testClearTreeOnDetachedNode() {
336         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
337         initDetachedNode(resolver);
338         final ImmutableNode rootNode = model.getRootNode();
339         model.clearTree("fields.field(1)", selector, resolver);
340         assertSame(rootNode, model.getRootNode());
341         final ImmutableNode nodeFields = fieldsNodeFromTrackedNode();
342         checkForRemovedField(nodeFields, 1);
343     }
344 
345     /**
346      * Tests whether clearTree() can operate on a tracked node.
347      */
348     @Test
349     public void testClearTreeOnTrackedNode() {
350         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
351         model.trackNode(selector, resolver);
352         model.clearTree("fields.field(1)", selector, resolver);
353         final ImmutableNode nodeFields = fieldsNodeFromModel();
354         checkForRemovedField(nodeFields, 1);
355     }
356 
357     /**
358      * Tests whether a tracked node can be queried even after the model was cleared.
359      */
360     @Test
361     public void testGetTrackedNodeAfterClear() {
362         final ImmutableNode node = NodeStructureHelper.nodeForKey(model, "tables/table(1)");
363         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
364         model.trackNode(selector, resolver);
365         model.clear(resolver);
366         assertSame(node, model.getTrackedNode(selector));
367     }
368 
369     /**
370      * Tests whether a tracked node can be queried after the root node was changed.
371      */
372     @Test
373     public void testGetTrackedNodeAfterSetRootNode() {
374         final ImmutableNode node = NodeStructureHelper.nodeForKey(model, "tables/table(1)");
375         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
376         model.trackNode(selector, resolver);
377         model.setRootNode(root);
378         assertSame(node, model.getTrackedNode(selector));
379     }
380 
381     /**
382      * Tests whether a tracked node survives updates of the node model.
383      */
384     @Test
385     public void testGetTrackedNodeAfterUpdate() {
386         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
387         model.trackNode(selector, resolver);
388         model.clearProperty("tables.table(1).fields.field(1).name", resolver);
389         final ImmutableNode node = model.getTrackedNode(selector);
390         assertEquals(NodeStructureHelper.table(1), node.getChildren().get(0).getValue());
391     }
392 
393     /**
394      * Tests whether a tracked node can be queried even if it was removed from the structure.
395      */
396     @Test
397     public void testGetTrackedNodeAfterUpdateNoLongerExisting() {
398         final ImmutableNode node = NodeStructureHelper.nodeForKey(model, "tables/table(1)");
399         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
400         initDetachedNode(resolver);
401         assertSame(node, model.getTrackedNode(selector));
402     }
403 
404     /**
405      * Tests whether a tracked node can be queried.
406      */
407     @Test
408     public void testGetTrackedNodeExisting() {
409         final ImmutableNode node = NodeStructureHelper.nodeForKey(model, "tables/table(1)");
410         model.trackNode(selector, createResolver());
411         assertSame(node, model.getTrackedNode(selector));
412     }
413 
414     /**
415      * Tests whether a node handler for a tracked node can be queried which is still active.
416      */
417     @Test
418     public void testGetTrackedNodeHandlerActive() {
419         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
420         model.trackNode(selector, resolver);
421         final NodeHandler<ImmutableNode> handler = model.getTrackedNodeHandler(selector);
422         final TrackedNodeHandler tnh = assertInstanceOf(TrackedNodeHandler.class, handler);
423         assertSame(model.getTrackedNode(selector), handler.getRootNode());
424         assertSame(model.getTreeData(), tnh.getParentHandler());
425     }
426 
427     /**
428      * Tests whether a node handler for a detached tracked node can be queried.
429      */
430     @Test
431     public void testGetTrackedNodeHandlerDetached() {
432         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
433         model.trackNode(selector, resolver);
434         initDetachedNode(resolver);
435         final NodeHandler<ImmutableNode> handler = model.getTrackedNodeHandler(selector);
436         assertSame(model.getTrackedNode(selector), handler.getRootNode());
437         assertInstanceOf(TreeData.class, handler);
438         assertNotSame(model.getNodeHandler(), handler);
439     }
440 
441     /**
442      * Tries to obtain a tracked node which is unknown.
443      */
444     @Test
445     public void testGetTrackedNodeNonExisting() {
446         assertThrows(ConfigurationRuntimeException.class, () -> model.getTrackedNode(selector));
447     }
448 
449     /**
450      * Tests whether a clear() operation causes nodes to be detached.
451      */
452     @Test
453     public void testIsDetachedAfterClear() {
454         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
455         model.trackNode(selector, resolver);
456         model.clear(resolver);
457         assertTrue(model.isTrackedNodeDetached(selector));
458     }
459 
460     /**
461      * Tests whether tracked nodes become detached when a new root node is set.
462      */
463     @Test
464     public void testIsDetachedAfterSetRoot() {
465         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
466         model.trackNode(selector, resolver);
467         model.clearProperty("tables.table(1).fields.field(1).name", resolver);
468         model.setRootNode(root);
469         assertTrue(model.isTrackedNodeDetached(selector));
470     }
471 
472     /**
473      * Tests isDetached() for a life node.
474      */
475     @Test
476     public void testIsDetachedFalseAfterUpdate() {
477         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
478         model.trackNode(selector, resolver);
479         model.clearProperty("tables.table(1).fields.field(1).name", resolver);
480         assertFalse(model.isTrackedNodeDetached(selector));
481     }
482 
483     /**
484      * Tests isDetached() for a node which has just been tracked.
485      */
486     @Test
487     public void testIsDetachedFalseNoUpdates() {
488         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
489         model.trackNode(selector, resolver);
490         assertFalse(model.isTrackedNodeDetached(selector));
491     }
492 
493     /**
494      * Tests isDetached() for an actually detached node.
495      */
496     @Test
497     public void testIsDetachedTrue() {
498         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
499         initDetachedNode(resolver);
500         assertTrue(model.isTrackedNodeDetached(selector));
501     }
502 
503     /**
504      * Tests whether an active tracked node can be replaced.
505      */
506     @Test
507     public void testReplaceTrackedNodeForActiveTrackedNode() {
508         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
509         model.trackNode(selector, resolver);
510         checkReplaceTrackedNode();
511     }
512 
513     /**
514      * Tests whether a detached tracked node can be replaced.
515      */
516     @Test
517     public void testReplaceTrackedNodeForDetachedNode() {
518         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
519         model.trackNode(selector, resolver);
520         initDetachedNode(resolver);
521         checkReplaceTrackedNode();
522     }
523 
524     /**
525      * Tries to replace a tracked node with a null node.
526      */
527     @Test
528     public void testReplaceTrackedNodeNull() {
529         model.trackNode(selector, createResolver());
530         assertThrows(IllegalArgumentException.class, () -> model.replaceTrackedNode(selector, null));
531     }
532 
533     /**
534      * Tests whether tracked nodes can be created from a key.
535      */
536     @Test
537     public void testSelectAndTrackNodes() {
538         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
539         final String nodeKey1 = "tables/table(0)";
540         final String nodeKey2 = "tables/table(1)";
541         final ImmutableNode node1 = NodeStructureHelper.nodeForKey(root, nodeKey1);
542         final ImmutableNode node2 = NodeStructureHelper.nodeForKey(root, nodeKey2);
543 
544         when(resolver.resolveNodeKey(root, TEST_KEY, model.getNodeHandler())).thenReturn(Arrays.asList(node1, node2));
545         prepareNodeKey(resolver, node1, nodeKey1);
546         prepareNodeKey(resolver, node2, nodeKey2);
547 
548         final Collection<NodeSelector> selectors = model.selectAndTrackNodes(TEST_KEY, resolver);
549         final Iterator<NodeSelector> it = selectors.iterator();
550         NodeSelector sel = it.next();
551         assertEquals(new NodeSelector(nodeKey1), sel);
552         assertSame(node1, model.getTrackedNode(sel));
553         sel = it.next();
554         assertEquals(new NodeSelector(nodeKey2), sel);
555         assertSame(node2, model.getTrackedNode(sel));
556         assertFalse(it.hasNext());
557     }
558 
559     /**
560      * Tests whether selectAndTrackNodes() works for nodes that are already tracked.
561      */
562     @Test
563     public void testSelectAndTrackNodesNodeAlreadyTracked() {
564         NodeKeyResolver<ImmutableNode> resolver = createResolver();
565         model.trackNode(selector, resolver);
566         resolver = createResolver();
567         final ImmutableNode node = model.getTrackedNode(selector);
568 
569         when(resolver.resolveNodeKey(root, TEST_KEY, model.getNodeHandler())).thenReturn(Collections.singletonList(node));
570         prepareNodeKey(resolver, node, SELECTOR_KEY);
571 
572         final Collection<NodeSelector> selectors = model.selectAndTrackNodes(TEST_KEY, resolver);
573         assertEquals(1, selectors.size());
574         assertEquals(selector, selectors.iterator().next());
575         model.untrackNode(selector);
576         assertSame(node, model.getTrackedNode(selector));
577     }
578 
579     /**
580      * Tests selectAndTrackNodes() if the key does not select any nodes.
581      */
582     @Test
583     public void testSelectAndTrackNodesNoSelection() {
584         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
585 
586         when(resolver.resolveNodeKey(root, TEST_KEY, model.getNodeHandler())).thenReturn(Collections.<ImmutableNode>emptyList());
587 
588         assertTrue(model.selectAndTrackNodes(TEST_KEY, resolver).isEmpty());
589     }
590 
591     /**
592      * Tests a setProperty() operation on a tracked node that is detached.
593      */
594     @Test
595     public void testSetPropertyOnDetachedNode() {
596         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
597         prepareResolverForUpdateKeys(resolver);
598         model.trackNode(selector, resolver);
599         initDetachedNode(resolver);
600         final ImmutableNode rootNode = model.getRootNode();
601         model.setProperty("fields.field(0).name", selector, NEW_FIELD, resolver);
602         assertSame(rootNode, model.getRootNode());
603         checkedForChangedField(fieldsNodeFromTrackedNode(), 0);
604     }
605 
606     /**
607      * Tests whether a setProperty() operation works on a tracked node.
608      */
609     @Test
610     public void testSetPropertyOnTrackedNode() {
611         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
612         prepareResolverForUpdateKeys(resolver);
613         model.trackNode(selector, resolver);
614         model.setProperty("fields.field(0).name", selector, NEW_FIELD, resolver);
615         checkedForChangedField(fieldsNodeFromModel(), 0);
616         checkedForChangedField(fieldsNodeFromTrackedNode(), 0);
617     }
618 
619     /**
620      * Tests whether all children of a node can be tracked at once.
621      */
622     @Test
623     public void testTrackChildNodes() {
624         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
625         final ImmutableNode node = NodeStructureHelper.nodeForKey(root, "tables");
626         final String[] keys = new String[node.getChildren().size()];
627 
628         for (int i = 0; i < keys.length; i++) {
629             final ImmutableNode child = node.getChildren().get(i);
630             keys[i] = String.format("%s.%s(%d)", node.getNodeName(), child.getNodeName(), i);
631             prepareNodeKey(resolver, child, keys[i]);
632         }
633         when(resolver.resolveNodeKey(root, TEST_KEY, model.getNodeHandler())).thenReturn(Collections.singletonList(node));
634 
635         final Collection<NodeSelector> selectors = model.trackChildNodes(TEST_KEY, resolver);
636         assertEquals(node.getChildren().size(), selectors.size());
637         int idx = 0;
638         for (final NodeSelector sel : selectors) {
639             assertEquals(new NodeSelector(keys[idx]), sel);
640             assertEquals(node.getChildren().get(idx), model.getTrackedNode(sel), "Wrong tracked node for " + sel);
641             idx++;
642         }
643     }
644 
645     /**
646      * Tests trackChildNodes() for a key that returns more than a single result.
647      */
648     @Test
649     public void testTrackChildNodesMultipleResults() {
650         checkTrackChildNodesNoResult(
651             Arrays.asList(NodeStructureHelper.nodeForKey(root, "tables/table(0)"), NodeStructureHelper.nodeForKey(root, "tables/table(1)")));
652     }
653 
654     /**
655      * Tests trackChildNodes() for a key pointing to a node with no children.
656      */
657     @Test
658     public void testTrackChildNodesNodeWithNoChildren() {
659         checkTrackChildNodesNoResult(Collections.singletonList(NodeStructureHelper.nodeForKey(root, "tables/table(0)/name")));
660     }
661 
662     /**
663      * Tests trackChildNodes() for a key that does not return any results.
664      */
665     @Test
666     public void testTrackChildNodesNoResults() {
667         checkTrackChildNodesNoResult(Collections.<ImmutableNode>emptyList());
668     }
669 
670     /**
671      * Tests whether an existing child of a selected node can be tracked.
672      */
673     @Test
674     public void testTrackChildNodeWithCreationExisting() {
675         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
676         final String childName = "name";
677         final String parentKey = "tables/table(0)";
678         final String childKey = parentKey + "/" + childName;
679         final ImmutableNode node = NodeStructureHelper.nodeForKey(model, parentKey);
680         final ImmutableNode child = NodeStructureHelper.nodeForKey(node, childName);
681 
682         when(resolver.resolveNodeKey(root, TEST_KEY, model.getNodeHandler())).thenReturn(Collections.singletonList(node));
683         prepareNodeKey(resolver, child, childKey);
684 
685         final NodeSelector childSelector = model.trackChildNodeWithCreation(TEST_KEY, childName, resolver);
686         assertEquals(new NodeSelector(childKey), childSelector);
687         assertSame(child, model.getTrackedNode(childSelector));
688     }
689 
690     /**
691      * Tests trackChildNodeWithCreation() if the passed in key selects multiple nodes.
692      */
693     @Test
694     public void testTrackChildNodeWithCreationMultipleResults() {
695         final List<ImmutableNode> nodes = Arrays.asList(NodeStructureHelper.nodeForKey(root, "tables/table(0)"),
696             NodeStructureHelper.nodeForKey(root, "tables/table(1)"));
697         checkTrackChildNodeWithCreationInvalidKey(nodes);
698     }
699 
700     /**
701      * Tests whether a child node to be tracked is created if necessary.
702      */
703     @Test
704     public void testTrackChildNodeWithCreationNonExisting() {
705         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
706         final String childName = "space";
707         final String parentKey = "tables/table(0)";
708         final String childKey = parentKey + "/" + childName;
709         final ImmutableNode node = NodeStructureHelper.nodeForKey(model, parentKey);
710 
711         when(resolver.resolveNodeKey(root, TEST_KEY, model.getNodeHandler())).thenReturn(Collections.singletonList(node));
712         when(resolver.nodeKey(any(), eq(new HashMap<>()), any())).thenReturn(childKey);
713 
714         final NodeSelector childSelector = model.trackChildNodeWithCreation(TEST_KEY, childName, resolver);
715         assertEquals(new NodeSelector(childKey), childSelector);
716         final ImmutableNode child = model.getTrackedNode(childSelector);
717         assertEquals(childName, child.getNodeName());
718         assertNull(child.getValue());
719         final ImmutableNode parent = model.getNodeHandler().getParent(child);
720         assertEquals("table", parent.getNodeName());
721         assertEquals(child, NodeStructureHelper.nodeForKey(model, childKey));
722     }
723 
724     /**
725      * Tests trackChildNodeWithCreation() if the passed in key does not select a node.
726      */
727     @Test
728     public void testTrackChildNodeWithCreationNoResults() {
729         checkTrackChildNodeWithCreationInvalidKey(new ArrayList<>());
730     }
731 
732     /**
733      * Tests whether a tracked node is handled correctly if an operation is executed on this node which causes the node to
734      * be detached. In this case, the node should be cleared (it makes no sense to use the last defined node instance).
735      */
736     @Test
737     public void testTrackedNodeClearedInOperation() {
738         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
739         model.trackNode(selector, resolver);
740         model.clearTree(null, selector, resolver);
741         assertTrue(model.isTrackedNodeDetached(selector));
742         final ImmutableNode node = model.getTrackedNode(selector);
743         assertEquals("table", node.getNodeName());
744         assertFalse(model.getNodeHandler().isDefined(node));
745     }
746 
747     /**
748      * Tries to call trackNode() with a key that selects multiple results.
749      */
750     @Test
751     public void testTrackNodeKeyMultipleResults() {
752         final NodeSelector nodeSelector = new NodeSelector("tables.table.fields.field.name");
753         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
754         assertThrows(ConfigurationRuntimeException.class, () -> model.trackNode(nodeSelector, resolver));
755     }
756 
757     /**
758      * Tries to call trackNode() with a key that does not yield any results.
759      */
760     @Test
761     public void testTrackNodeKeyNoResults() {
762         final NodeSelector nodeSelector = new NodeSelector("tables.unknown");
763         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
764         assertThrows(ConfigurationRuntimeException.class, () -> model.trackNode(nodeSelector, resolver));
765     }
766 
767     /**
768      * Tests whether a single node can be tracked multiple times.
769      */
770     @Test
771     public void testTrackNodeMultipleTimes() {
772         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
773         model.trackNode(selector, resolver);
774         model.trackNode(selector, resolver);
775         model.untrackNode(selector);
776         assertNotNull(model.getTrackedNode(selector));
777     }
778 
779     /**
780      * Tests whether tracking of a node can be stopped.
781      */
782     @Test
783     public void testUntrackNode() {
784         model.trackNode(selector, createResolver());
785         model.untrackNode(selector);
786         assertThrows(ConfigurationRuntimeException.class, () -> model.getTrackedNode(selector));
787     }
788 
789     /**
790      * Tries to stop tracking of a node which is not tracked.
791      */
792     @Test
793     public void testUntrackNodeNonExisting() {
794         assertThrows(ConfigurationRuntimeException.class, () -> model.untrackNode(selector));
795     }
796 }