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.apache.commons.configuration2.tree.NodeStructureHelper.ROOT_AUTHORS_TREE;
20  import static org.apache.commons.configuration2.tree.NodeStructureHelper.ROOT_PERSONAE_TREE;
21  import static org.apache.commons.configuration2.tree.NodeStructureHelper.nodeForKey;
22  import static org.apache.commons.configuration2.tree.NodeStructureHelper.nodePathWithEndNode;
23  import static org.junit.jupiter.api.Assertions.assertEquals;
24  import static org.junit.jupiter.api.Assertions.assertFalse;
25  import static org.junit.jupiter.api.Assertions.assertNotEquals;
26  import static org.junit.jupiter.api.Assertions.assertNotNull;
27  import static org.junit.jupiter.api.Assertions.assertNull;
28  import static org.junit.jupiter.api.Assertions.assertSame;
29  import static org.junit.jupiter.api.Assertions.assertThrows;
30  import static org.junit.jupiter.api.Assertions.assertTrue;
31  import static org.mockito.ArgumentMatchers.any;
32  import static org.mockito.ArgumentMatchers.eq;
33  import static org.mockito.Mockito.mock;
34  import static org.mockito.Mockito.when;
35  
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.Collection;
39  import java.util.Collections;
40  import java.util.HashMap;
41  import java.util.HashSet;
42  import java.util.List;
43  import java.util.Map;
44  import java.util.NoSuchElementException;
45  import java.util.Set;
46  import java.util.concurrent.CountDownLatch;
47  import java.util.regex.Matcher;
48  import java.util.regex.Pattern;
49  
50  import org.junit.jupiter.api.Test;
51  
52  /**
53   * Test class for {@code InMemoryNodeModel}.
54   */
55  public class TestInMemoryNodeModel {
56  
57      /** Constant for a test key. */
58      private static final String KEY = "aTestKey";
59  
60      /**
61       * Helper method for checking whether the expected nodes are encountered on a path from a start node to the root node.
62       *
63       * @param model the node model
64       * @param node the start node in the path
65       * @param path an array with the expected node names on the path
66       */
67      private static void checkPathToRoot(final InMemoryNodeModel model, ImmutableNode node, final String... path) {
68          final NodeHandler<ImmutableNode> handler = model.getNodeHandler();
69          for (int i = path.length - 1; i >= 0; i--) {
70              node = handler.getParent(node);
71              assertEquals(path[i], node.getNodeName());
72          }
73          assertSame(model.getRootNode(), handler.getParent(node));
74      }
75  
76      /**
77       * Creates a mock for a {@code NodeKeyResolver}.
78       *
79       * @return the mock for the resolver
80       */
81      @SuppressWarnings("unchecked")
82      private static NodeKeyResolver<ImmutableNode> createResolver() {
83          return mock(NodeKeyResolver.class);
84      }
85  
86      /**
87       * Helper method for testing the behavior of addNodes() if no nodes to be added are provided.
88       *
89       * @param newNodes the collection with new nodes
90       */
91      private void checkAddNodesNoNodes(final Collection<ImmutableNode> newNodes) {
92          final NodeKeyResolver<ImmutableNode> resolver = createResolver();
93          final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_AUTHORS_TREE);
94  
95          model.addNodes(KEY, newNodes, resolver);
96          assertSame(NodeStructureHelper.ROOT_AUTHORS_TREE, model.getRootNode());
97      }
98  
99      /**
100      * Helper method for testing whether nodes removed from the model can no longer be looked up in the parent mapping.
101      *
102      * @param pathToRemove the path to the node to be removed
103      * @param nodeToCheck the node to check in the parent mapping
104      */
105     private void checkClearTreeUpdatedParentMapping(final String pathToRemove, final ImmutableNode nodeToCheck) {
106         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
107         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
108         final QueryResult<ImmutableNode> result = QueryResult.createNodeResult(nodeForKey(model, pathToRemove));
109 
110         when(resolver.resolveKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(Collections.singletonList(result));
111 
112         model.clearTree(KEY, resolver);
113         final NodeHandler<ImmutableNode> nodeHandler = model.getNodeHandler();
114         assertThrows(IllegalArgumentException.class, () -> nodeHandler.getParent(nodeToCheck));
115     }
116 
117     /**
118      * Tests an add nodes operation if an empty collection is passed in.
119      */
120     @Test
121     void testAddNodesEmptyCollection() {
122         checkAddNodesNoNodes(Collections.<ImmutableNode>emptySet());
123     }
124 
125     /**
126      * Tests an add nodes operation if a null collection is passed in.
127      */
128     @Test
129     void testAddNodesNullCollection() {
130         checkAddNodesNoNodes(null);
131     }
132 
133     /**
134      * Tries to add new nodes if the key references an attribute.
135      */
136     @Test
137     void testAddNodesToAttribute() {
138         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
139         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_AUTHORS_TREE);
140         when(resolver.resolveKey(NodeStructureHelper.ROOT_AUTHORS_TREE, KEY, model.getNodeHandler()))
141                 .thenReturn(Collections.singletonList(QueryResult.createAttributeResult(nodeForKey(model, NodeStructureHelper.author(1)), "test")));
142 
143         final ImmutableNode newNode = new ImmutableNode.Builder().name("newNode").create();
144         final Set<ImmutableNode> nodes = Collections.singleton(newNode);
145         assertThrows(IllegalArgumentException.class, () -> model.addNodes(KEY, nodes, resolver));
146     }
147 
148     /**
149      * Tests whether new nodes can be added to an existing node in the model.
150      */
151     @Test
152     void testAddNodesToExistingNode() {
153         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
154         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_AUTHORS_TREE);
155         final String key = NodeStructureHelper.author(0);
156         final ImmutableNode newWork1 = new ImmutableNode.Builder().name("King Lear").create();
157         final ImmutableNode newWork2 = new ImmutableNode.Builder().name("The Taming of the Shrew").create();
158         when(resolver.resolveKey(NodeStructureHelper.ROOT_AUTHORS_TREE, KEY, model.getNodeHandler()))
159                 .thenReturn(Collections.singletonList(QueryResult.createNodeResult(nodeForKey(model, key))));
160 
161         model.addNodes(KEY, Arrays.asList(newWork1, newWork2), resolver);
162         final ImmutableNode node = nodeForKey(model, key);
163         final int size = node.getChildren().size();
164         assertSame(newWork1, node.getChildren().get(size - 2));
165         assertSame(newWork2, node.getChildren().get(size - 1));
166     }
167 
168     /**
169      * Tries to add new nodes to an non-existing key pointing to an attribute.
170      */
171     @Test
172     void testAddNodesToNewAttributeKey() {
173         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
174         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_AUTHORS_TREE);
175         when(resolver.resolveKey(NodeStructureHelper.ROOT_AUTHORS_TREE, KEY, model.getNodeHandler()))
176                 .thenReturn(Collections.<QueryResult<ImmutableNode>>emptyList());
177         when(resolver.resolveAddKey(NodeStructureHelper.ROOT_AUTHORS_TREE, KEY, model.getNodeHandler()))
178                 .thenReturn(new NodeAddData<>(NodeStructureHelper.ROOT_AUTHORS_TREE, "test", true, null));
179 
180         final ImmutableNode newNode = new ImmutableNode.Builder().name("newNode").create();
181         final Set<ImmutableNode> nodes = Collections.singleton(newNode);
182         assertThrows(IllegalArgumentException.class, () -> model.addNodes(KEY, nodes, resolver));
183     }
184 
185     /**
186      * Tests whether nodes can be added to a node which has to be created.
187      */
188     @Test
189     void testAddNodesToNewNode() {
190         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
191         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_AUTHORS_TREE);
192         final String newAuthor = "Goethe";
193         final String newWork = "Faust";
194         final String newPersona = "Mephisto";
195 
196         when(resolver.resolveKey(NodeStructureHelper.ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(new ArrayList<>(0));
197         when(resolver.resolveAddKey(NodeStructureHelper.ROOT_AUTHORS_TREE, KEY, model.getNodeHandler()))
198                 .thenReturn(new NodeAddData<>(NodeStructureHelper.ROOT_AUTHORS_TREE, newWork, false, Arrays.asList(newAuthor)));
199 
200         final ImmutableNode personaNode = new ImmutableNode.Builder().name(newPersona).create();
201         model.addNodes(KEY, Collections.singleton(personaNode), resolver);
202         assertSame(personaNode, nodeForKey(model, newAuthor + "/" + newWork + "/" + newPersona));
203     }
204 
205     /**
206      * Tests whether an attribute property can be added if there are no path nodes.
207      */
208     @Test
209     void testAddPropertyAttributeNoPathNodes() {
210         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
211         final NodeAddData<ImmutableNode> addData = new NodeAddData<>(nodeForKey(ROOT_AUTHORS_TREE, "Shakespeare/The Tempest"), "year", true, null);
212         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
213 
214         when(resolver.resolveAddKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(addData);
215 
216         model.addProperty(KEY, Collections.singleton(1611), resolver);
217         final ImmutableNode node = nodeForKey(model, "Shakespeare/The Tempest");
218         assertEquals(1611, node.getAttributes().get("year"));
219     }
220 
221     /**
222      * Tests whether an attribute can be added if there are some path nodes.
223      */
224     @Test
225     void testAddPropertyAttributeWithPathNodes() {
226         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
227         final NodeAddData<ImmutableNode> addData = new NodeAddData<>(nodeForKey(ROOT_AUTHORS_TREE, "Homer/Ilias"), "number", true,
228             Arrays.asList("scenes", "scene"));
229         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
230 
231         when(resolver.resolveAddKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(addData);
232 
233         model.addProperty(KEY, Collections.singleton(1), resolver);
234         final ImmutableNode node = nodeForKey(model, "Homer/Ilias/scenes/scene");
235         assertEquals(1, node.getAttributes().get("number"));
236     }
237 
238     /**
239      * Tests the special case that an attribute is added with a single path node.
240      */
241     @Test
242     void testAddPropertyAttributeWithSinglePathNode() {
243         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
244         final NodeAddData<ImmutableNode> addData = new NodeAddData<>(nodeForKey(ROOT_AUTHORS_TREE, NodeStructureHelper.author(0)), "year", true,
245             Arrays.asList("dateOfBirth"));
246         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
247 
248         when(resolver.resolveAddKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(addData);
249 
250         final Integer year = 1564;
251         model.addProperty(KEY, Collections.singleton(year), resolver);
252         final ImmutableNode node = nodeForKey(model, "Shakespeare/dateOfBirth");
253         assertEquals(year, node.getAttributes().get("year"));
254     }
255 
256     /**
257      * Tests whether a property can be added if there are no intermediate path nodes.
258      */
259     @Test
260     void testAddPropertyNoPathNodes() {
261         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
262         final NodeAddData<ImmutableNode> addData = new NodeAddData<>(nodeForKey(ROOT_AUTHORS_TREE, "Homer"), "work", false, null);
263         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
264 
265         when(resolver.resolveAddKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(addData);
266 
267         model.addProperty(KEY, Collections.singleton("Odyssee"), resolver);
268         final ImmutableNode node = nodeForKey(model, "Homer/work");
269         assertEquals("Odyssee", node.getValue());
270         assertNotNull(nodeForKey(model, "Homer/Ilias/Hektor"));
271     }
272 
273     /**
274      * Tests an addProperty() operation if no values are provided.
275      */
276     @Test
277     void testAddPropertyNoValues() {
278         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
279         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
280 
281         model.addProperty(KEY, Collections.emptySet(), resolver);
282         assertSame(ROOT_AUTHORS_TREE, model.getRootNode());
283     }
284 
285     /**
286      * Tests whether the parent node references are updated when nodes are added.
287      */
288     @Test
289     void testAddPropertyUpdateParentReferences() {
290         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
291         final NodeAddData<ImmutableNode> addData = new NodeAddData<>(nodeForKey(ROOT_AUTHORS_TREE, "Homer/Ilias"), "location", false,
292             Collections.singleton("locations"));
293         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
294 
295         when(resolver.resolveAddKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(addData);
296 
297         final String[] locations = {"Troja", "Beach", "Olympos"};
298 
299         model.addProperty(KEY, Arrays.asList(locations), resolver);
300         final String[] path = {"Homer", "Ilias", "locations"};
301         final ImmutableNode node = nodeForKey(model, nodePathWithEndNode("location(1)", path));
302         checkPathToRoot(model, node, path);
303     }
304 
305     /**
306      * Tests whether a property can be added to the node model if there are some additional path nodes to be created.
307      */
308     @Test
309     void testAddPropertyWithPathNodes() {
310         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
311         final NodeAddData<ImmutableNode> addData = new NodeAddData<>(nodeForKey(ROOT_AUTHORS_TREE, "Homer/Ilias"), "location", false,
312             Collections.singleton("locations"));
313         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
314 
315         when(resolver.resolveAddKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(addData);
316 
317         final String[] locations = {"Troja", "Beach", "Olympos"};
318 
319         model.addProperty(KEY, Arrays.asList(locations), resolver);
320         final ImmutableNode nodeLocs = nodeForKey(model, "Homer/Ilias/locations");
321         assertEquals(locations.length, nodeLocs.getChildren().size());
322         int idx = 0;
323         for (final ImmutableNode c : nodeLocs) {
324             assertEquals("location", c.getNodeName());
325             assertEquals(locations[idx], c.getValue());
326             assertTrue(c.getChildren().isEmpty());
327             assertTrue(c.getAttributes().isEmpty());
328             idx++;
329         }
330         assertNotNull(nodeForKey(model, "Homer/Ilias/Hektor"));
331     }
332 
333     /**
334      * Tests whether the whole node structure can be cleared.
335      */
336     @Test
337     void testClear() {
338         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
339         model.clear(createResolver());
340         assertFalse(model.getNodeHandler().isDefined(model.getRootNode()));
341         assertEquals(ROOT_AUTHORS_TREE.getNodeName(), model.getRootNode().getNodeName());
342     }
343 
344     /**
345      * Tests whether a property value stored as an attribute can be cleared.
346      */
347     @Test
348     void testClearPropertyAttribute() {
349         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
350         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_PERSONAE_TREE);
351         final String nodeKey = "Prospero/The Tempest/" + NodeStructureHelper.ELEM_ORG_VALUE;
352 
353         when(resolver.resolveKey(model.getRootNode(), KEY, model.getNodeHandler()))
354                 .thenReturn(Collections.singletonList(QueryResult.createAttributeResult(nodeForKey(model, nodeKey), NodeStructureHelper.ATTR_TESTED)));
355 
356         model.clearProperty(KEY, resolver);
357         final ImmutableNode node = nodeForKey(model, nodeKey);
358         assertTrue(node.getAttributes().isEmpty());
359     }
360 
361     /**
362      * Tests whether a property value can be cleared on a node.
363      */
364     @Test
365     void testClearPropertyNode() {
366         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
367         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_PERSONAE_TREE);
368         final String nodeKey = "Ariel/The Tempest/" + NodeStructureHelper.ELEM_ORG_VALUE;
369         when(resolver.resolveKey(model.getRootNode(), KEY, model.getNodeHandler()))
370                 .thenReturn(Collections.singletonList(QueryResult.createNodeResult(nodeForKey(model, nodeKey))));
371 
372         model.clearProperty(KEY, resolver);
373         final ImmutableNode node = nodeForKey(model, nodeKey);
374         assertNull(node.getValue());
375     }
376 
377     /**
378      * Tests clearProperty() for a non existing property.
379      */
380     @Test
381     void testClearPropertyNonExisting() {
382         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
383         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_PERSONAE_TREE);
384 
385         when(resolver.resolveKey(model.getRootNode(), KEY, model.getNodeHandler())).thenReturn(Collections.<QueryResult<ImmutableNode>>emptyList());
386 
387         final TreeData treeDataOld = model.getTreeData();
388         model.clearProperty(KEY, resolver);
389         assertNotNull(model.getNodeHandler().getRootNode());
390         assertSame(treeDataOld, model.getTreeData());
391     }
392 
393     /**
394      * Tests whether attributes can be cleared with clearTree().
395      */
396     @Test
397     void testClearTreeAttribute() {
398         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
399         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_PERSONAE_TREE);
400         final String nodeName = "Puck";
401         final QueryResult<ImmutableNode> result = QueryResult.createAttributeResult(nodeForKey(model, nodeName), NodeStructureHelper.ATTR_AUTHOR);
402 
403         when(resolver.resolveKey(ROOT_PERSONAE_TREE, KEY, model.getNodeHandler())).thenReturn(Collections.singletonList(result));
404 
405         final List<QueryResult<ImmutableNode>> removed = model.clearTree(KEY, resolver);
406         final ImmutableNode node = nodeForKey(model, nodeName);
407         assertTrue(node.getAttributes().isEmpty());
408         assertEquals(1, removed.size());
409         assertTrue(removed.contains(result));
410     }
411 
412     /**
413      * Tests whether the children of removed nodes are also removed from the parent mapping.
414      */
415     @Test
416     void testClearTreeChildrenRemovedFromParentMapping() {
417         final String path = "Homer/Ilias";
418         checkClearTreeUpdatedParentMapping(path, nodeForKey(ROOT_AUTHORS_TREE, path + "/Achilles"));
419     }
420 
421     /**
422      * Tests whether a removed node can no longer be passed to getParent().
423      */
424     @Test
425     void testClearTreeNodeRemovedFromParentMapping() {
426         final String path = "Homer/Ilias/Achilles";
427         checkClearTreeUpdatedParentMapping(path, nodeForKey(ROOT_AUTHORS_TREE, path));
428     }
429 
430     /**
431      * Tests whether a clearTree() operation can be performed if only nodes are involved.
432      */
433     @Test
434     void testClearTreeNodes() {
435         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
436         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
437         final QueryResult<ImmutableNode> result = QueryResult.createNodeResult(nodeForKey(model, "Homer/Ilias/Achilles"));
438 
439         when(resolver.resolveKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(Collections.singletonList(result));
440 
441         final List<QueryResult<ImmutableNode>> removed = model.clearTree(KEY, resolver);
442         final ImmutableNode node = nodeForKey(model, "Homer/Ilias");
443         assertEquals(2, node.getChildren().size());
444         for (final ImmutableNode c : node) {
445             assertNotEquals(result.getNode().getNodeName(), c.getNodeName());
446         }
447         assertEquals(1, removed.size());
448         assertTrue(removed.contains(result));
449     }
450 
451     /**
452      * Tests whether both nodes and attributes can be removed by a clearTree() operation. We remove all attributes and
453      * children from a node. The node becomes undefined and should be removed.
454      */
455     @Test
456     void testClearTreeNodesAndAttributes() {
457         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
458         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_PERSONAE_TREE);
459         final String nodeName = "Puck";
460         final ImmutableNode orgNode = nodeForKey(model, nodeName);
461         final List<QueryResult<ImmutableNode>> results = new ArrayList<>(2);
462         results.add(QueryResult.createAttributeResult(orgNode, NodeStructureHelper.ATTR_AUTHOR));
463         results.add(QueryResult.createNodeResult(orgNode.getChildren().get(0)));
464 
465         when(resolver.resolveKey(ROOT_PERSONAE_TREE, KEY, model.getNodeHandler())).thenReturn(results);
466 
467         model.clearTree(KEY, resolver);
468         assertThrows(NoSuchElementException.class, () -> nodeForKey(model, nodeName));
469     }
470 
471     /**
472      * Tests clearTree() if the passed in key does not exist.
473      */
474     @Test
475     void testClearTreeNonExistingKey() {
476         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
477         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_PERSONAE_TREE);
478 
479         when(resolver.resolveKey(ROOT_PERSONAE_TREE, KEY, model.getNodeHandler())).thenReturn(Collections.<QueryResult<ImmutableNode>>emptyList());
480 
481         final TreeData treeDataOld = model.getTreeData();
482         assertTrue(model.clearTree(KEY, resolver).isEmpty());
483         assertNotNull(model.getNodeHandler().getRootNode());
484         assertSame(treeDataOld, model.getTreeData());
485     }
486 
487     /**
488      * Tests whether undefined nodes are removed from the hierarchy when clearing properties.
489      */
490     @Test
491     void testClearTreeRemoveUndefinedNodes() {
492         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
493         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
494         final ImmutableNode node = nodeForKey(model, "Homer/Ilias");
495         final List<QueryResult<ImmutableNode>> results = new ArrayList<>(node.getChildren().size());
496         for (final ImmutableNode child : node) {
497             results.add(QueryResult.createNodeResult(child));
498         }
499 
500         when(resolver.resolveKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(results);
501 
502         model.clearTree(KEY, resolver);
503         assertEquals(NodeStructureHelper.authorsLength() - 1, model.getRootNode().getChildren().size());
504         for (final ImmutableNode child : model.getRootNode()) {
505             assertNotEquals("Homer", child.getNodeName());
506         }
507     }
508 
509     /**
510      * Tests a clearTree() operation which should yield an empty tree structure.
511      */
512     @Test
513     void testClearTreeResultIsEmpty() {
514         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
515         final ImmutableNode child = new ImmutableNode.Builder().name("child").value("test").create();
516         final ImmutableNode root = new ImmutableNode.Builder(1).addChild(child).create();
517         final InMemoryNodeModel model = new InMemoryNodeModel(root);
518 
519         when(resolver.resolveKey(root, KEY, model.getNodeHandler())).thenReturn(Collections.singletonList(QueryResult.createNodeResult(child)));
520 
521         model.clearTree(KEY, resolver);
522         assertFalse(model.getNodeHandler().isDefined(model.getRootNode()));
523     }
524 
525     /**
526      * Tests whether clearTree() handles the root node in a special way.
527      */
528     @Test
529     void testClearTreeRootNode() {
530         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
531         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
532         final List<QueryResult<ImmutableNode>> results = new ArrayList<>(2);
533         results.add(QueryResult.createNodeResult(nodeForKey(model, NodeStructureHelper.author(0))));
534         results.add(QueryResult.createNodeResult(ROOT_AUTHORS_TREE));
535 
536         when(resolver.resolveKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(results);
537 
538         model.clearTree(KEY, resolver);
539         assertFalse(model.getNodeHandler().isDefined(model.getRootNode()));
540     }
541 
542     /**
543      * Tests whether references to parent nodes are updated correctly when clearing properties.
544      */
545     @Test
546     void testClearTreeUpdateParentReferences() {
547         final String[] path = {"Homer", "Ilias"};
548         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
549         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
550         final QueryResult<ImmutableNode> result = QueryResult.createNodeResult(nodeForKey(model, nodePathWithEndNode("Achilles", path)));
551 
552         when(resolver.resolveKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(Collections.singletonList(result));
553 
554         model.clearTree(KEY, resolver);
555         checkPathToRoot(model, nodeForKey(model, nodePathWithEndNode("Hektor", path)), path);
556     }
557 
558     /**
559      * Tests whether the replacement mapping is automatically compacted if it gets too large.
560      */
561     @Test
562     void testCompactReplacementMapping() {
563         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
564         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
565         final int numberOfOperations = 200;
566         final String key = "Homer/Ilias";
567         for (int i = 0; i < numberOfOperations; i++) {
568             final int index = i;
569             when(resolver.resolveAddKey(any(), eq(KEY), any())).thenAnswer(invocation -> {
570                 assertSame(model.getRootNode(), invocation.getArgument(0));
571                 final ImmutableNode addParent = nodeForKey(model, key);
572                 return new NodeAddData<>(addParent, "Warrior" + index, false, null);
573             });
574         }
575 
576         for (int i = 0; i < numberOfOperations; i++) {
577             model.addProperty(KEY, Collections.singleton(i), resolver);
578         }
579         final ImmutableNode orgNode = nodeForKey(ROOT_AUTHORS_TREE, key);
580         final ImmutableNode changedNode = nodeForKey(model, key);
581         assertEquals(orgNode.getChildren().size() + numberOfOperations, changedNode.getChildren().size());
582         final Map<ImmutableNode, ImmutableNode> replacementMapping = model.getTreeData().copyReplacementMapping();
583         assertTrue(replacementMapping.size() < numberOfOperations);
584     }
585 
586     /**
587      * Tests whether concurrent updates of the model are handled correctly. This test adds a number of authors in parallel.
588      * Then it is checked whether all authors have been added correctly.
589      */
590     @Test
591     void testConcurrentUpdate() throws InterruptedException {
592         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
593         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_AUTHORS_TREE);
594 
595         when(resolver.resolveAddKey(any(), eq(KEY), any())).thenAnswer(invocation -> {
596             final ImmutableNode addParent = invocation.getArgument(0, ImmutableNode.class);
597             return new NodeAddData<>(addParent, "name", false, Collections.singleton("author"));
598         });
599 
600         final CountDownLatch latch = new CountDownLatch(1);
601         final String authorPrefix = "newAuthor";
602         final int threadCount = 32;
603         final Thread[] threads = new Thread[threadCount];
604         for (int i = 0; i < threadCount; i++) {
605             final String authorName = authorPrefix + i;
606             threads[i] = new Thread(() -> {
607                 try {
608                     latch.await();
609                     model.addProperty(KEY, Collections.singleton(authorName), resolver);
610                 } catch (final InterruptedException iex) {
611                     // ignore
612                 }
613             });
614             threads[i].start();
615         }
616         latch.countDown();
617         for (final Thread t : threads) {
618             t.join();
619         }
620 
621         final Pattern patternAuthorName = Pattern.compile(Pattern.quote(authorPrefix) + "(\\d+)");
622         final Set<Integer> indices = new HashSet<>();
623         for (int i = 0; i < threadCount; i++) {
624             final ImmutableNode node = nodeForKey(model, "author(" + i + ")/name");
625             final Matcher m = patternAuthorName.matcher(String.valueOf(node.getValue()));
626             assertTrue(m.matches(), "Wrong value: " + node.getValue());
627             final int idx = Integer.parseInt(m.group(1));
628             assertTrue(idx >= 0 && idx < threadCount, "Invalid index: " + idx);
629             indices.add(idx);
630         }
631         assertEquals(threadCount, indices.size());
632     }
633 
634     /**
635      * Tests whether the model's data can be represented as immutable node objects (which is trivial in this case).
636      */
637     @Test
638     void testGetInMemoryRepresentation() {
639         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_AUTHORS_TREE);
640         assertSame(NodeStructureHelper.ROOT_AUTHORS_TREE, model.getInMemoryRepresentation());
641     }
642 
643     /**
644      * Tests whether the correct node handler is returned.
645      */
646     @Test
647     void testGetNodeHandler() {
648         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_PERSONAE_TREE);
649         assertSame(model.getTreeData(), model.getNodeHandler());
650     }
651 
652     /**
653      * Tests whether the correct root node is returned if a tree was passed at construction time.
654      */
655     @Test
656     void testGetRootNodeFromConstructor() {
657         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
658         assertSame(ROOT_AUTHORS_TREE, model.getRootNode());
659     }
660 
661     /**
662      * Tests whether an undefined default root node is created if none is specified.
663      */
664     @Test
665     void testInitDefaultRoot() {
666         final InMemoryNodeModel model = new InMemoryNodeModel();
667         final ImmutableNode root = model.getRootNode();
668         assertNull(root.getNodeName());
669         assertNull(root.getValue());
670         assertTrue(root.getChildren().isEmpty());
671         assertTrue(root.getAttributes().isEmpty());
672     }
673 
674     /**
675      * Tests whether setProperty() can handle changes in node values.
676      */
677     @Test
678     void testSetPropertyChangedValues() {
679         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
680         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_PERSONAE_TREE);
681         final String nodeKey = "Ariel/The Tempest/" + NodeStructureHelper.ELEM_ORG_VALUE;
682         final Map<QueryResult<ImmutableNode>, Object> changedValues = new HashMap<>();
683         final String newValue = "of course";
684         final ImmutableNode changedNode = nodeForKey(model, nodeKey);
685         changedValues.put(QueryResult.createAttributeResult(changedNode, NodeStructureHelper.ATTR_TESTED), newValue);
686         changedValues.put(QueryResult.createNodeResult(changedNode), newValue);
687         final NodeUpdateData<ImmutableNode> updateData = new NodeUpdateData<>(changedValues, null, null, null);
688 
689         when(resolver.resolveUpdateKey(NodeStructureHelper.ROOT_PERSONAE_TREE, KEY, this, model.getNodeHandler())).thenReturn(updateData);
690 
691         model.setProperty(KEY, this, resolver);
692         final ImmutableNode node = nodeForKey(model, nodeKey);
693         assertEquals(newValue, node.getAttributes().get(NodeStructureHelper.ATTR_TESTED));
694         assertEquals(newValue, node.getValue());
695     }
696 
697     /**
698      * Tests whether setProperty() can handle nodes to be cleared.
699      */
700     @Test
701     void testSetPropertyClearValues() {
702         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
703         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_PERSONAE_TREE);
704         final String nodeKey = "Ariel/The Tempest/" + NodeStructureHelper.ELEM_ORG_VALUE;
705         final NodeUpdateData<ImmutableNode> updateData = new NodeUpdateData<>(null, null,
706             Collections.singletonList(QueryResult.createNodeResult(nodeForKey(model, nodeKey))), null);
707 
708         when(resolver.resolveUpdateKey(NodeStructureHelper.ROOT_PERSONAE_TREE, KEY, this, model.getNodeHandler())).thenReturn(updateData);
709 
710         model.setProperty(KEY, this, resolver);
711         final ImmutableNode node = nodeForKey(model, nodeKey);
712         assertNull(node.getValue());
713     }
714 
715     /**
716      * Tests whether setProperty() can handle newly added values.
717      */
718     @Test
719     void testSetPropertyNewValues() {
720         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
721         final NodeAddData<ImmutableNode> addData = new NodeAddData<>(nodeForKey(ROOT_AUTHORS_TREE, "Homer"), "work", false, null);
722         final NodeUpdateData<ImmutableNode> updateData = new NodeUpdateData<>(null, Collections.<Object>singleton("Odyssee"), null, KEY);
723         final InMemoryNodeModel model = new InMemoryNodeModel(ROOT_AUTHORS_TREE);
724 
725         when(resolver.resolveUpdateKey(ROOT_AUTHORS_TREE, KEY, this, model.getNodeHandler())).thenReturn(updateData);
726         when(resolver.resolveAddKey(ROOT_AUTHORS_TREE, KEY, model.getNodeHandler())).thenReturn(addData);
727 
728         model.setProperty(KEY, this, resolver);
729         final ImmutableNode node = nodeForKey(model, "Homer/work");
730         assertEquals("Odyssee", node.getValue());
731         assertNotNull(nodeForKey(model, "Homer/Ilias/Hektor"));
732     }
733 
734     /**
735      * Tests a set property operation which is a no-op.
736      */
737     @Test
738     void testSetPropertyNoChanges() {
739         final NodeKeyResolver<ImmutableNode> resolver = createResolver();
740         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_PERSONAE_TREE);
741 
742         when(resolver.resolveUpdateKey(NodeStructureHelper.ROOT_PERSONAE_TREE, KEY, this, model.getNodeHandler()))
743                 .thenReturn(new NodeUpdateData<>(null, null, null, null));
744 
745         model.setProperty(KEY, this, resolver);
746         assertSame(NodeStructureHelper.ROOT_PERSONAE_TREE, model.getRootNode());
747     }
748 
749     /**
750      * Tests whether a new root node can be set.
751      */
752     @Test
753     void testSetRoot() {
754         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_PERSONAE_TREE);
755         model.setRootNode(NodeStructureHelper.ROOT_AUTHORS_TREE);
756         assertSame(NodeStructureHelper.ROOT_AUTHORS_TREE, model.getRootNode());
757         final ImmutableNode node = nodeForKey(model, "Homer/Ilias");
758         assertEquals(nodeForKey(model, "Homer"), model.getNodeHandler().getParent(node));
759     }
760 
761     /**
762      * Tests whether the root node can be set to null.
763      */
764     @Test
765     void testSetRootNull() {
766         final InMemoryNodeModel model = new InMemoryNodeModel(NodeStructureHelper.ROOT_PERSONAE_TREE);
767         model.setRootNode(null);
768         final ImmutableNode rootNode = model.getRootNode();
769         assertTrue(rootNode.getChildren().isEmpty());
770     }
771 }