SLING-9040 only update intersecting ResourceProviderHandler

When a new ResourceProviderHandler gets activated or an existing one
deactivated currently all other registered ResourceProviderHandlers get
updated. To keep the exclude paths set for resource observation consistent
it is sufficient to only update those that are actually overlapping.
diff --git a/src/main/java/org/apache/sling/resourceresolver/impl/providers/ResourceProviderTracker.java b/src/main/java/org/apache/sling/resourceresolver/impl/providers/ResourceProviderTracker.java
index f8f41b8..6726300 100644
--- a/src/main/java/org/apache/sling/resourceresolver/impl/providers/ResourceProviderTracker.java
+++ b/src/main/java/org/apache/sling/resourceresolver/impl/providers/ResourceProviderTracker.java
@@ -19,6 +19,7 @@
 package org.apache.sling.resourceresolver.impl.providers;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Dictionary;
 import java.util.HashMap;
@@ -31,6 +32,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.sling.api.SlingConstants;
+import org.apache.sling.api.resource.ResourceUtil;
 import org.apache.sling.api.resource.observation.ResourceChange;
 import org.apache.sling.api.resource.observation.ResourceChange.ChangeType;
 import org.apache.sling.api.resource.path.Path;
@@ -347,7 +349,7 @@
      */
     private boolean activate(final ResourceProviderHandler handler) {
         synchronized (this.handlers) {
-            updateHandlers();
+            updateHandlers(findShadowedHandlers(handler));
         }
         if ( !handler.activate() ) {
             logger.warn("Activating resource provider {} failed", handler.getInfo());
@@ -366,7 +368,7 @@
     private void deactivate(final ResourceProviderHandler handler) {
         handler.deactivate();
         synchronized (this.handlers) {
-            updateHandlers();
+            updateHandlers(findShadowedHandlers(handler));
         }
         logger.debug("Deactivated resource provider {}", handler.getInfo());
     }
@@ -480,8 +482,33 @@
         }
     }
 
+    /**
+     * Returns a {@link Collection} of registered {@link ResourceProviderHandler}s that are shadowed by the given {@link ResourceProviderHandler}.
+     * This means that the path of each of the returned {@link ResourceProviderHandler}s is a parent of the path of the given {@link ResourceProviderHandler}
+     *
+     * @param handler
+     * @return
+     */
+    private Collection<List<ResourceProviderHandler>> findShadowedHandlers(ResourceProviderHandler handler) {
+        Collection<List<ResourceProviderHandler>> shadowedHandlers = new ArrayList<>(2);
+        String path = handler.getPath();
+        while(path != null) {
+            List<ResourceProviderHandler> list = handlers.get(path);
+            if (list != null && !list.isEmpty()) {
+                shadowedHandlers.add(list);
+            }
+            path = ResourceUtil.getParent(path);
+        }
+
+        return shadowedHandlers;
+    }
+
     private void updateHandlers() {
-        for (List<ResourceProviderHandler> list : handlers.values()) {
+        this.updateHandlers(this.handlers.values());
+    }
+
+    private void updateHandlers(Collection<List<ResourceProviderHandler>> givenHandlers) {
+        for (List<ResourceProviderHandler> list : givenHandlers) {
             if ( !list.isEmpty() ) {
                 final ResourceProviderHandler h = list.get(0);
                 if (h != null) {
diff --git a/src/test/java/org/apache/sling/resourceresolver/impl/providers/ResourceProviderTrackerTest.java b/src/test/java/org/apache/sling/resourceresolver/impl/providers/ResourceProviderTrackerTest.java
index 21ead48..5d83705 100644
--- a/src/test/java/org/apache/sling/resourceresolver/impl/providers/ResourceProviderTrackerTest.java
+++ b/src/test/java/org/apache/sling/resourceresolver/impl/providers/ResourceProviderTrackerTest.java
@@ -26,12 +26,15 @@
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThat;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.anyLong;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -291,17 +294,56 @@
         // register RP on /path, empty exclude set expected
         info = fixture.registerResourceProvider(rp, "/path", AuthType.no);
         assertNull(excludeSets.get("/"));
+        assertThat(excludeSets.get("/path"), hasSize(0));
         // register RP on /, expect /path excluded
         fixture.registerResourceProvider(rp, "/", AuthType.no);
         assertThat(excludeSets.get("/"), hasSize(1));
         assertThat(excludeSets.get("/"), contains("/path"));
+        assertThat(excludeSets.get("/path"), hasSize(0));
         // unregister RP on /path,  empty exclude set expected
         fixture.unregisterResourceProvider(info);
         assertThat(excludeSets.get("/"), hasSize(0));
+        assertThat(excludeSets.get("/path"), hasSize(0));
         // register RP on /path again, expect /path excluded
         fixture.registerResourceProvider(rp, "/path", AuthType.no);
         assertThat(excludeSets.get("/"), hasSize(1));
         assertThat(excludeSets.get("/"), contains("/path"));
+        assertThat(excludeSets.get("/path"), hasSize(0));
+    }
+
+    @Test
+    public void testUpdateOnlyOnIntersectingProviders() throws InvalidSyntaxException {
+        final ResourceProviderTracker tracker = new ResourceProviderTracker();
+
+        tracker.activate(context.bundleContext(), eventAdmin, null);
+        tracker.setObservationReporterGenerator(new SimpleObservationReporterGenerator(new DoNothingObservationReporter()));
+
+        ResourceProvider<?> rootRp = mock(ResourceProvider.class);
+        ResourceProvider<?> fooRp = mock(ResourceProvider.class);
+        ResourceProvider<?> barRp = mock(ResourceProvider.class);
+        ResourceProvider<?> foobarRp = mock(ResourceProvider.class);
+
+        ResourceProviderInfo info;
+
+        // register RPs and verify how often update() gets called
+        fixture.registerResourceProvider(rootRp, "/", AuthType.no);
+        verify(rootRp, never()).update(anyLong());
+        fixture.registerResourceProvider(fooRp, "/foo", AuthType.no);
+        verify(rootRp, times(1)).update(anyLong());
+        verify(fooRp, never()).update(anyLong());
+        info = fixture.registerResourceProvider(barRp, "/bar", AuthType.no);
+        verify(rootRp, times(2)).update(anyLong());
+        verify(fooRp, never()).update(anyLong());
+        verify(barRp, never()).update(anyLong());
+        fixture.unregisterResourceProvider(info);
+        verify(rootRp, times(3)).update(anyLong());
+        verify(fooRp, never()).update(anyLong());
+        verify(barRp, never()).update(anyLong());
+        fixture.registerResourceProvider(foobarRp, "/foo/bar", AuthType.no);
+        verify(rootRp, times(4)).update(anyLong());
+        verify(fooRp, times(1)).update(anyLong());
+        verify(barRp, never()).update(anyLong());
+        verify(foobarRp, never()).update(anyLong());
     }
 
     @Test