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