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;
18  
19  import static org.junit.jupiter.api.Assertions.assertEquals;
20  import static org.junit.jupiter.api.Assertions.assertInstanceOf;
21  import static org.junit.jupiter.api.Assertions.assertNotSame;
22  import static org.junit.jupiter.api.Assertions.assertSame;
23  import static org.junit.jupiter.api.Assertions.assertThrows;
24  import static org.junit.jupiter.api.Assertions.assertTrue;
25  import static org.mockito.Mockito.mock;
26  import static org.mockito.Mockito.verify;
27  import static org.mockito.Mockito.verifyNoMoreInteractions;
28  import static org.mockito.Mockito.when;
29  
30  import java.util.ArrayList;
31  import java.util.Arrays;
32  import java.util.Collections;
33  import java.util.HashSet;
34  import java.util.List;
35  import java.util.NoSuchElementException;
36  import java.util.Set;
37  
38  import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
39  import org.apache.commons.configuration2.convert.DisabledListDelimiterHandler;
40  import org.apache.commons.configuration2.convert.ListDelimiterHandler;
41  import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
42  import org.apache.commons.configuration2.tree.ImmutableNode;
43  import org.apache.commons.configuration2.tree.InMemoryNodeModel;
44  import org.apache.commons.configuration2.tree.NodeSelector;
45  import org.apache.commons.configuration2.tree.NodeStructureHelper;
46  import org.apache.commons.configuration2.tree.TrackedNodeModel;
47  import org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine;
48  import org.junit.jupiter.api.BeforeEach;
49  import org.junit.jupiter.api.Test;
50  
51  /**
52   * Test case for SubnodeConfiguration.
53   */
54  public class TestSubnodeConfiguration {
55      /** The key used for the SubnodeConfiguration. */
56      private static final String SUB_KEY = "tables.table(0)";
57  
58      /** The selector used by the test configuration. */
59      private static final NodeSelector SELECTOR = new NodeSelector(SUB_KEY);
60  
61      /**
62       * Adds a tree structure to the root node of the given configuration.
63       *
64       * @param configuration the configuration
65       * @param root the root of the tree structure to be added
66       */
67      private static void appendTree(final BaseHierarchicalConfiguration configuration, final ImmutableNode root) {
68          configuration.addNodes(null, Collections.singleton(root));
69      }
70  
71      /**
72       * Initializes the parent configuration. This method creates the typical structure of tables and fields nodes.
73       *
74       * @return the parent configuration
75       */
76      private static BaseHierarchicalConfiguration setUpParentConfig() {
77          final BaseHierarchicalConfiguration conf = new BaseHierarchicalConfiguration();
78          appendTree(conf, NodeStructureHelper.ROOT_TABLES_TREE);
79          return conf;
80      }
81  
82      /** The parent configuration. */
83      private BaseHierarchicalConfiguration parent;
84  
85      /** The subnode configuration to be tested. */
86      private SubnodeConfiguration config;
87  
88      /**
89       * Helper method for testing interpolation facilities between a sub and its parent configuration.
90       *
91       * @param withUpdates the supports updates flag
92       */
93      private void checkInterpolationFromConfigurationAt(final boolean withUpdates) {
94          parent.addProperty("base.dir", "/home/foo");
95          parent.addProperty("test.absolute.dir.dir1", "${base.dir}/path1");
96          parent.addProperty("test.absolute.dir.dir2", "${base.dir}/path2");
97          parent.addProperty("test.absolute.dir.dir3", "${base.dir}/path3");
98  
99          final Configuration sub = parent.configurationAt("test.absolute.dir", withUpdates);
100         for (int i = 1; i < 4; i++) {
101             assertEquals("/home/foo/path" + i, parent.getString("test.absolute.dir.dir" + i));
102             assertEquals("/home/foo/path" + i, sub.getString("dir" + i));
103         }
104     }
105 
106     /**
107      * Checks whether the sub configuration has the expected content.
108      */
109     private void checkSubConfigContent() {
110         assertEquals(NodeStructureHelper.table(0), config.getString("name"));
111         final List<Object> fields = config.getList("fields.field.name");
112 
113         final List<String> expected = new ArrayList<>();
114         for (int i = 0; i < NodeStructureHelper.fieldsLength(0); i++) {
115             expected.add(NodeStructureHelper.field(0, i));
116         }
117         assertEquals(expected, fields);
118     }
119 
120     @BeforeEach
121     public void setUp() throws Exception {
122         parent = setUpParentConfig();
123     }
124 
125     /**
126      * Performs a standard initialization of the subnode config to test.
127      */
128     private void setUpSubnodeConfig() {
129         setUpSubnodeConfig(SUB_KEY);
130     }
131 
132     /**
133      * Initializes the test configuration using the specified key.
134      *
135      * @param key the key
136      */
137     private void setUpSubnodeConfig(final String key) {
138         config = (SubnodeConfiguration) parent.configurationAt(key, true);
139     }
140 
141     /**
142      * Sets up the tracked model for the sub configuration.
143      *
144      * @param selector the selector
145      * @return the tracked model
146      */
147     private TrackedNodeModel setUpTrackedModel(final NodeSelector selector) {
148         final InMemoryNodeModel parentModel = (InMemoryNodeModel) parent.getModel();
149         parentModel.trackNode(selector, parent);
150         return new TrackedNodeModel(parent, selector, true);
151     }
152 
153     /**
154      * Tests adding of properties.
155      */
156     @Test
157     public void testAddProperty() {
158         setUpSubnodeConfig();
159         config.addProperty("[@table-type]", "test");
160         assertEquals("test", parent.getString("tables.table(0)[@table-type]"));
161 
162         parent.addProperty("tables.table(0).fields.field(-1).name", "newField");
163         final List<Object> fields = config.getList("fields.field.name");
164         assertEquals(NodeStructureHelper.fieldsLength(0) + 1, fields.size());
165         assertEquals("newField", fields.get(fields.size() - 1));
166     }
167 
168     /**
169      * Tests whether a clone of a sub configuration can be created.
170      */
171     @Test
172     public void testClone() {
173         setUpSubnodeConfig();
174         final SubnodeConfiguration copy = (SubnodeConfiguration) config.clone();
175         assertNotSame(config.getModel(), copy.getModel());
176         final TrackedNodeModel subModel = (TrackedNodeModel) copy.getModel();
177         assertEquals(SELECTOR, subModel.getSelector());
178         final InMemoryNodeModel parentModel = (InMemoryNodeModel) parent.getModel();
179         assertEquals(parentModel, subModel.getParentModel());
180 
181         // Check whether the track count was increased
182         parentModel.untrackNode(SELECTOR);
183         parentModel.untrackNode(SELECTOR);
184         assertTrue(subModel.isReleaseTrackedNodeOnFinalize());
185     }
186 
187     /**
188      * Tests whether the configuration can be closed.
189      */
190     @Test
191     public void testClose() {
192         final TrackedNodeModel model = mock(TrackedNodeModel.class);
193 
194         when(model.getSelector()).thenReturn(SELECTOR);
195 
196         final SubnodeConfiguration config = new SubnodeConfiguration(parent, model);
197         config.close();
198 
199         verify(model).getSelector();
200         verify(model).close();
201         verifyNoMoreInteractions(model);
202     }
203 
204     /**
205      * Tests the configurationAt() method if updates are not supported.
206      */
207     @Test
208     public void testConfiguarationAtNoUpdates() {
209         setUpSubnodeConfig();
210         final HierarchicalConfiguration<ImmutableNode> sub2 = config.configurationAt("fields.field(1)");
211         assertEquals(NodeStructureHelper.field(0, 1), sub2.getString("name"));
212         parent.setProperty("tables.table(0).fields.field(1).name", "otherName");
213         assertEquals(NodeStructureHelper.field(0, 1), sub2.getString("name"));
214     }
215 
216     /**
217      * Tests configurationAt() if updates are supported.
218      */
219     @Test
220     public void testConfigurationAtWithUpdateSupport() {
221         setUpSubnodeConfig();
222         final SubnodeConfiguration sub2 = (SubnodeConfiguration) config.configurationAt("fields.field(1)", true);
223         assertEquals(NodeStructureHelper.field(0, 1), sub2.getString("name"));
224         assertEquals(config, sub2.getParent());
225     }
226 
227     /**
228      * Tests listing the defined keys.
229      */
230     @Test
231     public void testGetKeys() {
232         setUpSubnodeConfig();
233         final Set<String> keys = new HashSet<>(ConfigurationAssert.keysToList(config));
234         assertEquals(new HashSet<>(Arrays.asList("name", "fields.field.name")), keys);
235     }
236 
237     /**
238      * Tests whether a correct node model is returned for the sub configuration. This test is related to CONFIGURATION-670.
239      */
240     @Test
241     public void testGetNodeModel() {
242         setUpSubnodeConfig();
243         final InMemoryNodeModel nodeModel = config.getNodeModel();
244 
245         assertEquals("table", nodeModel.getNodeHandler().getRootNode().getNodeName());
246     }
247 
248     /**
249      * Tests if properties of the sub node can be accessed.
250      */
251     @Test
252     public void testGetProperties() {
253         setUpSubnodeConfig();
254         checkSubConfigContent();
255     }
256 
257     /**
258      * Tests creation of a subnode config.
259      */
260     @Test
261     public void testInitSubNodeConfig() {
262         setUpSubnodeConfig();
263         assertSame(NodeStructureHelper.nodeForKey(parent.getModel().getNodeHandler().getRootNode(), "tables/table(0)"),
264                 config.getModel().getNodeHandler().getRootNode());
265         assertSame(parent, config.getParent());
266     }
267 
268     /**
269      * Tests constructing a subnode configuration with a null node model. This should cause an exception.
270      */
271     @Test
272     public void testInitSubNodeConfigWithNullNode() {
273         assertThrows(IllegalArgumentException.class, () -> new SubnodeConfiguration(parent, null));
274     }
275 
276     /**
277      * Tests constructing a subnode configuration with a null parent. This should cause an exception.
278      */
279     @Test
280     public void testInitSubNodeConfigWithNullParent() {
281         final TrackedNodeModel model = setUpTrackedModel(SELECTOR);
282         assertThrows(IllegalArgumentException.class, () -> new SubnodeConfiguration(null, model));
283     }
284 
285     /**
286      * Tests interpolation features. The subnode config should use its parent for interpolation.
287      */
288     @Test
289     public void testInterpolation() {
290         parent.addProperty("tablespaces.tablespace.name", "default");
291         parent.addProperty("tablespaces.tablespace(-1).name", "test");
292         parent.addProperty("tables.table(0).tablespace", "${tablespaces.tablespace(0).name}");
293         assertEquals("default", parent.getString("tables.table(0).tablespace"));
294 
295         setUpSubnodeConfig();
296         assertEquals("default", config.getString("tablespace"));
297     }
298 
299     /**
300      * Tests whether interpolation works for a sub configuration obtained via configurationAt() if updates are not
301      * supported.
302      */
303     @Test
304     public void testInterpolationFromConfigurationAtNoUpdateSupport() {
305         checkInterpolationFromConfigurationAt(false);
306     }
307 
308     /**
309      * Tests whether interpolation works for a sub configuration obtained via configurationAt() if updates are supported.
310      */
311     @Test
312     public void testInterpolationFromConfigurationAtWithUpdateSupport() {
313         checkInterpolationFromConfigurationAt(true);
314     }
315 
316     /**
317      * Tests manipulating the interpolator.
318      */
319     @Test
320     public void testInterpolator() {
321         parent.addProperty("tablespaces.tablespace.name", "default");
322         parent.addProperty("tablespaces.tablespace(-1).name", "test");
323 
324         setUpSubnodeConfig();
325         InterpolationTestHelper.testGetInterpolator(config);
326     }
327 
328     /**
329      * An additional test for interpolation when the configurationAt() method is involved for a local interpolation.
330      */
331     @Test
332     public void testLocalInterpolationFromConfigurationAt() {
333         parent.addProperty("base.dir", "/home/foo");
334         parent.addProperty("test.absolute.dir.dir1", "${base.dir}/path1");
335         parent.addProperty("test.absolute.dir.dir2", "${dir1}");
336 
337         final Configuration sub = parent.configurationAt("test.absolute.dir");
338         assertEquals("/home/foo/path1", sub.getString("dir1"));
339         assertEquals("/home/foo/path1", sub.getString("dir2"));
340     }
341 
342     @Test
343     public void testLocalLookupsInInterpolatorAreInherited() {
344         parent.addProperty("tablespaces.tablespace.name", "default");
345         parent.addProperty("tablespaces.tablespace(-1).name", "test");
346         parent.addProperty("tables.table(0).var", "${brackets:x}");
347 
348         final ConfigurationInterpolator interpolator = parent.getInterpolator();
349         interpolator.registerLookup("brackets", key -> "(" + key + ")");
350         setUpSubnodeConfig();
351         assertEquals("(x)", config.getString("var", ""));
352     }
353 
354     /**
355      * Tests a manipulation of the parent configuration that causes the subnode configuration to become invalid. In this
356      * case the sub config should be detached and keep its old values.
357      */
358     @Test
359     public void testParentChangeDetach() {
360         setUpSubnodeConfig();
361         parent.clear();
362         checkSubConfigContent();
363     }
364 
365     /**
366      * Tests detaching a subnode configuration if an exception is thrown during reconstruction. This can happen e.g. if the
367      * expression engine is changed for the parent.
368      */
369     @Test
370     public void testParentChangeDetatchException() {
371         setUpSubnodeConfig();
372         parent.setExpressionEngine(new XPathExpressionEngine());
373         parent.addProperty("newProp", "value");
374         checkSubConfigContent();
375     }
376 
377     /**
378      * Tests changing the expression engine.
379      */
380     @Test
381     public void testSetExpressionEngine() {
382         parent.setExpressionEngine(new XPathExpressionEngine());
383         setUpSubnodeConfig("tables/table[1]");
384         assertEquals(NodeStructureHelper.field(0, 1), config.getString("fields/field[2]/name"));
385         final Set<String> keys = ConfigurationAssert.keysToSet(config);
386         assertEquals(new HashSet<>(Arrays.asList("name", "fields/field/name")), keys);
387         config.setExpressionEngine(null);
388         assertInstanceOf(XPathExpressionEngine.class, parent.getExpressionEngine());
389     }
390 
391     /**
392      * Tests manipulating the list delimiter handler. This object is derived from the parent.
393      */
394     @Test
395     public void testSetListDelimiterHandler() {
396         final ListDelimiterHandler handler1 = new DefaultListDelimiterHandler('/');
397         final ListDelimiterHandler handler2 = new DefaultListDelimiterHandler(';');
398         parent.setListDelimiterHandler(handler1);
399         setUpSubnodeConfig();
400         parent.setListDelimiterHandler(handler2);
401         assertEquals(handler1, config.getListDelimiterHandler());
402         config.addProperty("newProp", "test1,test2/test3");
403         assertEquals("test1,test2", parent.getString("tables.table(0).newProp"));
404         config.setListDelimiterHandler(DisabledListDelimiterHandler.INSTANCE);
405         assertEquals(handler2, parent.getListDelimiterHandler());
406     }
407 
408     /**
409      * Tests setting of properties in both the parent and the subnode configuration and whether the changes are visible to
410      * each other.
411      */
412     @Test
413     public void testSetProperty() {
414         setUpSubnodeConfig();
415         config.setProperty(null, "testTable");
416         config.setProperty("name", NodeStructureHelper.table(0) + "_tested");
417         assertEquals("testTable", parent.getString("tables.table(0)"));
418         assertEquals(NodeStructureHelper.table(0) + "_tested", parent.getString("tables.table(0).name"));
419 
420         parent.setProperty("tables.table(0).fields.field(1).name", "testField");
421         assertEquals("testField", config.getString("fields.field(1).name"));
422     }
423 
424     /**
425      * Tests setting the exception on missing flag. The subnode config obtains this flag from its parent.
426      */
427     @Test
428     public void testSetThrowExceptionOnMissing() {
429         parent.setThrowExceptionOnMissing(true);
430         setUpSubnodeConfig();
431         assertTrue(config.isThrowExceptionOnMissing());
432         assertThrows(NoSuchElementException.class, () -> config.getString("non existing key"));
433     }
434 
435     /**
436      * Tests whether the exception flag can be set independently from the parent.
437      */
438     @Test
439     public void testSetThrowExceptionOnMissingAffectsParent() {
440         parent.setThrowExceptionOnMissing(true);
441         setUpSubnodeConfig();
442         config.setThrowExceptionOnMissing(false);
443         assertTrue(parent.isThrowExceptionOnMissing());
444     }
445 }