Merge pull request #23 from apache/SLING-10011

SLING-10011 : Use JackrabbitSession.getParentOrNull when resolving parent JCR node in JcrResourceProvider#getParent
diff --git a/pom.xml b/pom.xml
index 8aeb840..fd803b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,7 +44,9 @@
     <properties>
         <site.jira.version.id>12314286</site.jira.version.id>
         <site.javadoc.exclude>**.internal.**</site.javadoc.exclude>
-        <oak.version>1.10.0</oak.version><!-- first version compatible with Jackrabbit API 2.18 (https://issues.apache.org/jira/browse/OAK-7943) -->
+        <!-- aok 1.10.0 was first version compatible with Jackrabbit API 2.18 (https://issues.apache.org/jira/browse/OAK-7943) -->
+        <!-- aok 1.42.0 first version to include JackrabbitSession.getParentOrNull in oak-jackrabbit-api (https://issues.apache.org/jira/browse/SLING-10011) -->
+        <oak.version>1.42.0</oak.version>
         <jackrabbit.version>2.18.0</jackrabbit.version><!-- required for direct binary access, https://issues.apache.org/jira/browse/JCR-4335 -->
         <project.build.outputTimestamp>1</project.build.outputTimestamp>
     </properties>
@@ -112,8 +114,8 @@
     <!-- Jackrabbit / Oak -->
         <dependency>
             <groupId>org.apache.jackrabbit</groupId>
-            <artifactId>jackrabbit-api</artifactId>
-            <version>${jackrabbit.version}</version>
+            <artifactId>oak-jackrabbit-api</artifactId>
+            <version>${oak.version}</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrItemResourceFactory.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrItemResourceFactory.java
index 22b210e..6858f19 100644
--- a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrItemResourceFactory.java
+++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrItemResourceFactory.java
@@ -35,6 +35,8 @@
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.jcr.resource.internal.HelperData;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -197,4 +199,21 @@
         return item;
     }
 
+    @Nullable Node getParentOrNull(@NotNull Item child, @NotNull String parentPath) {
+        Node parent = null;
+        try {
+            // Use fast getParentOrNull if session is a JackrabbitSession
+            if (this.isJackrabbit) {
+                parent = ((JackrabbitSession) session).getParentOrNull(child);
+            } else if (session.nodeExists(parentPath)) {
+                // Fallback to slower nodeExists & getNode pattern
+                parent = session.getNode(parentPath);
+            }
+        } catch (RepositoryException e) {
+            log.debug("Unable to access node at {}", parentPath, e);
+        }
+
+        return parent;
+    }
+
 }
diff --git a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java
index bdb7790..f28774f 100644
--- a/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java
+++ b/src/main/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProvider.java
@@ -57,7 +57,6 @@
 import org.apache.sling.jcr.resource.internal.JcrListenerBaseConfig;
 import org.apache.sling.jcr.resource.internal.JcrModifiableValueMap;
 import org.apache.sling.jcr.resource.internal.JcrResourceListener;
-import org.apache.sling.jcr.resource.internal.NodeUtil;
 import org.apache.sling.spi.resource.provider.ObserverConfiguration;
 import org.apache.sling.spi.resource.provider.ProviderContext;
 import org.apache.sling.spi.resource.provider.QueryLanguageProvider;
@@ -358,28 +357,21 @@
     @Override
     public @Nullable Resource getParent(final @NotNull ResolveContext<JcrProviderState> ctx, final @NotNull Resource child) {
         if (child instanceof JcrItemResource<?>) {
-            try {
-                String version = null;
-                if (child.getResourceMetadata().getParameterMap() != null) {
-                    version = child.getResourceMetadata().getParameterMap().get("v");
-                }
-                if (version == null) {
-                    String parentPath = ResourceUtil.getParent(child.getPath());
-                    if (parentPath != null) {
-                        Item parentItem = ctx.getProviderState().getResourceFactory()
-                            .getItemOrNull(parentPath);
-                        if (parentItem != null && parentItem.isNode()) {
-                            return new JcrNodeResource(ctx.getResourceResolver(),
-                                parentPath, null, (Node)parentItem,
-                                ctx.getProviderState().getHelperData());
-                        }
-                    }
-                    return null;
-                }
-            } catch (RepositoryException e) {
-                logger.warn("Can't get parent for {}", child, e);
-                return null;
+            String version = null;
+            if (child.getResourceMetadata().getParameterMap() != null) {
+                version = child.getResourceMetadata().getParameterMap().get("v");
             }
+            if (version == null) {
+                String parentPath = ResourceUtil.getParent(child.getPath());
+                if (parentPath != null) {
+                    Item childItem = ((JcrItemResource) child).getItem();
+                    Node parentNode = ctx.getProviderState().getResourceFactory().getParentOrNull(childItem, parentPath);
+                    if (parentNode != null) {
+                        return new JcrNodeResource(ctx.getResourceResolver(), parentPath, null, parentNode, ctx.getProviderState().getHelperData());
+                    }
+                }
+            }
+            return null;
         }
         return super.getParent(ctx, child);
     }
diff --git a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrItemResourceFactoryTest.java b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrItemResourceFactoryTest.java
index 3d65ebd..a1c4dc9 100644
--- a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrItemResourceFactoryTest.java
+++ b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrItemResourceFactoryTest.java
@@ -18,20 +18,28 @@
  */
 package org.apache.sling.jcr.resource.internal.helper.jcr;
 
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.commons.JcrUtils;
+import org.apache.jackrabbit.commons.jackrabbit.authorization.AccessControlUtils;
+import org.apache.jackrabbit.oak.commons.PathUtils;
+import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
+import org.apache.sling.api.resource.external.URIProvider;
+import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
+import org.apache.sling.jcr.resource.internal.HelperData;
+
+import javax.jcr.GuestCredentials;
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.security.Privilege;
 import java.lang.reflect.InvocationHandler;
 import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 import java.util.concurrent.atomic.AtomicReference;
 
-import javax.jcr.Item;
-import javax.jcr.Node;
-import javax.jcr.RepositoryException;
-import javax.jcr.Session;
-
-import org.apache.jackrabbit.commons.JcrUtils;
-import org.apache.sling.api.resource.external.URIProvider;
-import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
-import org.apache.sling.jcr.resource.internal.HelperData;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 public class JcrItemResourceFactoryTest extends SlingRepositoryTestBase {
 
@@ -41,7 +49,7 @@
 
     private Node node;
     private Session nonJackrabbitSession;
-
+    
     @Override
     protected void setUp() throws Exception {
         super.setUp();
@@ -58,6 +66,9 @@
                 return method.invoke(session, args);
             }
         });
+
+        AccessControlUtils.allow(node, EveryonePrincipal.NAME, Privilege.JCR_READ);
+        session.save();
     }
 
     @Override
@@ -100,5 +111,53 @@
             assertEquals(expectedPath, item2.getPath());
         }
     }
+    
+    public void testGetParentOrNullExistingNode() throws RepositoryException {
+        compareGetParentOrNull(session, EXISTING_NODE_PATH, false);
+        compareGetParentOrNull(nonJackrabbitSession, EXISTING_NODE_PATH, false);
+    }
 
+    public void testGetParentOrNullExistingProperty() throws RepositoryException {
+        compareGetParentOrNull(session,EXISTING_NODE_PATH + "/" + JcrConstants.JCR_PRIMARYTYPE, false);
+        compareGetParentOrNull(nonJackrabbitSession,EXISTING_NODE_PATH + "/" + JcrConstants.JCR_PRIMARYTYPE, false);
+    }
+    
+    public void testGetParentOrNullNonAccessibleParent() throws RepositoryException {
+        Session guestSession = null;
+        try {
+            guestSession = session.getRepository().login(new GuestCredentials());
+            compareGetParentOrNull(guestSession, EXISTING_NODE_PATH, true);
+        } finally {
+            if (guestSession != null) {
+                guestSession.logout();
+            }
+        }
+    }
+    
+    public void testGetParentOrNullNonExistingParentNode() throws RepositoryException {
+        Session s = mock(Session.class);
+        when(s.getItem(EXISTING_NODE_PATH)).thenReturn(nonJackrabbitSession.getItem(EXISTING_NODE_PATH));
+        when(s.nodeExists(PathUtils.getParentPath(EXISTING_NODE_PATH))).thenReturn(false);
+        compareGetParentOrNull(s, EXISTING_NODE_PATH, true);
+    }
+
+    public void testGetParentOrNullWithException() throws RepositoryException {
+        Session s = mock(Session.class);
+        when(s.getItem(EXISTING_NODE_PATH)).thenReturn(nonJackrabbitSession.getItem(EXISTING_NODE_PATH));
+        when(s.nodeExists(PathUtils.getParentPath(EXISTING_NODE_PATH))).thenThrow(new RepositoryException());
+        compareGetParentOrNull(s, EXISTING_NODE_PATH, true);
+    }
+ 
+    private void compareGetParentOrNull(Session s, String path, boolean nullExpected) throws RepositoryException {
+        HelperData helper = new HelperData(new AtomicReference<DynamicClassLoaderManager>(), new AtomicReference<URIProvider[]>());
+
+        String parentPath = PathUtils.getParentPath(path);
+        Node parent = new JcrItemResourceFactory(s, helper).getParentOrNull(s.getItem(path), parentPath);
+        if (nullExpected) {
+            assertNull(parent);
+        } else {
+            assertNotNull(parent);
+            assertEquals(parentPath, parent.getPath());
+        }
+    }
 }
diff --git a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java
index 684ce80..1c97445 100644
--- a/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java
+++ b/src/test/java/org/apache/sling/jcr/resource/internal/helper/jcr/JcrResourceProviderTest.java
@@ -19,6 +19,7 @@
 package org.apache.sling.jcr.resource.internal.helper.jcr;
 
 import java.security.Principal;
+import java.util.concurrent.atomic.AtomicReference;
 
 import javax.jcr.Node;
 import javax.jcr.Repository;
@@ -26,16 +27,24 @@
 import javax.jcr.Session;
 import javax.jcr.nodetype.NodeType;
 
-import org.apache.jackrabbit.commons.JcrUtils;
+import org.apache.jackrabbit.oak.commons.PathUtils;
 import org.apache.sling.api.resource.PersistenceException;
 import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.external.URIProvider;
+import org.apache.sling.commons.classloader.DynamicClassLoaderManager;
+import org.apache.sling.jcr.resource.internal.HelperData;
 import org.apache.sling.spi.resource.provider.ResolveContext;
 import org.apache.sling.spi.resource.provider.ResourceContext;
+import org.jetbrains.annotations.NotNull;
 import org.junit.Assert;
 import org.mockito.Mockito;
 import org.osgi.framework.ServiceReference;
 import org.osgi.service.component.ComponentContext;
 
+import static javax.jcr.nodetype.NodeType.NT_UNSTRUCTURED;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
 public class JcrResourceProviderTest extends SlingRepositoryTestBase {
 
     JcrResourceProvider jcrResourceProvider;
@@ -47,34 +56,48 @@
         // create the session
         session = getSession();
         Repository repo = getRepository();
-        ComponentContext ctx = Mockito.mock(ComponentContext.class);
-        Mockito.when(ctx.locateService(Mockito.anyString(), Mockito.any(ServiceReference.class))).thenReturn(repo);
+        ComponentContext ctx = mock(ComponentContext.class);
+        when(ctx.locateService(Mockito.anyString(), Mockito.any(ServiceReference.class))).thenReturn(repo);
         jcrResourceProvider = new JcrResourceProvider();
         jcrResourceProvider.activate(ctx);
     }
 
     @Override
     protected void tearDown() throws Exception {
-        jcrResourceProvider.deactivate();
-        super.tearDown();
+        try {
+            if (session.nodeExists("/parent")) {
+                session.removeItem("/parent");
+                session.save();
+            }
+        } finally {
+            jcrResourceProvider.deactivate();
+            super.tearDown();
+        }
+    }
+    
+    private @NotNull JcrProviderState createProviderState() {
+        return new JcrProviderState(session, new HelperData(new AtomicReference<>(), new AtomicReference<>()), false);
+    }
+    
+    private @NotNull ResolveContext mockResolveContext() {
+        ResolveContext ctx = mock(ResolveContext.class);
+        when(ctx.getProviderState()).thenReturn(createProviderState());
+        return ctx;
     }
 
     public void testAdaptTo_Principal() {
-        ResolveContext ctx = Mockito.mock(ResolveContext.class);
-        Mockito.when(ctx.getProviderState()).thenReturn(new JcrProviderState(session, null, false));
+        ResolveContext ctx = mockResolveContext();
         Assert.assertNotNull(jcrResourceProvider.adaptTo(ctx, Principal.class));
     }
 
     public void testOrderBefore() throws RepositoryException, PersistenceException {
-        Node parentNode = session.getRootNode().addNode("parent", NodeType.NT_UNSTRUCTURED);
-        parentNode.addNode("child1", NodeType.NT_UNSTRUCTURED);
-        parentNode.addNode("child2", NodeType.NT_UNSTRUCTURED);
-        parentNode.addNode("child3", NodeType.NT_UNSTRUCTURED);
+        Node parentNode = session.getRootNode().addNode("parent", NT_UNSTRUCTURED);
+        parentNode.addNode("child1", NT_UNSTRUCTURED);
+        parentNode.addNode("child2", NT_UNSTRUCTURED);
+        parentNode.addNode("child3", NT_UNSTRUCTURED);
         session.save();
 
-        ResolveContext ctx = Mockito.mock(ResolveContext.class);
-        JcrProviderState state = new JcrProviderState(session, null, false);
-        Mockito.when(ctx.getProviderState()).thenReturn(state);
+        ResolveContext ctx = mockResolveContext();
         Resource parent = jcrResourceProvider.getResource(ctx, "/parent", ResourceContext.EMPTY_CONTEXT, null);
         Assert.assertNotNull(parent);
         // order with invalid names
@@ -100,6 +123,45 @@
         
         Assert.assertTrue(jcrResourceProvider.orderBefore(ctx, parent, "child2", null));
     }
+    
+    public void testGetParent() throws Exception {
+        Node parentNode = session.getRootNode().addNode("parent", NT_UNSTRUCTURED);
+        Node child = parentNode.addNode("child", NT_UNSTRUCTURED);
+        Node grandchild = child.addNode("grandchild", NT_UNSTRUCTURED);
+        session.save();
+
+        ResolveContext ctx = mockResolveContext();
+        Resource rootResource = jcrResourceProvider.getResource(ctx, PathUtils.ROOT_PATH, ResourceContext.EMPTY_CONTEXT, null);
+        Resource parentResource = jcrResourceProvider.getResource(ctx, parentNode.getPath(), ResourceContext.EMPTY_CONTEXT, rootResource);
+        Resource childResource = jcrResourceProvider.getResource(ctx, child.getPath(), ResourceContext.EMPTY_CONTEXT, parentResource);
+        Resource grandChildResource = jcrResourceProvider.getResource(ctx, grandchild.getPath(), ResourceContext.EMPTY_CONTEXT, childResource);
+        assertResources(rootResource, parentResource, childResource, grandChildResource);
+        
+        assertParent(jcrResourceProvider.getParent(ctx, grandChildResource), child.getPath());
+        assertParent(jcrResourceProvider.getParent(ctx, childResource), parentNode.getPath());
+        assertParent(jcrResourceProvider.getParent(ctx, parentResource), PathUtils.ROOT_PATH);
+        assertNull(jcrResourceProvider.getParent(ctx, rootResource));
+    }
+    
+    public void testGetParentDifferentResource() {
+        Resource r = mock(Resource.class);
+        when(r.getPath()).thenReturn("/test/path");
+        Resource parent = jcrResourceProvider.getParent(mockResolveContext(), r);
+        assertFalse(parent instanceof JcrNodeResource);
+    }
+    
+    private static void assertResources(Resource... resources) {
+        for (Resource r : resources) {
+            assertNotNull(r);
+            assertTrue(r instanceof JcrNodeResource);
+        }
+    }
+    
+    private static void assertParent(Resource parent, String expectedPath) {
+        assertNotNull(parent);
+        assertTrue(parent instanceof JcrNodeResource);
+        assertEquals(expectedPath, parent.getPath());
+    }
 }