blob: 15cc7a271b6697b18262e22756d544e4d1c08083 [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.tree;
import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.dbunit.dataset.common.handlers.NoHandler;
import org.easymock.EasyMock;
/**
* A helper class for tests related to hierarchies of {@code ImmutableNode}
* objects. This class provides functionality for creating test trees and
* accessing single nodes. It can be used by various test classes.
*
*/
public class NodeStructureHelper
{
/** A pattern for parsing node keys with optional indices. */
private static final Pattern PAT_KEY_WITH_INDEX = Pattern
.compile("(\\w+)\\((\\d+)\\)");
/** The character for splitting node path elements. */
private static final String PATH_SEPARATOR = "/";
/** An array with authors. */
private static final String[] AUTHORS = {
"Shakespeare", "Homer", "Simmons"
};
/** An array with the works of the test authors. */
private static final String[][] WORKS = {
{
"Troilus and Cressida", "The Tempest",
"A Midsummer Night's Dream"
}, {
"Ilias"
}, {
"Ilium", "Hyperion"
}
};
/** An array with the personae in the works. */
private static final String[][][] PERSONAE = {
{
// Works of Shakespeare
{
"Troilus", "Cressidia", "Ajax", "Achilles"
}, {
"Prospero", "Ariel"
}, {
"Oberon", "Titania", "Puck"
}
}, {
// Works of Homer
{
"Achilles", "Agamemnon", "Hektor"
}
}, {
// Works of Dan Simmons
{
"Hockenberry", "Achilles"
}, {
"Shrike", "Moneta", "Consul", "Weintraub"
}
}
};
/** An array with table names used for the TABLES tree. */
private static final String[] TABLES = {
"users", "documents"
};
/**
* An array with the names of columns to be used for the TABLES tree.
*/
private static final String[][] FIELDS = {
{
"uid", "uname", "firstName", "lastName", "email"
}, {
"docid", "name", "creationDate", "authorID", "version", "length"
}
};
/** Constant for the author attribute. */
public static final String ATTR_AUTHOR = "author";
/** Constant for the original value element in the personae tree. */
public static final String ELEM_ORG_VALUE = "originalValue";
/** Constant for the tested attribute. */
public static final String ATTR_TESTED = "tested";
/** The root node of the authors tree. */
public static final ImmutableNode ROOT_AUTHORS_TREE = createAuthorsTree();
/** The root node of the personae tree. */
public static final ImmutableNode ROOT_PERSONAE_TREE = createPersonaeTree();
/** The root node of the TABLES tree. */
public static final ImmutableNode ROOT_TABLES_TREE = createTablesTree();
/**
* Returns the number of authors.
*
* @return the number of authors
*/
public static int authorsLength()
{
return AUTHORS.length;
}
/**
* Returns the name of the author at the given index.
*
* @param idx the index
* @return the name of this author
*/
public static String author(final int idx)
{
return AUTHORS[idx];
}
/**
* Returns the number of works for the author with the given index.
*
* @param authorIdx the author index
* @return the number of works of this author
*/
public static int worksLength(final int authorIdx)
{
return WORKS[authorIdx].length;
}
/**
* Returns the work of an author with a given index.
*
* @param authorIdx the author index
* @param idx the index of the work
* @return the desired work
*/
public static String work(final int authorIdx, final int idx)
{
return WORKS[authorIdx][idx];
}
/**
* Returns the number of personae in the given work of the specified author.
*
* @param authorIdx the author index
* @param workIdx the index of the work
* @return the number of personae in this work
*/
public static int personaeLength(final int authorIdx, final int workIdx)
{
return PERSONAE[authorIdx][workIdx].length;
}
/**
* Returns the name of a persona.
*
* @param authorIdx the author index
* @param workIdx the index of the work
* @param personaIdx the index of the persona
* @return the name of this persona
*/
public static String persona(final int authorIdx, final int workIdx, final int personaIdx)
{
return PERSONAE[authorIdx][workIdx][personaIdx];
}
/**
* Returns the number of tables in the tables tree.
*
* @return the number of tables
*/
public static int tablesLength()
{
return TABLES.length;
}
/**
* Returns the name of the test table with the given index.
*
* @param idx the index of the table
* @return the name of the test table with this index
*/
public static String table(final int idx)
{
return TABLES[idx];
}
/**
* Returns the number of fields in the test table with the given index.
*
* @param tabIdx the index of the table
* @return the number of fields in this table
*/
public static int fieldsLength(final int tabIdx)
{
return FIELDS[tabIdx].length;
}
/**
* Returns the name of the specified field in the tables tree.
*
* @param tabIdx the index of the table
* @param fldIdx the index of the field
* @return the name of this field
*/
public static String field(final int tabIdx, final int fldIdx)
{
return FIELDS[tabIdx][fldIdx];
}
/**
* Appends a component to a node path. The component is added separated by a
* path separator.
*
* @param path the path
* @param component the component to be added
* @return the resulting path
*/
public static String appendPath(final String path, final String component)
{
final StringBuilder buf =
new StringBuilder(StringUtils.length(path)
+ StringUtils.length(component) + 1);
buf.append(path).append(PATH_SEPARATOR).append(component);
return buf.toString();
}
/**
* Evaluates the given key and finds the corresponding child node of the
* specified root. Keys have the form {@code path/to/node}. If there are
* multiple sibling nodes with the same name, a numerical index can be
* specified in parenthesis.
*
* @param root the root node
* @param key the key to the desired node
* @return the node with this key
* @throws NoSuchElementException if the key cannot be resolved
*/
public static ImmutableNode nodeForKey(final ImmutableNode root, final String key)
{
final String[] components = key.split(PATH_SEPARATOR);
return findNode(root, components, 0);
}
/**
* Evaluates the given key and finds the corresponding child node of the
* root node of the specified model. This is a convenience method that works
* like the method with the same name, but obtains the root node from the
* given model.
*
* @param model the node model
* @param key the key to the desired node
* @return the found target node
* @throws NoSuchElementException if the desired node cannot be found
*/
public static ImmutableNode nodeForKey(final InMemoryNodeModel model, final String key)
{
return nodeForKey(model.getRootNode(), key);
}
/**
* Evaluates the given key and finds the corresponding child node of the
* root node of the specified {@code NodeHandler} object. This is a
* convenience method that works like the method with the same name, but
* obtains the root node from the given handler object.
*
* @param handler the {@code NodeHandler} object
* @param key the key to the desired node
* @return the found target node
* @throws NoSuchElementException if the desired node cannot be found
*/
public static ImmutableNode nodeForKey(final NodeHandler<ImmutableNode> handler,
final String key)
{
return nodeForKey(handler.getRootNode(), key);
}
/**
* Convenience method for creating a path for accessing a node based on the
* node names.
*
* @param path an array with the expected node names on the path
* @return the resulting path as string
*/
public static String nodePath(final String... path)
{
return StringUtils.join(path, PATH_SEPARATOR);
}
/**
* Convenience method for creating a node path with a special end node.
*
* @param endNode the name of the last path component
* @param path an array with the expected node names on the path
* @return the resulting path as string
*/
public static String nodePathWithEndNode(final String endNode, final String... path)
{
return nodePath(path) + PATH_SEPARATOR + endNode;
}
/**
* Helper method for creating an immutable node with a name and a value.
*
* @param name the node's name
* @param value the node's value
* @return the new node
*/
public static ImmutableNode createNode(final String name, final Object value)
{
return new ImmutableNode.Builder().name(name).value(value).create();
}
/**
* Helper method for creating a field node with its children. Nodes of this
* type are used within the tables tree. They define a single column of a
* table.
*
* @param name the name of the field
* @return the field node
*/
public static ImmutableNode createFieldNode(final String name)
{
final ImmutableNode.Builder fldBuilder = new ImmutableNode.Builder(1);
fldBuilder.addChild(createNode("name", name));
return fldBuilder.name("field").create();
}
/**
* Creates a mock for a resolver.
*
* @return the resolver mock
*/
public static NodeKeyResolver<ImmutableNode> createResolverMock()
{
@SuppressWarnings("unchecked")
final
NodeKeyResolver<ImmutableNode> mock =
EasyMock.createMock(NodeKeyResolver.class);
return mock;
}
/**
* Prepares a mock for a resolver to expect arbitrary resolve operations.
* These operations are implemented on top of a default expression engine.
*
* @param resolver the mock resolver
*/
@SuppressWarnings("unchecked")
public static void expectResolveKeyForQueries(
final NodeKeyResolver<ImmutableNode> resolver)
{
EasyMock.expect(
resolver.resolveKey(EasyMock.anyObject(ImmutableNode.class),
EasyMock.anyObject(String.class),
(NodeHandler<ImmutableNode>) EasyMock
.anyObject(NoHandler.class)))
.andAnswer(() -> {
final ImmutableNode root =
(ImmutableNode) EasyMock.getCurrentArguments()[0];
final String key = (String) EasyMock.getCurrentArguments()[1];
final NodeHandler<ImmutableNode> handler =
(NodeHandler<ImmutableNode>) EasyMock
.getCurrentArguments()[2];
return DefaultExpressionEngine.INSTANCE.query(root,
key, handler);
}).anyTimes();
}
/**
* Prepares the passed in resolver mock to resolve add keys. They are
* interpreted on a default expression engine.
*
* @param resolver the {@code NodeKeyResolver} mock
*/
public static void expectResolveAddKeys(
final NodeKeyResolver<ImmutableNode> resolver)
{
EasyMock.expect(
resolver.resolveAddKey(EasyMock.anyObject(ImmutableNode.class),
EasyMock.anyString(),
EasyMock.anyObject(TreeData.class)))
.andAnswer(() -> {
final ImmutableNode root =
(ImmutableNode) EasyMock.getCurrentArguments()[0];
final String key = (String) EasyMock.getCurrentArguments()[1];
final TreeData handler =
(TreeData) EasyMock.getCurrentArguments()[2];
return DefaultExpressionEngine.INSTANCE.prepareAdd(
root, key, handler);
}).anyTimes();
}
/**
* Creates as tree with database table data based on the passed in arrays of
* table names and fields for tables. Works like the method without
* parameters, but allows defining the data of the structure.
*
* @param tables an array with the names of the tables
* @param fields an array with the fields of the single tables
* @return the resulting nodes structure
*/
public static ImmutableNode createTablesTree(final String[] tables,
final String[][] fields)
{
final ImmutableNode.Builder bldTables =
new ImmutableNode.Builder(tables.length);
bldTables.name("tables");
for (int i = 0; i < tables.length; i++)
{
final ImmutableNode.Builder bldTable = new ImmutableNode.Builder(2);
bldTable.addChild(createNode("name", tables[i]));
final ImmutableNode.Builder bldFields =
new ImmutableNode.Builder(fields[i].length);
bldFields.name("fields");
for (int j = 0; j < fields[i].length; j++)
{
bldFields.addChild(createFieldNode(fields[i][j]));
}
bldTable.addChild(bldFields.create());
bldTables.addChild(bldTable.name("table").create());
}
return bldTables.create();
}
/**
* Returns a clone of the array with the table names. This is useful if a
* slightly different tree structure should be created.
*
* @return the cloned table names
*/
public static String[] getClonedTables()
{
return TABLES.clone();
}
/**
* Returns a clone of the array with the table fields. This is useful if a
* slightly different tree structure should be created.
*
* @return the cloned field names
*/
public static String[][] getClonedFields()
{
final String[][] fieldNamesNew = new String[FIELDS.length][];
for (int i = 0; i < FIELDS.length; i++)
{
fieldNamesNew[i] = FIELDS[i].clone();
}
return fieldNamesNew;
}
/**
* Creates a tree with a root node whose children are the test authors. Each
* other has his works as child nodes. Each work has its personae as
* children.
*
* @return the root node of the authors tree
*/
private static ImmutableNode createAuthorsTree()
{
final ImmutableNode.Builder rootBuilder =
new ImmutableNode.Builder(AUTHORS.length);
for (int author = 0; author < AUTHORS.length; author++)
{
final ImmutableNode.Builder authorBuilder = new ImmutableNode.Builder();
authorBuilder.name(AUTHORS[author]);
for (int work = 0; work < WORKS[author].length; work++)
{
final ImmutableNode.Builder workBuilder = new ImmutableNode.Builder();
workBuilder.name(WORKS[author][work]);
for (final String person : PERSONAE[author][work])
{
workBuilder.addChild(new ImmutableNode.Builder().name(
person).create());
}
authorBuilder.addChild(workBuilder.create());
}
rootBuilder.addChild(authorBuilder.create());
}
return rootBuilder.name("authorTree").create();
}
/**
* Creates a tree with a root node whose children are the test personae.
* Each node represents a person and has an attribute pointing to the author
* who invented this person. There is a single child node for the associated
* work which has again a child and an attribute.
*
* @return the root node of the personae tree
*/
private static ImmutableNode createPersonaeTree()
{
final ImmutableNode.Builder rootBuilder = new ImmutableNode.Builder();
for (int author = 0; author < AUTHORS.length; author++)
{
for (int work = 0; work < WORKS[author].length; work++)
{
for (final String person : PERSONAE[author][work])
{
final ImmutableNode orgValue =
new ImmutableNode.Builder().name(ELEM_ORG_VALUE)
.value("yes")
.addAttribute(ATTR_TESTED, Boolean.FALSE)
.create();
final ImmutableNode workNode =
new ImmutableNode.Builder(1)
.name(WORKS[author][work])
.addChild(orgValue).create();
final ImmutableNode personNode =
new ImmutableNode.Builder(1).name(person)
.addAttribute(ATTR_AUTHOR, AUTHORS[author])
.addChild(workNode).create();
rootBuilder.addChild(personNode);
}
}
}
return rootBuilder.create();
}
/**
* Creates a tree with database table data with the following structure:
*
* tables
* table
* name
* fields
* field
* name
* field
* name
* @return the resulting nodes structure
*/
private static ImmutableNode createTablesTree()
{
return createTablesTree(TABLES, FIELDS);
}
/**
* Helper method for evaluating a single component of a node key.
*
* @param parent the current parent node
* @param components the array with the components of the node key
* @param currentIdx the index of the current path component
* @return the found target node
* @throws NoSuchElementException if the desired node cannot be found
*/
private static ImmutableNode findNode(final ImmutableNode parent,
final String[] components, final int currentIdx)
{
if (currentIdx >= components.length)
{
return parent;
}
final Matcher m = PAT_KEY_WITH_INDEX.matcher(components[currentIdx]);
String childName;
int childIndex;
if (m.matches())
{
childName = m.group(1);
childIndex = Integer.parseInt(m.group(2));
}
else
{
childName = components[currentIdx];
childIndex = 0;
}
int foundIdx = 0;
for (final ImmutableNode node : parent.getChildren())
{
if (childName.equals(node.getNodeName()))
{
if (foundIdx++ == childIndex)
{
return findNode(node, components, currentIdx + 1);
}
}
}
throw new NoSuchElementException("Cannot resolve child "
+ components[currentIdx]);
}
}