blob: 3329a81999a11d66c2dffc22120100ee46b3f6a4 [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
*
* https://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.tree;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
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 java.util.Arrays;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Test class for DefaultExpressionEngine.
*/
public class TestDefaultExpressionEngine {
/** Stores the names of the test nodes representing tables. */
private static final String[] TABLES = {"users", "documents"};
/** Stores the types of the test table nodes. */
private static final String[] TAB_TYPES = {"system", "application"};
/** Test data fields for the node hierarchy. */
private static final String[][] FIELDS = {{"uid", "uname", "firstName", "lastName", "email"}, {"docid", "name", "creationDate", "authorID", "version"}};
/** The root of a hierarchy with test nodes. */
private static ImmutableNode root;
/** A node handler for the hierarchy of test nodes. */
private static NodeHandler<ImmutableNode> handler;
/**
* Helper method for creating a field node with its children for the test node hierarchy.
*
* @param name the name of the field
* @return the field node
*/
private static ImmutableNode createFieldNode(final String name) {
final ImmutableNode.Builder nodeFieldBuilder = new ImmutableNode.Builder(1);
nodeFieldBuilder.addChild(createNode("name", name));
return nodeFieldBuilder.name("field").create();
}
/**
* Convenience method for creating a simple node with a name and a value.
*
* @param name the node name
* @param value the node value
* @return the node instance
*/
private static ImmutableNode createNode(final String name, final Object value) {
return new ImmutableNode.Builder().name(name).value(value).create();
}
@BeforeAll
public static void setUpBeforeClass() {
root = setUpNodes();
handler = new InMemoryNodeModel(root).getNodeHandler();
}
/**
* Creates a node hierarchy for testing that consists of tables, their fields, and some additional data:
*
* <pre>
* tables
* table
* name
* fields
* field
* name
* field
* name
* </pre>
*
* @return the root of the test node hierarchy
*/
private static ImmutableNode setUpNodes() {
final ImmutableNode.Builder nodeTablesBuilder = new ImmutableNode.Builder(TABLES.length);
nodeTablesBuilder.name("tables");
for (int i = 0; i < TABLES.length; i++) {
final ImmutableNode.Builder nodeTableBuilder = new ImmutableNode.Builder(2);
nodeTableBuilder.name("table");
nodeTableBuilder.addChild(new ImmutableNode.Builder().name("name").value(TABLES[i]).create());
nodeTableBuilder.addAttribute("type", TAB_TYPES[i]);
final ImmutableNode.Builder nodeFieldsBuilder = new ImmutableNode.Builder(FIELDS[i].length);
for (int j = 0; j < FIELDS[i].length; j++) {
nodeFieldsBuilder.addChild(createFieldNode(FIELDS[i][j]));
}
nodeTableBuilder.addChild(nodeFieldsBuilder.name("fields").create());
nodeTablesBuilder.addChild(nodeTableBuilder.create());
}
final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder();
rootBuilder.addChild(nodeTablesBuilder.create());
final ImmutableNode.Builder nodeConnBuilder = new ImmutableNode.Builder();
nodeConnBuilder.name("connection.settings");
nodeConnBuilder.addChild(createNode("usr.name", "scott"));
nodeConnBuilder.addChild(createNode("usr.pwd", "tiger"));
rootBuilder.addAttribute("test", "true");
rootBuilder.addChild(nodeConnBuilder.create());
return rootBuilder.create();
}
/** The object to be tested. */
private DefaultExpressionEngine engine;
/**
* Helper method for checking whether an attribute key is correctly evaluated.
*
* @param key the attribute key
* @param attr the attribute name
* @param expValue the expected attribute value
*/
private void checkAttributeValue(final String key, final String attr, final Object expValue) {
final List<QueryResult<ImmutableNode>> results = checkKey(key, attr, 1);
final QueryResult<ImmutableNode> result = results.get(0);
assertTrue(result.isAttributeResult());
assertEquals(expValue, result.getAttributeValue(handler), "Wrong attribute value for key " + key);
}
/**
* Helper method for checking the evaluation of a key. Queries the expression engine and tests if the expected results
* are returned.
*
* @param key the key
* @param name the name of the nodes to be returned
* @param count the number of expected result nodes
* @return the list with the results of the query
*/
private List<QueryResult<ImmutableNode>> checkKey(final String key, final String name, final int count) {
final List<QueryResult<ImmutableNode>> nodes = query(key, count);
for (final QueryResult<ImmutableNode> result : nodes) {
if (result.isAttributeResult()) {
assertEquals(name, result.getAttributeName(), "Wrong attribute name for key " + key);
} else {
assertEquals(name, result.getNode().getNodeName(), "Wrong result node for key " + key);
}
}
return nodes;
}
/**
* Helper method for checking the value of a node specified by the given key. This method evaluates the key and checks
* whether the resulting node has the expected value.
*
* @param key the key
* @param name the expected name of the result node
* @param value the expected value of the result node
*/
private void checkKeyValue(final String key, final String name, final String value) {
final List<QueryResult<ImmutableNode>> results = checkKey(key, name, 1);
final QueryResult<ImmutableNode> result = results.get(0);
assertFalse(result.isAttributeResult());
assertEquals(value, result.getNode().getValue(), "Wrong value for key " + key);
}
/**
* Helper method for checking the path of an add operation.
*
* @param data the add data object
* @param expected the expected path nodes
*/
private void checkNodePath(final NodeAddData<ImmutableNode> data, final String... expected) {
assertEquals(Arrays.asList(expected), data.getPathNodes());
}
/**
* Helper method for testing a query for the root node.
*
* @param key the key to be used
*/
private void checkQueryRootNode(final String key) {
final List<QueryResult<ImmutableNode>> results = checkKey(key, null, 1);
final QueryResult<ImmutableNode> result = results.get(0);
assertFalse(result.isAttributeResult());
assertSame(root, result.getNode());
}
/**
* Helper method for fetching a specific node by its key.
*
* @param key the key
* @return the node with this key
*/
private ImmutableNode fetchNode(final String key) {
final QueryResult<ImmutableNode> result = query(key, 1).get(0);
assertFalse(result.isAttributeResult());
return result.getNode();
}
/**
* Helper method for querying the test engine for a specific key.
*
* @param key the key
* @param expCount the expected number of result nodes
* @return the collection of retrieved nodes
*/
private List<QueryResult<ImmutableNode>> query(final String key, final int expCount) {
final List<QueryResult<ImmutableNode>> nodes = engine.query(root, key, handler);
assertEquals(expCount, nodes.size());
return nodes;
}
@BeforeEach
public void setUp() throws Exception {
engine = DefaultExpressionEngine.INSTANCE;
}
/**
* Configures the test expression engine to use a special matcher. This matcher ignores underscore characters in node
* names.
*/
private void setUpAlternativeMatcher() {
final NodeMatcher<String> matcher = new NodeMatcher<String>() {
@Override
public <T> boolean matches(final T node, final NodeHandler<T> handler, final String criterion) {
return handler.nodeName(node).equals(StringUtils.remove(criterion, '_'));
}
};
engine = new DefaultExpressionEngine(engine.getSymbols(), matcher);
}
/**
* Configures the expression engine to use a different syntax.
*/
private void setUpAlternativeSyntax() {
final DefaultExpressionEngineSymbols symbols = new DefaultExpressionEngineSymbols.Builder().setAttributeEnd(null).setAttributeStart("@")
.setPropertyDelimiter("/").setEscapedDelimiter(null).setIndexStart("[").setIndexEnd("]").create();
engine = new DefaultExpressionEngine(symbols);
}
/**
* Tests obtaining keys for attribute nodes.
*/
@Test
void testAttributeKey() {
assertEquals("tables.table[@type]", engine.attributeKey("tables.table", "type"));
}
/**
* Tests that a null parent key is ignored when constructing an attribute key.
*/
@Test
void testAttributeKeyNoParent() {
assertEquals("[@test]", engine.attributeKey(null, "test"));
}
/**
* Tests whether an attribute key can be queried if the root node is involved.
*/
@Test
void testAttributeKeyRoot() {
assertEquals("[@test]", engine.attributeKey("", "test"));
}
/**
* Tests whether a correct attribute key with alternative syntax is generated.
*/
@Test
void testAttributeKeyWithAlternativeSyntax() {
setUpAlternativeSyntax();
assertEquals("@test", engine.attributeKey("", "test"));
}
/**
* Tests whether a canonical key can be queried if all child nodes have different names.
*/
@Test
void testCanonicalKeyNoDuplicates() {
final ImmutableNode node = fetchNode("tables.table(0).name");
assertEquals("table.name(0)", engine.canonicalKey(node, "table", handler));
}
/**
* Tests whether the parent key can be undefined when querying a canonical key.
*/
@Test
void testCanonicalKeyNoParentKey() {
final ImmutableNode node = fetchNode("tables.table(0).fields.field(1).name");
assertEquals("name(0)", engine.canonicalKey(node, null, handler));
}
/**
* Tests whether a canonical key for the parent node can be queried if no parent key was passed in.
*/
@Test
void testCanonicalKeyRootNoParentKey() {
assertEquals("", engine.canonicalKey(root, null, handler));
}
/**
* Tests whether a parent key is evaluated when determining the canonical key of the root node.
*/
@Test
void testCanonicalKeyRootWithParentKey() {
assertEquals("parent", engine.canonicalKey(root, "parent", handler));
}
/**
* Tests whether duplicates are correctly resolved when querying for canonical keys.
*/
@Test
void testCanonicalKeyWithDuplicates() {
final ImmutableNode tab1 = fetchNode("tables.table(0)");
final ImmutableNode tab2 = fetchNode("tables.table(1)");
assertEquals("tables.table(0)", engine.canonicalKey(tab1, "tables", handler));
assertEquals("tables.table(1)", engine.canonicalKey(tab2, "tables", handler));
}
/**
* Tests whether the default instance is initialized with default symbols.
*/
@Test
void testDefaultSymbols() {
assertSame(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS, engine.getSymbols());
}
/**
* Tries to create an instance without symbols.
*/
@Test
void testInitNoSymbols() {
assertThrows(IllegalArgumentException.class, () -> new DefaultExpressionEngine(null));
}
/**
* Tests obtaining keys for nodes.
*/
@Test
void testNodeKey() {
final ImmutableNode node = root.getChildren().get(0);
assertEquals("tables", engine.nodeKey(node, "", handler));
assertEquals("test.tables", engine.nodeKey(node, "test", handler));
assertEquals("a.full.parent.key.tables", engine.nodeKey(node, "a.full.parent.key", handler));
}
/**
* Tests obtaining node keys if a different syntax is set.
*/
@Test
void testNodeKeyWithAlternativeSyntax() {
setUpAlternativeSyntax();
assertEquals("tables/table", engine.nodeKey(root.getChildren().get(0).getChildren().get(0), "tables", handler));
}
/**
* Tests obtaining node keys if a different syntax is set and the same string is used as property delimiter and
* attribute start marker.
*/
@Test
void testNodeKeyWithAlternativeSyntaxAttributePropertyDelimiter() {
setUpAlternativeSyntax();
final DefaultExpressionEngineSymbols symbols = new DefaultExpressionEngineSymbols.Builder(engine.getSymbols())
.setAttributeStart(engine.getSymbols().getPropertyDelimiter()).create();
engine = new DefaultExpressionEngine(symbols);
assertEquals("/test", engine.attributeKey("", "test"));
}
/**
* Tests obtaining keys for nodes that contain the delimiter character.
*/
@Test
void testNodeKeyWithEscapedDelimiters() {
final ImmutableNode node = root.getChildren().get(1);
assertEquals("connection..settings", engine.nodeKey(node, "", handler));
assertEquals("connection..settings.usr..name", engine.nodeKey(node.getChildren().get(0), engine.nodeKey(node, "", handler), handler));
}
/**
* Tests obtaining keys if the root node is involved.
*/
@Test
void testNodeKeyWithRoot() {
assertEquals("", engine.nodeKey(root, null, handler));
assertEquals("test", engine.nodeKey(root, "test", handler));
}
/**
* Tests adding new attributes.
*/
@Test
void testPrepareAddAttribute() {
final NodeAddData<ImmutableNode> data = engine.prepareAdd(root, "tables.table(0)[@tableSpace]", handler);
assertEquals(TABLES[0], data.getParent().getChildren().get(0).getValue());
assertEquals("tableSpace", data.getNewNodeName());
assertTrue(data.isAttribute());
assertTrue(data.getPathNodes().isEmpty());
}
/**
* Tests whether an attribute to the root node can be added.
*/
@Test
void testPrepareAddAttributeRoot() {
final NodeAddData<ImmutableNode> data = engine.prepareAdd(root, "[@newAttr]", handler);
assertSame(root, data.getParent());
assertEquals("newAttr", data.getNewNodeName());
assertTrue(data.isAttribute());
}
/**
* Tests adding direct child nodes to the existing hierarchy.
*/
@Test
void testPrepareAddDirectly() {
NodeAddData<ImmutableNode> data = engine.prepareAdd(root, "newNode", handler);
assertSame(root, data.getParent());
assertTrue(data.getPathNodes().isEmpty());
assertEquals("newNode", data.getNewNodeName());
assertFalse(data.isAttribute());
data = engine.prepareAdd(root, "tables.table.fields.field.name", handler);
assertEquals("name", data.getNewNodeName());
assertTrue(data.getPathNodes().isEmpty());
assertEquals("field", data.getParent().getNodeName());
final ImmutableNode nd = data.getParent().getChildren().get(0);
assertEquals("name", nd.getNodeName());
assertEquals("version", nd.getValue());
}
@Test
void testPrepareAddEmptyKey() {
assertThrows(IllegalArgumentException.class, () -> engine.prepareAdd(root, "", handler));
}
/**
* Tests using invalid keys, for example if something should be added to attributes.
*/
@Test
void testPrepareAddInvalidKey() {
assertThrows(IllegalArgumentException.class, () -> engine.prepareAdd(root, "tables.table(0)[@type].new", handler));
}
@Test
void testPrepareAddInvalidKeyAttribute() {
assertThrows(IllegalArgumentException.class,
() -> engine.prepareAdd(root, "a.complete.new.path.with.an[@attribute].at.a.non.allowed[@position]", handler));
}
@Test
void testPrepareAddNullKey() {
assertThrows(IllegalArgumentException.class, () -> engine.prepareAdd(root, null, handler));
}
/**
* Tests whether the node matcher is used when adding keys.
*/
@Test
void testPrepareAddWithAlternativeMatcher() {
setUpAlternativeMatcher();
final NodeAddData<ImmutableNode> data = engine.prepareAdd(root, "tables_.table._fields__._field.name", handler);
assertEquals("name", data.getNewNodeName());
assertTrue(data.getPathNodes().isEmpty());
}
/**
* Tests add operations when an alternative syntax is set.
*/
@Test
void testPrepareAddWithAlternativeSyntax() {
setUpAlternativeSyntax();
NodeAddData<ImmutableNode> data = engine.prepareAdd(root, "tables/table[0]/test", handler);
assertEquals("test", data.getNewNodeName());
assertFalse(data.isAttribute());
assertEquals(TABLES[0], data.getParent().getChildren().get(0).getValue());
data = engine.prepareAdd(root, "a/complete/new/path@attr", handler);
assertEquals("attr", data.getNewNodeName());
checkNodePath(data, "a", "complete", "new", "path");
assertSame(root, data.getParent());
}
/**
* Tests adding if indices are involved.
*/
@Test
void testPrepareAddWithIndex() {
NodeAddData<ImmutableNode> data = engine.prepareAdd(root, "tables.table(0).tableSpace", handler);
assertEquals("tableSpace", data.getNewNodeName());
assertTrue(data.getPathNodes().isEmpty());
assertEquals("table", data.getParent().getNodeName());
final ImmutableNode node = data.getParent().getChildren().get(0);
assertEquals(TABLES[0], node.getValue());
data = engine.prepareAdd(root, "tables.table(1).fields.field(2).alias", handler);
assertEquals("alias", data.getNewNodeName());
assertEquals("field", data.getParent().getNodeName());
assertEquals("creationDate", data.getParent().getChildren().get(0).getValue());
}
/**
* Tests add operations where complete paths are added.
*/
@Test
void testPrepareAddWithPath() {
NodeAddData<ImmutableNode> data = engine.prepareAdd(root, "tables.table(1).fields.field(-1).name", handler);
assertEquals("name", data.getNewNodeName());
checkNodePath(data, "field");
assertEquals("fields", data.getParent().getNodeName());
data = engine.prepareAdd(root, "tables.table(-1).name", handler);
assertEquals("name", data.getNewNodeName());
checkNodePath(data, "table");
assertEquals("tables", data.getParent().getNodeName());
data = engine.prepareAdd(root, "a.complete.new.path", handler);
assertEquals("path", data.getNewNodeName());
checkNodePath(data, "a", "complete", "new");
assertSame(root, data.getParent());
}
/**
* Tests add operations if property and attribute delimiters are equal. Then it is not possible to add new attribute
* nodes.
*/
@Test
void testPrepareAddWithSameAttributeDelimiter() {
final DefaultExpressionEngineSymbols symbols = new DefaultExpressionEngineSymbols.Builder(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS)
.setAttributeEnd(null).setAttributeStart(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS.getPropertyDelimiter()).create();
engine = new DefaultExpressionEngine(symbols);
NodeAddData<ImmutableNode> data = engine.prepareAdd(root, "tables.table(0).test", handler);
assertEquals("test", data.getNewNodeName());
assertFalse(data.isAttribute());
assertEquals("table", data.getParent().getNodeName());
data = engine.prepareAdd(root, "a.complete.new.path", handler);
assertFalse(data.isAttribute());
checkNodePath(data, "a", "complete", "new");
}
/**
* Tests a different query syntax. Sets other strings for the typical tokens used by the expression engine.
*/
@Test
void testQueryAlternativeSyntax() {
setUpAlternativeSyntax();
checkKeyValue("tables/table[1]/name", "name", TABLES[1]);
checkAttributeValue("tables/table[0]@type", "type", TAB_TYPES[0]);
checkAttributeValue("@test", "test", "true");
checkKeyValue("connection.settings/usr.name", "usr.name", "scott");
}
/**
* Tests some queries when the same delimiter is used for properties and attributes.
*/
@Test
void testQueryAttributeEmulation() {
final DefaultExpressionEngineSymbols symbols = new DefaultExpressionEngineSymbols.Builder(DefaultExpressionEngineSymbols.DEFAULT_SYMBOLS)
.setAttributeEnd(null).setAttributeStart(DefaultExpressionEngineSymbols.DEFAULT_PROPERTY_DELIMITER).create();
engine = new DefaultExpressionEngine(symbols);
checkKeyValue("tables.table(0).name", "name", TABLES[0]);
checkAttributeValue("tables.table(0).type", "type", TAB_TYPES[0]);
checkKey("tables.table.type", "type", 2);
}
/**
* Tests querying nodes whose names contain a delimiter.
*/
@Test
void testQueryEscapedKeys() {
checkKeyValue("connection..settings.usr..name", "usr.name", "scott");
checkKeyValue("connection..settings.usr..pwd", "usr.pwd", "tiger");
}
/**
* Tests some simple queries.
*/
@Test
void testQueryKeys() {
checkKey("tables.table.name", "name", 2);
checkKey("tables.table.fields.field.name", "name", 10);
checkKey("tables.table[@type]", "type", 2);
checkKey("tables.table(0).fields.field.name", "name", 5);
checkKey("tables.table(1).fields.field.name", "name", 5);
checkKey("tables.table.fields.field(1).name", "name", 2);
}
/**
* Tests whether the node matcher is used when querying keys.
*/
@Test
void testQueryKeyWithAlternativeMatcher() {
setUpAlternativeMatcher();
checkKey("tables_._table_.name_", "name", 2);
}
/**
* Performs some queries and evaluates the values of the result nodes.
*/
@Test
void testQueryNodes() {
for (int i = 0; i < TABLES.length; i++) {
checkKeyValue("tables.table(" + i + ").name", "name", TABLES[i]);
checkAttributeValue("tables.table(" + i + ")[@type]", "type", TAB_TYPES[i]);
for (int j = 0; j < FIELDS[i].length; j++) {
checkKeyValue("tables.table(" + i + ").fields.field(" + j + ").name", "name", FIELDS[i][j]);
}
}
}
/**
* Tests querying keys that do not exist.
*/
@Test
void testQueryNonExistingKeys() {
checkKey("tables.tablespace.name", null, 0);
checkKey("tables.table(2).name", null, 0);
checkKey("a complete unknown key", null, 0);
checkKey("tables.table(0).fields.field(-1).name", null, 0);
checkKey("tables.table(0).fields.field(28).name", null, 0);
checkKey("tables.table(0).fields.field().name", null, 0);
checkKey("connection.settings.usr.name", null, 0);
checkKey("tables.table(0)[@type].additional", null, 0);
}
/**
* Tests whether an attribute of the root node can be queried.
*/
@Test
void testQueryRootAttribute() {
checkAttributeValue("[@test]", "test", "true");
}
/**
* Tests whether the root node can be retrieved using the empty key.
*/
@Test
void testQueryRootNodeEmptyKey() {
checkQueryRootNode("");
}
/**
* Tests whether the root node can be retrieved using the null key.
*/
@Test
void testQueryRootNodeNullKey() {
checkQueryRootNode(null);
}
}