blob: 8eec56bd08037b77f46438a4834b7b4f265edf55 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.configuration2;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
import org.apache.commons.configuration2.convert.DisabledListDelimiterHandler;
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
import org.apache.commons.configuration2.interpol.ConfigurationInterpolator;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.commons.configuration2.tree.InMemoryNodeModel;
import org.apache.commons.configuration2.tree.NodeSelector;
import org.apache.commons.configuration2.tree.NodeStructureHelper;
import org.apache.commons.configuration2.tree.TrackedNodeModel;
import org.apache.commons.configuration2.tree.xpath.XPathExpressionEngine;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Test case for SubnodeConfiguration.
*/
public class TestSubnodeConfiguration {
/** The key used for the SubnodeConfiguration. */
private static final String SUB_KEY = "tables.table(0)";
/** The selector used by the test configuration. */
private static final NodeSelector SELECTOR = new NodeSelector(SUB_KEY);
/**
* Adds a tree structure to the root node of the given configuration.
*
* @param configuration the configuration
* @param root the root of the tree structure to be added
*/
private static void appendTree(final BaseHierarchicalConfiguration configuration, final ImmutableNode root) {
configuration.addNodes(null, Collections.singleton(root));
}
/**
* Initializes the parent configuration. This method creates the typical structure of tables and fields nodes.
*
* @return the parent configuration
*/
private static BaseHierarchicalConfiguration setUpParentConfig() {
final BaseHierarchicalConfiguration conf = new BaseHierarchicalConfiguration();
appendTree(conf, NodeStructureHelper.ROOT_TABLES_TREE);
return conf;
}
/** The parent configuration. */
private BaseHierarchicalConfiguration parent;
/** The subnode configuration to be tested. */
private SubnodeConfiguration config;
/**
* Helper method for testing interpolation facilities between a sub and its parent configuration.
*
* @param withUpdates the supports updates flag
*/
private void checkInterpolationFromConfigurationAt(final boolean withUpdates) {
parent.addProperty("base.dir", "/home/foo");
parent.addProperty("test.absolute.dir.dir1", "${base.dir}/path1");
parent.addProperty("test.absolute.dir.dir2", "${base.dir}/path2");
parent.addProperty("test.absolute.dir.dir3", "${base.dir}/path3");
final Configuration sub = parent.configurationAt("test.absolute.dir", withUpdates);
for (int i = 1; i < 4; i++) {
assertEquals("/home/foo/path" + i, parent.getString("test.absolute.dir.dir" + i));
assertEquals("/home/foo/path" + i, sub.getString("dir" + i));
}
}
/**
* Checks whether the sub configuration has the expected content.
*/
private void checkSubConfigContent() {
assertEquals(NodeStructureHelper.table(0), config.getString("name"));
final List<Object> fields = config.getList("fields.field.name");
final List<String> expected = new ArrayList<>();
for (int i = 0; i < NodeStructureHelper.fieldsLength(0); i++) {
expected.add(NodeStructureHelper.field(0, i));
}
assertEquals(expected, fields);
}
@BeforeEach
public void setUp() throws Exception {
parent = setUpParentConfig();
}
/**
* Performs a standard initialization of the subnode config to test.
*/
private void setUpSubnodeConfig() {
setUpSubnodeConfig(SUB_KEY);
}
/**
* Initializes the test configuration using the specified key.
*
* @param key the key
*/
private void setUpSubnodeConfig(final String key) {
config = (SubnodeConfiguration) parent.configurationAt(key, true);
}
/**
* Sets up the tracked model for the sub configuration.
*
* @param selector the selector
* @return the tracked model
*/
private TrackedNodeModel setUpTrackedModel(final NodeSelector selector) {
final InMemoryNodeModel parentModel = (InMemoryNodeModel) parent.getModel();
parentModel.trackNode(selector, parent);
return new TrackedNodeModel(parent, selector, true);
}
/**
* Tests adding of properties.
*/
@Test
public void testAddProperty() {
setUpSubnodeConfig();
config.addProperty("[@table-type]", "test");
assertEquals("test", parent.getString("tables.table(0)[@table-type]"));
parent.addProperty("tables.table(0).fields.field(-1).name", "newField");
final List<Object> fields = config.getList("fields.field.name");
assertEquals(NodeStructureHelper.fieldsLength(0) + 1, fields.size());
assertEquals("newField", fields.get(fields.size() - 1));
}
/**
* Tests whether a clone of a sub configuration can be created.
*/
@Test
public void testClone() {
setUpSubnodeConfig();
final SubnodeConfiguration copy = (SubnodeConfiguration) config.clone();
assertNotSame(config.getModel(), copy.getModel());
final TrackedNodeModel subModel = (TrackedNodeModel) copy.getModel();
assertEquals(SELECTOR, subModel.getSelector());
final InMemoryNodeModel parentModel = (InMemoryNodeModel) parent.getModel();
assertEquals(parentModel, subModel.getParentModel());
// Check whether the track count was increased
parentModel.untrackNode(SELECTOR);
parentModel.untrackNode(SELECTOR);
assertTrue(subModel.isReleaseTrackedNodeOnFinalize());
}
/**
* Tests whether the configuration can be closed.
*/
@Test
public void testClose() {
final TrackedNodeModel model = mock(TrackedNodeModel.class);
when(model.getSelector()).thenReturn(SELECTOR);
final SubnodeConfiguration config = new SubnodeConfiguration(parent, model);
config.close();
verify(model).getSelector();
verify(model).close();
verifyNoMoreInteractions(model);
}
/**
* Tests the configurationAt() method if updates are not supported.
*/
@Test
public void testConfiguarationAtNoUpdates() {
setUpSubnodeConfig();
final HierarchicalConfiguration<ImmutableNode> sub2 = config.configurationAt("fields.field(1)");
assertEquals(NodeStructureHelper.field(0, 1), sub2.getString("name"));
parent.setProperty("tables.table(0).fields.field(1).name", "otherName");
assertEquals(NodeStructureHelper.field(0, 1), sub2.getString("name"));
}
/**
* Tests configurationAt() if updates are supported.
*/
@Test
public void testConfigurationAtWithUpdateSupport() {
setUpSubnodeConfig();
final SubnodeConfiguration sub2 = (SubnodeConfiguration) config.configurationAt("fields.field(1)", true);
assertEquals(NodeStructureHelper.field(0, 1), sub2.getString("name"));
assertEquals(config, sub2.getParent());
}
/**
* Tests listing the defined keys.
*/
@Test
public void testGetKeys() {
setUpSubnodeConfig();
final Set<String> keys = new HashSet<>(ConfigurationAssert.keysToList(config));
assertEquals(new HashSet<>(Arrays.asList("name", "fields.field.name")), keys);
}
/**
* Tests whether a correct node model is returned for the sub configuration. This test is related to CONFIGURATION-670.
*/
@Test
public void testGetNodeModel() {
setUpSubnodeConfig();
final InMemoryNodeModel nodeModel = config.getNodeModel();
assertEquals("table", nodeModel.getNodeHandler().getRootNode().getNodeName());
}
/**
* Tests if properties of the sub node can be accessed.
*/
@Test
public void testGetProperties() {
setUpSubnodeConfig();
checkSubConfigContent();
}
/**
* Tests creation of a subnode config.
*/
@Test
public void testInitSubNodeConfig() {
setUpSubnodeConfig();
assertSame(NodeStructureHelper.nodeForKey(parent.getModel().getNodeHandler().getRootNode(), "tables/table(0)"),
config.getModel().getNodeHandler().getRootNode());
assertSame(parent, config.getParent());
}
/**
* Tests constructing a subnode configuration with a null node model. This should cause an exception.
*/
@Test
public void testInitSubNodeConfigWithNullNode() {
assertThrows(IllegalArgumentException.class, () -> new SubnodeConfiguration(parent, null));
}
/**
* Tests constructing a subnode configuration with a null parent. This should cause an exception.
*/
@Test
public void testInitSubNodeConfigWithNullParent() {
final TrackedNodeModel model = setUpTrackedModel(SELECTOR);
assertThrows(IllegalArgumentException.class, () -> new SubnodeConfiguration(null, model));
}
/**
* Tests interpolation features. The subnode config should use its parent for interpolation.
*/
@Test
public void testInterpolation() {
parent.addProperty("tablespaces.tablespace.name", "default");
parent.addProperty("tablespaces.tablespace(-1).name", "test");
parent.addProperty("tables.table(0).tablespace", "${tablespaces.tablespace(0).name}");
assertEquals("default", parent.getString("tables.table(0).tablespace"));
setUpSubnodeConfig();
assertEquals("default", config.getString("tablespace"));
}
/**
* Tests whether interpolation works for a sub configuration obtained via configurationAt() if updates are not
* supported.
*/
@Test
public void testInterpolationFromConfigurationAtNoUpdateSupport() {
checkInterpolationFromConfigurationAt(false);
}
/**
* Tests whether interpolation works for a sub configuration obtained via configurationAt() if updates are supported.
*/
@Test
public void testInterpolationFromConfigurationAtWithUpdateSupport() {
checkInterpolationFromConfigurationAt(true);
}
/**
* Tests manipulating the interpolator.
*/
@Test
public void testInterpolator() {
parent.addProperty("tablespaces.tablespace.name", "default");
parent.addProperty("tablespaces.tablespace(-1).name", "test");
setUpSubnodeConfig();
InterpolationTestHelper.testGetInterpolator(config);
}
/**
* An additional test for interpolation when the configurationAt() method is involved for a local interpolation.
*/
@Test
public void testLocalInterpolationFromConfigurationAt() {
parent.addProperty("base.dir", "/home/foo");
parent.addProperty("test.absolute.dir.dir1", "${base.dir}/path1");
parent.addProperty("test.absolute.dir.dir2", "${dir1}");
final Configuration sub = parent.configurationAt("test.absolute.dir");
assertEquals("/home/foo/path1", sub.getString("dir1"));
assertEquals("/home/foo/path1", sub.getString("dir2"));
}
@Test
public void testLocalLookupsInInterpolatorAreInherited() {
parent.addProperty("tablespaces.tablespace.name", "default");
parent.addProperty("tablespaces.tablespace(-1).name", "test");
parent.addProperty("tables.table(0).var", "${brackets:x}");
final ConfigurationInterpolator interpolator = parent.getInterpolator();
interpolator.registerLookup("brackets", key -> "(" + key + ")");
setUpSubnodeConfig();
assertEquals("(x)", config.getString("var", ""));
}
/**
* Tests a manipulation of the parent configuration that causes the subnode configuration to become invalid. In this
* case the sub config should be detached and keep its old values.
*/
@Test
public void testParentChangeDetach() {
setUpSubnodeConfig();
parent.clear();
checkSubConfigContent();
}
/**
* Tests detaching a subnode configuration if an exception is thrown during reconstruction. This can happen e.g. if the
* expression engine is changed for the parent.
*/
@Test
public void testParentChangeDetatchException() {
setUpSubnodeConfig();
parent.setExpressionEngine(new XPathExpressionEngine());
parent.addProperty("newProp", "value");
checkSubConfigContent();
}
/**
* Tests changing the expression engine.
*/
@Test
public void testSetExpressionEngine() {
parent.setExpressionEngine(new XPathExpressionEngine());
setUpSubnodeConfig("tables/table[1]");
assertEquals(NodeStructureHelper.field(0, 1), config.getString("fields/field[2]/name"));
final Set<String> keys = ConfigurationAssert.keysToSet(config);
assertEquals(new HashSet<>(Arrays.asList("name", "fields/field/name")), keys);
config.setExpressionEngine(null);
assertInstanceOf(XPathExpressionEngine.class, parent.getExpressionEngine());
}
/**
* Tests manipulating the list delimiter handler. This object is derived from the parent.
*/
@Test
public void testSetListDelimiterHandler() {
final ListDelimiterHandler handler1 = new DefaultListDelimiterHandler('/');
final ListDelimiterHandler handler2 = new DefaultListDelimiterHandler(';');
parent.setListDelimiterHandler(handler1);
setUpSubnodeConfig();
parent.setListDelimiterHandler(handler2);
assertEquals(handler1, config.getListDelimiterHandler());
config.addProperty("newProp", "test1,test2/test3");
assertEquals("test1,test2", parent.getString("tables.table(0).newProp"));
config.setListDelimiterHandler(DisabledListDelimiterHandler.INSTANCE);
assertEquals(handler2, parent.getListDelimiterHandler());
}
/**
* Tests setting of properties in both the parent and the subnode configuration and whether the changes are visible to
* each other.
*/
@Test
public void testSetProperty() {
setUpSubnodeConfig();
config.setProperty(null, "testTable");
config.setProperty("name", NodeStructureHelper.table(0) + "_tested");
assertEquals("testTable", parent.getString("tables.table(0)"));
assertEquals(NodeStructureHelper.table(0) + "_tested", parent.getString("tables.table(0).name"));
parent.setProperty("tables.table(0).fields.field(1).name", "testField");
assertEquals("testField", config.getString("fields.field(1).name"));
}
/**
* Tests setting the exception on missing flag. The subnode config obtains this flag from its parent.
*/
@Test
public void testSetThrowExceptionOnMissing() {
parent.setThrowExceptionOnMissing(true);
setUpSubnodeConfig();
assertTrue(config.isThrowExceptionOnMissing());
assertThrows(NoSuchElementException.class, () -> config.getString("non existing key"));
}
/**
* Tests whether the exception flag can be set independently from the parent.
*/
@Test
public void testSetThrowExceptionOnMissingAffectsParent() {
parent.setThrowExceptionOnMissing(true);
setUpSubnodeConfig();
config.setThrowExceptionOnMissing(false);
assertTrue(parent.isThrowExceptionOnMissing());
}
}