Author: oheger
Date: Fri Feb 7 20:38:07 2014
New Revision: 1565802
URL: http://svn.apache.org/r1565802
Log:
Reworked XPathExpressionEngine.
The class now correctly implements the extended ExpressionEngine interface.
This means that XPATH expressions can be executed on arbitrary node models
for which a NodeHandler implementation exists.
Modified:
commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/xpath/XPathExpressionEngine.java
commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngine.java
Modified:
commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/xpath/XPathExpressionEngine.java
URL:
http://svn.apache.org/viewvc/commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/xpath/XPathExpressionEngine.java?rev=1565802&r1=1565801&r2=1565802&view=diff
==============================================================================
---
commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/xpath/XPathExpressionEngine.java
(original)
+++
commons/proper/configuration/branches/immutableNodes/src/main/java/org/apache/commons/configuration/tree/xpath/XPathExpressionEngine.java
Fri Feb 7 20:38:07 2014
@@ -16,21 +16,24 @@
*/
package org.apache.commons.configuration.tree.xpath;
+import java.util.ArrayList;
import java.util.Collections;
+import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
-import org.apache.commons.configuration.tree.ConfigurationNode;
import org.apache.commons.configuration.tree.ExpressionEngine;
import org.apache.commons.configuration.tree.NodeAddData;
+import org.apache.commons.configuration.tree.NodeHandler;
+import org.apache.commons.configuration.tree.QueryResult;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
import org.apache.commons.lang3.StringUtils;
/**
* <p>
- * A specialized implementation of the {@code ExpressionEngine} interface
- * that is able to evaluate XPATH expressions.
+ * A specialized implementation of the {@code ExpressionEngine} interface that
+ * is able to evaluate XPATH expressions.
* </p>
* <p>
* This class makes use of <a href="http://commons.apache.org/jxpath/"> Commons
@@ -41,9 +44,9 @@ import org.apache.commons.lang3.StringUt
* <p>
* For selecting properties arbitrary XPATH expressions can be used, which
* select single or multiple configuration nodes. The associated
- * {@code Configuration} instance will directly pass the specified property
- * keys into this engine. If a key is not syntactically correct, an exception
- * will be thrown.
+ * {@code Configuration} instance will directly pass the specified property
keys
+ * into this engine. If a key is not syntactically correct, an exception will
be
+ * thrown.
* </p>
* <p>
* For adding new properties, this expression engine uses a specific syntax:
the
@@ -68,8 +71,8 @@ import org.apache.commons.lang3.StringUt
*
* </p>
* <p>
- * This will add a new {@code type} node as a child of the first
- * {@code table} element.
+ * This will add a new {@code type} node as a child of the first {@code table}
+ * element.
* </p>
* <p>
*
@@ -92,8 +95,7 @@ import org.apache.commons.lang3.StringUt
* <p>
* This example shows how a complex path can be added. Parent node is the
* {@code tables} element. Here a new branch consisting of the nodes
- * {@code table}, {@code fields}, {@code field}, and
- * {@code name} will be added.
+ * {@code table}, {@code fields}, {@code field}, and {@code name} will be
added.
* </p>
* <p>
*
@@ -108,15 +110,15 @@ import org.apache.commons.lang3.StringUt
* </p>
* <p>
* <strong>Note:</strong> This extended syntax for adding properties only works
- * with the {@code addProperty()} method. {@code setProperty()} does
- * not support creating new nodes this way.
+ * with the {@code addProperty()} method. {@code setProperty()} does not
support
+ * creating new nodes this way.
* </p>
* <p>
* From version 1.7 on, it is possible to use regular keys in calls to
- * {@code addProperty()} (i.e. keys that do not have to contain a
- * whitespace as delimiter). In this case the key is evaluated, and the biggest
- * part pointing to an existing node is determined. The remaining part is then
- * added as new path. As an example consider the key
+ * {@code addProperty()} (i.e. keys that do not have to contain a whitespace as
+ * delimiter). In this case the key is evaluated, and the biggest part pointing
+ * to an existing node is determined. The remaining part is then added as new
+ * path. As an example consider the key
*
* <pre>
* "tables/table[last()]/fields/field/name"
@@ -124,22 +126,19 @@ import org.apache.commons.lang3.StringUt
*
* If the key does not point to an existing node, the engine will check the
* paths {@code "tables/table[last()]/fields/field"},
- * {@code "tables/table[last()]/fields"},
- * {@code "tables/table[last()]"}, and so on, until a key is
- * found which points to a node. Let's assume that the last key listed above
can
- * be resolved in this way. Then from this key the following key is derived:
- * {@code "tables/table[last()] fields/field/name"} by appending
- * the remaining part after a whitespace. This key can now be processed using
- * the original algorithm. Keys of this form can also be used with the
- * {@code setProperty()} method. However, it is still recommended to use
- * the old format because it makes explicit at which position new nodes should
- * be added. For keys without a whitespace delimiter there may be ambiguities.
+ * {@code "tables/table[last()]/fields"}, {@code "tables/table[last()]"}, and
so
+ * on, until a key is found which points to a node. Let's assume that the last
+ * key listed above can be resolved in this way. Then from this key the
+ * following key is derived: {@code "tables/table[last()] fields/field/name"}
by
+ * appending the remaining part after a whitespace. This key can now be
+ * processed using the original algorithm. Keys of this form can also be used
+ * with the {@code setProperty()} method. However, it is still recommended to
+ * use the old format because it makes explicit at which position new nodes
+ * should be added. For keys without a whitespace delimiter there may be
+ * ambiguities.
* </p>
*
* @since 1.3
- * @author <a
- *
href="http://commons.apache.org/configuration/team-list.html">Commons
- * Configuration team</a>
* @version $Id$
*/
public class XPathExpressionEngine implements ExpressionEngine
@@ -160,6 +159,38 @@ public class XPathExpressionEngine imple
*/
private static final String SPACE = " ";
+ /** Constant for a default size of a key buffer. */
+ private static final int BUF_SIZE = 128;
+
+ /** Constant for the start of an index expression. */
+ private static final char START_INDEX = '[';
+
+ /** Constant for the end of an index expression. */
+ private static final char END_INDEX = ']';
+
+ /** The internally used context factory. */
+ private final XPathContextFactory contextFactory;
+
+ /**
+ * Creates a new instance of {@code XPathExpressionEngine} with default
+ * settings.
+ */
+ public XPathExpressionEngine()
+ {
+ this(new XPathContextFactory());
+ }
+
+ /**
+ * Creates a new instance of {@code XPathExpressionEngine} and sets the
+ * context factory. This constructor is mainly used for testing purposes.
+ *
+ * @param factory the {@code XPathContextFactory}
+ */
+ XPathExpressionEngine(XPathContextFactory factory)
+ {
+ contextFactory = factory;
+ }
+
/**
* Executes a query. The passed in property key is directly passed to a
* JXPath context.
@@ -168,48 +199,43 @@ public class XPathExpressionEngine imple
* @param key the query to be executed
* @return a list with the nodes that are selected by the query
*/
- public List<ConfigurationNode> query(ConfigurationNode root, String key)
+ public <T> List<QueryResult<T>> query(T root, String key,
+ NodeHandler<T> handler)
{
if (StringUtils.isEmpty(key))
{
- return Collections.singletonList(root);
+ QueryResult<T> result = createResult(root);
+ return Collections.singletonList(result);
}
else
{
- JXPathContext context = createContext(root, key);
- // This is safe because our node pointer implementations will
return
- // a list of configuration nodes.
- @SuppressWarnings("unchecked")
- List<ConfigurationNode> result = context.selectNodes(key);
- if (result == null)
+ JXPathContext context = createContext(root, handler);
+ List<?> results = context.selectNodes(key);
+ if (results == null)
{
- result = Collections.emptyList();
+ results = Collections.emptyList();
}
- return result;
+ return convertResults(results);
}
}
/**
- * Returns a (canonical) key for the given node based on the parent's key.
- * This implementation will create an XPATH expression that selects the
- * given node (under the assumption that the passed in parent key is
valid).
- * As the {@code nodeKey()} implementation of
- * {@link org.apache.commons.configuration.tree.DefaultExpressionEngine
DefaultExpressionEngine}
- * this method will not return indices for nodes. So all child nodes of a
- * given parent with the same name will have the same key.
- *
- * @param node the node for which a key is to be constructed
- * @param parentKey the key of the parent node
- * @return the key for the given node
+ * {@inheritDoc} This implementation creates an XPATH expression that
+ * selects the given node (under the assumption that the passed in parent
+ * key is valid). As the {@code nodeKey()} implementation of
+ * {@link org.apache.commons.configuration.tree.DefaultExpressionEngine
+ * DefaultExpressionEngine} this method does not return indices for nodes.
+ * So all child nodes of a given parent with the same name have the same
+ * key.
*/
- public String nodeKey(ConfigurationNode node, String parentKey)
+ public <T> String nodeKey(T node, String parentKey, NodeHandler<T> handler)
{
if (parentKey == null)
{
// name of the root node
return StringUtils.EMPTY;
}
- else if (node.getName() == null)
+ else if (handler.nodeName(node) == null)
{
// paranoia check for undefined node names
return parentKey;
@@ -217,32 +243,66 @@ public class XPathExpressionEngine imple
else
{
- StringBuilder buf = new StringBuilder(parentKey.length()
- + node.getName().length() + PATH_DELIMITER.length());
+ StringBuilder buf =
+ new StringBuilder(parentKey.length()
+ + handler.nodeName(node).length()
+ + PATH_DELIMITER.length());
if (parentKey.length() > 0)
{
buf.append(parentKey);
buf.append(PATH_DELIMITER);
}
- if (node.isAttribute())
- {
- buf.append(ATTR_DELIMITER);
- }
- buf.append(node.getName());
+ buf.append(handler.nodeName(node));
return buf.toString();
}
}
+ public String attributeKey(String parentKey, String attributeName)
+ {
+ StringBuilder buf =
+ new StringBuilder(StringUtils.length(parentKey)
+ + StringUtils.length(attributeName)
+ + PATH_DELIMITER.length() + ATTR_DELIMITER.length());
+ if (StringUtils.isNotEmpty(parentKey))
+ {
+ buf.append(parentKey).append(PATH_DELIMITER);
+ }
+ buf.append(ATTR_DELIMITER).append(attributeName);
+ return buf.toString();
+ }
+
/**
- * Prepares an add operation for a configuration property. The expected
- * format of the passed in key is explained in the class comment.
- *
- * @param root the configuration's root node
- * @param key the key describing the target of the add operation and the
- * path of the new node
- * @return a data object to be evaluated by the calling configuration
object
+ * {@inheritDoc} This implementation works similar to {@code nodeKey()},
but
+ * always adds an index expression to the resulting key.
+ */
+ public <T> String canonicalKey(T node, String parentKey,
+ NodeHandler<T> handler)
+ {
+ T parent = handler.getParent(node);
+ if (parent == null)
+ {
+ // this is the root node
+ return StringUtils.defaultString(parentKey);
+ }
+
+ StringBuilder buf = new StringBuilder(BUF_SIZE);
+ if (StringUtils.isNotEmpty(parentKey))
+ {
+ buf.append(parentKey).append(PATH_DELIMITER);
+ }
+ buf.append(handler.nodeName(node));
+ buf.append(START_INDEX);
+ buf.append(determineIndex(parent, node, handler));
+ buf.append(END_INDEX);
+ return buf.toString();
+ }
+
+ /**
+ * {@inheritDoc} The expected format of the passed in key is explained in
+ * the class comment.
*/
- public NodeAddData prepareAdd(ConfigurationNode root, String key)
+ public <T> NodeAddData<T> prepareAdd(T root, String key,
+ NodeHandler<T> handler)
{
if (key == null)
{
@@ -254,55 +314,61 @@ public class XPathExpressionEngine imple
int index = findKeySeparator(addKey);
if (index < 0)
{
- addKey = generateKeyForAdd(root, addKey);
+ addKey = generateKeyForAdd(root, addKey, handler);
index = findKeySeparator(addKey);
}
+ else if (index >= addKey.length() - 1)
+ {
+ invalidPath(addKey, " new node path must not be empty.");
+ }
- List<ConfigurationNode> nodes = query(root, addKey.substring(0,
index).trim());
+ List<QueryResult<T>> nodes =
+ query(root, addKey.substring(0, index).trim(), handler);
if (nodes.size() != 1)
{
- throw new IllegalArgumentException(
- "prepareAdd: key must select exactly one target node!");
+ throw new IllegalArgumentException("prepareAdd: key '" + key
+ + "' must select exactly one target node!");
}
- NodeAddData data = new NodeAddData();
- data.setParent(nodes.get(0));
- initNodeAddData(data, addKey.substring(index).trim());
- return data;
+ return createNodeAddData(addKey.substring(index).trim(), nodes.get(0));
}
/**
- * Creates the {@code JXPathContext} used for executing a query. This
- * method will create a new context and ensure that it is correctly
- * initialized.
+ * Creates the {@code JXPathContext} to be used for executing a query. This
+ * method delegates to the context factory.
*
* @param root the configuration root node
- * @param key the key to be queried
+ * @param handler the node handler
* @return the new context
*/
- protected JXPathContext createContext(ConfigurationNode root, String key)
+ private <T> JXPathContext createContext(T root, NodeHandler<T> handler)
{
- JXPathContext context = JXPathContext.newContext(root);
- context.setLenient(true);
- return context;
+ return getContextFactory().createContext(root, handler);
}
/**
- * Initializes most properties of a {@code NodeAddData} object. This
- * method is called by {@code prepareAdd()} after the parent node has
- * been found. Its task is to interpret the passed in path of the new node.
+ * Creates a {@code NodeAddData} object as a result of a
+ * {@code prepareAdd()} operation. This method interprets the passed in
path
+ * of the new node.
*
- * @param data the data object to initialize
* @param path the path of the new node
+ * @param parentNodeResult the parent node
+ * @param <T> the type of the nodes involved
*/
- protected void initNodeAddData(NodeAddData data, String path)
+ <T> NodeAddData<T> createNodeAddData(String path,
+ QueryResult<T> parentNodeResult)
{
+ if (parentNodeResult.isAttributeResult())
+ {
+ invalidPath(path, " cannot add properties to an attribute.");
+ }
+ List<String> pathNodes = new LinkedList<String>();
String lastComponent = null;
boolean attr = false;
boolean first = true;
- StringTokenizer tok = new StringTokenizer(path, NODE_PATH_DELIMITERS,
- true);
+ StringTokenizer tok =
+ new StringTokenizer(path, NODE_PATH_DELIMITERS, true);
while (tok.hasMoreTokens())
{
String token = tok.nextToken();
@@ -311,14 +377,14 @@ public class XPathExpressionEngine imple
if (attr)
{
invalidPath(path, " contains an attribute"
- + " delimiter at an unallowed position.");
+ + " delimiter at a disallowed position.");
}
if (lastComponent == null)
{
invalidPath(path,
- " contains a '/' at an unallowed position.");
+ " contains a '/' at a disallowed position.");
}
- data.addPathNode(lastComponent);
+ pathNodes.add(lastComponent);
lastComponent = null;
}
@@ -332,11 +398,11 @@ public class XPathExpressionEngine imple
if (lastComponent == null && !first)
{
invalidPath(path,
- " contains an attribute delimiter at an unallowed
position.");
+ " contains an attribute delimiter at a disallowed
position.");
}
if (lastComponent != null)
{
- data.addPathNode(lastComponent);
+ pathNodes.add(lastComponent);
}
attr = true;
lastComponent = null;
@@ -353,29 +419,42 @@ public class XPathExpressionEngine imple
{
invalidPath(path, "contains no components.");
}
- data.setNewNodeName(lastComponent);
- data.setAttribute(attr);
+
+ return new NodeAddData<T>(parentNodeResult.getNode(), lastComponent,
+ attr, pathNodes);
+ }
+
+ /**
+ * Returns the {@code XPathContextFactory} used by this instance.
+ *
+ * @return the {@code XPathContextFactory}
+ */
+ XPathContextFactory getContextFactory()
+ {
+ return contextFactory;
}
/**
* Tries to generate a key for adding a property. This method is called if
a
* key was used for adding properties which does not contain a space
* character. It splits the key at its single components and searches for
- * the last existing component. Then a key compatible for adding properties
- * is generated.
+ * the last existing component. Then a key compatible key for adding
+ * properties is generated.
*
* @param root the root node of the configuration
* @param key the key in question
+ * @param handler the node handler
* @return the key to be used for adding the property
*/
- private String generateKeyForAdd(ConfigurationNode root, String key)
+ private <T> String generateKeyForAdd(T root, String key,
+ NodeHandler<T> handler)
{
int pos = key.lastIndexOf(PATH_DELIMITER, key.length());
while (pos >= 0)
{
String keyExisting = key.substring(0, pos);
- if (!query(root, keyExisting).isEmpty())
+ if (!query(root, keyExisting, handler).isEmpty())
{
StringBuilder buf = new StringBuilder(key.length() + 1);
buf.append(keyExisting).append(SPACE);
@@ -389,12 +468,29 @@ public class XPathExpressionEngine imple
}
/**
+ * Determines the index of the given child node in the node list of its
+ * parent.
+ *
+ * @param parent the parent node
+ * @param child the child node
+ * @param handler the node handler
+ * @param <T> the type of the nodes involved
+ * @return the index of this child node
+ */
+ private static <T> int determineIndex(T parent, T child,
+ NodeHandler<T> handler)
+ {
+ return handler.getChildren(parent, handler.nodeName(child)).indexOf(
+ child) + 1;
+ }
+
+ /**
* Helper method for throwing an exception about an invalid path.
*
* @param path the invalid path
* @param msg the exception message
*/
- private void invalidPath(String path, String msg)
+ private static void invalidPath(String path, String msg)
{
throw new IllegalArgumentException("Invalid node path: \"" + path
+ "\" " + msg);
@@ -417,6 +513,54 @@ public class XPathExpressionEngine imple
return index;
}
+ /**
+ * Converts the objects returned as query result from the JXPathContext to
+ * query result objects.
+ *
+ * @param results the list with results from the context
+ * @param <T> the type of results to be produced
+ * @return the result list
+ */
+ private static <T> List<QueryResult<T>> convertResults(List<?> results)
+ {
+ List<QueryResult<T>> queryResults =
+ new ArrayList<QueryResult<T>>(results.size());
+ for (Object res : results)
+ {
+ QueryResult<T> queryResult = createResult(res);
+ queryResults.add(queryResult);
+ }
+ return queryResults;
+ }
+
+ /**
+ * Creates a {@code QueryResult} object from the given result object of a
+ * query. Because of the node pointers involved result objects can only be
+ * of two types:
+ * <ul>
+ * <li>nodes of type T</li>
+ * <li>attribute results already wrapped in {@code QueryResult}
objects</li>
+ * </ul>
+ * This method performs a corresponding cast. Warnings can be suppressed
+ * because of the implementation of the query functionality.
+ *
+ * @param resObj the query result object
+ * @param <T> the type of the result to be produced
+ * @return the {@code QueryResult}
+ */
+ @SuppressWarnings("unchecked")
+ private static <T> QueryResult<T> createResult(Object resObj)
+ {
+ if (resObj instanceof QueryResult)
+ {
+ return (QueryResult<T>) resObj;
+ }
+ else
+ {
+ return QueryResult.createNodeResult((T) resObj);
+ }
+ }
+
// static initializer: registers the configuration node pointer factory
static
{
Modified:
commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngine.java
URL:
http://svn.apache.org/viewvc/commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngine.java?rev=1565802&r1=1565801&r2=1565802&view=diff
==============================================================================
---
commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngine.java
(original)
+++
commons/proper/configuration/branches/immutableNodes/src/test/java/org/apache/commons/configuration/tree/xpath/TestXPathExpressionEngine.java
Fri Feb 7 20:38:07 2014
@@ -17,59 +17,124 @@
package org.apache.commons.configuration.tree.xpath;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
-import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
-import org.apache.commons.configuration.tree.ConfigurationNode;
-import org.apache.commons.configuration.tree.DefaultConfigurationNode;
+import org.apache.commons.configuration.tree.ImmutableNode;
+import org.apache.commons.configuration.tree.InMemoryNodeModel;
import org.apache.commons.configuration.tree.NodeAddData;
+import org.apache.commons.configuration.tree.NodeHandler;
+import org.apache.commons.configuration.tree.QueryResult;
import org.apache.commons.jxpath.JXPathContext;
import org.apache.commons.jxpath.ri.JXPathContextReferenceImpl;
import org.apache.commons.jxpath.ri.model.NodePointerFactory;
-import org.junit.Before;
+import org.easymock.EasyMock;
+import org.junit.BeforeClass;
import org.junit.Test;
/**
* Test class for XPathExpressionEngine.
*
- * @author <a
- * href="http://commons.apache.org/configuration/team-list.html">Commons
- * Configuration team</a>
* @version $Id$
*/
public class TestXPathExpressionEngine
{
- /** Constant for the test root node. */
- static final ConfigurationNode ROOT = new DefaultConfigurationNode(
- "testRoot");
-
/** Constant for the valid test key. */
- static final String TEST_KEY = "TESTKEY";
+ private static final String TEST_KEY = "TESTKEY";
+
+ /** Constant for the name of the root node. */
+ private static final String ROOT_NAME = "testRoot";
+
+ /** The test root node. */
+ private static ImmutableNode root;
- /** The expression engine to be tested. */
- XPathExpressionEngine engine;
+ /** A test node handler. */
+ private static NodeHandler<ImmutableNode> handler;
+
+ @BeforeClass
+ public static void setUpBeforeClass() throws Exception
+ {
+ root = new ImmutableNode.Builder().name(ROOT_NAME).create();
+ handler = new InMemoryNodeModel(root);
+ }
- @Before
- public void setUp() throws Exception
+ /**
+ * Creates a mock for a context and prepares it to expect a select
+ * invocation yielding the provided results.
+ *
+ * @param results the results
+ * @return the mock context
+ */
+ private JXPathContext expectSelect(Object... results)
{
- engine = new MockJXPathContextExpressionEngine();
+ JXPathContext ctx = EasyMock.createMock(JXPathContext.class);
+ EasyMock.expect(ctx.selectNodes(TEST_KEY)).andReturn(
+ Arrays.asList(results));
+ EasyMock.replay(ctx);
+ return ctx;
}
/**
- * Tests the query() method with a normal expression.
+ * Creates a test engine instance configured with a context factory which
+ * returns the given test context.
+ *
+ * @param ctx the context mock
+ * @return the test engine instance
+ */
+ private XPathExpressionEngine setUpEngine(JXPathContext ctx)
+ {
+ XPathContextFactory factory =
+ EasyMock.createMock(XPathContextFactory.class);
+ EasyMock.expect(factory.createContext(root, handler)).andReturn(ctx);
+ EasyMock.replay(factory);
+ return new XPathExpressionEngine(factory);
+ }
+
+ /**
+ * Tests whether a correct default context factory is created.
*/
@Test
- public void testQueryExpression()
+ public void testDefaultContextFactory()
{
- List<ConfigurationNode> nodes = engine.query(ROOT, TEST_KEY);
- assertEquals("Incorrect number of results", 1, nodes.size());
- assertSame("Wrong result node", ROOT, nodes.get(0));
- checkSelectCalls(1);
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertNotNull("No context factory", engine.getContextFactory());
+ }
+
+ /**
+ * Tests the query() method with an expression yielding a node.
+ */
+ @Test
+ public void testQueryNodeExpression()
+ {
+ JXPathContext ctx = expectSelect(root);
+ XPathExpressionEngine engine = setUpEngine(ctx);
+ List<QueryResult<ImmutableNode>> result =
+ engine.query(root, TEST_KEY, handler);
+ assertEquals("Incorrect number of results", 1, result.size());
+ assertSame("Wrong result node", root, result.get(0).getNode());
+ assertFalse("No node result", result.get(0).isAttributeResult());
+ }
+
+ /**
+ * Tests a query which yields an attribute result.
+ */
+ @Test
+ public void testQueryAttributeExpression()
+ {
+ QueryResult<ImmutableNode> attrResult =
+ QueryResult.createAttributeResult(root, "attr");
+ JXPathContext ctx = expectSelect(attrResult);
+ XPathExpressionEngine engine = setUpEngine(ctx);
+ List<QueryResult<ImmutableNode>> result =
+ engine.query(root, TEST_KEY, handler);
+ assertEquals("Incorrect number of results", 1, result.size());
+ assertSame("Wrong result", attrResult, result.get(0));
}
/**
@@ -78,9 +143,10 @@ public class TestXPathExpressionEngine
@Test
public void testQueryWithoutResult()
{
- List<ConfigurationNode> nodes = engine.query(ROOT, "a non existing
key");
- assertTrue("Result list is not empty", nodes.isEmpty());
- checkSelectCalls(1);
+ JXPathContext ctx = expectSelect();
+ XPathExpressionEngine engine = setUpEngine(ctx);
+ assertTrue("Got results", engine.query(root, TEST_KEY, handler)
+ .isEmpty());
}
/**
@@ -103,36 +169,36 @@ public class TestXPathExpressionEngine
}
/**
- * Helper method for testing undefined keys.
+ * Helper method for testing queries with undefined keys.
*
* @param key the key
*/
private void checkEmptyKey(String key)
{
- List<ConfigurationNode> nodes = engine.query(ROOT, key);
- assertEquals("Incorrect number of results", 1, nodes.size());
- assertSame("Wrong result node", ROOT, nodes.get(0));
- checkSelectCalls(0);
+ XPathContextFactory factory =
+ EasyMock.createMock(XPathContextFactory.class);
+ EasyMock.replay(factory);
+ XPathExpressionEngine engine = new XPathExpressionEngine(factory);
+ List<QueryResult<ImmutableNode>> results =
+ engine.query(root, key, handler);
+ assertEquals("Incorrect number of results", 1, results.size());
+ assertSame("Wrong result node", root, results.get(0).getNode());
}
/**
- * Tests if the used JXPathContext is correctly initialized.
+ * Tests if the JXPathContext is correctly initialized with the node
pointer
+ * factory.
*/
@Test
- public void testCreateContext()
+ public void testNodePointerFactory()
{
- JXPathContext ctx = new XPathExpressionEngine().createContext(ROOT,
- TEST_KEY);
- assertNotNull("Context is null", ctx);
- assertTrue("Lenient mode is not set", ctx.isLenient());
- assertSame("Incorrect context bean set", ROOT, ctx.getContextBean());
-
- NodePointerFactory[] factories = JXPathContextReferenceImpl
- .getNodePointerFactories();
+ JXPathContext.newContext(this);
+ NodePointerFactory[] factories =
+ JXPathContextReferenceImpl.getNodePointerFactories();
boolean found = false;
- for (int i = 0; i < factories.length; i++)
+ for (NodePointerFactory factory : factories)
{
- if (factories[i] instanceof ConfigurationNodePointerFactory)
+ if (factory instanceof ConfigurationNodePointerFactory)
{
found = true;
}
@@ -146,31 +212,31 @@ public class TestXPathExpressionEngine
@Test
public void testNodeKeyNormal()
{
- assertEquals("Wrong node key", "parent/child", engine.nodeKey(
- new DefaultConfigurationNode("child"), "parent"));
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertEquals("Wrong node key", "parent/" + ROOT_NAME,
+ engine.nodeKey(root, "parent", handler));
}
/**
- * Tests nodeKey() for an attribute node.
+ * Tests nodeKey() for the root node.
*/
@Test
- public void testNodeKeyAttribute()
+ public void testNodeKeyForRootNode()
{
- ConfigurationNode node = new DefaultConfigurationNode("attr");
- node.setAttribute(true);
- assertEquals("Wrong attribute key", "node/@attr", engine.nodeKey(node,
- "node"));
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertEquals("Wrong key for root node", "",
+ engine.nodeKey(root, null, handler));
}
/**
- * Tests nodeKey() for the root node.
+ * Tests a node key if the node does not have a name.
*/
@Test
- public void testNodeKeyForRootNode()
+ public void testNodeKeyNoNodeName()
{
- assertEquals("Wrong key for root node", "", engine.nodeKey(ROOT,
null));
+ XPathExpressionEngine engine = new XPathExpressionEngine();
assertEquals("Null name not detected", "test", engine.nodeKey(
- new DefaultConfigurationNode(), "test"));
+ new ImmutableNode.Builder().create(), "test", handler));
}
/**
@@ -179,12 +245,31 @@ public class TestXPathExpressionEngine
@Test
public void testNodeKeyForRootChild()
{
- ConfigurationNode node = new DefaultConfigurationNode("child");
- assertEquals("Wrong key for root child node", "child", engine.nodeKey(
- node, ""));
- node.setAttribute(true);
- assertEquals("Wrong key for root attribute", "@child", engine.nodeKey(
- node, ""));
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertEquals("Wrong key for root child node", ROOT_NAME,
+ engine.nodeKey(root, "", handler));
+ }
+
+ /**
+ * Tests whether the key of an attribute can be generated..
+ */
+ @Test
+ public void testNodeKeyAttribute()
+ {
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertEquals("Wrong attribute key", "node/@attr",
+ engine.attributeKey("node", "attr"));
+ }
+
+ /**
+ * Tests the key of an attribute which belongs to the root node.
+ */
+ @Test
+ public void testAttributeKeyOfRootNode()
+ {
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertEquals("Wrong key for root attribute", "@child",
+ engine.attributeKey(null, "child"));
}
/**
@@ -193,10 +278,11 @@ public class TestXPathExpressionEngine
@Test
public void testPrepareAddNode()
{
- NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + " newNode");
- checkAddPath(data, new String[]
- { "newNode" }, false);
- checkSelectCalls(1);
+ JXPathContext ctx = expectSelect(root);
+ XPathExpressionEngine engine = setUpEngine(ctx);
+ NodeAddData<ImmutableNode> data =
+ engine.prepareAdd(root, TEST_KEY + " newNode", handler);
+ checkAddPath(data, false, "newNode");
}
/**
@@ -205,10 +291,11 @@ public class TestXPathExpressionEngine
@Test
public void testPrepareAddAttribute()
{
- NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY + "\t@newAttr");
- checkAddPath(data, new String[]
- { "newAttr" }, true);
- checkSelectCalls(1);
+ JXPathContext ctx = expectSelect(root);
+ XPathExpressionEngine engine = setUpEngine(ctx);
+ NodeAddData<ImmutableNode> data =
+ engine.prepareAdd(root, TEST_KEY + "\t@newAttr", handler);
+ checkAddPath(data, true, "newAttr");
}
/**
@@ -217,11 +304,12 @@ public class TestXPathExpressionEngine
@Test
public void testPrepareAddPath()
{
- NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY
- + " \t a/full/path/node");
- checkAddPath(data, new String[]
- { "a", "full", "path", "node" }, false);
- checkSelectCalls(1);
+ JXPathContext ctx = expectSelect(root);
+ XPathExpressionEngine engine = setUpEngine(ctx);
+ NodeAddData<ImmutableNode> data =
+ engine.prepareAdd(root, TEST_KEY + " \t a/full/path/node",
+ handler);
+ checkAddPath(data, false, "a", "full", "path", "node");
}
/**
@@ -230,11 +318,11 @@ public class TestXPathExpressionEngine
@Test
public void testPrepareAddAttributePath()
{
- NodeAddData data = engine.prepareAdd(ROOT, TEST_KEY
- + " a/full/path@attr");
- checkAddPath(data, new String[]
- { "a", "full", "path", "attr" }, true);
- checkSelectCalls(1);
+ JXPathContext ctx = expectSelect(root);
+ XPathExpressionEngine engine = setUpEngine(ctx);
+ NodeAddData<ImmutableNode> data =
+ engine.prepareAdd(root, TEST_KEY + " a/full/path@attr",
handler);
+ checkAddPath(data, true, "a", "full", "path", "attr");
}
/**
@@ -243,10 +331,11 @@ public class TestXPathExpressionEngine
@Test
public void testPrepareAddRootChild()
{
- NodeAddData data = engine.prepareAdd(ROOT, " newNode");
- checkAddPath(data, new String[]
- { "newNode" }, false);
- checkSelectCalls(0);
+ JXPathContext ctx = expectSelect(root);
+ XPathExpressionEngine engine = setUpEngine(ctx);
+ NodeAddData<ImmutableNode> data =
+ engine.prepareAdd(root, " newNode", handler);
+ checkAddPath(data, false, "newNode");
}
/**
@@ -255,10 +344,11 @@ public class TestXPathExpressionEngine
@Test
public void testPrepareAddRootAttribute()
{
- NodeAddData data = engine.prepareAdd(ROOT, " @attr");
- checkAddPath(data, new String[]
- { "attr" }, true);
- checkSelectCalls(0);
+ JXPathContext ctx = expectSelect(root);
+ XPathExpressionEngine engine = setUpEngine(ctx);
+ NodeAddData<ImmutableNode> data =
+ engine.prepareAdd(root, " @attr", handler);
+ checkAddPath(data, true, "attr");
}
/**
@@ -267,7 +357,9 @@ public class TestXPathExpressionEngine
@Test(expected = IllegalArgumentException.class)
public void testPrepareAddInvalidParent()
{
- engine.prepareAdd(ROOT, "invalidKey newNode");
+ JXPathContext ctx = expectSelect();
+ XPathExpressionEngine engine = setUpEngine(ctx);
+ engine.prepareAdd(root, TEST_KEY + " test", handler);
}
/**
@@ -276,7 +368,8 @@ public class TestXPathExpressionEngine
@Test(expected = IllegalArgumentException.class)
public void testPrepareAddEmptyPath()
{
- engine.prepareAdd(ROOT, TEST_KEY + " ");
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ engine.prepareAdd(root, TEST_KEY + " ", handler);
}
/**
@@ -285,16 +378,32 @@ public class TestXPathExpressionEngine
@Test(expected = IllegalArgumentException.class)
public void testPrepareAddNullKey()
{
- engine.prepareAdd(ROOT, null);
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ engine.prepareAdd(root, null, handler);
}
/**
- * Tests an add operation where the key is null.
+ * Tests an add operation where the key is empty.
*/
@Test(expected = IllegalArgumentException.class)
public void testPrepareAddEmptyKey()
{
- engine.prepareAdd(ROOT, "");
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ engine.prepareAdd(root, "", handler);
+ }
+
+ /**
+ * Helper method for checking whether an exception is thrown for an invalid
+ * path passed to prepareAdd().
+ *
+ * @param path the path to be tested
+ * @throws IllegalArgumentException if the test is successful
+ */
+ private void checkInvalidAddPath(String path)
+ {
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ QueryResult<ImmutableNode> res = QueryResult.createNodeResult(root);
+ engine.createNodeAddData(path, res);
}
/**
@@ -303,7 +412,7 @@ public class TestXPathExpressionEngine
@Test(expected = IllegalArgumentException.class)
public void testPrepareAddInvalidPath()
{
- engine.prepareAdd(ROOT, TEST_KEY + " an/invalid//path");
+ checkInvalidAddPath("an/invalid//path");
}
/**
@@ -313,7 +422,7 @@ public class TestXPathExpressionEngine
@Test(expected = IllegalArgumentException.class)
public void testPrepareAddInvalidAttributePath()
{
- engine.prepareAdd(ROOT, TEST_KEY + " a/path/with@an/attribute");
+ checkInvalidAddPath("a/path/with@an/attribute");
}
/**
@@ -323,7 +432,7 @@ public class TestXPathExpressionEngine
@Test(expected = IllegalArgumentException.class)
public void testPrepareAddInvalidAttributePath2()
{
- engine.prepareAdd(ROOT, TEST_KEY + " a/path/with/@attribute");
+ checkInvalidAddPath("a/path/with/@attribute");
}
/**
@@ -332,7 +441,7 @@ public class TestXPathExpressionEngine
@Test(expected = IllegalArgumentException.class)
public void testPrepareAddInvalidPathWithSlash()
{
- engine.prepareAdd(ROOT, TEST_KEY + " /a/path/node");
+ checkInvalidAddPath("/a/path/node");
}
/**
@@ -342,27 +451,40 @@ public class TestXPathExpressionEngine
@Test(expected = IllegalArgumentException.class)
public void testPrepareAddInvalidPathMultipleAttributes()
{
- engine.prepareAdd(ROOT, TEST_KEY + " an@attribute@path");
+ checkInvalidAddPath("an@attribute@path");
+ }
+
+ /**
+ * Tests that it is not possible to add nodes to an attribute.
+ */
+ @Test(expected = IllegalArgumentException.class)
+ public void testPrepareAddToAttributeResult()
+ {
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ QueryResult<ImmutableNode> result =
+ QueryResult.createAttributeResult(root, TEST_KEY);
+ engine.createNodeAddData("path", result);
}
/**
* Helper method for testing the path nodes in the given add data object.
*
* @param data the data object to check
- * @param expected an array with the expected path elements
* @param attr a flag if the new node is an attribute
+ * @param expected an array with the expected path elements
*/
- private void checkAddPath(NodeAddData data, String[] expected, boolean
attr)
+ private static void checkAddPath(NodeAddData<ImmutableNode> data,
+ boolean attr, String... expected)
{
- assertSame("Wrong parent node", ROOT, data.getParent());
+ assertSame("Wrong parent node", root, data.getParent());
List<String> path = data.getPathNodes();
assertEquals("Incorrect number of path nodes", expected.length - 1,
path.size());
Iterator<String> it = path.iterator();
for (int idx = 0; idx < expected.length - 1; idx++)
{
- assertEquals("Wrong node at position " + idx, expected[idx], it
- .next());
+ assertEquals("Wrong node at position " + idx, expected[idx],
+ it.next());
}
assertEquals("Wrong name of new node", expected[expected.length - 1],
data.getNewNodeName());
@@ -370,80 +492,82 @@ public class TestXPathExpressionEngine
}
/**
- * Checks if the JXPath context's selectNodes() method was called as often
- * as expected.
- *
- * @param expected the number of expected calls
+ * Tests whether a canonical key can be queried if all child nodes have
+ * different names.
*/
- protected void checkSelectCalls(int expected)
+ @Test
+ public void testCanonicalKeyNoDuplicates()
{
- MockJXPathContext ctx = ((MockJXPathContextExpressionEngine)
engine).getContext();
- int calls = (ctx == null) ? 0 : ctx.selectInvocations;
- assertEquals("Incorrect number of select calls", expected, calls);
+ ImmutableNode.Builder parentBuilder = new ImmutableNode.Builder(2);
+ ImmutableNode c1 = new ImmutableNode.Builder().name("child").create();
+ ImmutableNode c2 =
+ new ImmutableNode.Builder().name("child_other").create();
+ parentBuilder.addChildren(Arrays.asList(c2, c1));
+ ImmutableNode parent = parentBuilder.create();
+ NodeHandler<ImmutableNode> testHandler = new InMemoryNodeModel(parent);
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertEquals("Wrong canonical key", "parent/child[1]",
+ engine.canonicalKey(c1, "parent", testHandler));
}
/**
- * A mock implementation of the JXPathContext class. This implementation
- * will overwrite the <code>selectNodes()</code> method that is used by
- * <code>XPathExpressionEngine</code> to count the invocations of this
- * method.
+ * Tests whether duplicates are correctly resolved when querying for
+ * canonical keys.
*/
- static class MockJXPathContext extends JXPathContextReferenceImpl
+ @Test
+ public void testCanonicalKeyWithDuplicates()
{
- int selectInvocations;
-
- public MockJXPathContext(Object bean)
- {
- super(null, bean);
- }
-
- /**
- * Dummy implementation of this method. If the passed in string is the
- * test key, the root node will be returned in the list. Otherwise the
- * return value is <b>null</b>.
- */
- @Override
- public List<?> selectNodes(String xpath)
- {
- selectInvocations++;
- if (TEST_KEY.equals(xpath))
- {
- List<ConfigurationNode> result = new
ArrayList<ConfigurationNode>(1);
- result.add(ROOT);
- return result;
- }
- else
- {
- return null;
- }
- }
+ ImmutableNode.Builder parentBuilder = new ImmutableNode.Builder(3);
+ ImmutableNode c1 = new ImmutableNode.Builder().name("child").create();
+ ImmutableNode c2 = new ImmutableNode.Builder().name("child").create();
+ ImmutableNode c3 =
+ new ImmutableNode.Builder().name("child_other").create();
+ parentBuilder.addChildren(Arrays.asList(c1, c2, c3));
+ ImmutableNode parent = parentBuilder.create();
+ NodeHandler<ImmutableNode> testHandler = new InMemoryNodeModel(parent);
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertEquals("Wrong key 1", "parent/child[1]",
+ engine.canonicalKey(c1, "parent", testHandler));
+ assertEquals("Wrong key 2", "parent/child[2]",
+ engine.canonicalKey(c2, "parent", testHandler));
}
/**
- * A special implementation of XPathExpressionEngine that overrides
- * createContext() to return a mock context object.
+ * Tests whether the parent key can be undefined when querying a canonical
+ * key.
*/
- static class MockJXPathContextExpressionEngine extends
- XPathExpressionEngine
+ @Test
+ public void testCanonicalKeyNoParentKey()
{
- /** Stores the context instance. */
- private MockJXPathContext context;
+ ImmutableNode.Builder parentBuilder = new ImmutableNode.Builder(1);
+ ImmutableNode c1 = new ImmutableNode.Builder().name("child").create();
+ ImmutableNode parent = parentBuilder.addChild(c1).create();
+ NodeHandler<ImmutableNode> testHandler = new InMemoryNodeModel(parent);
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertEquals("Wrong key", "child[1]",
+ engine.canonicalKey(c1, null, testHandler));
+ }
- @Override
- protected JXPathContext createContext(ConfigurationNode root, String
key)
- {
- context = new MockJXPathContext(root);
- return context;
- }
+ /**
+ * Tests whether a canonical key for the parent node can be queried if no
+ * parent key was passed in.
+ */
+ @Test
+ public void testCanonicalKeyRootNoParentKey()
+ {
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertEquals("Wrong key", "", engine.canonicalKey(root, null,
handler));
+ }
- /**
- * Returns the context created by the last newContext() call.
- *
- * @return the current context
- */
- public MockJXPathContext getContext()
- {
- return context;
- }
+ /**
+ * Tests whether a parent key is evaluated when determining the canonical
+ * key of the root node.
+ */
+ @Test
+ public void testCanonicalKeyRootWithParentKey()
+ {
+ XPathExpressionEngine engine = new XPathExpressionEngine();
+ assertEquals("Wrong key", "parent",
+ engine.canonicalKey(root, "parent", handler));
}
}