FELIX-4085 : [Core R5] Implement updates to the Bundle Hook Specification. Apply patch from David Bosschaert

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1549745 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/framework/src/main/java/org/apache/felix/framework/BundleImpl.java b/framework/src/main/java/org/apache/felix/framework/BundleImpl.java
index be8447d..4a2ecfe 100644
--- a/framework/src/main/java/org/apache/felix/framework/BundleImpl.java
+++ b/framework/src/main/java/org/apache/felix/framework/BundleImpl.java
@@ -26,8 +26,8 @@
 import java.util.*;
 
 import org.apache.felix.framework.cache.BundleArchive;
-import org.apache.felix.framework.ext.SecurityProvider;
 import org.apache.felix.framework.util.SecurityManagerEx;
+import org.apache.felix.framework.util.ShrinkableCollection;
 import org.apache.felix.framework.util.StringMap;
 import org.apache.felix.framework.util.Util;
 import org.osgi.framework.AdaptPermission;
@@ -40,6 +40,7 @@
 import org.osgi.framework.ServicePermission;
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.Version;
+import org.osgi.framework.hooks.bundle.CollisionHook;
 import org.osgi.framework.startlevel.BundleStartLevel;
 import org.osgi.framework.wiring.BundleRevision;
 import org.osgi.framework.wiring.BundleRevisions;
@@ -93,7 +94,7 @@
         m_activator = null;
         m_context = null;
 
-        BundleRevision revision = createRevision();
+        BundleRevision revision = createRevision(false);
         addRevision(revision);
     }
 
@@ -1138,7 +1139,7 @@
         m_archive.revise(location, is);
         try
         {
-            BundleRevision revision = createRevision();
+            BundleRevision revision = createRevision(true);
             addRevision(revision);
         }
         catch (Exception ex)
@@ -1189,7 +1190,7 @@
         }
     }
 
-    private BundleRevision createRevision() throws Exception
+    private BundleRevision createRevision(boolean isUpdate) throws Exception
     {
         // Get and parse the manifest from the most recent revision and
         // create an associated revision object for it.
@@ -1208,7 +1209,7 @@
         String allowMultiple =
             (String) getFramework().getConfig().get(Constants.FRAMEWORK_BSNVERSION);
         allowMultiple = (allowMultiple == null)
-            ? Constants.FRAMEWORK_BSNVERSION_SINGLE
+            ? Constants.FRAMEWORK_BSNVERSION_MANAGED
             : allowMultiple;
         if (revision.getManifestVersion().equals("2")
             && !allowMultiple.equals(Constants.FRAMEWORK_BSNVERSION_MULTIPLE))
@@ -1217,25 +1218,44 @@
             bundleVersion = (bundleVersion == null) ? Version.emptyVersion : bundleVersion;
             String symName = revision.getSymbolicName();
 
+            List<Bundle> collisionCanditates = new ArrayList<Bundle>();
             Bundle[] bundles = getFramework().getBundles();
             for (int i = 0; (bundles != null) && (i < bundles.length); i++)
             {
                 long id = ((BundleImpl) bundles[i]).getBundleId();
                 if (id != getBundleId())
                 {
-                    String sym = bundles[i].getSymbolicName();
-                    Version ver = bundles[i].getVersion();
-                    if ((symName != null)
-                        && (sym != null)
-                        && symName.equals(sym)
-                        && bundleVersion.equals(ver))
+                    if (symName.equals(bundles[i].getSymbolicName()) &&
+                         bundleVersion.equals(bundles[i].getVersion()))
+                     {
+                        collisionCanditates.add(bundles[i]);
+                     }
+                }
+            }
+            if (!collisionCanditates.isEmpty() && allowMultiple.equals(Constants.FRAMEWORK_BSNVERSION_MANAGED))
+            {
+                Set<ServiceReference<CollisionHook>> hooks = getFramework().getHooks(CollisionHook.class);
+                if (!hooks.isEmpty())
+                {
+                    Collection<Bundle> shrinkableCollisionCandidates = new ShrinkableCollection<Bundle>(collisionCanditates);
+                    for (ServiceReference<CollisionHook> hook : hooks)
                     {
-                        throw new BundleException(
-                            "Bundle symbolic name and version are not unique: "
-                            + sym + ':' + ver, BundleException.DUPLICATE_BUNDLE_ERROR);
+                        CollisionHook ch = getFramework().getService(getFramework(), hook);
+                        if (ch != null)
+                        {
+                            Felix.m_secureAction.invokeBundleCollisionHook(ch,
+                                isUpdate ? CollisionHook.UPDATING : CollisionHook.INSTALLING,
+                                this, shrinkableCollisionCandidates);
+                        }
                     }
                 }
             }
+            if (!collisionCanditates.isEmpty())
+            {
+                throw new BundleException(
+                    "Bundle symbolic name and version are not unique: "
+                    + symName + ':' + bundleVersion, BundleException.DUPLICATE_BUNDLE_ERROR);
+            }
         }
 
         return revision;
diff --git a/framework/src/main/java/org/apache/felix/framework/ServiceRegistry.java b/framework/src/main/java/org/apache/felix/framework/ServiceRegistry.java
index 83bdcff..32494e2 100644
--- a/framework/src/main/java/org/apache/felix/framework/ServiceRegistry.java
+++ b/framework/src/main/java/org/apache/felix/framework/ServiceRegistry.java
@@ -19,6 +19,7 @@
 package org.apache.felix.framework;
 
 import java.util.*;
+
 import org.apache.felix.framework.capabilityset.CapabilitySet;
 import org.apache.felix.framework.capabilityset.SimpleFilter;
 import org.apache.felix.framework.wiring.BundleCapabilityImpl;
@@ -54,6 +55,7 @@
         new WeakHashMap<ServiceReference, ServiceReference>();
 
     private final static Class<?>[] m_hookClasses = {
+        org.osgi.framework.hooks.bundle.CollisionHook.class,
         org.osgi.framework.hooks.bundle.FindHook.class,
         org.osgi.framework.hooks.bundle.EventHook.class,
         org.osgi.framework.hooks.service.EventHook.class,
@@ -805,4 +807,4 @@
     {
         void serviceChanged(ServiceEvent event, Dictionary oldProps);
     }
-}
\ No newline at end of file
+}
diff --git a/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java b/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java
index 853c9ef..a6a2584 100644
--- a/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java
+++ b/framework/src/main/java/org/apache/felix/framework/util/SecureAction.java
@@ -27,8 +27,8 @@
 import java.util.Hashtable;
 import java.util.Map;
 import java.util.zip.ZipFile;
-import org.osgi.framework.Bundle;
 
+import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleEvent;
@@ -1056,6 +1056,30 @@
         }
     }
 
+    public void invokeBundleCollisionHook(
+        org.osgi.framework.hooks.bundle.CollisionHook ch, int operationType,
+        Bundle targetBundle, Collection<Bundle> collisionCandidates)
+        throws Exception
+    {
+        if (System.getSecurityManager() != null)
+        {
+            Actions actions = (Actions) m_actions.get();
+            actions.set(Actions.INVOKE_BUNDLE_COLLISION_HOOK, ch, operationType, targetBundle, collisionCandidates);
+            try
+            {
+                AccessController.doPrivileged(actions, m_acc);
+            }
+            catch (PrivilegedActionException e)
+            {
+                throw e.getException();
+            }
+        }
+        else
+        {
+            ch.filterCollisions(operationType, targetBundle, collisionCandidates);
+        }
+    }
+
     public void invokeBundleFindHook(
         org.osgi.framework.hooks.bundle.FindHook fh,
         BundleContext bc, Collection<Bundle> bundles)
@@ -1428,6 +1452,7 @@
         public static final int INVOKE_RESOLVER_HOOK_SINGLETON = 50;
         public static final int INVOKE_RESOLVER_HOOK_MATCHES = 51;
         public static final int INVOKE_RESOLVER_HOOK_END = 52;
+        public static final int INVOKE_BUNDLE_COLLISION_HOOK = 53;
 
         private int m_action = -1;
         private Object m_arg1 = null;
@@ -1673,9 +1698,13 @@
                 case INVOKE_RESOLVER_HOOK_END:
                     ((org.osgi.framework.hooks.resolver.ResolverHook) arg1).end();
                     return null;
+                case INVOKE_BUNDLE_COLLISION_HOOK:
+                    ((org.osgi.framework.hooks.bundle.CollisionHook) arg1).filterCollisions((Integer) arg2,
+                        (Bundle) arg3, (Collection<Bundle>) arg4);
+                    return null;
             }
 
             return null;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/framework/src/test/java/org/apache/felix/framework/CollisionHookTest.java b/framework/src/test/java/org/apache/felix/framework/CollisionHookTest.java
new file mode 100644
index 0000000..3ecbfdb
--- /dev/null
+++ b/framework/src/test/java/org/apache/felix/framework/CollisionHookTest.java
@@ -0,0 +1,256 @@
+/*
+ * 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.felix.framework;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.framework.cache.BundleArchive;
+import org.apache.felix.framework.cache.BundleArchiveRevision;
+import org.mockito.Mockito;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.Version;
+import org.osgi.framework.hooks.bundle.CollisionHook;
+
+public class CollisionHookTest extends TestCase {
+    public void testCollisionHookInstall() throws Exception {
+        BundleImpl identicalBundle = mockBundleImpl(1L, "foo", "1.2.1.a");
+        BundleImpl differentBundle = mockBundleImpl(2L, "bar", "1.2.1.a");
+
+        CollisionHook testCollisionHook = new CollisionHook() {
+            public void filterCollisions(int operationType, Bundle target, Collection<Bundle> collisionCandidates) {
+                if ((target.getBundleId() == 3L) && (operationType == CollisionHook.INSTALLING)) {
+                    collisionCandidates.clear();
+                }
+            }
+        };
+
+        @SuppressWarnings("unchecked")
+        ServiceReference<CollisionHook> chRef = Mockito.mock(ServiceReference.class);
+
+        // Mock the framework
+        StatefulResolver mockResolver = Mockito.mock(StatefulResolver.class);
+        Felix felixMock = Mockito.mock(Felix.class);
+        Mockito.when(felixMock.getHooks(CollisionHook.class)).thenReturn(Collections.singleton(chRef));
+        Mockito.when(felixMock.getResolver()).thenReturn(mockResolver);
+        Mockito.when(felixMock.getBundles()).thenReturn(new Bundle[] {differentBundle, identicalBundle});
+        Mockito.when(felixMock.getService(felixMock, chRef)).thenReturn(testCollisionHook);
+
+        // Mock the archive of the bundle being installed
+        Map<String,String> headerMap = new HashMap<String, String>();
+        headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "foo");
+        headerMap.put(Constants.BUNDLE_VERSION, "1.2.1.a");
+        headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2");
+
+        BundleArchiveRevision archiveRevision = Mockito.mock(BundleArchiveRevision.class);
+        Mockito.when(archiveRevision.getManifestHeader()).thenReturn(headerMap);
+
+        BundleArchive archive = Mockito.mock(BundleArchive.class);
+        Mockito.when(archive.getCurrentRevision()).thenReturn(archiveRevision);
+        Mockito.when(archive.getId()).thenReturn(3L);
+
+        BundleImpl bi = new BundleImpl(felixMock, archive);
+        assertEquals(3L, bi.getBundleId());
+
+        // Do the revise operation.
+        try {
+            bi.revise(null, null);
+            fail("Should have thrown a BundleException because the installed bundle is not unique");
+        } catch (BundleException be) {
+            // good
+            assertTrue(be.getMessage().contains("not unique"));
+        }
+    }
+
+    public void testCollisionHookUpdate() throws Exception {
+        BundleImpl identicalBundle = mockBundleImpl(1L, "foo", "1.2.1.a");
+        BundleImpl differentBundle = mockBundleImpl(2L, "foo", "1.2.1");
+
+        CollisionHook testCollisionHook = new CollisionHook() {
+            public void filterCollisions(int operationType, Bundle target, Collection<Bundle> collisionCandidates) {
+                if ((target.getBundleId() == 3L) && (operationType == CollisionHook.UPDATING)) {
+                    collisionCandidates.clear();
+                }
+            }
+        };
+
+        @SuppressWarnings("unchecked")
+        ServiceReference<CollisionHook> chRef = Mockito.mock(ServiceReference.class);
+
+        Map<String, String> config = new HashMap<String, String>();
+        config.put(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_MANAGED);
+
+        // Mock the framework
+        StatefulResolver mockResolver = Mockito.mock(StatefulResolver.class);
+        Felix felixMock = Mockito.mock(Felix.class);
+        Mockito.when(felixMock.getConfig()).thenReturn(config);
+        Mockito.when(felixMock.getHooks(CollisionHook.class)).thenReturn(Collections.singleton(chRef));
+        Mockito.when(felixMock.getResolver()).thenReturn(mockResolver);
+        Mockito.when(felixMock.getBundles()).thenReturn(new Bundle[] {differentBundle, identicalBundle});
+        Mockito.when(felixMock.getService(felixMock, chRef)).thenReturn(testCollisionHook);
+
+        // Mock the archive of the bundle being installed
+        Map<String,String> headerMap = new HashMap<String, String>();
+        headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "zar");
+        headerMap.put(Constants.BUNDLE_VERSION, "1.2.1.a");
+        headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2");
+
+        BundleArchiveRevision archiveRevision = Mockito.mock(BundleArchiveRevision.class);
+        Mockito.when(archiveRevision.getManifestHeader()).thenReturn(headerMap);
+
+        BundleArchive archive = Mockito.mock(BundleArchive.class);
+        Mockito.when(archive.getCurrentRevision()).thenReturn(archiveRevision);
+        Mockito.when(archive.getId()).thenReturn(3L);
+
+        BundleImpl bi = new BundleImpl(felixMock, archive);
+        assertEquals("zar", bi.getSymbolicName());
+
+        // Do the revise operation, change the bsn to foo
+        headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "foo");
+        bi.revise(null, null);
+        assertEquals("foo", bi.getSymbolicName());
+    }
+
+    public void testCollisionNotEnabled() throws Exception {
+        BundleImpl identicalBundle = mockBundleImpl(1L, "foo", "1.2.1.a");
+        BundleImpl differentBundle = mockBundleImpl(2L, "bar", "1.2.1.a");
+
+        CollisionHook testCollisionHook = new CollisionHook() {
+            public void filterCollisions(int operationType, Bundle target, Collection<Bundle> collisionCandidates) {
+                if ((target.getBundleId() == 3L) && (operationType == CollisionHook.INSTALLING)) {
+                    collisionCandidates.clear();
+                }
+            }
+        };
+
+        @SuppressWarnings("unchecked")
+        ServiceReference<CollisionHook> chRef = Mockito.mock(ServiceReference.class);
+
+        Map<String, String> config = new HashMap<String, String>();
+        config.put(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_SINGLE);
+
+        // Mock the framework
+        StatefulResolver mockResolver = Mockito.mock(StatefulResolver.class);
+        Felix felixMock = Mockito.mock(Felix.class);
+        Mockito.when(felixMock.getConfig()).thenReturn(config);
+        Mockito.when(felixMock.getHooks(CollisionHook.class)).thenReturn(Collections.singleton(chRef));
+        Mockito.when(felixMock.getResolver()).thenReturn(mockResolver);
+        Mockito.when(felixMock.getBundles()).thenReturn(new Bundle[] {differentBundle, identicalBundle});
+        Mockito.when(felixMock.getService(felixMock, chRef)).thenReturn(testCollisionHook);
+
+        // Mock the archive of the bundle being installed
+        Map<String,String> headerMap = new HashMap<String, String>();
+        headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "foo");
+        headerMap.put(Constants.BUNDLE_VERSION, "1.2.1.a");
+        headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2");
+
+        BundleArchiveRevision archiveRevision = Mockito.mock(BundleArchiveRevision.class);
+        Mockito.when(archiveRevision.getManifestHeader()).thenReturn(headerMap);
+
+        BundleArchive archive = Mockito.mock(BundleArchive.class);
+        Mockito.when(archive.getCurrentRevision()).thenReturn(archiveRevision);
+        Mockito.when(archive.getId()).thenReturn(3L);
+
+        try {
+            new BundleImpl(felixMock, archive);
+            fail("Should have thrown a BundleException because the collision hook is not enabled");
+        } catch (BundleException be) {
+            // good
+            assertTrue(be.getMessage().contains("not unique"));
+        }
+    }
+
+    public void testAllowMultiple() throws Exception {
+        BundleImpl identicalBundle = mockBundleImpl(1L, "foo", "1.2.1.a");
+        BundleImpl differentBundle = mockBundleImpl(2L, "bar", "1.2.1.a");
+
+        Map<String, String> config = new HashMap<String, String>();
+        config.put(Constants.FRAMEWORK_BSNVERSION, Constants.FRAMEWORK_BSNVERSION_MULTIPLE);
+
+        // Mock the framework
+        StatefulResolver mockResolver = Mockito.mock(StatefulResolver.class);
+        Felix felixMock = Mockito.mock(Felix.class);
+        Mockito.when(felixMock.getConfig()).thenReturn(config);
+        Mockito.when(felixMock.getResolver()).thenReturn(mockResolver);
+        Mockito.when(felixMock.getBundles()).thenReturn(new Bundle[] {differentBundle, identicalBundle});
+
+        // Mock the archive of the bundle being installed
+        Map<String,String> headerMap = new HashMap<String, String>();
+        headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "foo");
+        headerMap.put(Constants.BUNDLE_VERSION, "1.2.1.a");
+        headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2");
+
+        BundleArchiveRevision archiveRevision = Mockito.mock(BundleArchiveRevision.class);
+        Mockito.when(archiveRevision.getManifestHeader()).thenReturn(headerMap);
+
+        BundleArchive archive = Mockito.mock(BundleArchive.class);
+        Mockito.when(archive.getCurrentRevision()).thenReturn(archiveRevision);
+        Mockito.when(archive.getId()).thenReturn(3L);
+
+        BundleImpl bi = new BundleImpl(felixMock, archive);
+        assertEquals(3L, bi.getBundleId());
+    }
+
+    public void testNoCollisionHook() throws Exception {
+        BundleImpl identicalBundle = mockBundleImpl(1L, "foo", "1.2.1.a");
+        BundleImpl differentBundle = mockBundleImpl(2L, "bar", "1.2.1.a");
+
+        // Mock the framework
+        StatefulResolver mockResolver = Mockito.mock(StatefulResolver.class);
+        Felix felixMock = Mockito.mock(Felix.class);
+        Mockito.when(felixMock.getResolver()).thenReturn(mockResolver);
+        Mockito.when(felixMock.getBundles()).thenReturn(new Bundle[] {differentBundle, identicalBundle});
+
+        // Mock the archive of the bundle being installed
+        Map<String,String> headerMap = new HashMap<String, String>();
+        headerMap.put(Constants.BUNDLE_SYMBOLICNAME, "foo");
+        headerMap.put(Constants.BUNDLE_VERSION, "1.2.1.a");
+        headerMap.put(Constants.BUNDLE_MANIFESTVERSION, "2");
+
+        BundleArchiveRevision archiveRevision = Mockito.mock(BundleArchiveRevision.class);
+        Mockito.when(archiveRevision.getManifestHeader()).thenReturn(headerMap);
+
+        BundleArchive archive = Mockito.mock(BundleArchive.class);
+        Mockito.when(archive.getCurrentRevision()).thenReturn(archiveRevision);
+        Mockito.when(archive.getId()).thenReturn(3L);
+
+        try {
+            new BundleImpl(felixMock, archive);
+            fail("Should have thrown a BundleException because the installed bundle is not unique");
+        } catch (BundleException be) {
+            // good
+            assertTrue(be.getMessage().contains("not unique"));
+        }
+    }
+
+    private BundleImpl mockBundleImpl(long id, String bsn, String version) {
+        BundleImpl identicalBundle = Mockito.mock(BundleImpl.class);
+        Mockito.when(identicalBundle.getSymbolicName()).thenReturn(bsn);
+        Mockito.when(identicalBundle.getVersion()).thenReturn(Version.parseVersion(version));
+        Mockito.when(identicalBundle.getBundleId()).thenReturn(id);
+        return identicalBundle;
+    }
+}