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