From f2a2500c8d2cdc83dd74cd921eade676d69646b3 Mon Sep 17 00:00:00 2001
From: Jukka Zitting <jukka@apache.org>
Date: Wed, 24 Aug 2011 17:41:44 +0200
Subject: [PATCH] JCR-3060: Add utility methods for path creation

Extend the getOrAddNode() and related methods to support relative
(or absolute) paths instead of just single path elements.

Also add new addNode() methods that support "*" node name suffixes
for creating (mostly) unique node names with a sequence number.
---
 jackrabbit-jcr-commons/pom.xml                     |    5 +
 .../org/apache/jackrabbit/commons/JcrUtils.java    |  279 +++++++++++++++-----
 .../apache/jackrabbit/commons/JcrUtilsTest.java    |  115 ++++++++
 jackrabbit-parent/pom.xml                          |    4 +-
 4 files changed, 341 insertions(+), 62 deletions(-)

diff --git a/jackrabbit-jcr-commons/pom.xml b/jackrabbit-jcr-commons/pom.xml
index 6597269..7ac3847 100644
--- a/jackrabbit-jcr-commons/pom.xml
+++ b/jackrabbit-jcr-commons/pom.xml
@@ -82,6 +82,11 @@
       <scope>test</scope>
     </dependency>
     <dependency>
+      <groupId>org.easymock</groupId>
+      <artifactId>easymock</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>cglib</groupId>
       <artifactId>cglib</artifactId>
       <scope>test</scope>
diff --git a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/JcrUtils.java b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/JcrUtils.java
index ebee853..22cb261 100644
--- a/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/JcrUtils.java
+++ b/jackrabbit-jcr-commons/src/main/java/org/apache/jackrabbit/commons/JcrUtils.java
@@ -36,6 +36,7 @@ import javax.jcr.Binary;
 import javax.jcr.Item;
 import javax.jcr.Node;
 import javax.jcr.NodeIterator;
+import javax.jcr.PathNotFoundException;
 import javax.jcr.Property;
 import javax.jcr.PropertyIterator;
 import javax.jcr.PropertyType;
@@ -471,58 +472,203 @@ public class JcrUtils {
     }
 
     /**
-     * Returns the named child of the given node, creating the child if
-     * it does not already exist. If the child node gets added, then its
-     * type will be determined by the child node definitions associated
-     * with the parent node. The caller is expected to take care of saving
-     * or discarding any transient changes.
+     * Creates a node with the given name and type under the given parent.
+     * This is a transient change that needs to be saved or discarded
+     * by the caller.
+     * <p>
+     * An optional "*" at the end of the node name is replaced with a
+     * sequence number that is incremented until a child node with the
+     * same name no longer exist. Note that the check for a child node
+     * with such a sequence-numbered name is not atomic, so this feature
+     * should only be used when no other thread is concurrently adding
+     * nodes under the same parent.
+     * <p>
+     * The child node will be created with default type determined by the
+     * child node definitions of the parent node.
      *
-     * @see Node#getNode(String)
      * @see Node#addNode(String)
+     * @since Apache Jackrabbit 2.3
      * @param parent parent node
-     * @param name name of the child node
-     * @return the child node
-     * @throws RepositoryException if the child node can not be
-     *                             accessed or created
+     * @param name name of the child node, optionally with a "*" suffix
+     * @return the created child node
+     * @throws RepositoryException if the child node could not be created
+     */
+    public static Node addNode(Node parent, String name)
+            throws RepositoryException {
+        return addNode(parent, name, null);
+    }
+
+    /**
+     * Creates a node with the given name and type under the given parent.
+     * This is a transient change that needs to be saved or discarded
+     * by the caller.
+     * <p>
+     * An optional "*" at the end of the node name is replaced with a
+     * sequence number that is incremented until a child node with the
+     * same name no longer exist. Note that the check for a child node
+     * with such a sequence-numbered name is not atomic, so this feature
+     * should only be used when no other thread is concurrently adding
+     * nodes under the same parent.
+     * <p>
+     * The child node will be created with the given node type, or the
+     * default type determined by the child node definitions of the parent
+     * node when the given type is <code>null</code>.
+     *
+     * @see Node#addNode(String, Type)
+     * @since Apache Jackrabbit 2.3
+     * @param parent parent node
+     * @param name name of the child node, optionally with a "*" suffix
+     * @param type type of the child node, or <code>null</code>
+     * @return the created child node
+     * @throws RepositoryException if the child node could not be created
      */
-    public static Node getOrAddNode(Node parent, String name)
+    public static Node addNode(Node parent, String name, String type)
             throws RepositoryException {
-        if (parent.hasNode(name)) {
-            return parent.getNode(name);
+        if (name.endsWith("*")) {
+            String base = name.substring(0, name.length() - 1);
+            int count = 1;
+            do {
+                name = base + count++;
+            } while (parent.hasNode(name));
+        }
+        if (type != null) {
+            return parent.addNode(name, type);
         } else {
             return parent.addNode(name);
         }
     }
 
     /**
-     * Returns the named child of the given node, creating the child if
-     * it does not already exist. If the child node gets added, then it
-     * is created with the given node type. The caller is expected to take
-     * care of saving or discarding any transient changes.
+     * Returns the identified node, creating the node and any intermediate
+     * nodes along the path if they don't already exist. The given parent
+     * node is used for resolving relative paths, and any new nodes are
+     * created using the {@link #addNode(Node, String)} method. Only the last
+     * element of the path may use the "*" suffix as described in the addNode
+     * method. This is a transient change that needs to be saved or discarded
+     * by the caller.
+     *
+     * @see Node#getNode(String)
+     * @see Node#addNode(String)
+     * @param parent root of a relative path
+     * @param path path of the node to be returned
+     * @return the identified node
+     * @throws RepositoryException if the identified node can not be
+     *                             accessed or created
+     */
+    public static Node getOrAddNode(Node parent, String path)
+            throws RepositoryException {
+        return getOrAddNode(parent, path, null);
+    }
+
+    /**
+     * Returns the identified node, creating the node and any intermediate
+     * nodes along the path if they don't already exist. The given parent
+     * node is used for resolving relative paths, and any new nodes are
+     * created using the {@link #addNode(Node, String, String)} method with
+     * the given type as the last argument. Only the last element of the path
+     * may use the "*" suffix as described in the addNode method. This is a
+     * transient change that needs to be saved or discarded by the caller.
      *
      * @see Node#getNode(String)
      * @see Node#addNode(String, String)
-     * @see Node#isNodeType(String)
-     * @param parent parent node
-     * @param name name of the child node
-     * @param type type of the child node, ignored if the child already exists
-     * @return the child node
-     * @throws RepositoryException if the child node can not be accessed
-     *                             or created
+     * @param parent root of a relative path
+     * @param path path of the node to be returned
+     * @param type type of any new nodes or <code>null</code>,
+     *             ignored if the path already exists
+     * @return the identified node
+     * @throws RepositoryException if the identified node can not be
+     *                             accessed or created
+     */
+    public static Node getOrAddNode(Node parent, String path, String type)
+            throws RepositoryException {
+        return getOrAddNode(parent, path, type, type);
+    }
+
+    /**
+     * Returns the identified node, creating the node and any intermediate
+     * nodes along the path if they don't already exist. The given parent
+     * node is used for resolving relative paths, and any new nodes are
+     * created using the {@link #addNode(Node, String, String)} method with
+     * the given path type for intermediate nodes and leaf type for the final
+     * node identified by the given path. Only the last element of the path
+     * may use the "*" suffix as described in the addNode method. This is a
+     * transient change that needs to be saved or discarded by the caller.
+     *
+     * @see Node#getNode(String)
+     * @see Node#addNode(String, String)
+     * @param parent root of a relative path
+     * @param path path of the node to be returned
+     * @param pathType type of any new intermediate nodes or <code>null</code>,
+     *                 ignored if the path already exists
+     * @param leafType type of a new leaf node or <code>null</code>,
+     *                 ignored if the path already exists
+     * @return the identified node
+     * @throws RepositoryException if the identified node can not be
+     *                             accessed or created
      */
-    public static Node getOrAddNode(Node parent, String name, String type)
+    public static Node getOrAddNode(
+            Node parent, String path, String pathType, String leafType)
             throws RepositoryException {
-        if (parent.hasNode(name)) {
-            return parent.getNode(name);
+        // Find the first slash that separates path elements.
+        // Need to skip a possible a namespace URI in the {URI}name form.
+        int skip = 0;
+        if (path.startsWith("{")) {
+            skip = path.indexOf("}") + 1;
+        }
+        int slash = path.indexOf('/', skip);
+
+        // Split the path into the first path element (head) and
+        // the remaining path (tail). We're at the last element if the
+        // remaining path is empty.
+        String head = path;
+        String tail = "";
+        if (slash != -1) {
+            head = path.substring(0, slash);
+            tail = path.substring(slash + 1);
+            // Skip any duplicate slashes
+            while (tail.startsWith("/")) {
+                tail = tail.substring(1);
+            }
+        }
+        boolean unique = head.endsWith("*");
+        boolean last = tail.equals("");
+
+        // Process one path element, creating a node if necessary
+        if (head.equals("")) {
+            // The path is absolute, start at the root node
+            parent = parent.getSession().getRootNode();
+        } else if (head.equals("..")) {
+            parent = parent.getParent();
+        } else if (head.equals(".")) {
+            // do nothing
+        } else if (!unique && parent.hasNode(head)) {
+            parent = parent.getNode(head);
+        } else if (last) {
+            parent = addNode(parent, head, leafType);
+        } else if (unique) {
+            // The "*" name suffix only makes sense in the last path element
+            throw new PathNotFoundException(
+                    "Invalid path: " + parent.getPath() + "/" + head);
         } else {
-            return parent.addNode(name, type);
+            parent = addNode(parent, head, pathType);
+        }
+
+        // Process the rest of the path
+        if (last) {
+            return parent;
+        } else {
+            return getOrAddNode(parent, tail, leafType, pathType);
         }
     }
 
     /**
-     * Returns the named child of the given node, creating it as an
-     * nt:folder node if it does not already exist. The caller is expected
-     * to take care of saving or discarding any transient changes.
+     * Returns the identified node, creating the node and any intermediate
+     * nodes along the path if they don't already exist. The given parent
+     * node is used for resolving relative paths, and any new nodes are
+     * created using the {@link #addNode(Node, String, String)} method with
+     * the nt:folder as the node type. Only the last element of the path
+     * may use the "*" suffix as described in the addNode method. This is a
+     * transient change that needs to be saved or discarded by the caller.
      * <p>
      * Note that the type of the returned node is <em>not</em> guaranteed
      * to match nt:folder in case the node already existed. The caller can
@@ -530,21 +676,27 @@ public class JcrUtils {
      * simply use a data-first approach and not worry about the node type
      * until a constraint violation is encountered.
      *
-     * @param parent parent node
-     * @param name name of the child node
-     * @return the child node
-     * @throws RepositoryException if the child node can not be accessed
-     *                             or created
+     * @param parent root of a relative path
+     * @param path path of the node to be returned
+     * @return the identified node
+     * @throws RepositoryException if the identified node can not be
+     *                             accessed or created
      */
-    public static Node getOrAddFolder(Node parent, String name)
+    public static Node getOrAddFolder(Node parent, String path)
             throws RepositoryException {
-        return getOrAddNode(parent, name, NodeType.NT_FOLDER);
+        return getOrAddNode(parent, path, NodeType.NT_FOLDER);
     }
 
     /**
-     * Creates or updates the named child of the given node. If the child
-     * does not already exist, then it is created using the nt:file node type.
-     * This file child node is returned from this method.
+     * Returns the identified file node, creating the node and any
+     * intermediate nodes along the path if they don't already exist.
+     * The given parent node is used for resolving relative paths, and
+     * any new nodes are created using the
+     * {@link #addNode(Node, String, String)} method with the nt:folder as
+     * the node type for path nodes and nt:file for the leaf. Only the last
+     * element of the path may use the "*" suffix as described in the addNode
+     * method. This is a transient change that needs to be saved or discarded
+     * by the caller.
      * <p>
      * If the file node does not already contain a jcr:content child, then
      * one is created using the nt:resource node type. The following
@@ -569,24 +721,30 @@ public class JcrUtils {
      * <p>
      * The given binary content stream is closed by this method.
      *
-     * @param parent parent node
-     * @param name name of the file
+     * @param parent root of a relative path
+     * @param path path of the file to be returned
      * @param mime media type of the file
      * @param data binary content of the file
-     * @return the child node
-     * @throws RepositoryException if the child node can not be created
-     *                             or updated
+     * @return the file node
+     * @throws RepositoryException if the file node can not be accessed
+     *                             or created
      */
     public static Node putFile(
-            Node parent, String name, String mime, InputStream data)
+            Node parent, String path, String mime, InputStream data)
             throws RepositoryException {
-        return putFile(parent, name, mime, data, Calendar.getInstance());
+        return putFile(parent, path, mime, data, Calendar.getInstance());
     }
 
     /**
-     * Creates or updates the named child of the given node. If the child
-     * does not already exist, then it is created using the nt:file node type.
-     * This file child node is returned from this method.
+     * Returns the identified file node, creating the node and any
+     * intermediate nodes along the path if they don't already exist.
+     * The given parent node is used for resolving relative paths, and
+     * any new nodes are created using the
+     * {@link #addNode(Node, String, String)} method with the nt:folder as
+     * the node type for path nodes and nt:file for the leaf. Only the last
+     * element of the path may use the "*" suffix as described in the
+     * addNode method.This is a transient change that needs to be saved or
+     * discarded by the caller.
      * <p>
      * If the file node does not already contain a jcr:content child, then
      * one is created using the nt:resource node type. The following
@@ -611,24 +769,25 @@ public class JcrUtils {
      * <p>
      * The given binary content stream is closed by this method.
      *
-     * @param parent parent node
-     * @param name name of the file
+     * @param parent root of a relative path
+     * @param path path of the file to be returned
      * @param mime media type of the file
      * @param data binary content of the file
      * @param date date of last modification
-     * @return the child node
-     * @throws RepositoryException if the child node can not be created
-     *                             or updated
+     * @return the file node
+     * @throws RepositoryException if the file node can not be accessed
+     *                             or created
      */
     public static Node putFile(
-            Node parent, String name, String mime,
+            Node parent, String path, String mime,
             InputStream data, Calendar date) throws RepositoryException {
         Binary binary =
             parent.getSession().getValueFactory().createBinary(data);
         try {
-            Node file = getOrAddNode(parent, name, NodeType.NT_FILE);
-            Node content =
-                getOrAddNode(file, Node.JCR_CONTENT, NodeType.NT_RESOURCE);
+            Node file = getOrAddNode(
+                    parent, path, NodeType.NT_FOLDER, NodeType.NT_FILE);
+            Node content = getOrAddNode(
+                    file, Node.JCR_CONTENT, NodeType.NT_RESOURCE);
 
             content.setProperty(Property.JCR_MIMETYPE, mime);
             String[] parameters = mime.split(";");
diff --git a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/JcrUtilsTest.java b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/JcrUtilsTest.java
index c3d8f42..fc9a8c9 100644
--- a/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/JcrUtilsTest.java
+++ b/jackrabbit-jcr-commons/src/test/java/org/apache/jackrabbit/commons/JcrUtilsTest.java
@@ -20,10 +20,14 @@ import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.Map;
 
+import javax.jcr.Node;
 import javax.jcr.PropertyType;
 import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
 import javax.naming.InitialContext;
 
+import org.easymock.EasyMock;
+
 public class JcrUtilsTest extends MockCase {
 
     public void testGetRepository() throws Exception {
@@ -76,4 +80,115 @@ public class JcrUtilsTest extends MockCase {
         assertEquals(PropertyType.DATE, JcrUtils.getPropertyType(
                 PropertyType.TYPENAME_DATE.toUpperCase()));
     }
+
+    public void testAddNodeBasic() throws RepositoryException {
+        Node child = EasyMock.createMock(Node.class);
+        Node parent = EasyMock.createStrictMock(Node.class);
+
+        EasyMock.expect(parent.addNode("test")).andReturn(child);
+        EasyMock.replay(parent);
+
+        assertSame(child, JcrUtils.addNode(parent, "test"));
+        EasyMock.verify(parent);
+    }
+
+    public void testAddNodeWithSequence1() throws RepositoryException {
+        Node child = EasyMock.createMock(Node.class);
+        Node parent = EasyMock.createStrictMock(Node.class);
+
+        EasyMock.expect(parent.hasNode("test1")).andReturn(false);
+        EasyMock.expect(parent.addNode("test1")).andReturn(child);
+        EasyMock.replay(parent);
+
+        assertSame(child, JcrUtils.addNode(parent, "test*"));
+        EasyMock.verify(parent);
+    }
+
+    public void testAddNodeWithSequence2() throws RepositoryException {
+        Node child = EasyMock.createMock(Node.class);
+        Node parent = EasyMock.createStrictMock(Node.class);
+
+        EasyMock.expect(parent.hasNode("test1")).andReturn(true);
+        EasyMock.expect(parent.hasNode("test2")).andReturn(false);
+        EasyMock.expect(parent.addNode("test2")).andReturn(child);
+        EasyMock.replay(parent);
+
+        assertSame(child, JcrUtils.addNode(parent, "test*"));
+        EasyMock.verify(parent);
+    }
+
+    public void testAddNodeWithType() throws RepositoryException {
+        Node child = EasyMock.createMock(Node.class);
+        Node parent = EasyMock.createStrictMock(Node.class);
+
+        EasyMock.expect(parent.addNode("test", "nt:file")).andReturn(child);
+        EasyMock.replay(parent);
+
+        assertSame(child, JcrUtils.addNode(parent, "test", "nt:file"));
+        EasyMock.verify(parent);
+    }
+
+    public void testAddNodeWithSequenceAndType() throws RepositoryException {
+        Node child = EasyMock.createMock(Node.class);
+        Node parent = EasyMock.createStrictMock(Node.class);
+
+        EasyMock.expect(parent.hasNode("test1")).andReturn(false);
+        EasyMock.expect(parent.addNode("test1", "nt:file")).andReturn(child);
+        EasyMock.replay(parent);
+
+        assertSame(child, JcrUtils.addNode(parent, "test*", "nt:file"));
+        EasyMock.verify(parent);
+    }
+
+    public void testGetOrAddNodeExists() throws RepositoryException {
+        Node parent = EasyMock.createStrictMock(Node.class);
+        Node a = EasyMock.createStrictMock(Node.class);
+        Node b = EasyMock.createStrictMock(Node.class);
+
+        EasyMock.expect(parent.hasNode("a")).andReturn(true);
+        EasyMock.expect(parent.getNode("a")).andReturn(a);
+        EasyMock.expect(a.hasNode("b")).andReturn(true);
+        EasyMock.expect(a.getNode("b")).andReturn(b);
+        EasyMock.replay(parent);
+        EasyMock.replay(a);
+
+        assertSame(b, JcrUtils.getOrAddNode(parent, "a/b"));
+        EasyMock.verify(parent);
+        EasyMock.verify(a);
+    }
+
+    public void testGetOrAddNodeNew() throws RepositoryException {
+        Node parent = EasyMock.createStrictMock(Node.class);
+        Node a = EasyMock.createStrictMock(Node.class);
+        Node b = EasyMock.createStrictMock(Node.class);
+
+        EasyMock.expect(parent.hasNode("a")).andReturn(true);
+        EasyMock.expect(parent.getNode("a")).andReturn(a);
+        EasyMock.expect(a.hasNode("b")).andReturn(false);
+        EasyMock.expect(a.addNode("b")).andReturn(b);
+        EasyMock.replay(parent);
+        EasyMock.replay(a);
+
+        assertSame(b, JcrUtils.getOrAddNode(parent, "a/b"));
+        EasyMock.verify(parent);
+        EasyMock.verify(a);
+    }
+
+    public void testGetOrAddFolder() throws RepositoryException {
+        Node parent = EasyMock.createStrictMock(Node.class);
+        Node a = EasyMock.createStrictMock(Node.class);
+        Node b = EasyMock.createStrictMock(Node.class);
+
+        EasyMock.expect(parent.hasNode("a")).andReturn(true);
+        EasyMock.expect(parent.getNode("a")).andReturn(a);
+        EasyMock.expect(a.hasNode("b")).andReturn(false);
+        EasyMock.expect(a.addNode("b", NodeType.NT_FOLDER)).andReturn(b);
+        EasyMock.replay(parent);
+        EasyMock.replay(a);
+
+        assertSame(b, JcrUtils.getOrAddFolder(parent, "a/b"));
+        EasyMock.verify(parent);
+        EasyMock.verify(a);
+    }
+
 }
diff --git a/jackrabbit-parent/pom.xml b/jackrabbit-parent/pom.xml
index d9a669a..44d4f12 100644
--- a/jackrabbit-parent/pom.xml
+++ b/jackrabbit-parent/pom.xml
@@ -441,12 +441,12 @@
       <dependency>
         <groupId>cglib</groupId>
         <artifactId>cglib</artifactId>
-        <version>2.1_3</version>
+        <version>2.2</version>
       </dependency>
       <dependency>
         <groupId>org.easymock</groupId>
         <artifactId>easymock</artifactId>
-        <version>2.5.2</version>
+        <version>3.0</version>
       </dependency>
       <dependency>
         <groupId>org.json</groupId>
-- 
1.7.4.4

