Applying patch for RFC-126 service hooks. (FELIX-905, FELIX-906)


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@751101 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/framework/src/main/java/org/apache/felix/framework/Felix.java b/framework/src/main/java/org/apache/felix/framework/Felix.java
index 1f1f210..5b23ae0 100644
--- a/framework/src/main/java/org/apache/felix/framework/Felix.java
+++ b/framework/src/main/java/org/apache/felix/framework/Felix.java
@@ -32,7 +32,7 @@
 import org.apache.felix.framework.util.manifestparser.*;
 import org.apache.felix.moduleloader.*;
 import org.osgi.framework.*;
-import org.osgi.framework.hooks.service.ListenerHook;
+import org.osgi.framework.hooks.service.*;
 import org.osgi.service.packageadmin.ExportedPackage;
 import org.osgi.service.startlevel.StartLevel;
 
@@ -611,6 +611,8 @@
 
             // Create service registry.
             m_registry = new ServiceRegistry(m_logger);
+            m_dispatcher.setServiceRegistry(m_registry);
+
             // Add a listener to the service registry; this is
             // used to distribute service registry events to
             // service listeners.
@@ -2412,11 +2414,11 @@
             bundle, ServiceListener.class, l, (f == null) ? null : new FilterImpl(m_logger, f));
 
         // Invoke the ListenerHook.added() on all hooks.
-        ListenerHook[] hooks = m_registry.getListenerHooks();
+        List listenerHooks = m_registry.getListenerHooks();
         Collection c = Collections.singleton(new ListenerHookInfoImpl(bundle.getBundleContext(), f));
-        for (int i = 0; i < hooks.length; i++)
+        for (int i = 0; i < listenerHooks.size(); i++)
         {
-            hooks[i].added(c);
+            ((ListenerHook) listenerHooks.get(i)).added(c);
         }
     }
 
@@ -2435,11 +2437,11 @@
         if (listener != null)
         {
             // Invoke the ListenerHook.removed() on all hooks.
-            ListenerHook[] hooks = m_registry.getListenerHooks();
+            List listenerHooks = m_registry.getListenerHooks();
             Collection c = Collections.singleton(listener);
-            for (int i = 0; i < hooks.length; i++)
+            for (int i = 0; i < listenerHooks.size(); i++)
             {
-                hooks[i].removed(c);
+                ((ListenerHook) listenerHooks.get(i)).removed(c);
             }
         }
     }
@@ -2591,6 +2593,18 @@
 
         if (refList.size() > 0)
         {
+            // activate findhooks
+            List findHooks = m_registry.getFindHooks();
+            for (int i = 0; i < findHooks.size(); i++)
+            {
+                ((FindHook) findHooks.get(i)).find(
+                    bundle.getBundleContext(),
+                    className,
+                    expr,
+                    !checkAssignable,
+                    new ShrinkableCollection(refList));
+            }
+
             return (ServiceReference[]) refList.toArray(new ServiceReference[refList.size()]);
         }
 
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 2d60732..60258aa 100644
--- a/framework/src/main/java/org/apache/felix/framework/ServiceRegistry.java
+++ b/framework/src/main/java/org/apache/felix/framework/ServiceRegistry.java
@@ -22,7 +22,7 @@
 
 import org.apache.felix.framework.util.FelixConstants;
 import org.osgi.framework.*;
-import org.osgi.framework.hooks.service.ListenerHook;
+import org.osgi.framework.hooks.service.*;
 
 public class ServiceRegistry
 {
@@ -39,7 +39,12 @@
 
     private ServiceListener m_serviceListener = null;
 
-    private final List m_listenerHooks = new ArrayList();
+    private final Object m_eventHookLock = new Object();
+    private Object[] m_eventHooks = new Object[0];
+    private final Object m_findHookLock = new Object();
+    private Object[] m_findHooks = new Object[0];
+    private final Object m_listenerHookLock = new Object();
+    private Object[] m_listenerHooks = new Object[0];
 
     public ServiceRegistry(Logger logger)
     {
@@ -728,13 +733,37 @@
 
     private void addHooks(String[] classNames, Object svcObj)
     {
-        if (isHook(classNames, ListenerHook.class, svcObj))
+        if (isHook(classNames, EventHook.class, svcObj))
         {
-            synchronized (m_listenerHooks)
+            synchronized (m_eventHookLock)
             {
-                m_listenerHooks.add(svcObj);
+                m_eventHooks = addToArray(m_eventHooks, svcObj);
             }
         }
+
+        if (isHook(classNames, FindHook.class, svcObj))
+        {
+            synchronized (m_findHookLock)
+            {
+                m_findHooks = addToArray(m_findHooks, svcObj);
+            }
+        }
+
+        if (isHook(classNames, ListenerHook.class, svcObj))
+        {
+            synchronized (m_listenerHookLock)
+            {
+                m_listenerHooks = addToArray(m_listenerHooks, svcObj);
+            }
+        }
+    }
+
+    private static Object[] addToArray(Object[] src, Object svcObj)
+    {
+        Object[] dst = new Object[src.length + 1];
+        System.arraycopy(src, 0, dst, 0, src.length);
+        dst[src.length] = svcObj;
+        return dst;
     }
 
     boolean isHook(String[] classNames, Class hookClass, Object svcObj)
@@ -755,21 +784,87 @@
 
     private void removeHook(Object svcObj)
     {
+        if (svcObj instanceof EventHook)
+        {
+            synchronized (m_eventHookLock)
+            {
+                m_eventHooks = removeFromArray(m_eventHooks, svcObj);
+            }
+        }
+
+        if (svcObj instanceof FindHook)
+        {
+            synchronized (m_findHookLock)
+            {
+                m_findHooks = removeFromArray(m_findHooks, svcObj);
+            }
+        }
+
         if (svcObj instanceof ListenerHook)
         {
-            synchronized (m_listenerHooks)
+            synchronized (m_listenerHookLock)
             {
-                m_listenerHooks.remove(svcObj);
+                m_listenerHooks = removeFromArray(m_listenerHooks, svcObj);
             }
         }
     }
 
-    ListenerHook[] getListenerHooks()
+    private static Object[] removeFromArray(Object[] src, Object svcObj)
+    {
+        Object[] dst = src;
+
+        int idx = -1;
+        for (int i = 0; i < src.length; i++)
+        {
+            if (src[i].equals(svcObj))
+            {
+                idx = i;
+                break;
+            }
+        }
+
+        if (idx >= 0)
+        {
+            if (src.length == 1)
+            {
+                dst = new Object[0];
+            }
+            else
+            {
+                dst = new Object[src.length - 1];
+                System.arraycopy(dst, 0, src, 0, idx);
+                if (idx < dst.length)
+                {
+                    System.arraycopy(
+                        src, idx + 1, dst, idx, dst.length - idx);
+                }
+            }
+        }
+
+        return dst;
+    }
+
+    public List getEventHooks()
+    {
+        synchronized (m_eventHooks)
+        {
+            return Collections.unmodifiableList(Arrays.asList(m_eventHooks));
+        }
+    }
+
+    List getFindHooks()
+    {
+        synchronized (m_findHooks)
+        {
+            return Collections.unmodifiableList(Arrays.asList(m_findHooks));
+        }
+    }
+
+    List getListenerHooks()
     {
         synchronized (m_listenerHooks)
         {
-            return (ListenerHook[])
-                m_listenerHooks.toArray(new ListenerHook[m_listenerHooks.size()]);
+            return Collections.unmodifiableList(Arrays.asList(m_listenerHooks));
         }
     }
 
diff --git a/framework/src/main/java/org/apache/felix/framework/util/EventDispatcher.java b/framework/src/main/java/org/apache/felix/framework/util/EventDispatcher.java
index 5213202..96b7e97 100644
--- a/framework/src/main/java/org/apache/felix/framework/util/EventDispatcher.java
+++ b/framework/src/main/java/org/apache/felix/framework/util/EventDispatcher.java
@@ -24,11 +24,15 @@
 import java.util.Collection;
 import java.util.EventListener;
 import java.util.EventObject;
-
+import java.util.Iterator;
 import java.util.List;
+import java.util.NoSuchElementException;
+
 import org.apache.felix.framework.Logger;
+import org.apache.felix.framework.ServiceRegistry;
 import org.osgi.framework.AllServiceListener;
 import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleEvent;
 import org.osgi.framework.BundleListener;
 import org.osgi.framework.Constants;
@@ -41,6 +45,7 @@
 import org.osgi.framework.ServiceReference;
 import org.osgi.framework.SynchronousBundleListener;
 import org.osgi.framework.hooks.service.ListenerHook;
+import org.osgi.framework.hooks.service.EventHook;
 
 public class EventDispatcher
 {
@@ -51,8 +56,8 @@
     private static final int LISTENER_SECURITY_OFFSET = 4;
     private static final int LISTENER_ARRAY_INCREMENT = 5;
 
-    // Framework instance for this dispatcher.
     private Logger m_logger = null;
+    private volatile ServiceRegistry m_serviceRegistry = null;
 
     // Representation of an empty listener list.
     private static final Object[] m_emptyList = new Object[0];
@@ -120,6 +125,11 @@
         return eventDispatcher;
     }
 
+    public void setServiceRegistry(ServiceRegistry sr)
+    {
+        m_serviceRegistry = sr;
+    }
+
     public static void shutdown()
     {
         synchronized (m_threadLock)
@@ -605,6 +615,22 @@
             listeners = m_serviceListeners;
         }
 
+        if (m_serviceRegistry != null)
+        {
+            List eventHooks = m_serviceRegistry.getEventHooks();
+            if ((eventHooks != null) && (eventHooks.size() > 0))
+            {
+                ListenerBundleContextCollectionWrapper wrapper =
+                    new ListenerBundleContextCollectionWrapper(listeners);
+                for (int i = 0; i < eventHooks.size(); i++)
+                {
+                    ((EventHook) eventHooks.get(i)).event(event, wrapper);
+                }
+
+                listeners = wrapper.getListeners();
+            }
+        }
+
         // Fire all service events immediately on the calling thread.
         fireEventImmediately(m_logger, Request.SERVICE_EVENT, listeners, event);
     }
@@ -885,6 +911,201 @@
         }
     }
 
+    static class ListenerBundleContextCollectionWrapper implements Collection
+    {
+        private Object[] m_listeners;
+
+        ListenerBundleContextCollectionWrapper(Object [] listeners)
+        {
+            m_listeners = listeners;
+        }
+
+        Object [] getListeners()
+        {
+            return m_listeners;
+        }
+
+        public boolean add(Object o)
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        public boolean addAll(Collection c)
+        {
+            throw new UnsupportedOperationException();
+        }
+
+        public void clear()
+        {
+            m_listeners = new Object[0];
+        }
+
+        public boolean contains(Object o)
+        {
+            return indexOf(o) >= 0;
+        }
+
+        public boolean containsAll(Collection c)
+        {
+            for (Iterator it = c.iterator(); it.hasNext(); )
+            {
+                if (!contains(it.next()))
+                {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        private int indexOf(Object o)
+        {
+            if (!(o instanceof BundleContext))
+            {
+                return -1;
+            }
+
+            for (int i = m_listeners.length - LISTENER_ARRAY_INCREMENT;
+                i >= 0;
+                i -= LISTENER_ARRAY_INCREMENT)
+            {
+                Bundle bundle = (Bundle) m_listeners[i + LISTENER_BUNDLE_OFFSET];
+                if (bundle != null)
+                {
+                    if (bundle.getBundleContext().equals(o))
+                    {
+                        return i;
+                    }
+                }
+            }
+            return -1;
+        }
+
+        public boolean isEmpty()
+        {
+            return m_listeners.length == 0;
+        }
+
+        public Iterator iterator()
+        {
+            return new WrapperIterator();
+        }
+
+        public boolean remove(Object o)
+        {
+            return removeIndex(indexOf(o));
+        }
+
+        private boolean removeIndex(int idx)
+        {
+            if (idx < 0)
+            {
+                return false;
+            }
+
+            Object [] newListeners = new Object[m_listeners.length - LISTENER_ARRAY_INCREMENT];
+            System.arraycopy(m_listeners, 0, newListeners, 0, idx);
+            System.arraycopy(m_listeners, idx + LISTENER_ARRAY_INCREMENT,
+                    newListeners, idx, newListeners.length - idx);
+            m_listeners = newListeners;
+
+            return true;
+        }
+
+        public boolean removeAll(Collection c)
+        {
+            boolean rv = false;
+
+            for (Iterator it = c.iterator(); it.hasNext(); )
+            {
+                if (remove(it.next()))
+                {
+                    rv = true;
+                }
+            }
+
+            return rv;
+        }
+
+        public boolean retainAll(Collection c)
+        {
+            boolean rv = false;
+
+            for (Iterator it = iterator(); it.hasNext(); )
+            {
+                if (!(c.contains(it.next())))
+                {
+                    it.remove();
+                    rv = true;
+                }
+            }
+
+            return rv;
+        }
+
+        public int size()
+        {
+            return m_listeners.length / LISTENER_ARRAY_INCREMENT;
+        }
+
+        public Object[] toArray()
+        {
+            Object [] array = new Object[size()];
+            int idx = 0;
+            for (Iterator it = iterator(); it.hasNext(); )
+            {
+                array[idx++] = it.next();
+            }
+            return array;
+        }
+
+        public Object[] toArray(Object[] a)
+        {
+            if (!(a.getClass().equals(Object[].class)))
+            {
+                throw new ArrayStoreException();
+            }
+            return toArray();
+        }
+
+        private class WrapperIterator implements Iterator
+        {
+            int curIdx = 0;
+            int lastIdx = -1;
+
+            private WrapperIterator() {}
+
+            public boolean hasNext()
+            {
+                return curIdx < m_listeners.length;
+            }
+
+            public Object next()
+            {
+                if (!hasNext())
+                {
+                    throw new NoSuchElementException();
+                }
+
+                Bundle b = (Bundle) m_listeners[curIdx + LISTENER_BUNDLE_OFFSET];
+                lastIdx = curIdx;
+                curIdx += LISTENER_ARRAY_INCREMENT;
+                return b.getBundleContext();
+            }
+
+            public void remove()
+            {
+                if (lastIdx < 0)
+                {
+                    throw new IllegalStateException();
+                }
+                removeIndex(lastIdx);
+
+                curIdx = lastIdx;
+                lastIdx = -1;
+            }
+        }
+    }
+
     private static class Request
     {
         public static final int FRAMEWORK_EVENT = 0;
diff --git a/framework/src/main/java/org/apache/felix/framework/util/ShrinkableCollection.java b/framework/src/main/java/org/apache/felix/framework/util/ShrinkableCollection.java
new file mode 100644
index 0000000..5719e17
--- /dev/null
+++ b/framework/src/main/java/org/apache/felix/framework/util/ShrinkableCollection.java
@@ -0,0 +1,110 @@
+/*
+ * 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.util;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+/** This collection wraps any other collection but prohibits calls to add
+ *  elements to the collection.
+ */
+public class ShrinkableCollection implements Collection
+{
+    private final Collection m_delegate;
+
+    public ShrinkableCollection(Collection delegate)
+    {
+        m_delegate = delegate;
+    }
+
+    public boolean add(Object o)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean addAll(Collection c)
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    public void clear()
+    {
+        m_delegate.clear();
+    }
+
+    public boolean contains(Object o)
+    {
+        return m_delegate.contains(o);
+    }
+
+    public boolean containsAll(Collection c)
+    {
+        return m_delegate.containsAll(c);
+    }
+
+    public boolean equals(Object o)
+    {
+        return m_delegate.equals(o);
+    }
+
+    public int hashCode()
+    {
+        return m_delegate.hashCode();
+    }
+
+    public boolean isEmpty()
+    {
+        return m_delegate.isEmpty();
+    }
+
+    public Iterator iterator()
+    {
+        return m_delegate.iterator();
+    }
+
+    public boolean remove(Object o)
+    {
+        return m_delegate.remove(o);
+    }
+
+    public boolean removeAll(Collection c)
+    {
+        return m_delegate.removeAll(c);
+    }
+
+    public boolean retainAll(Collection c)
+    {
+        return m_delegate.retainAll(c);
+    }
+
+    public int size()
+    {
+        return m_delegate.size();
+    }
+
+    public Object[] toArray()
+    {
+        return m_delegate.toArray();
+    }
+
+    public Object[] toArray(Object[] a)
+    {
+        return m_delegate.toArray(a);
+    }
+}
\ No newline at end of file
diff --git a/framework/src/main/java/org/osgi/framework/hooks/service/EventHook.java b/framework/src/main/java/org/osgi/framework/hooks/service/EventHook.java
new file mode 100644
index 0000000..7db29b3
--- /dev/null
+++ b/framework/src/main/java/org/osgi/framework/hooks/service/EventHook.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) OSGi Alliance (2000, 2009). All Rights Reserved.
+ * 
+ * Licensed 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.osgi.framework.hooks.service;
+
+import java.util.Collection;
+
+import org.osgi.framework.ServiceEvent;
+
+public interface EventHook {
+	void event(ServiceEvent event, Collection/* <? extends BundleContext> */contexts);
+}
diff --git a/framework/src/main/java/org/osgi/framework/hooks/service/FindHook.java b/framework/src/main/java/org/osgi/framework/hooks/service/FindHook.java
new file mode 100644
index 0000000..2d3af3b
--- /dev/null
+++ b/framework/src/main/java/org/osgi/framework/hooks/service/FindHook.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) OSGi Alliance (2000, 2009). All Rights Reserved.
+ * 
+ * Licensed 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.osgi.framework.hooks.service;
+
+import java.util.Collection;
+
+import org.osgi.framework.BundleContext;
+
+public interface FindHook {
+    void find(BundleContext context, 
+              String name, 
+              String filter, 
+              boolean allServices, 
+              Collection /* <? extends ServiceReference> */ references);
+}
diff --git a/framework/src/test/java/org/apache/felix/framework/util/EventDispatcherListenerWrapperTest.java b/framework/src/test/java/org/apache/felix/framework/util/EventDispatcherListenerWrapperTest.java
new file mode 100644
index 0000000..741b3cb
--- /dev/null
+++ b/framework/src/test/java/org/apache/felix/framework/util/EventDispatcherListenerWrapperTest.java
@@ -0,0 +1,396 @@
+/*
+ * 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.util;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import junit.framework.TestCase;
+
+import org.easymock.MockControl;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+
+
+public class EventDispatcherListenerWrapperTest extends TestCase {
+    public void testRemove() {
+        Bundle b1 = getMockBundle();
+        BundleContext bc1 = b1.getBundleContext();
+        Bundle b2 = getMockBundle();
+        BundleContext bc2 = b2.getBundleContext();
+        Bundle b3 = getMockBundle();
+        BundleContext bc3 = b3.getBundleContext();
+        Bundle b4 = getMockBundle();
+        BundleContext bc4 = b4.getBundleContext();
+        
+        Object [] listeners = new Object[] {
+                b1,
+                String.class,
+                new Object(),
+                "(some=filter)",
+                null,
+                
+                b2,
+                Integer.class,
+                new Object(),
+                "(some.other=filter)",
+                new Integer(15),
+                
+                b3,
+                BundleContext.class,
+                new Object(),
+                null,
+                Boolean.TRUE,               
+        };
+        
+        Collection c = new EventDispatcher.ListenerBundleContextCollectionWrapper(listeners);
+        assertEquals(3, c.size());
+        assertFalse(c.isEmpty());
+        assertTrue(c.contains(bc1));
+        assertTrue(c.contains(bc2));
+        assertTrue(c.contains(bc3));
+        assertFalse(c.contains(bc4));
+        
+        assertTrue(c.remove(bc2));
+        assertEquals(2, c.size());
+        assertTrue(c.contains(bc1));
+        assertFalse(c.contains(bc2));
+        assertTrue(c.contains(bc3));
+        assertFalse(c.contains(bc4));
+        assertFalse("Already removed", c.remove(bc2));        
+
+        Object [] actualListeners = 
+            ((EventDispatcher.ListenerBundleContextCollectionWrapper) c).getListeners();
+        Object [] expectedListeners = new Object[10];
+        System.arraycopy(listeners, 0, expectedListeners, 0, 5);
+        System.arraycopy(listeners, 10, expectedListeners, 5, 5);
+        assertTrue(Arrays.equals(expectedListeners, actualListeners));
+
+        assertTrue(c.remove(bc1));
+        assertEquals(1, c.size());
+        assertFalse(c.contains(bc1));
+        assertFalse(c.contains(bc2));
+        assertTrue(c.contains(bc3));
+        assertFalse(c.contains(bc4));
+        assertFalse(c.isEmpty());
+
+        Object [] actualListeners2 = 
+            ((EventDispatcher.ListenerBundleContextCollectionWrapper) c).getListeners();
+        Object [] expectedListeners2 = new Object[5];
+        System.arraycopy(listeners, 10, expectedListeners2, 0, 5);
+        assertTrue(Arrays.equals(expectedListeners2, actualListeners2));
+
+        assertTrue(c.remove(bc3));
+        assertEquals(0, c.size());
+        assertFalse(c.contains(bc1));
+        assertFalse(c.contains(bc2));
+        assertFalse(c.contains(bc3));
+        assertFalse(c.contains(bc4));
+        assertTrue(c.isEmpty());
+
+        Object [] actualListeners3 = 
+            ((EventDispatcher.ListenerBundleContextCollectionWrapper) c).getListeners();
+        assertEquals(0, actualListeners3.length);
+    }    
+    
+    public void testIterator() {
+        Bundle b1 = getMockBundle();
+        BundleContext bc1 = b1.getBundleContext();
+        Bundle b2 = getMockBundle();
+        BundleContext bc2 = b2.getBundleContext();
+        
+        Object [] listeners = new Object[] {
+                b1,
+                String.class,
+                new Object(),
+                "(some=filter)",
+                null,
+                
+                b2,
+                Integer.class,
+                new Object(),
+                "(some.other=filter)",
+                new Integer(15)                
+        };
+
+        Collection c = new EventDispatcher.ListenerBundleContextCollectionWrapper(listeners);
+        Iterator it = c.iterator();
+        
+        assertEquals(2, c.size());
+        assertTrue(it.hasNext());
+        try {
+            it.remove();
+            fail("Should throw an exception");
+        } catch (IllegalStateException ise) {
+            // good
+        }        
+        assertSame(bc1, it.next());
+        it.remove();
+        assertEquals(1, c.size());
+        
+        // Create another iterator and make sure it sees the removal of it
+        Iterator it2 = c.iterator();
+        assertTrue(it2.hasNext());
+        assertSame(bc2, it2.next());
+        assertFalse(it2.hasNext());
+        
+        // back to the origial iterator
+        
+        try {
+            it.remove();
+            fail("Should throw an exception");
+        } catch (IllegalStateException ise) {
+            // good
+        }        
+        assertTrue(it.hasNext());
+        try {
+            it.remove();
+            fail("Should throw an exception");
+        } catch (IllegalStateException ise) {
+            // good
+        }        
+        assertSame(bc2, it.next());
+        it.remove();
+        assertEquals(0, c.size());
+        
+        assertFalse(it.hasNext());
+        try {
+            it.next();
+            fail("Should throw an exception");
+        } catch (NoSuchElementException nse) {
+            // good
+        }        
+        try {
+            it.remove();
+            fail("Should throw an exception");
+        } catch (IllegalStateException ise) {
+            // good
+        }                
+    }
+    
+    public void testAdd() {
+        Bundle b1 = getMockBundle();
+        BundleContext bc1 = b1.getBundleContext();
+        
+        Object [] listeners = new Object[] {
+                b1,
+                String.class,
+                new Object(),
+                "(some=filter)",
+                null,
+        };
+
+        Collection c = new EventDispatcher.ListenerBundleContextCollectionWrapper(listeners);
+        try {
+            c.add(new Object());
+            fail("Should not have been able to add to the collection");
+        } catch (UnsupportedOperationException uoe) {
+            // good
+        }
+    }
+
+    public void testAddAll() {       
+        Bundle b1 = getMockBundle();
+        BundleContext bc1 = b1.getBundleContext();
+        Object [] listeners = {};
+
+        Collection c = new EventDispatcher.ListenerBundleContextCollectionWrapper(listeners);
+        try {
+            c.addAll(Collections.singleton(bc1));
+            fail("Should not have been able to add to the collection");
+        } catch (UnsupportedOperationException uoe) {
+            // good
+        }
+    }
+    
+    public void testContainsAll() {
+        Bundle b1 = getMockBundle();
+        BundleContext bc1 = b1.getBundleContext();
+        Bundle b2 = getMockBundle();
+        BundleContext bc2 = b2.getBundleContext();
+        Bundle b3 = getMockBundle();
+        BundleContext bc3 = b3.getBundleContext();
+        
+        Object [] listeners = new Object[] {
+                b1,
+                String.class,
+                new Object(),
+                "(some=filter)",
+                null,
+                
+                b2,
+                Integer.class,
+                new Object(),
+                "(some.other=filter)",
+                new Integer(15),
+                
+                b3,
+                BundleContext.class,
+                new Object(),
+                null,
+                Boolean.TRUE,               
+        };
+        
+        Collection c = new EventDispatcher.ListenerBundleContextCollectionWrapper(listeners);
+        
+        assertTrue(c.containsAll(Collections.emptySet()));
+        assertTrue(c.containsAll(Collections.singleton(bc2)));
+        assertTrue(c.containsAll(Arrays.asList(new Object [] {bc2, bc1})));
+        assertTrue(c.containsAll(Arrays.asList(new Object [] {bc3, bc2, bc1})));
+        assertFalse(c.containsAll(Arrays.asList(new Object [] {bc3, bc2, bc1, new Object()})));
+        
+        assertEquals(3, c.size());
+        c.clear();
+        assertEquals(0, c.size());
+    }
+
+    public void testRemoveAll() {
+        Bundle b1 = getMockBundle();
+        BundleContext bc1 = b1.getBundleContext();
+        Bundle b2 = getMockBundle();
+        BundleContext bc2 = b2.getBundleContext();
+        Bundle b3 = getMockBundle();
+        BundleContext bc3 = b3.getBundleContext();
+        
+        Object [] listeners = new Object[] {
+                b1,
+                String.class,
+                new Object(),
+                "(some=filter)",
+                null,
+                
+                b2,
+                Integer.class,
+                new Object(),
+                "(some.other=filter)",
+                new Integer(15),
+                
+                b3,
+                BundleContext.class,
+                new Object(),
+                null,
+                Boolean.TRUE,               
+        };
+        
+        Collection c = new EventDispatcher.ListenerBundleContextCollectionWrapper(listeners);
+        assertFalse(c.removeAll(Collections.emptyList()));
+        assertFalse(c.removeAll(Collections.singleton(new Object())));
+        assertTrue(c.contains(bc2));
+        assertTrue(c.removeAll(Arrays.asList(new Object [] {new Object(), bc2})));
+        assertFalse(c.contains(bc2));
+        
+        assertEquals(2, c.size());
+        assertTrue(c.removeAll(Arrays.asList(new Object [] {bc1, bc3})));
+        assertEquals(0, c.size());
+    }
+    
+    public void testRetainAll() {
+        Bundle b1 = getMockBundle();
+        BundleContext bc1 = b1.getBundleContext();
+        Bundle b2 = getMockBundle();
+        BundleContext bc2 = b2.getBundleContext();
+        Bundle b3 = getMockBundle();
+        BundleContext bc3 = b3.getBundleContext();
+        
+        Object [] listeners = new Object[] {
+                b1,
+                String.class,
+                new Object(),
+                "(some=filter)",
+                null,
+                
+                b2,
+                Integer.class,
+                new Object(),
+                "(some.other=filter)",
+                new Integer(15),
+                
+                b3,
+                BundleContext.class,
+                new Object(),
+                null,
+                Boolean.TRUE,               
+        };
+        
+        Collection c = new EventDispatcher.ListenerBundleContextCollectionWrapper(listeners);
+        assertFalse(c.retainAll(Arrays.asList(new Object [] {bc3, bc1, bc2})));
+        assertTrue(Arrays.equals(new Object [] {bc1, bc2, bc3}, c.toArray()));
+        
+        assertTrue(c.retainAll(Arrays.asList(new Object [] {bc1, bc2, new Object()})));
+        assertTrue(Arrays.equals(new Object [] {bc1, bc2}, c.toArray()));
+        
+        assertTrue(c.retainAll(Collections.emptyList()));
+        assertEquals(0, c.size());
+    }
+    
+    public void testToArray() {
+        Bundle b1 = getMockBundle();
+        BundleContext bc1 = b1.getBundleContext();
+        Bundle b2 = getMockBundle();
+        BundleContext bc2 = b2.getBundleContext();        
+        
+        Object [] listeners = new Object[] {
+                b1,
+                String.class,
+                new Object(),
+                "(some=filter)",
+                null,
+                
+                b2,
+                Integer.class,
+                new Object(),
+                "(some.other=filter)",
+                new Integer(15),                
+        };
+        
+        Collection c = new EventDispatcher.ListenerBundleContextCollectionWrapper(listeners);
+        assertTrue(Arrays.equals(new Object [] {bc1, bc2}, c.toArray()));
+        assertTrue(Arrays.equals(new Object [] {bc1, bc2}, c.toArray(new Object [] {})));
+        
+        try {
+            c.toArray(new String [] {});
+            fail("Should not be allowed");
+        } catch (ArrayStoreException ase) {
+            // good
+        }
+    }
+    
+    private Bundle getMockBundle() {
+        MockControl bcControl = MockControl.createNiceControl(BundleContext.class);
+        BundleContext bc = (BundleContext) bcControl.getMock();
+        
+        MockControl bControl = MockControl.createNiceControl(Bundle.class);
+        Bundle b = (Bundle) bControl.getMock();
+        b.getBundleContext();
+        bControl.setReturnValue(bc, MockControl.ZERO_OR_MORE);
+        b.getState();
+        bControl.setReturnValue(Bundle.ACTIVE, MockControl.ZERO_OR_MORE);
+        
+        bc.getBundle();
+        bcControl.setReturnValue(b, MockControl.ZERO_OR_MORE);                
+
+        bcControl.replay();
+        bControl.replay();
+        
+        return b;
+    }
+}
diff --git a/framework/src/test/java/org/apache/felix/framework/util/EventDispatcherTest.java b/framework/src/test/java/org/apache/felix/framework/util/EventDispatcherTest.java
new file mode 100644
index 0000000..de1f4e5
--- /dev/null
+++ b/framework/src/test/java/org/apache/felix/framework/util/EventDispatcherTest.java
@@ -0,0 +1,178 @@
+/*
+ * 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.util;
+
+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.List;
+import java.util.Set;
+
+import junit.framework.TestCase;
+
+import org.apache.felix.framework.Logger;
+import org.apache.felix.framework.ServiceRegistry;
+import org.easymock.MockControl;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceEvent;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.hooks.service.EventHook;
+
+public class EventDispatcherTest extends TestCase
+{
+    public void testFireServiceEvent()
+    {
+        final Bundle b1 = getMockBundle();
+        final Bundle b2 = getMockBundle();
+        final Bundle b3 = getMockBundle();
+
+        final Set calledHooks = new HashSet();
+        final EventHook ph1 = new EventHook()
+        {
+            public void event(ServiceEvent event, Collection contexts)
+            {
+                calledHooks.add(this);
+            }
+        };
+        final EventHook ph2 = new EventHook()
+        {
+            public void event(ServiceEvent event, Collection contexts)
+            {
+                calledHooks.add(this);
+                for (Iterator it = contexts.iterator(); it.hasNext();)
+                {
+                    BundleContext bc = (BundleContext) it.next();
+                    if (bc.getBundle() == b1)
+                    {
+                        it.remove();
+                    }
+                    if (bc.getBundle() == b2)
+                    {
+                        it.remove();
+                    }
+                }
+            }
+        };
+
+        Logger logger = new Logger();
+        ServiceRegistry registry = new ServiceRegistry(logger)
+        {
+            public List getEventHooks()
+            {
+                return Collections.unmodifiableList(
+                    Arrays.asList(new EventHook[]
+                        {
+                            ph1, ph2
+                        }));
+            }
+        };
+
+        // -- Set up event dispatcher
+
+        EventDispatcher ed = EventDispatcher.start(logger);
+        EventDispatcher.shutdown(); // stop the thread - would be nicer if we could create one without a thread for testing
+        ed.setServiceRegistry(registry);
+
+        // -- Register some listeners
+        final List fired = Collections.synchronizedList(new ArrayList());
+        ServiceListener sl1 = new ServiceListener()
+        {
+            public void serviceChanged(ServiceEvent arg0)
+            {
+                fired.add(this);
+                System.out.println("*** sl1");
+            }
+        };
+        ed.addListener(b1, ServiceListener.class, sl1, null);
+
+        ServiceListener sl2 = new ServiceListener()
+        {
+            public void serviceChanged(ServiceEvent arg0)
+            {
+                fired.add(this);
+                System.out.println("*** sl2");
+            }
+        };
+        ed.addListener(b2, ServiceListener.class, sl2, null);
+
+        ServiceListener sl3 = new ServiceListener()
+        {
+            public void serviceChanged(ServiceEvent arg0)
+            {
+                fired.add(this);
+                System.out.println("*** sl3");
+            }
+        };
+        ed.addListener(b3, ServiceListener.class, sl3, null);
+
+        // --- make the invocation
+        MockControl control = MockControl.createNiceControl(ServiceReference.class);
+        ServiceReference sr = (ServiceReference) control.getMock();
+        sr.getProperty(Constants.OBJECTCLASS);
+        control.setReturnValue(new String[]
+            {
+                "java.lang.String"
+            }, MockControl.ZERO_OR_MORE);
+        sr.isAssignableTo(b1, String.class.getName());
+        control.setReturnValue(true, MockControl.ZERO_OR_MORE);
+        sr.isAssignableTo(b2, String.class.getName());
+        control.setReturnValue(true, MockControl.ZERO_OR_MORE);
+        sr.isAssignableTo(b3, String.class.getName());
+        control.setReturnValue(true, MockControl.ZERO_OR_MORE);
+        control.replay();
+
+        ServiceEvent event = new ServiceEvent(ServiceEvent.REGISTERED, sr);
+
+        assertEquals("Precondition failed", 0, fired.size());
+        ed.fireServiceEvent(event);
+        assertEquals(1, fired.size());
+        assertSame(sl3, fired.iterator().next());
+
+        assertEquals(2, calledHooks.size());
+        assertTrue(calledHooks.contains(ph1));
+        assertTrue(calledHooks.contains(ph2));
+    }
+
+    private Bundle getMockBundle()
+    {
+        MockControl bcControl = MockControl.createNiceControl(BundleContext.class);
+        BundleContext bc = (BundleContext) bcControl.getMock();
+
+        MockControl bControl = MockControl.createNiceControl(Bundle.class);
+        Bundle b = (Bundle) bControl.getMock();
+        b.getBundleContext();
+        bControl.setReturnValue(bc, MockControl.ZERO_OR_MORE);
+        b.getState();
+        bControl.setReturnValue(Bundle.ACTIVE, MockControl.ZERO_OR_MORE);
+
+        bc.getBundle();
+        bcControl.setReturnValue(b, MockControl.ZERO_OR_MORE);
+
+        bcControl.replay();
+        bControl.replay();
+
+        return b;
+    }
+}
\ No newline at end of file