You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by cz...@apache.org on 2016/01/31 19:34:18 UTC

svn commit: r1727849 - in /sling/trunk/bundles/resourceresolver/src: main/java/org/apache/sling/resourceresolver/impl/ main/java/org/apache/sling/resourceresolver/impl/helper/ main/java/org/apache/sling/resourceresolver/impl/providers/stateful/ test/ja...

Author: cziegeler
Date: Sun Jan 31 18:34:18 2016
New Revision: 1727849

URL: http://svn.apache.org/viewvc?rev=1727849&view=rev
Log:
SLING-5470 : Resource providers might not be closed

Added:
    sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContextTest.java   (with props)
Removed:
    sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/CombinedResourceProvider.java
    sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/stateful/CombinedResourceProviderTest.java
Modified:
    sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
    sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContext.java
    sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/AuthenticatedResourceProvider.java
    sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/BasicResolveContext.java
    sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/ResourceProviderAuthenticator.java

Modified: sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java?rev=1727849&r1=1727848&r2=1727849&view=diff
==============================================================================
--- sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java (original)
+++ sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/ResourceResolverImpl.java Sun Jan 31 18:34:18 2016
@@ -61,7 +61,6 @@ import org.apache.sling.resourceresolver
 import org.apache.sling.resourceresolver.impl.mapping.MapEntry;
 import org.apache.sling.resourceresolver.impl.params.ParsedParameters;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
-import org.apache.sling.resourceresolver.impl.providers.stateful.CombinedResourceProvider;
 import org.apache.sling.resourceresolver.impl.providers.stateful.ResourceProviderAuthenticator;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -105,8 +104,6 @@ public class ResourceResolverImpl extend
     /** Resource resolver context. */
     private final ResourceResolverContext context;
 
-    private final CombinedResourceProvider provider;
-
     private final Map<String, Object> authenticationInfo;
 
     private volatile Exception closedResolverException;
@@ -118,8 +115,7 @@ public class ResourceResolverImpl extend
     ResourceResolverImpl(final CommonResourceResolverFactoryImpl factory, final boolean isAdmin, final Map<String, Object> authenticationInfo, final ResourceProviderStorage storage) throws LoginException {
         this.factory = factory;
         this.authenticationInfo = authenticationInfo;
-        this.provider = createProvider(storage);
-        this.context = new ResourceResolverContext(isAdmin);
+        this.context = createContext(storage, isAdmin);
         this.factory.register(this, context);
     }
 
@@ -138,14 +134,14 @@ public class ResourceResolverImpl extend
         if (authenticationInfo != null) {
             this.authenticationInfo.putAll(authenticationInfo);
         }
-        this.provider = createProvider(factory.getResourceProviderTracker().getResourceProviderStorage());
-        this.context = new ResourceResolverContext(resolver.context.isAdmin());
+        this.context = createContext(factory.getResourceProviderTracker().getResourceProviderStorage(), resolver.context.isAdmin());
         this.factory.register(this, context);
     }
 
-    private CombinedResourceProvider createProvider(ResourceProviderStorage storage) throws LoginException {
+    private ResourceResolverContext createContext(ResourceProviderStorage storage, boolean isAdmin)
+    throws LoginException {
         final ResourceProviderAuthenticator authenticator = new ResourceProviderAuthenticator(this, authenticationInfo, this.factory.getResourceAccessSecurityTracker());
-        final CombinedResourceProvider provider = new CombinedResourceProvider(storage, this, authenticator);
+        final ResourceResolverContext provider = new ResourceResolverContext(isAdmin, storage, this, authenticator);
         authenticator.authenticateAll(storage.getAuthRequiredHandlers(), provider);
         return provider;
     }
@@ -168,7 +164,7 @@ public class ResourceResolverImpl extend
      */
     @Override
     public boolean isLive() {
-        return !this.isClosed.get() && this.provider.isLive() && this.factory.isLive();
+        return !this.isClosed.get() && this.context.isLive() && this.factory.isLive();
     }
 
     /**
@@ -181,8 +177,6 @@ public class ResourceResolverImpl extend
         }
         if ( this.isClosed.compareAndSet(false, true)) {
             this.factory.unregister(this, this.context);
-            provider.logout();
-            context.close();
         }
     }
 
@@ -212,7 +206,7 @@ public class ResourceResolverImpl extend
     @Override
     public Iterator<String> getAttributeNames() {
         checkClosed();
-        return this.provider.getAttributeNames().iterator();
+        return this.context.getAttributeNames().iterator();
     }
 
     /**
@@ -225,7 +219,7 @@ public class ResourceResolverImpl extend
             throw new NullPointerException("name");
         }
 
-        return this.provider.getAttribute(name);
+        return this.context.getAttribute(name);
     }
 
     // ---------- resolving resources
@@ -716,7 +710,7 @@ public class ResourceResolverImpl extend
         if (parent instanceof ResourceWrapper) {
             return listChildren(((ResourceWrapper) parent).getResource());
         }
-        return new ResourceIteratorDecorator(this.factory.getResourceDecoratorTracker(), this.provider.listChildren(parent));
+        return new ResourceIteratorDecorator(this.factory.getResourceDecoratorTracker(), this.context.listChildren(parent));
     }
 
     /**
@@ -746,7 +740,7 @@ public class ResourceResolverImpl extend
         checkClosed();
 
         return new ResourceIteratorDecorator(this.factory.getResourceDecoratorTracker(),
-                provider.findResources(query, defaultString(language, DEFAULT_QUERY_LANGUAGE)));
+                context.findResources(query, defaultString(language, DEFAULT_QUERY_LANGUAGE)));
     }
 
     /**
@@ -758,7 +752,7 @@ public class ResourceResolverImpl extend
             throws SlingException {
         checkClosed();
 
-        return provider.queryResources(query, defaultString(language, DEFAULT_QUERY_LANGUAGE));
+        return context.queryResources(query, defaultString(language, DEFAULT_QUERY_LANGUAGE));
     }
 
     /**
@@ -808,7 +802,7 @@ public class ResourceResolverImpl extend
     private Session getSession() {
         if ( !this.searchedSession ) {
             this.searchedSession = true;
-            this.cachedSession = this.provider.adaptTo(Session.class);
+            this.cachedSession = this.context.adaptTo(Session.class);
         }
         return this.cachedSession;
     }
@@ -826,7 +820,7 @@ public class ResourceResolverImpl extend
         if (type == Session.class) {
             return (AdapterType) getSession();
         }
-        final AdapterType result = this.provider.adaptTo(type);
+        final AdapterType result = this.context.adaptTo(type);
         if ( result != null ) {
             return result;
         }
@@ -1045,7 +1039,7 @@ public class ResourceResolverImpl extend
             parentToUse = null;
         }
 
-        final Resource resource = this.provider.getResource(path, parentToUse, parameters, isResolve);
+        final Resource resource = this.context.getResource(path, parentToUse, parameters, isResolve);
         if (resource != null) {
             resource.getResourceMetadata().setResolutionPath(path);
             resource.getResourceMetadata().setParameterMap(parameters);
@@ -1164,7 +1158,7 @@ public class ResourceResolverImpl extend
             return;
         }
         // if resource is null, we get an NPE as stated in the API
-        this.provider.delete(resource);
+        this.context.delete(resource);
     }
 
     /**
@@ -1196,7 +1190,7 @@ public class ResourceResolverImpl extend
                 throw new IllegalArgumentException("Can't create child on a synthetic root");
             }
         }
-        final Resource rsrc = this.provider.create(path, properties);
+        final Resource rsrc = this.context.create(path, properties);
         return this.factory.getResourceDecoratorTracker().decorate(rsrc);
     }
 
@@ -1205,7 +1199,7 @@ public class ResourceResolverImpl extend
      */
     @Override
     public void revert() {
-        this.provider.revert();
+        this.context.revert();
     }
 
     /**
@@ -1213,7 +1207,7 @@ public class ResourceResolverImpl extend
      */
     @Override
     public void commit() throws PersistenceException {
-        this.provider.commit();
+        this.context.commit();
     }
 
     /**
@@ -1221,7 +1215,7 @@ public class ResourceResolverImpl extend
      */
     @Override
     public boolean hasChanges() {
-        return this.provider.hasChanges();
+        return this.context.hasChanges();
     }
 
     /**
@@ -1292,21 +1286,21 @@ public class ResourceResolverImpl extend
      */
     @Override
     public void refresh() {
-        this.provider.refresh();
+        this.context.refresh();
     }
 
     @Override
     public Resource getParent(final Resource child) {
-        return this.provider.getParent(child);
+        return this.context.getParent(child);
     }
 
     @Override
     public Resource copy(final String srcAbsPath, final String destAbsPath) throws PersistenceException {
-        return this.provider.copy(srcAbsPath, destAbsPath);
+        return this.context.copy(srcAbsPath, destAbsPath);
     }
 
     @Override
     public Resource move(final String srcAbsPath, final String destAbsPath) throws PersistenceException {
-        return this.provider.move(srcAbsPath, destAbsPath);
+        return this.context.move(srcAbsPath, destAbsPath);
     }
 }

Modified: sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContext.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContext.java?rev=1727849&r1=1727848&r2=1727849&view=diff
==============================================================================
--- sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContext.java (original)
+++ sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContext.java Sun Jan 31 18:34:18 2016
@@ -17,20 +17,60 @@
  */
 package org.apache.sling.resourceresolver.impl.helper;
 
+import static org.apache.sling.api.resource.ResourceUtil.getName;
+import static org.apache.sling.spi.resource.provider.ResourceProvider.RESOURCE_TYPE_SYNTHETIC;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+import org.apache.commons.collections.iterators.IteratorChain;
+import org.apache.commons.lang.ArrayUtils;
 import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.api.resource.ResourceResolverFactory;
 import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.SyntheticResource;
 import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.commons.paths.PathBuilder;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderInfo;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
+import org.apache.sling.resourceresolver.impl.providers.stateful.AbstractIterator;
+import org.apache.sling.resourceresolver.impl.providers.stateful.EmptyResourceProvider;
+import org.apache.sling.resourceresolver.impl.providers.stateful.ResourceProviderAuthenticator;
+import org.apache.sling.resourceresolver.impl.providers.stateful.StatefulResourceProvider;
+import org.apache.sling.resourceresolver.impl.providers.tree.Node;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
+ * This class takes a number of {@link StatefulResourceProvider} objects and
+ * exposes it as one such object. Provider appropriate for the given operation
+ * is chosen basing on its {@link ResourceProviderInfo#getPath()} (more specific
+ * first) and service ranking.
+ *
  * Like a resource resolver itself, this class is not thread safe.
  */
 public class ResourceResolverContext {
 
+    private static final Logger logger = LoggerFactory.getLogger(ResourceResolverContext.class);
+
     /** Is this a resource resolver for an admin? */
     private final boolean isAdmin;
 
@@ -40,11 +80,23 @@ public class ResourceResolverContext {
     /** Flag for handling multiple calls to close. */
     private final AtomicBoolean isClosed = new AtomicBoolean(false);
 
+    private final ResourceProviderStorage storage;
+
+    private final ResourceResolver resolver;
+
+    private final ResourceProviderAuthenticator authenticator;
+
     /**
      * Create a new resource resolver context.
      */
-    public ResourceResolverContext(final boolean isAdmin) {
+    public ResourceResolverContext(final boolean isAdmin,
+            ResourceProviderStorage storage,
+            ResourceResolver resolver,
+            ResourceProviderAuthenticator authenticator) {
         this.isAdmin = isAdmin;
+        this.storage = storage;
+        this.resolver = resolver;
+        this.authenticator = authenticator;
     }
 
     public boolean isAdmin() {
@@ -52,10 +104,588 @@ public class ResourceResolverContext {
     }
 
     /**
+     * Logs out from all providers.
+     */
+    private void logout() {
+        for (StatefulResourceProvider p : authenticator.getAllUsedAuthenticated()) {
+            p.logout();
+        }
+    }
+
+    /**
+     * Refreshes all refreshable providers.
+     */
+    public void refresh() {
+        for (StatefulResourceProvider p : authenticator.getAllUsedRefreshable()) {
+            p.refresh();
+        }
+    }
+
+    /**
+     * Returns {@code true} if all providers are live.
+     */
+    public boolean isLive() {
+        for (StatefulResourceProvider p : authenticator.getAllUsedAuthenticated()) {
+            if (!p.isLive()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns parent from the most appropriate resource provider accepting the
+     * given children.
+     *
+     * In some cases the {@link SyntheticResource} can be returned if no
+     * resource provider returns parent for this child. See
+     * {@link #getResource(String, Resource, Map, boolean)} for more details
+     */
+    public Resource getParent(Resource child) {
+        final String path = child.getPath();
+        try {
+            final StatefulResourceProvider provider = getBestMatchingProvider(path);
+            final Resource parentCandidate = provider.getParent(child);
+            if (parentCandidate != null) {
+                return parentCandidate;
+            }
+        } catch ( final LoginException le ) {
+            // ignore
+        }
+        final String parentPath = ResourceUtil.getParent(path);
+        if (parentPath != null && isIntermediatePath(parentPath)) {
+            return new SyntheticResource(resolver, parentPath, ResourceProvider.RESOURCE_TYPE_SYNTHETIC);
+        }
+        return null;
+    }
+
+    /**
+     * Returns resource from the most appropriate resource provider.
+     * <br/><br/>
+     * If there's no such provider and the path is a part of some resource
+     * provider path, then the {@link SyntheticResource} will be returned. For
+     * instance, if we have resource provider under
+     * {@code /libs/sling/servlet/default/GET.servlet} and no resource provider
+     * returns a resource for {@code /libs/sling/servlet/default}, then the
+     * {@link SyntheticResource} will be returned to provide a consistent
+     * resource tree.
+     * <br/><br/>
+     * The same behaviour occurs in {@link #getParent(Resource)} and
+     * {@link #listChildren(Resource)}.
+     */
+    public Resource getResource(String path, Resource parent, Map<String, String> parameters, boolean isResolve) {
+        if (path == null || path.length() == 0 || path.charAt(0) != '/') {
+            logger.debug("Not absolute {}", path);
+            return null; // path must be absolute
+        }
+
+        try {
+            final StatefulResourceProvider provider = this.getBestMatchingProvider(path);
+            final Resource resourceCandidate = provider.getResource(path, parent, parameters, isResolve);
+            if (resourceCandidate != null) {
+                return resourceCandidate;
+            }
+        } catch ( LoginException le ) {
+            // ignore
+        }
+        // query: /libs/sling/servlet/default
+        // resource Provider: libs/sling/servlet/default/GET.servlet
+        // list will match libs, sling, servlet, default
+        // and there will be no resource provider at the end
+        // SLING-3482 : this is only done for getResource but not resolve
+        //              as it is important e.g. for servlet resolution
+        //              to get the parent resource for resource traversal.
+        if (!isResolve && isIntermediatePath(path)) {
+            logger.debug("Resolved Synthetic {}", path);
+            return new SyntheticResource(resolver, path, ResourceProvider.RESOURCE_TYPE_SYNTHETIC);
+        }
+        logger.debug("Resource null {} ", path);
+        return null;
+    }
+
+    private boolean isIntermediatePath(final String fullPath) {
+        return storage.getTree().getNode(fullPath) != null;
+    }
+
+    /**
+     * This method asks all matching resource providers for the children iterators,
+     * merges them, adds {@link SyntheticResource}s (see
+     * {@link #getResource(String, Resource, Map, boolean)} for more details),
+     * filters out the duplicates and returns the resulting iterator. All
+     * transformations are done lazily, during the {@link Iterator#hasNext()}
+     * invocation on the result.
+     */
+    @SuppressWarnings("unchecked")
+    public Iterator<Resource> listChildren(final Resource parent) {
+        final String parentPath = parent.getPath();
+
+        // 3 sources are combined: children of the provider which owns 'parent',
+        // providers which are directly mounted at a child path,
+        // synthetic resources for providers mounted at a lower level
+
+        // children of the 'parent' provider
+        Iterator<Resource> realChildren = null;
+        try {
+            final StatefulResourceProvider provider = this.getBestMatchingProvider(parentPath);
+            realChildren = provider.listChildren(parent);
+        } catch ( final LoginException le ) {
+            // ignore, realChildren will be null
+        }
+
+        final Set<String> visitedNames = new HashSet<String>();
+
+        IteratorChain chain = new IteratorChain();
+        if ( realChildren != null ) {
+            chain.addIterator(realChildren);
+        }
+
+        // synthetic and providers are done in one loop
+        final Node<ResourceProviderHandler> node = storage.getTree().getNode(parent.getPath());
+        if (node != null) {
+            final List<Resource> syntheticList = new ArrayList<Resource>();
+            final List<Resource> providerList = new ArrayList<Resource>();
+
+            for (final Entry<String, Node<ResourceProviderHandler>> entry : node.getChildren().entrySet()) {
+                final String name = entry.getKey();
+                final ResourceProviderHandler handler = entry.getValue().getValue();
+                PathBuilder pathBuilder = new PathBuilder(parent.getPath());
+                pathBuilder.append(name);
+                final String childPath = pathBuilder.toString();
+                if (handler == null) {
+                    syntheticList.add(new SyntheticResource(resolver, childPath, RESOURCE_TYPE_SYNTHETIC));
+                } else {
+                    Resource rsrc = null;
+                    try {
+                        rsrc = authenticator.getStateful(handler, this).getResource(childPath, parent, null, false);
+                    } catch ( final LoginException ignore) {
+                        // ignore
+                    }
+                    if ( rsrc != null ) {
+                        providerList.add(rsrc);
+                    } else {
+                        // if there is a child provider underneath, we need to create a synthetic resource
+                        // otherwise we need to make sure that no one else is providing this child
+                        if ( entry.getValue().getChildren().isEmpty() ) {
+                            syntheticList.add(new SyntheticResource(resolver, childPath, RESOURCE_TYPE_SYNTHETIC));
+                        } else {
+                            visitedNames.add(name);
+                        }
+                    }
+                }
+            }
+            if ( !providerList.isEmpty() ) {
+                chain.addIterator(providerList.iterator());
+            }
+            if ( !syntheticList.isEmpty() ) {
+                chain.addIterator(syntheticList.iterator());
+            }
+        }
+        if ( chain.size() == 0 ) {
+            return Collections.EMPTY_LIST.iterator();
+        }
+        return new UniqueIterator(visitedNames, chain);
+    }
+
+    /**
+     * Returns the union of all attribute names.
+     */
+    public Collection<String> getAttributeNames() {
+        final Set<String> names = new LinkedHashSet<String>();
+        for (StatefulResourceProvider p : authenticator.getAllBestEffort(storage.getAttributableHandlers(), this)) {
+            final Collection<String> newNames = p.getAttributeNames();
+            if (newNames != null) {
+                names.addAll(newNames);
+            }
+        }
+        return names;
+    }
+
+    /**
+     * Returns the first non-null result of the
+     * {@link StatefulResourceProvider#getAttribute(String)} invocation on
+     * the providers.
+     */
+    public Object getAttribute(String name) {
+        for (StatefulResourceProvider p : authenticator.getAllBestEffort(storage.getAttributableHandlers(), this)) {
+            Object attribute = p.getAttribute(name);
+            if (attribute != null) {
+                return attribute;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Create a resource.
+     *
+     * @throws UnsupportedOperationException
+     *             If creation is not allowed/possible
+     * @throws PersistenceException
+     *             If creation fails
+     * @return The new resource
+     */
+    public Resource create(String path, Map<String, Object> properties) throws PersistenceException {
+        try {
+            final StatefulResourceProvider provider = getBestMatchingModifiableProvider(path);
+            if ( provider != null ) {
+                final Resource creationResultResource = provider.create(resolver, path, properties);
+                if (creationResultResource != null) {
+                    return creationResultResource;
+                }
+            }
+        } catch (LoginException le) {
+            // ignore and throw (see below)
+        }
+        throw new UnsupportedOperationException("create '" + getName(path) + "' at " + ResourceUtil.getParent(path));
+    }
+
+    /**
+     * Delete the resource. Iterate over all modifiable ResourceProviders
+     * giving each an opportunity to delete the resource if they are able.
+     *
+     * @throws NullPointerException
+     *             if resource is null
+     * @throws UnsupportedOperationException
+     *             If deletion is not allowed/possible
+     * @throws PersistenceException
+     *             If deletion fails
+     */
+    public void delete(final Resource resource) throws PersistenceException {
+        final String path = resource.getPath();
+        try {
+            final StatefulResourceProvider provider = getBestMatchingModifiableProvider(path);
+            if ( provider != null ) {
+                provider.delete(resource);
+                return;
+            }
+        } catch (LoginException le) {
+            // ignore and throw (see below)
+        }
+        throw new UnsupportedOperationException("delete at '" + path + "'");
+    }
+
+    /**
+     * Revert changes on all modifiable ResourceProviders.
+     */
+    public void revert() {
+        for (StatefulResourceProvider p : authenticator.getAllUsedModifiable()) {
+            p.revert();
+        }
+    }
+
+    /**
+     * Commit changes on all modifiable ResourceProviders.
+     */
+    public void commit() throws PersistenceException {
+        for (StatefulResourceProvider p : authenticator.getAllUsedModifiable()) {
+            p.commit();
+        }
+    }
+
+    /**
+     * Check if any modifiable ResourceProvider has uncommited changes.
+     */
+    public boolean hasChanges() {
+        for (StatefulResourceProvider p : authenticator.getAllUsedModifiable()) {
+            if (p.hasChanges()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Return the union of query languages supported by the providers.
+     */
+    public String[] getSupportedLanguages() {
+        Set<String> supportedLanguages = new LinkedHashSet<String>();
+        for (StatefulResourceProvider p : authenticator.getAllBestEffort(storage.getLanguageQueryableHandlers(), this)) {
+            supportedLanguages.addAll(Arrays.asList(p.getSupportedLanguages()));
+        }
+        return supportedLanguages.toArray(new String[supportedLanguages.size()]);
+    }
+
+    /**
+     * Queries all resource providers and combines the results.
+     */
+    public Iterator<Resource> findResources(final String query, final String language) {
+        List<StatefulResourceProvider> queryableRP = getQueryableProviders(language);
+        List<Iterator<Resource>> iterators = new ArrayList<Iterator<Resource>>(queryableRP.size());
+        for (StatefulResourceProvider p : queryableRP) {
+            iterators.add(p.findResources(query, language));
+        }
+        return new ChainedIterator<Resource>(iterators.iterator());
+    }
+
+    private List<StatefulResourceProvider> getQueryableProviders(String language) {
+        List<StatefulResourceProvider> queryableProviders = new ArrayList<StatefulResourceProvider>();
+        for (StatefulResourceProvider p : authenticator.getAllBestEffort(storage.getLanguageQueryableHandlers(), this)) {
+            if (ArrayUtils.contains(p.getSupportedLanguages(), language)) {
+                queryableProviders.add(p);
+            }
+        }
+        return queryableProviders;
+    }
+
+    /**
+     * Queries all resource providers and combines the results.
+     */
+    public Iterator<Map<String, Object>> queryResources(final String query, final String language) {
+        List<StatefulResourceProvider> queryableRP = getQueryableProviders(language);
+        List<Iterator<Map<String, Object>>> iterators = new ArrayList<Iterator<Map<String, Object>>>(queryableRP.size());
+        for (StatefulResourceProvider p : queryableRP) {
+            iterators.add(p.queryResources(query, language));
+        }
+        return new ChainedIterator<Map<String, Object>>(iterators.iterator());
+    }
+
+    /**
+     * Returns the first non-null result of the adaptTo() method invoked on the
+     * providers.
+     */
+    @SuppressWarnings("unchecked")
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        for (StatefulResourceProvider p : authenticator.getAllBestEffort(storage.getAdaptableHandlers(), this)) {
+            final Object adaptee = p.adaptTo(type);
+            if (adaptee != null) {
+                return (AdapterType) adaptee;
+            }
+        }
+        return null;
+    }
+
+    private StatefulResourceProvider checkSourceAndDest(final String srcAbsPath, final String destAbsPath) throws PersistenceException {
+        // check source
+        final Node<ResourceProviderHandler> srcNode = storage.getTree().getBestMatchingNode(srcAbsPath);
+        if ( srcNode == null ) {
+            throw new PersistenceException("Source resource does not exist.", null, srcAbsPath, null);
+        }
+        StatefulResourceProvider srcProvider = null;
+        try {
+            srcProvider = authenticator.getStateful(srcNode.getValue(), this);
+        } catch (LoginException e) {
+            // ignore
+        }
+        if ( srcProvider == null ) {
+            throw new PersistenceException("Source resource does not exist.", null, srcAbsPath, null);
+        }
+        final Resource srcResource = srcProvider.getResource(srcAbsPath, null, null, false);
+        if ( srcResource == null ) {
+            throw new PersistenceException("Source resource does not exist.", null, srcAbsPath, null);
+        }
+
+        // check destination
+        final Node<ResourceProviderHandler> destNode = storage.getTree().getBestMatchingNode(destAbsPath);
+        if ( destNode == null ) {
+            throw new PersistenceException("Destination resource does not exist.", null, destAbsPath, null);
+        }
+        StatefulResourceProvider destProvider = null;
+        try {
+            destProvider = authenticator.getStateful(destNode.getValue(), this);
+        } catch (LoginException e) {
+            // ignore
+        }
+        if ( destProvider == null ) {
+            throw new PersistenceException("Destination resource does not exist.", null, destAbsPath, null);
+        }
+        final Resource destResource = destProvider.getResource(destAbsPath, null, null, false);
+        if ( destResource == null ) {
+            throw new PersistenceException("Destination resource does not exist.", null, destAbsPath, null);
+        }
+
+        // check for sub providers of src and dest
+        if ( srcProvider == destProvider && !collectProviders(srcNode) && !collectProviders(destNode) ) {
+            return srcProvider;
+        }
+        return null;
+    }
+
+    private boolean collectProviders(final Node<ResourceProviderHandler> parent) {
+        boolean hasMoreProviders = false;
+        for (final Entry<String, Node<ResourceProviderHandler>> entry : parent.getChildren().entrySet()) {
+            if ( entry.getValue().getValue() != null ) {
+                try {
+                    authenticator.getStateful(entry.getValue().getValue(), this);
+                    hasMoreProviders = true;
+                } catch ( final LoginException ignore) {
+                    // ignore
+                }
+            }
+            if ( collectProviders(entry.getValue())) {
+                hasMoreProviders = true;
+            }
+        }
+
+        return hasMoreProviders;
+    }
+
+    private void copy(final Resource src, final String dstPath, final List<Resource> newNodes) throws PersistenceException {
+        final ValueMap vm = src.getValueMap();
+        final String createPath = new PathBuilder(dstPath).append(src.getName()).toString();
+        newNodes.add(this.create(createPath, vm));
+        for(final Resource c : src.getChildren()) {
+            copy(c, createPath, newNodes);
+        }
+    }
+
+    /**
+     * Tries to find a resource provider accepting both paths and invokes
+     * {@link StatefulResourceProvider#copy(String, String)} method on it.
+     * Returns false if there's no such provider.
+     */
+    public Resource copy(final String srcAbsPath, final String destAbsPath) throws PersistenceException {
+        final StatefulResourceProvider optimizedSourceProvider = checkSourceAndDest(srcAbsPath, destAbsPath);
+        if ( optimizedSourceProvider != null && optimizedSourceProvider.copy(srcAbsPath, destAbsPath) ) {
+            return this.getResource(destAbsPath + '/' + ResourceUtil.getName(srcAbsPath), null, null, false);
+        }
+
+        final Resource srcResource = this.getResource(srcAbsPath, null, null, false);
+        final List<Resource> newResources = new ArrayList<Resource>();
+        boolean rollback = true;
+        try {
+            this.copy(srcResource, destAbsPath, newResources);
+            rollback = false;
+            return newResources.get(0);
+        } finally {
+            if ( rollback ) {
+                for(final Resource rsrc : newResources) {
+                    this.delete(rsrc);
+                }
+            }
+        }
+    }
+
+    /**
+     * Tries to find a resource provider accepting both paths and invokes
+     * {@link StatefulResourceProvider#move(String, String)} method on it.
+     * Returns false if there's no such provider.
+     */
+    public Resource move(String srcAbsPath, String destAbsPath) throws PersistenceException {
+        final StatefulResourceProvider optimizedSourceProvider = checkSourceAndDest(srcAbsPath, destAbsPath);
+        if ( optimizedSourceProvider != null && optimizedSourceProvider.move(srcAbsPath, destAbsPath) ) {
+            return this.getResource(destAbsPath + '/' + ResourceUtil.getName(srcAbsPath), null, null, false);
+        }
+        final Resource srcResource = this.getResource(srcAbsPath, null, null, false);
+        final List<Resource> newResources = new ArrayList<Resource>();
+        boolean rollback = true;
+        try {
+            this.copy(srcResource, destAbsPath, newResources);
+            this.delete(srcResource);
+            rollback = false;
+            return newResources.get(0);
+        } finally {
+            if ( rollback ) {
+                for(final Resource rsrc : newResources) {
+                    this.delete(rsrc);
+                }
+            }
+        }
+    }
+
+    public ResourceProviderStorage getResourceProviderStorage() {
+        return this.storage;
+    }
+
+    public @CheckForNull StatefulResourceProvider getStatefulResourceProvider(@Nonnull final ResourceProviderHandler handler)
+    throws LoginException {
+        if ( handler != null ) {
+            return authenticator.getStateful(handler, this);
+        }
+        return null;
+    }
+
+    /**
+     * @param path
+     * @return
+     * @throws LoginException
+     */
+    private @Nonnull StatefulResourceProvider getBestMatchingProvider(final String path) throws LoginException {
+        final Node<ResourceProviderHandler> node = storage.getTree().getBestMatchingNode(path);
+        return node == null ? EmptyResourceProvider.SINGLETON : authenticator.getStateful(node.getValue(), this);
+    }
+
+    /**
+     * @param path
+     * @return The modifiable provider or {@code null}
+     * @throws LoginException
+     */
+    private @CheckForNull StatefulResourceProvider getBestMatchingModifiableProvider(final String path) throws LoginException {
+        final Node<ResourceProviderHandler> node = storage.getTree().getBestMatchingNode(path);
+        if ( node != null && node.getValue().getInfo().isModifiable() ) {
+            return authenticator.getStateful(node.getValue(), this);
+        }
+        return null;
+    }
+
+    private static class ChainedIterator<T> extends AbstractIterator<T> {
+
+        private final Iterator<Iterator<T>> iterators;
+
+        private Iterator<T> currentIterator;
+
+        public ChainedIterator(Iterator<Iterator<T>> iterators) {
+            this.iterators = iterators;
+        }
+
+        @Override
+        protected T seek() {
+            while (true) {
+                if (currentIterator == null) {
+                    if (!iterators.hasNext()) {
+                        return null;
+                    }
+                    currentIterator = iterators.next();
+                    continue;
+                }
+                if (currentIterator.hasNext()) {
+                    return currentIterator.next();
+                } else {
+                    currentIterator = null;
+                }
+            }
+        }
+    }
+
+    /**
+     * This iterator removes duplicated Resource entries. Regular resources
+     * overrides the synthetic ones.
+     */
+    private static class UniqueIterator extends AbstractIterator<Resource> {
+
+        private final Iterator<Resource> input;
+
+        private final Set<String> visited;
+
+        public UniqueIterator(final Set<String> visited, final Iterator<Resource> input) {
+            this.input = input;
+            this.visited = visited;
+        }
+
+        @Override
+        protected Resource seek() {
+            while (input.hasNext()) {
+                final Resource next = input.next();
+                final String name = next.getName();
+
+                if (visited.contains(name)) {
+                    continue;
+                } else {
+                    visited.add(name);
+                    next.getResourceMetadata().setResolutionPath(next.getPath());
+                    return next;
+                }
+            }
+
+            return null;
+        }
+    }
+
+    /**
      * Close all dynamic resource providers.
      */
     public void close() {
         if (this.isClosed.compareAndSet(false, true)) {
+            this.logout();
             if ( this.resourceTypeResourceResolver != null ) {
                 try {
                     this.resourceTypeResourceResolver.close();

Modified: sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/AuthenticatedResourceProvider.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/AuthenticatedResourceProvider.java?rev=1727849&r1=1727848&r2=1727849&view=diff
==============================================================================
--- sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/AuthenticatedResourceProvider.java (original)
+++ sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/AuthenticatedResourceProvider.java Sun Jan 31 18:34:18 2016
@@ -32,6 +32,7 @@ import org.apache.sling.api.resource.Res
 import org.apache.sling.api.resource.ResourceResolverFactory;
 import org.apache.sling.api.resource.ResourceUtil;
 import org.apache.sling.api.resource.runtime.dto.AuthType;
+import org.apache.sling.resourceresolver.impl.helper.ResourceResolverContext;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderInfo;
 import org.apache.sling.spi.resource.provider.QueryLanguageProvider;
 import org.apache.sling.spi.resource.provider.ResolverContext;
@@ -66,14 +67,14 @@ public class AuthenticatedResourceProvid
 
     private ResolverContext<Object> cachedContext;
 
-    private final CombinedResourceProvider combinedProvider;
+    private final ResourceResolverContext combinedProvider;
 
     @SuppressWarnings("unchecked")
     public AuthenticatedResourceProvider(ResourceProvider<?> rp,
             ResourceProviderInfo info,
             ResourceResolver resolver,
             Map<String, Object> authInfo,
-            CombinedResourceProvider combinedProvider) throws LoginException {
+            ResourceResolverContext combinedProvider) throws LoginException {
         this.rp = (ResourceProvider<Object>) rp;
         this.info = info;
         this.authInfo = authInfo;

Modified: sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/BasicResolveContext.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/BasicResolveContext.java?rev=1727849&r1=1727848&r2=1727849&view=diff
==============================================================================
--- sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/BasicResolveContext.java (original)
+++ sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/BasicResolveContext.java Sun Jan 31 18:34:18 2016
@@ -21,6 +21,7 @@ package org.apache.sling.resourceresolve
 import org.apache.sling.api.resource.LoginException;
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.resourceresolver.impl.helper.ResourceResolverContext;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
 import org.apache.sling.resourceresolver.impl.providers.tree.Node;
@@ -35,7 +36,7 @@ public class BasicResolveContext<T> impl
 
     private final T providerState;
 
-    private final CombinedResourceProvider combinedProvider;
+    private final ResourceResolverContext combinedProvider;
 
     private volatile boolean parentLookupDone = false;
 
@@ -46,7 +47,7 @@ public class BasicResolveContext<T> impl
     public BasicResolveContext(ResourceResolver resourceResolver,
             T providerState,
             String parentPath,
-            CombinedResourceProvider combinedProvider) {
+            ResourceResolverContext combinedProvider) {
         this.resourceResolver = resourceResolver;
         this.parentPath = parentPath;
         this.providerState = providerState;

Modified: sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/ResourceProviderAuthenticator.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/ResourceProviderAuthenticator.java?rev=1727849&r1=1727848&r2=1727849&view=diff
==============================================================================
--- sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/ResourceProviderAuthenticator.java (original)
+++ sling/trunk/bundles/resourceresolver/src/main/java/org/apache/sling/resourceresolver/impl/providers/stateful/ResourceProviderAuthenticator.java Sun Jan 31 18:34:18 2016
@@ -30,6 +30,7 @@ import org.apache.sling.api.resource.Log
 import org.apache.sling.api.resource.ResourceResolver;
 import org.apache.sling.api.resource.runtime.dto.AuthType;
 import org.apache.sling.resourceresolver.impl.ResourceAccessSecurityTracker;
+import org.apache.sling.resourceresolver.impl.helper.ResourceResolverContext;
 import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
 import org.apache.sling.spi.resource.provider.ResourceProvider;
 
@@ -73,7 +74,7 @@ public class ResourceProviderAuthenticat
      * @throws LoginException
      */
     public void authenticateAll(final List<ResourceProviderHandler> handlers,
-                                final CombinedResourceProvider combinedProvider)
+                                final ResourceResolverContext combinedProvider)
     throws LoginException {
         final List<StatefulResourceProvider> successfulHandlers = new ArrayList<StatefulResourceProvider>();
         for (final ResourceProviderHandler h : handlers) {
@@ -90,7 +91,7 @@ public class ResourceProviderAuthenticat
     }
 
     private @Nonnull StatefulResourceProvider authenticate(final ResourceProviderHandler handler,
-            CombinedResourceProvider combinedProvider) throws LoginException {
+            ResourceResolverContext combinedProvider) throws LoginException {
         StatefulResourceProvider rp = stateful.get(handler);
         if (rp == null) {
             rp = createStateful(handler, combinedProvider);
@@ -112,7 +113,7 @@ public class ResourceProviderAuthenticat
         return stateful.values();
     }
 
-    public @Nonnull StatefulResourceProvider getStateful(ResourceProviderHandler handler, CombinedResourceProvider combinedProvider)
+    public @Nonnull StatefulResourceProvider getStateful(ResourceProviderHandler handler, ResourceResolverContext combinedProvider)
     throws LoginException {
         return authenticate(handler, combinedProvider);
     }
@@ -130,7 +131,7 @@ public class ResourceProviderAuthenticat
     }
 
     public Collection<StatefulResourceProvider> getAllBestEffort(List<ResourceProviderHandler> handlers,
-            CombinedResourceProvider combinedProvider) {
+            ResourceResolverContext combinedProvider) {
         List<StatefulResourceProvider> result = new ArrayList<StatefulResourceProvider>(handlers.size());
         for (ResourceProviderHandler h : handlers) {
             try {
@@ -151,7 +152,7 @@ public class ResourceProviderAuthenticat
      */
     private @Nonnull StatefulResourceProvider createStateful(
             final ResourceProviderHandler handler,
-            final CombinedResourceProvider combinedProvider)
+            final ResourceResolverContext combinedProvider)
     throws LoginException {
         final ResourceProvider<?> rp = handler.getResourceProvider();
         StatefulResourceProvider authenticated;

Added: sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContextTest.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContextTest.java?rev=1727849&view=auto
==============================================================================
--- sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContextTest.java (added)
+++ sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContextTest.java Sun Jan 31 18:34:18 2016
@@ -0,0 +1,427 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.sling.resourceresolver.impl.helper;
+
+import static org.hamcrest.Matchers.arrayContainingInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasEntry;
+import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceMetadata;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.resource.runtime.dto.AuthType;
+import org.apache.sling.resourceresolver.impl.Fixture;
+import org.apache.sling.resourceresolver.impl.ResourceAccessSecurityTracker;
+import org.apache.sling.resourceresolver.impl.SimpleValueMapImpl;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderHandler;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderInfo;
+import org.apache.sling.resourceresolver.impl.providers.ResourceProviderStorage;
+import org.apache.sling.resourceresolver.impl.providers.stateful.ResourceProviderAuthenticator;
+import org.apache.sling.spi.resource.provider.QueryLanguageProvider;
+import org.apache.sling.spi.resource.provider.ResolverContext;
+import org.apache.sling.spi.resource.provider.ResourceContext;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.apache.sling.testing.mock.osgi.MockOsgi;
+import org.hamcrest.Matchers;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+
+@SuppressWarnings("unchecked")
+public class ResourceResolverContextTest {
+
+    // query language names
+    private static final String QL_MOCK = "MockQueryLanguage";
+    private static final String QL_ANOTHER_MOCK = "AnotherMockQueryLanguage";
+    private static final String QL_NOOP = "NoopQueryLanguage";
+
+    // query definitions
+    private static final String QUERY_MOCK_FIND_ALL = "FIND ALL";
+
+    private ResourceProviderAuthenticator authenticator;
+    private ResourceResolverContext crp;
+    private List<ResourceProviderHandler> handlers;
+    private ResourceProvider<Object> subProvider;
+    private Map<String, Object> authInfo;
+    private ResourceProvider<Object> rootProvider;
+    private Resource subProviderResource;
+    private Resource somethingResource;
+
+    @Before
+    public void prepare() throws Exception {
+
+        BundleContext bc = MockOsgi.newBundleContext();
+
+        Fixture fixture = new Fixture(bc);
+
+        // sub-provider
+        subProvider = Mockito.mock(ResourceProvider.class);
+        ResourceProviderInfo info = fixture.registerResourceProvider(subProvider, "/some/path", AuthType.required);
+        ResourceProviderHandler handler = new ResourceProviderHandler(bc, info);
+        when(subProvider.getQueryLanguageProvider()).thenReturn(new SimpleQueryLanguageProvider(QL_MOCK, QL_ANOTHER_MOCK) {
+            @Override
+            public Iterator<ValueMap> queryResources(ResolverContext<Object> ctx, String query, String language) {
+                if ( query.equals(QUERY_MOCK_FIND_ALL) && language.equals(QL_MOCK)) {
+                    SimpleValueMapImpl valueMap = new SimpleValueMapImpl();
+                    valueMap.put("key", "value");
+                    return Collections.<ValueMap> singletonList(valueMap).iterator();
+                }
+
+                throw new UnsupportedOperationException();
+            }
+
+            @Override
+            public Iterator<Resource> findResources(ResolverContext<Object> ctx, String query, String language) {
+
+                if ( query.equals(QUERY_MOCK_FIND_ALL) && language.equals(QL_MOCK)) {
+                    return Collections.<Resource> singletonList(newMockResource("/some/path/object")).iterator();
+                }
+
+                throw new UnsupportedOperationException();
+
+            }
+        });
+        handler.activate();
+
+        rootProvider = mock(ResourceProvider.class);
+        ResourceProviderInfo rootInfo = fixture.registerResourceProvider(rootProvider, "/", AuthType.required);
+        ResourceProviderHandler rootHandler = new ResourceProviderHandler(bc, rootInfo);
+        when(rootProvider.getQueryLanguageProvider()).thenReturn(new SimpleQueryLanguageProvider(QL_NOOP));
+        rootHandler.activate();
+
+        // configure mock resources
+        Resource root = configureResourceAt(rootProvider, "/");
+        somethingResource = configureResourceAt(rootProvider, "/something");
+        subProviderResource = configureResourceAt(subProvider, "/some/path/object");
+
+        // configure query at '/'
+        when(rootProvider.listChildren((ResolverContext<Object>) Mockito.anyObject(), Mockito.eq(root))).thenReturn(Collections.singleton(somethingResource).iterator());
+
+        ResourceResolver rr = mock(ResourceResolver.class);
+        ResourceAccessSecurityTracker securityTracker = Mockito.mock(ResourceAccessSecurityTracker.class);
+        authInfo = Collections.emptyMap();
+
+        handlers = Arrays.asList(rootHandler, handler);
+        ResourceProviderStorage storage = new ResourceProviderStorage(handlers);
+        authenticator = new ResourceProviderAuthenticator(rr, authInfo, securityTracker);
+
+        crp = new ResourceResolverContext(false, storage, rr, authenticator);
+    }
+
+    /**
+     * Configures the provider to return a mock resource for the specified path
+     * @return
+     */
+    private <T> Resource configureResourceAt(ResourceProvider<T> provider, String path) {
+
+        Resource mockResource = newMockResource(path);
+
+        when(provider.getResource((ResolverContext<T>) Mockito.any(), Mockito.eq(path), (ResourceContext) Mockito.any(), (Resource) Mockito.any()))
+            .thenReturn(mockResource);
+
+        return mockResource;
+    }
+
+    private Resource newMockResource(String path) {
+
+        Resource mockResource = mock(Resource.class);
+        when(mockResource.getPath()).thenReturn(path);
+        when(mockResource.getName()).thenReturn(ResourceUtil.getName(path));
+        when(mockResource.getResourceMetadata()).thenReturn(mock(ResourceMetadata.class));
+        when(mockResource.getChildren()).thenReturn(Collections.<Resource> emptyList());
+
+        return mockResource;
+    }
+
+    /**
+     * Verifies that login and logout calls are invoked as expected on
+     * ResourceProviders with authType = {@link AuthType#required}
+     */
+    @Test
+    public void loginLogout() throws LoginException {
+
+        authenticator.authenticateAll(handlers, crp);
+
+        verify(subProvider).authenticate(authInfo);
+
+        crp.close();
+
+        verify(subProvider).logout(mockContext());
+    }
+
+    private ResolverContext<Object> mockContext() {
+        return (ResolverContext<Object>) Mockito.any();
+    }
+
+    /**
+     * Verifies that a synthetic resource is returned for a path which holds no
+     * actual resource but is an ancestor of another resource provider
+     */
+    @Test
+    public void getResource_synthetic() {
+
+        Resource resource = crp.getResource("/some", null, null, false);
+
+        assertTrue("Not a syntethic resource : " + resource, ResourceUtil.isSyntheticResource(resource));
+    }
+
+    /**
+     * Verifies that a getResource call for a missing resource returns null
+     */
+    @Test
+    public void getResource_missing() {
+        assertThat(crp.getResource("/nothing", null, null, false), nullValue());
+    }
+
+    /**
+     * Verifies that a resource is returned when it should be
+     */
+    @Test
+    public void getResource_found() {
+        assertThat(crp.getResource("/something", null, null, false), not(nullValue()));
+        assertThat(crp.getResource("/some/path/object", null, null, false), not(nullValue()));
+    }
+
+
+    /**
+     * Verifies that the existing parent of a resource is found
+     */
+    @Test
+    public void getParent_found() {
+        Resource parent = crp.getParent(somethingResource);
+        assertThat(parent, notNullValue());
+        assertThat("parent.path", parent.getPath(), equalTo("/"));
+    }
+
+
+
+    /**
+     * Verifies that a synthetic parent is returned for a resource without an actual parent
+     */
+    @Test
+    public void getParent_synthetic() {
+        Resource parent = crp.getParent(subProviderResource);
+        assertThat(parent, notNullValue());
+        assertTrue("parent is a synthetic resource", ResourceUtil.isSyntheticResource(parent));
+    }
+
+    /**
+     * Verifies that listing the children at root lists both the synthetic and the 'real' children
+     */
+    @Test
+    public void listChildren_root() {
+        Resource root = crp.getResource("/", null, null, false);
+        Iterator<Resource> children = crp.listChildren(root);
+
+        Map<String, Resource> all = new HashMap<String, Resource>();
+        while ( children.hasNext() ) {
+            Resource child = children.next();
+            all.put(child.getPath(), child);
+        }
+
+        assertThat(all.entrySet(), Matchers.hasSize(2));
+        assertThat("Resource at /something", all.get("/something"), not(nullValue()));
+        assertThat("Resource at /some", all.get("/some"), not(nullValue()));
+    }
+
+    /**
+     * Verifies listing the children at a level below the root
+     */
+    @Test
+    public void listChildren_lowerLevel() {
+
+        Resource root = crp.getResource("/some", null, null, false);
+        Iterator<Resource> children = crp.listChildren(root);
+        Map<String, Resource> all = new HashMap<String, Resource>();
+
+        while ( children.hasNext() ) {
+            Resource child = children.next();
+            all.put(child.getPath(), child);
+        }
+
+        assertThat(all.entrySet(), Matchers.hasSize(1));
+        assertThat("Resource at /some/path", all.get("/some/path"), not(nullValue()));
+
+    }
+
+    /**
+     * Verifies copying resources between the same ResourceProvider
+     *
+     * @throws PersistenceException persistence exception
+     */
+    @Test
+    public void copy_sameProvider() throws PersistenceException {
+
+        when(subProvider.copy(mockContext(), Mockito.eq("/some/path/object"), Mockito.eq("/some/path/new")))
+            .thenReturn(true);
+        configureResourceAt(subProvider, "/some/path/new/object");
+        configureResourceAt(subProvider, "/some/path/new");
+
+        Resource resource = crp.copy("/some/path/object", "/some/path/new");
+
+
+        assertThat(resource, not(nullValue()));
+    }
+
+    /**
+     * Verifies copying resources between different ResourceProviders
+     *
+     * @throws PersistenceException persistence exception
+     */
+    @Test
+    public void copy_differentProvider() throws PersistenceException {
+
+        Resource newRes = newMockResource("/object");
+        when(rootProvider.create(mockContext(), Mockito.eq("/object"), Mockito.anyMap()))
+            .thenReturn(newRes);
+
+        Resource resource = crp.copy("/some/path/object", "/");
+
+        assertThat(resource, not(nullValue()));
+    }
+
+    /**
+     * Verifies moving resources between the same ResourceProvider
+     *
+     * @throws PersistenceException persistence exception
+     */
+    @Test
+    public void move_sameProvider() throws PersistenceException {
+
+        when(subProvider.move(mockContext(), Mockito.eq("/some/path/object"), Mockito.eq("/some/path/new")))
+                .thenReturn(true);
+        configureResourceAt(subProvider, "/some/path/new/object");
+        configureResourceAt(subProvider, "/some/path/new");
+
+        Resource resource = crp.move("/some/path/object", "/some/path/new");
+
+        assertThat(resource, not(nullValue()));
+    }
+
+    /**
+     * Verifies moving resources between different ResourceProviders
+     *
+     * @throws PersistenceException persistence exception
+     */
+    @Test
+    public void move_differentProvider() throws PersistenceException {
+
+        Resource newRes = newMockResource("/object");
+        when(rootProvider.create(mockContext(), Mockito.eq("/object"), Mockito.anyMap())).thenReturn(newRes);
+
+        Resource resource = crp.move("/some/path/object", "/");
+
+        assertThat(resource, not(nullValue()));
+
+        verify(subProvider).delete(mockContext(), Mockito.eq(subProviderResource));
+    }
+
+    /**
+     * Verifies listing the query languages
+     */
+    @Test
+    public void queryLanguages() throws PersistenceException {
+
+        assertThat(crp.getSupportedLanguages(), arrayContainingInAnyOrder(QL_NOOP, QL_MOCK, QL_ANOTHER_MOCK));
+    }
+
+    /**
+     * Verifies running a query
+     */
+    @Test
+    public void queryResources() throws PersistenceException {
+
+        Iterator<Map<String, Object>> queryResources = crp.queryResources(QUERY_MOCK_FIND_ALL, QL_MOCK);
+
+        int count = 0;
+
+        while ( queryResources.hasNext() ) {
+            assertThat("ValueMap returned from query", queryResources.next(), hasEntry("key", (Object) "value"));
+            count++;
+        }
+
+        assertThat("query result count", count, Matchers.equalTo(1));
+    }
+
+    /**
+     * Verifies finding resources
+     */
+    @Test
+    public void findResource() throws PersistenceException {
+
+        Iterator<Resource> resources = crp.findResources(QUERY_MOCK_FIND_ALL, QL_MOCK);
+
+        int count = 0;
+
+        while ( resources.hasNext() ) {
+            assertThat("resources[0].path", resources.next().getPath(), equalTo("/some/path/object"));
+            count++;
+        }
+
+        assertThat("query result count", count, Matchers.equalTo(1));
+    }
+
+    /**
+     * Simple test-only QueryLanguageProvider
+     *
+     */
+    private static class SimpleQueryLanguageProvider implements QueryLanguageProvider<Object> {
+
+        private final String[] queryLanguages;
+
+        public SimpleQueryLanguageProvider(String... queryLanguages) {
+            this.queryLanguages = queryLanguages;
+        }
+
+        @Override
+        public String[] getSupportedLanguages(ResolverContext<Object> ctx) {
+            return queryLanguages;
+        }
+
+        @Override
+        public Iterator<ValueMap> queryResources(ResolverContext<Object> ctx, String query, String language) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public Iterator<Resource> findResources(ResolverContext<Object> ctx, String query, String language) {
+            throw new UnsupportedOperationException();
+        }
+    }
+}

Propchange: sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContextTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sling/trunk/bundles/resourceresolver/src/test/java/org/apache/sling/resourceresolver/impl/helper/ResourceResolverContextTest.java
------------------------------------------------------------------------------
    svn:keywords = author date id revision rev url