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