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>
  * &quot;tables/table[last()]/fields/field/name&quot;
@@ -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));
     }
 }


Reply via email to