SLING-5792 : API to manage Authentication Requirement. Add first implementation and add test based on patch provided by Angela Schreiber

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1756370 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index 545b9c0..92ca7fd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -125,6 +125,12 @@
         </dependency>
         <dependency>
             <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component</artifactId>
+            <version>1.3.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
             <artifactId>org.osgi.service.event</artifactId>
             <version>1.3.1</version>
             <scope>provided</scope>
@@ -139,7 +145,10 @@
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
         </dependency>
-
+        <dependency>
+            <groupId>org.jmock</groupId>
+            <artifactId>jmock-junit4</artifactId>
+        </dependency>
         <dependency>
             <groupId>commons-codec</groupId>
             <artifactId>commons-codec</artifactId>
@@ -153,8 +162,10 @@
             <artifactId>junit</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.jmock</groupId>
-            <artifactId>jmock-junit4</artifactId>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>1.10.19</version>
+            <scope>test</scope>
         </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
@@ -166,5 +177,11 @@
             <version>1.4</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>15.0</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/auth/core/impl/AuthenticationRequirementHolder.java b/src/main/java/org/apache/sling/auth/core/impl/AuthenticationRequirementHolder.java
index f89b973..04d8c21 100644
--- a/src/main/java/org/apache/sling/auth/core/impl/AuthenticationRequirementHolder.java
+++ b/src/main/java/org/apache/sling/auth/core/impl/AuthenticationRequirementHolder.java
@@ -25,7 +25,7 @@
     private final boolean requiresAuthentication;
 
     static AuthenticationRequirementHolder fromConfig(final String config,
-            final ServiceReference serviceReference) {
+            final ServiceReference<?> serviceReference) {
         if (config == null || config.length() == 0) {
             throw new IllegalArgumentException(
                 "Configuration must not be null or empty");
@@ -50,7 +50,7 @@
 
     AuthenticationRequirementHolder(final String fullPath,
             final boolean requiresAuthentication,
-            final ServiceReference serviceReference) {
+            final ServiceReference<?> serviceReference) {
         super(fullPath, serviceReference);
         this.requiresAuthentication = requiresAuthentication;
     }
diff --git a/src/main/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImpl.java b/src/main/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImpl.java
new file mode 100644
index 0000000..291edcd
--- /dev/null
+++ b/src/main/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImpl.java
@@ -0,0 +1,147 @@
+/*
+ * 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.auth.core.impl;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+
+import org.apache.sling.api.auth.Authenticator;
+import org.apache.sling.auth.core.spi.BundleAuthenticationRequirement;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Deactivate;
+import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.component.annotations.ServiceScope;
+
+@Component(service = BundleAuthenticationRequirement.class,
+           scope = ServiceScope.BUNDLE,
+           property = Constants.SERVICE_VENDOR + "=The Apache Software Foundation")
+public class BundleAuthenticationRequirementImpl implements BundleAuthenticationRequirement {
+
+    static final String PREFIX = "Apache Sling Authentication Requirements for Bundle ";
+
+    /** The client bundle id. */
+    private long bundleId;
+
+    /** Provider string for info */
+    private String provider;
+
+    private PathBasedHolderCache<AuthenticationRequirementHolder> authRequiredCache;
+
+    @Reference
+    private Authenticator slingAuthenticator;
+
+    @Activate
+    private void activate(final ComponentContext componentCtx) {
+        final Bundle bundle = componentCtx.getUsingBundle();
+        this.bundleId = bundle.getBundleId();
+        this.provider = PREFIX +
+                bundle.getSymbolicName() +
+                ":" +
+                bundle.getVersion() +
+                " (" +
+                String.valueOf(bundle.getBundleId()) +
+                ")";
+        this.authRequiredCache = ((SlingAuthenticator)slingAuthenticator).authRequiredCache;
+    }
+
+    @Deactivate
+    private void deactivate() {
+        clearRequirements();
+    }
+
+    @Override
+    public void setRequirements(@Nonnull final Map<String, Boolean> requirements) {
+        final Collection<AuthenticationRequirementHolder> reqHolders = createHolders(requirements);
+
+        // remove existing entries
+        clearRequirements();
+
+        // register the new entries
+        register(reqHolders);
+    }
+
+    @Override
+    public void appendRequirements(@Nonnull final Map<String, Boolean> requirements) {
+        final Collection<AuthenticationRequirementHolder> reqHolders = createHolders(requirements);
+        register(reqHolders);
+    }
+
+    @Override
+    public void removeRequirements(@Nonnull final Map<String, Boolean> requirements) {
+        final Collection<AuthenticationRequirementHolder> reqHolders = createHolders(requirements);
+        for (AuthenticationRequirementHolder authReq : reqHolders) {
+            authRequiredCache.removeHolder(authReq);
+        }
+    }
+
+    @Override
+    public void clearRequirements() {
+        authRequiredCache.removeAllMatchingHolders(new AuthenticationRequirementHolder("", false, null) {
+
+            @Override
+            public boolean equals(final Object other) {
+                if ( other instanceof BundleAuthenticationRequirementHolder
+                     && ((BundleAuthenticationRequirementHolder)other).getBundleId() == bundleId ) {
+                    return true;
+                }
+                return false;
+            }
+        });
+
+    }
+
+    private Set<AuthenticationRequirementHolder> createHolders(@Nonnull Map<String,Boolean> requirements) {
+        final Set<AuthenticationRequirementHolder> holders = new HashSet<AuthenticationRequirementHolder>(requirements.size());
+        for (final Map.Entry<String, Boolean> entry : requirements.entrySet()) {
+            holders.add(new BundleAuthenticationRequirementHolder(entry.getKey(), entry.getValue()));
+        }
+        return holders;
+    }
+
+    private void register(@Nonnull final Collection<AuthenticationRequirementHolder> authReqs) {
+        for (AuthenticationRequirementHolder authReq : authReqs) {
+            authRequiredCache.addHolder(authReq);
+        }
+    }
+
+    private final class BundleAuthenticationRequirementHolder extends AuthenticationRequirementHolder {
+
+        public BundleAuthenticationRequirementHolder(final String fullPath,
+                final boolean requiresAuthentication) {
+            super(fullPath, requiresAuthentication, null);
+        }
+
+        @Override
+        String getProvider() {
+            return provider;
+        }
+
+        long getBundleId() {
+            return bundleId;
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolder.java b/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolder.java
index 97c59df..c634174 100644
--- a/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolder.java
+++ b/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolder.java
@@ -129,7 +129,7 @@
      * is ordered the service description of the {@link SlingAuthenticator} is
      * returned.
      */
-    final String getProvider() {
+    String getProvider() {
         // assume the commons/auth SlingAuthenticator provides the entry
         if (serviceReference == null) {
             return SlingAuthenticator.DESCRIPTION;
@@ -176,6 +176,9 @@
         // now compare the service references giving priority to
         // to the higher priority service
         if (serviceReference == null) {
+            if ( other.serviceReference == null ) {
+                return 0;
+            }
             return -1;
         } else if (other.serviceReference == null) {
             return 1;
diff --git a/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolderCache.java b/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolderCache.java
index 54267dc..28fbec5 100644
--- a/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolderCache.java
+++ b/src/main/java/org/apache/sling/auth/core/impl/PathBasedHolderCache.java
@@ -21,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.SortedSet;
@@ -99,7 +100,35 @@
         }
     }
 
-    public Collection<Type>[] findApplicableHolder(final HttpServletRequest request) {
+    /**
+     * Remove all holders which "equal" the provided holder
+     * @param holder Template holder
+     */
+    public void removeAllMatchingHolders(final Type holder) {
+        this.rwLock.writeLock().lock();
+        try {
+            for(final Map.Entry<String,  Map<String, SortedSet<Type>>> entry : this.cache.entrySet()) {
+                final Iterator<Map.Entry<String, SortedSet<Type>>> innerIter = entry.getValue().entrySet().iterator();
+                while ( innerIter.hasNext() ) {
+                    final Map.Entry<String, SortedSet<Type>> innerEntry = innerIter.next();
+                    final Iterator<Type> iter = innerEntry.getValue().iterator();
+                    while ( iter.hasNext() ) {
+                        final Type current = iter.next();
+                        if ( holder.equals(current) ) {
+                            iter.remove();
+                        }
+                    }
+                    if ( innerEntry.getValue().isEmpty() ) {
+                        innerIter.remove();
+                    }
+                }
+            }
+        } finally {
+            this.rwLock.writeLock().unlock();
+        }
+    }
+
+    public Collection<Type>[] findApplicableHolders(final HttpServletRequest request) {
         this.rwLock.readLock().lock();
         try {
             final String hostname = request.getServerName()
diff --git a/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java b/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java
index 72efadf..60434b4 100644
--- a/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java
+++ b/src/main/java/org/apache/sling/auth/core/impl/SlingAuthenticator.java
@@ -529,7 +529,7 @@
 
         // select path used for authentication handler selection
         final Collection<AbstractAuthenticationHandlerHolder>[] holdersArray = this.authHandlerCache
-                .findApplicableHolder(request);
+                .findApplicableHolders(request);
         final String path = getHandlerSelectionPath(request);
         boolean done = false;
         for (int m = 0; !done && m < holdersArray.length; m++) {
@@ -592,7 +592,7 @@
 
         final String path = getHandlerSelectionPath(request);
         final Collection<AbstractAuthenticationHandlerHolder>[] holdersArray = this.authHandlerCache
-                .findApplicableHolder(request);
+                .findApplicableHolders(request);
         for (int m = 0; m < holdersArray.length; m++) {
             final Collection<AbstractAuthenticationHandlerHolder> holderSet = holdersArray[m];
             if (holderSet != null) {
@@ -711,7 +711,7 @@
         }
 
         final Collection<AbstractAuthenticationHandlerHolder>[] localArray = this.authHandlerCache
-                .findApplicableHolder(request);
+                .findApplicableHolders(request);
         for (int m = 0; m < localArray.length; m++) {
             final Collection<AbstractAuthenticationHandlerHolder> local = localArray[m];
             if (local != null) {
@@ -905,7 +905,7 @@
         }
 
         final Collection<AuthenticationRequirementHolder>[] holderSetArray = authRequiredCache
-                .findApplicableHolder(request);
+                .findApplicableHolders(request);
         for (int m = 0; m < holderSetArray.length; m++) {
             final Collection<AuthenticationRequirementHolder> holders = holderSetArray[m];
             if (holders != null) {
diff --git a/src/test/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImplTest.java b/src/test/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImplTest.java
new file mode 100644
index 0000000..c3d5d07
--- /dev/null
+++ b/src/test/java/org/apache/sling/auth/core/impl/BundleAuthenticationRequirementImplTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.auth.core.impl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.sling.auth.core.spi.BundleAuthenticationRequirement;
+import org.junit.Before;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Version;
+import org.osgi.service.component.ComponentContext;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
+
+import junitx.util.PrivateAccessor;
+
+public class BundleAuthenticationRequirementImplTest {
+
+    private final SlingAuthenticator slingAuthenticator = new SlingAuthenticator();
+
+    private Predicate<AuthenticationRequirementHolder> providerFilter = new Predicate<AuthenticationRequirementHolder>() {
+        @Override
+        public boolean apply(AuthenticationRequirementHolder input) {
+            return input != null && input.getProvider().startsWith(BundleAuthenticationRequirementImpl.PREFIX);
+        }
+    };
+
+    Map<String, Boolean> initialRequirements = new HashMap<String, Boolean>();
+
+    BundleAuthenticationRequirement requirement;
+
+    @Before public void setUp() throws Throwable {
+        final Bundle bundle = mock(Bundle.class);
+        final ComponentContext cmpCtx = mock(ComponentContext.class);
+        when(cmpCtx.getUsingBundle()).thenReturn(bundle);
+        when(bundle.getBundleId()).thenReturn(5L);
+        when(bundle.getSymbolicName()).thenReturn("testbundle");
+        when(bundle.getVersion()).thenReturn(new Version("1.0.0"));
+
+        this.requirement = new BundleAuthenticationRequirementImpl();
+        PrivateAccessor.setField(this.requirement, "slingAuthenticator", slingAuthenticator);
+        PrivateAccessor.invoke(requirement, "activate", new Class[] {ComponentContext.class}, new Object[] {cmpCtx});
+
+        // add initial values for external auth requirements of the test service reference
+        initialRequirements.put("/a", true);
+        initialRequirements.put("/b", true);
+        initialRequirements.put("/b/c", false);
+        initialRequirements.put("/c", false);
+        requirement.setRequirements(initialRequirements);
+    }
+
+    private Iterable<AuthenticationRequirementHolder> filterRequirements() {
+        return Iterables.filter(slingAuthenticator.getAuthenticationRequirements(), providerFilter);
+    }
+
+    private void assertRequirements(Map<String, Boolean> expected, Iterable<AuthenticationRequirementHolder> requirements) {
+        assertEquals(expected.size(), Iterables.size(requirements));
+
+        for (AuthenticationRequirementHolder holder : requirements) {
+            assertTrue(holder.fullPath, expected.containsKey(holder.fullPath));
+            boolean b = expected.get(holder.fullPath);
+            assertEquals(holder.fullPath, b, holder.requiresAuthentication());
+        }
+    }
+
+    @Test public void test_VerifyInitialSetup() throws Exception {
+        assertRequirements(initialRequirements, filterRequirements());
+    }
+
+    @Test public void test_SetRequirements() throws Exception {
+        Map<String, Boolean> toReplace =  ImmutableMap.of("/a", false, "/d", true);
+        requirement.setRequirements(toReplace);
+
+        // it's expected that all existing values have been replaced
+        assertRequirements(toReplace, filterRequirements());
+    }
+
+    @Test public void test_AppendRequirements() throws Exception {
+        Map<String, Boolean> toAppend =  ImmutableMap.of("/d", true, "/e/f", false);
+        requirement.appendRequirements(toAppend);
+
+        // the expected result is the combination of the entries to append plus
+        // the initial values.
+        Map<String, Boolean> expected = new HashMap<String, Boolean>(initialRequirements);
+        expected.putAll(toAppend);
+        assertRequirements(expected, filterRequirements());
+    }
+
+    @Test public void test_AppendRequirementsWithConflict() throws Exception {
+        Map<String, Boolean> toAppend =  ImmutableMap.of("/a", false, "/d", true, "/e/f", false);
+        requirement.appendRequirements(toAppend);
+
+        // the expected result is the combination of the entries to append plus
+        // the initial values; conflicting values (same path again) wont'be replaced
+        // and the 'conflicting' entries are ignored.
+        Map<String, Boolean> expected = new HashMap<String, Boolean>(toAppend);
+        expected.putAll(initialRequirements);
+        assertRequirements(expected, filterRequirements());
+    }
+
+    @Test public void test_RemoveRequirements() throws Exception {
+        Map<String, Boolean> toRemove =  ImmutableMap.of("/a", true, "/b", true);
+        requirement.removeRequirements(toRemove);
+
+        // the expected result is initial set without the entries to be removed.
+        Map<String, Boolean> expected = new HashMap<String, Boolean>(initialRequirements);
+        for (String key : toRemove.keySet()) {
+            expected.remove(key);
+        }
+        assertRequirements(expected, filterRequirements());
+    }
+
+    @Test public void test_RemoveRequirements2() throws Exception {
+        // paths to remove are contained but have a different value
+        Map<String, Boolean> toRemove =  ImmutableMap.of("/a", false, "/b", false);
+        requirement.removeRequirements(toRemove);
+
+        // the expected result is the initial set without the entries to be removed.
+        Map<String, Boolean> expected = new HashMap<String, Boolean>(initialRequirements);
+        for (String key : toRemove.keySet()) {
+            expected.remove(key);
+        }
+        assertRequirements(expected, filterRequirements());
+    }
+
+    @Test public void test_RemoveNonExistingRequirements() throws Exception {
+        // paths to remove are contained but have a different value
+        Map<String, Boolean> toRemove =  ImmutableMap.of("/nonExisting", true);
+        requirement.removeRequirements(toRemove);
+
+        // the expected result is the initial set
+        assertRequirements(initialRequirements, filterRequirements());
+    }
+
+    @Test public void test_ClearRequirements() throws Exception {
+        requirement.clearRequirements();
+
+        assertRequirements(ImmutableMap.<String, Boolean>of(), filterRequirements());
+
+    }
+}
\ No newline at end of file