Reintegrate R5 sanbox branch:
FELIX-3480 Implement support for SynchronousConfigurationListener
FELIX-3479 implement and test Configuration.getChangeCount
FELIX-3483 Export cm API at 1.5


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1348823 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/configadmin/pom.xml b/configadmin/pom.xml
index 7d236d1..3c45b19 100644
--- a/configadmin/pom.xml
+++ b/configadmin/pom.xml
@@ -32,7 +32,7 @@
 
     <name>Apache Felix Configuration Admin Service</name>
     <description>
-        Implementation of the OSGi Configuration Admin Service Specification 1.4
+        Implementation of the OSGi Configuration Admin Service Specification 1.5
     </description>
 
     <scm>
@@ -74,8 +74,11 @@
         <bundle.file.name>
             ${bundle.build.name}/${project.build.finalName}.jar
         </bundle.file.name>
-    </properties>
 
+        <felix.build.source>5</felix.build.source>
+        <felix.build.target>5</felix.build.target>
+        <felix.java.signature.artifactId>java15</felix.java.signature.artifactId>
+    </properties>
 
     <dependencies>
         <dependency>
@@ -156,19 +159,23 @@
                             <!-- just list, version from package-info classes -->
                             org.apache.felix.cm;
                             org.apache.felix.cm.file,
-                            org.osgi.service.cm;provide:=true
+                            org.osgi.service.cm;provide:=true;version=1.5;-split-package:=merge-first
                         </Export-Package>
                         <Private-Package>
                             org.apache.felix.cm.impl,
                             org.osgi.util.tracker
                         </Private-Package>
+                        <Import-Package>
+                            org.osgi.service.cm;version="[1.5,1.6)",
+                            *
+                        </Import-Package>
                         <DynamicImport-Package>
                             <!-- overwrite version from compendium bundle -->
                             org.osgi.service.log;version="1.3"
                         </DynamicImport-Package>
 						<Export-Service>
 							org.osgi.service.cm.ConfigurationAdmin;
-								service.description="Configuration Admin Service Specification 1.4 Implementation";
+								service.description="Configuration Admin Service Specification 1.5 Implementation";
 								service.pid="org.osgi.service.cm.ConfigurationAdmin";
 								service.vendor="Apache Software Foundation",
 							org.apache.felix.cm.PersistenceManager;
diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdapter.java b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdapter.java
index 2a3d2c3..f1e95bc 100644
--- a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdapter.java
+++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationAdapter.java
@@ -132,9 +132,6 @@
     }
 
 
-    /**
-     * @see org.apache.felix.cm.impl.ConfigurationImpl#getProperties()
-     */
     public Dictionary getProperties()
     {
         delegatee.getConfigurationManager().log( LogService.LOG_DEBUG, "getProperties()", ( Throwable ) null );
@@ -147,6 +144,16 @@
     }
 
 
+    public long getChangeCount()
+    {
+        delegatee.getConfigurationManager().log( LogService.LOG_DEBUG, "getChangeCount()", ( Throwable ) null );
+
+        checkDeleted();
+
+        return delegatee.getRevision();
+    }
+
+
     /**
      * @throws IOException
      * @see org.apache.felix.cm.impl.ConfigurationImpl#delete()
diff --git a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java
index 27c0f7c..bd9d01d 100644
--- a/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java
+++ b/configadmin/src/main/java/org/apache/felix/cm/impl/ConfigurationManager.java
@@ -635,6 +635,7 @@
     void fireConfigurationEvent( int type, String pid, String factoryPid )
     {
         FireConfigurationEvent event = new FireConfigurationEvent( type, pid, factoryPid );
+        event.fireSynchronousEvents();
         if ( event.hasConfigurationEventListeners() )
         {
             eventThread.schedule( event );
@@ -718,7 +719,38 @@
     private ServiceReference getServiceReference()
     {
         ServiceRegistration reg = configurationAdminRegistration;
-        return ( reg != null ) ? reg.getReference() : null;
+        if (reg != null) {
+            return reg.getReference();
+        }
+
+        // probably called for firing an event during service registration
+        // since we didn't get the service registration yet we use the
+        // service registry to get our service reference
+        BundleContext context = bundleContext;
+        if ( context != null )
+        {
+            try
+            {
+                ServiceReference[] refs = context.getServiceReferences( ConfigurationAdmin.class.getName(), null );
+                if ( refs != null )
+                {
+                    for ( int i = 0; i < refs.length; i++ )
+                    {
+                        if ( refs[i].getBundle().getBundleId() == context.getBundle().getBundleId() )
+                        {
+                            return refs[i];
+                        }
+                    }
+                }
+            }
+            catch ( InvalidSyntaxException e )
+            {
+                // unexpected since there is no filter
+            }
+        }
+
+        // service references
+        return null;
     }
 
 
@@ -1909,6 +1941,7 @@
 
         private final Bundle[] listenerProvider;
 
+        private ConfigurationEvent event;
 
         private FireConfigurationEvent( final int type, final String pid, final String factoryPid)
         {
@@ -1937,6 +1970,21 @@
         }
 
 
+        void fireSynchronousEvents()
+        {
+            if ( hasConfigurationEventListeners() && getServiceReference() != null )
+            {
+                for ( int i = 0; i < this.listeners.length; i++ )
+                {
+                    if ( this.listeners[i] instanceof SynchronousConfigurationListener )
+                    {
+                        sendEvent( i );
+                    }
+                }
+            }
+        }
+
+
         boolean hasConfigurationEventListeners()
         {
             return this.listenerReferences != null;
@@ -1961,34 +2009,51 @@
 
         public void run()
         {
-            final String typeName = getTypeName();
-            final ConfigurationEvent event = new ConfigurationEvent( getServiceReference(), type, factoryPid, pid );
-
             for ( int i = 0; i < listeners.length; i++ )
             {
-                if ( listenerProvider[i].getState() == Bundle.ACTIVE )
-                {
-                    log( LogService.LOG_DEBUG, "Sending {0} event for {1} to {2}", new Object[]
-                        { typeName, pid, ConfigurationManager.toString( listenerReferences[i] ) } );
-
-                    try
-                    {
-                        listeners[i].configurationEvent( event );
-                    }
-                    catch ( Throwable t )
-                    {
-                        log( LogService.LOG_ERROR, "Unexpected problem delivering configuration event to {0}",
-                            new Object[]
-                                { ConfigurationManager.toString( listenerReferences[i] ), t } );
-                    }
-                }
+                sendEvent( i );
             }
         }
 
+
         public String toString()
         {
             return "Fire ConfigurationEvent: pid=" + pid;
         }
+
+
+        private ConfigurationEvent getConfigurationEvent()
+        {
+            if ( event == null )
+            {
+                this.event = new ConfigurationEvent( getServiceReference(), type, factoryPid, pid );
+            }
+            return event;
+        }
+
+
+        private void sendEvent( final int serviceIndex )
+        {
+            if ( listenerProvider[serviceIndex].getState() == Bundle.ACTIVE && this.listeners[serviceIndex] != null )
+            {
+                log( LogService.LOG_DEBUG, "Sending {0} event for {1} to {2}", new Object[]
+                    { getTypeName(), pid, ConfigurationManager.toString( listenerReferences[serviceIndex] ) } );
+
+                try
+                {
+                    listeners[serviceIndex].configurationEvent( getConfigurationEvent() );
+                }
+                catch ( Throwable t )
+                {
+                    log( LogService.LOG_ERROR, "Unexpected problem delivering configuration event to {0}", new Object[]
+                        { ConfigurationManager.toString( listenerReferences[serviceIndex] ), t } );
+                }
+                finally
+                {
+                    this.listeners[serviceIndex] = null;
+                }
+            }
+        }
     }
 
     private static class ManagedServiceTracker extends ServiceTracker
diff --git a/configadmin/src/main/java/org/osgi/service/cm/Configuration.java b/configadmin/src/main/java/org/osgi/service/cm/Configuration.java
new file mode 100644
index 0000000..20136e2
--- /dev/null
+++ b/configadmin/src/main/java/org/osgi/service/cm/Configuration.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (c) OSGi Alliance (2001, 2012). 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.service.cm;
+
+import java.io.IOException;
+import java.util.Dictionary;
+import org.osgi.framework.Filter;
+
+/**
+ * The configuration information for a {@code ManagedService} or
+ * {@code ManagedServiceFactory} object.
+ * 
+ * The Configuration Admin service uses this interface to represent the
+ * configuration information for a {@code ManagedService} or for a service
+ * instance of a {@code ManagedServiceFactory}.
+ * 
+ * <p>
+ * A {@code Configuration} object contains a configuration dictionary and allows
+ * the properties to be updated via this object. Bundles wishing to receive
+ * configuration dictionaries do not need to use this class - they register a
+ * {@code ManagedService} or {@code ManagedServiceFactory}. Only administrative
+ * bundles, and bundles wishing to update their own configurations need to use
+ * this class.
+ * 
+ * <p>
+ * The properties handled in this configuration have case insensitive
+ * {@code String} objects as keys. However, case must be preserved from the last
+ * set key/value.
+ * <p>
+ * A configuration can be <i>bound</i> to a specific bundle or to a region of
+ * bundles using the <em>location</em>. In its simplest form the location is the
+ * location of the target bundle that registered a Managed Service or a Managed
+ * Service Factory. However, if the location starts with {@code ?} then the
+ * location indicates multiple delivery. In such a case the configuration must
+ * be delivered to all targets.
+ * 
+ * If security is on, the Configuration Permission can be used to restrict the
+ * targets that receive updates. The Configuration Admin must only update a
+ * target when the configuration location matches the location of the target's
+ * bundle or the target bundle has a Configuration Permission with the action
+ * {@link ConfigurationPermission#TARGET} and a name that matches the
+ * configuration location. The name in the permission may contain wildcards (
+ * {@code '*'}) to match the location using the same substring matching rules as
+ * {@link Filter}.
+ * 
+ * Bundles can always create, manipulate, and be updated from configurations
+ * that have a location that matches their bundle location.
+ * 
+ * <p>
+ * If a configuration's location is {@code null}, it is not yet bound to a
+ * location. It will become bound to the location of the first bundle that
+ * registers a {@code ManagedService} or {@code ManagedServiceFactory} object
+ * with the corresponding PID.
+ * <p>
+ * The same {@code Configuration} object is used for configuring both a Managed
+ * Service Factory and a Managed Service. When it is important to differentiate
+ * between these two the term "factory configuration" is used.
+ * 
+ * @noimplement
+ * @version $Id: 4e016c45b463ae5c7b665ca53931441141088860 $
+ */
+public interface Configuration {
+	/**
+	 * Get the PID for this {@code Configuration} object.
+	 * 
+	 * @return the PID for this {@code Configuration} object.
+	 * @throws IllegalStateException if this configuration has been deleted
+	 */
+	public String getPid();
+
+	/**
+	 * Return the properties of this {@code Configuration} object.
+	 * 
+	 * The {@code Dictionary} object returned is a private copy for the caller
+	 * and may be changed without influencing the stored configuration. The keys
+	 * in the returned dictionary are case insensitive and are always of type
+	 * {@code String}.
+	 * 
+	 * <p>
+	 * If called just after the configuration is created and before update has
+	 * been called, this method returns {@code null}.
+	 * 
+	 * @return A private copy of the properties for the caller or {@code null}.
+	 *         These properties must not contain the "service.bundleLocation"
+	 *         property. The value of this property may be obtained from the
+	 *         {@link #getBundleLocation()} method.
+	 * @throws IllegalStateException If this configuration has been deleted.
+	 */
+	public Dictionary<String, Object> getProperties();
+
+	/**
+	 * Update the properties of this {@code Configuration} object.
+	 * 
+	 * Stores the properties in persistent storage after adding or overwriting
+	 * the following properties:
+	 * <ul>
+	 * <li>"service.pid" : is set to be the PID of this configuration.</li>
+	 * <li>"service.factoryPid" : if this is a factory configuration it is set
+	 * to the factory PID else it is not set.</li>
+	 * </ul>
+	 * These system properties are all of type {@code String}.
+	 * 
+	 * <p>
+	 * If the corresponding Managed Service/Managed Service Factory is
+	 * registered, its updated method must be called asynchronously. Else, this
+	 * callback is delayed until aforementioned registration occurs.
+	 * 
+	 * <p>
+	 * Also initiates an asynchronous call to all {@link ConfigurationListener}s
+	 * with a {@link ConfigurationEvent#CM_UPDATED} event.
+	 * 
+	 * @param properties the new set of properties for this configuration
+	 * @throws IOException if update cannot be made persistent
+	 * @throws IllegalArgumentException if the {@code Dictionary} object
+	 *         contains invalid configuration types or contains case variants of
+	 *         the same key name.
+	 * @throws IllegalStateException If this configuration has been deleted.
+	 */
+	public void update(Dictionary<String, ?> properties) throws IOException;
+
+	/**
+	 * Delete this {@code Configuration} object.
+	 * 
+	 * Removes this configuration object from the persistent store. Notify
+	 * asynchronously the corresponding Managed Service or Managed Service
+	 * Factory. A {@link ManagedService} object is notified by a call to its
+	 * {@code updated} method with a {@code null} properties argument. A
+	 * {@link ManagedServiceFactory} object is notified by a call to its
+	 * {@code deleted} method.
+	 * 
+	 * <p>
+	 * Also initiates an asynchronous call to all {@link ConfigurationListener}s
+	 * with a {@link ConfigurationEvent#CM_DELETED} event.
+	 * 
+	 * @throws IOException If delete fails.
+	 * @throws IllegalStateException If this configuration has been deleted.
+	 */
+	public void delete() throws IOException;
+
+	/**
+	 * For a factory configuration return the PID of the corresponding Managed
+	 * Service Factory, else return {@code null}.
+	 * 
+	 * @return factory PID or {@code null}
+	 * @throws IllegalStateException If this configuration has been deleted.
+	 */
+	public String getFactoryPid();
+
+	/**
+	 * Update the {@code Configuration} object with the current properties.
+	 * 
+	 * Initiate the {@code updated} callback to the Managed Service or Managed
+	 * Service Factory with the current properties asynchronously.
+	 * 
+	 * <p>
+	 * This is the only way for a bundle that uses a Configuration Plugin
+	 * service to initiate a callback. For example, when that bundle detects a
+	 * change that requires an update of the Managed Service or Managed Service
+	 * Factory via its {@code ConfigurationPlugin} object.
+	 * 
+	 * @see ConfigurationPlugin
+	 * @throws IOException if update cannot access the properties in persistent
+	 *         storage
+	 * @throws IllegalStateException If this configuration has been deleted.
+	 */
+	public void update() throws IOException;
+
+	/**
+	 * Bind this {@code Configuration} object to the specified location.
+	 * 
+	 * If the location parameter is {@code null} then the {@code Configuration}
+	 * object will not be bound to a location/region. It will be set to the
+	 * bundle's location before the first time a Managed Service/Managed Service
+	 * Factory receives this {@code Configuration} object via the updated method
+	 * and before any plugins are called. The bundle location or region will be
+	 * set persistently.
+	 * 
+	 * <p>
+	 * If the location starts with {@code ?} then all targets registered with
+	 * the given PID must be updated.
+	 * 
+	 * <p>
+	 * If the location is changed then existing targets must be informed. If
+	 * they can no longer see this configuration, the configuration must be
+	 * deleted or updated with {@code null}. If this configuration becomes
+	 * visible then they must be updated with this configuration.
+	 * 
+	 * <p>
+	 * Also initiates an asynchronous call to all {@link ConfigurationListener}s
+	 * with a {@link ConfigurationEvent#CM_LOCATION_CHANGED} event.
+	 * 
+	 * @param location a location, region, or {@code null}
+	 * @throws IllegalStateException If this configuration has been deleted.
+	 * @throws SecurityException when the required permissions are not available
+	 * @throws SecurityException when the required permissions are not available
+	 * @security ConfigurationPermission[this.location,CONFIGURE] if
+	 *           this.location is not {@code null}
+	 * @security ConfigurationPermission[location,CONFIGURE] if location is not
+	 *           {@code null}
+	 * @security ConfigurationPermission["*",CONFIGURE] if this.location is
+	 *           {@code null} or if location is {@code null}
+	 */
+	public void setBundleLocation(String location);
+
+	/**
+	 * Get the bundle location.
+	 * 
+	 * Returns the bundle location or region to which this configuration is
+	 * bound, or {@code null} if it is not yet bound to a bundle location or
+	 * region. If the location starts with {@code ?} then the configuration is
+	 * delivered to all targets and not restricted to a single bundle.
+	 * 
+	 * @return location to which this configuration is bound, or {@code null}.
+	 * @throws IllegalStateException If this configuration has been deleted.
+	 * @throws SecurityException when the required permissions are not available
+	 * @security ConfigurationPermission[this.location,CONFIGURE] if
+	 *           this.location is not {@code null}
+	 * @security ConfigurationPermission["*",CONFIGURE] if this.location is
+	 *           {@code null}
+	 * 
+	 */
+	public String getBundleLocation();
+
+	/**
+	 * Get the change count.
+	 * 
+	 * The Configuration must maintain a change counter that every time when
+	 * this configuration is updated and its properties are stored is
+	 * incremented with a positive value. The counter must be changed after the
+	 * properties are persisted but before the targets are updated and events
+	 * are sent out.
+	 * 
+	 * @return A monotonously increasing value reflecting changes in this
+	 *         Configuration
+	 * 
+	 * @since 1.5
+	 */
+	public long getChangeCount();
+
+	/**
+	 * Equality is defined to have equal PIDs
+	 * 
+	 * Two Configuration objects are equal when their PIDs are equal.
+	 * 
+	 * @param other {@code Configuration} object to compare against
+	 * @return {@code true} if equal, {@code false} if not a
+	 *         {@code Configuration} object or one with a different PID.
+	 */
+	public boolean equals(Object other);
+
+	/**
+	 * Hash code is based on PID.
+	 * 
+	 * The hash code for two Configuration objects must be the same when the
+	 * Configuration PID's are the same.
+	 * 
+	 * @return hash code for this Configuration object
+	 */
+	public int hashCode();
+}
diff --git a/configadmin/src/main/java/org/osgi/service/cm/SynchronousConfigurationListener.java b/configadmin/src/main/java/org/osgi/service/cm/SynchronousConfigurationListener.java
new file mode 100644
index 0000000..322376b
--- /dev/null
+++ b/configadmin/src/main/java/org/osgi/service/cm/SynchronousConfigurationListener.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) OSGi Alliance (2012). 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.service.cm;
+
+/**
+ * Synchronous Listener for Configuration Events. When a
+ * {@code ConfigurationEvent} is fired, it is synchronously delivered to a
+ * {@code SynchronousConfigurationListener}.
+ * 
+ * <p>
+ * {@code SynchronousConfigurationListener} objects are registered with the
+ * Framework service registry and are synchronously notified with a
+ * {@code ConfigurationEvent} object when an event is fired.
+ * <p>
+ * {@code SynchronousConfigurationListener} objects can inspect the received
+ * {@code ConfigurationEvent} object to determine its type, the PID of the
+ * {@code Configuration} object with which it is associated, and the
+ * Configuration Admin service that fired the event.
+ * 
+ * <p>
+ * Security Considerations. Bundles wishing to synchronously monitor
+ * configuration events will require
+ * {@code ServicePermission[SynchronousConfigurationListener,REGISTER]} to
+ * register a {@code SynchronousConfigurationListener} service.
+ * 
+ * @version $Id: 0255bdb6d59c98dd25bfc3c90e35b20f2912f9e1 $
+ * @since 1.5
+ */
+public interface SynchronousConfigurationListener extends ConfigurationListener {
+	// Marker interface
+}
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationAdminUpdateStressTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationAdminUpdateStressTest.java
index e1a4892..0f7b8f3 100644
--- a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationAdminUpdateStressTest.java
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationAdminUpdateStressTest.java
@@ -24,7 +24,6 @@
 import java.util.Dictionary;
 import java.util.HashSet;
 import java.util.Hashtable;
-import java.util.Properties;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
@@ -259,7 +258,7 @@
                 ConfigurationAdmin cm = ( ConfigurationAdmin ) _tracker.waitForService( 2000 );
                 setupLatches();
                 Factory factory = new Factory();
-                Properties serviceProps = new Properties();
+                Hashtable<String, Object> serviceProps = new Hashtable<String, Object>();
                 serviceProps.put( "service.pid", _FACTORYPID );
                 _bc.registerService( ManagedServiceFactory.class.getName(), factory, serviceProps );
 
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationBaseTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationBaseTest.java
index 1035712..f3fba51 100644
--- a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationBaseTest.java
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationBaseTest.java
@@ -386,6 +386,24 @@
 
 
     @Test
+    public void test_configuration_change_counter() throws IOException
+    {
+        // 1. create config with pid and locationA
+        // 2. update config with properties
+        final String pid = "test_configuration_change_counter";
+        final Configuration config = configure( pid, null, false );
+
+        TestCase.assertEquals("Expect first version to be 1", 1, config.getChangeCount());
+
+        config.update(new Hashtable(){{put("x", "x");}});
+        TestCase.assertEquals("Expect second version to be 2", 2, config.getChangeCount());
+
+        // delete
+        config.delete();
+    }
+
+
+    @Test
     public void test_basic_configuration_configure_then_start() throws BundleException, IOException
     {
         // 1. create config with pid and locationA
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationListenerTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationListenerTest.java
new file mode 100644
index 0000000..3301a19
--- /dev/null
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/ConfigurationListenerTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.cm.integration;
+
+
+import java.io.IOException;
+import java.util.Hashtable;
+
+import org.apache.felix.cm.integration.helper.SynchronousTestListener;
+import org.apache.felix.cm.integration.helper.TestListener;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.JUnit4TestRunner;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationEvent;
+import org.osgi.service.cm.ConfigurationListener;
+
+
+@RunWith(JUnit4TestRunner.class)
+public class ConfigurationListenerTest extends ConfigurationTestBase
+{
+
+    static
+    {
+        // uncomment to enable debugging of this test class
+        // paxRunnerVmOption = DEBUG_VM_OPTION;
+    }
+
+
+    @Test
+    public void test_async_listener() throws IOException
+    {
+        final String pid = "test_listener";
+        final TestListener testListener = new TestListener();
+        final ServiceRegistration listener = this.bundleContext.registerService( ConfigurationListener.class.getName(),
+            testListener, null );
+        int eventCount = 0;
+
+        Configuration config = configure( pid, null, false );
+        try
+        {
+            delay();
+            testListener.assertNoEvent();
+
+            config.update( new Hashtable<String, Object>()
+            {
+                {
+                    put( "x", "x" );
+                }
+            } );
+            delay();
+            testListener.assertEvent( ConfigurationEvent.CM_UPDATED, pid, null, true, ++eventCount );
+
+            config.update( new Hashtable<String, Object>()
+            {
+                {
+                    put( "x", "x" );
+                }
+            } );
+            delay();
+            testListener.assertEvent( ConfigurationEvent.CM_UPDATED, pid, null, true, ++eventCount );
+
+            config.setBundleLocation( "new_Location" );
+            delay();
+            testListener.assertEvent( ConfigurationEvent.CM_LOCATION_CHANGED, pid, null, true, ++eventCount );
+
+            config.update();
+            testListener.assertNoEvent();
+
+            config.delete();
+            config = null;
+            delay();
+            testListener.assertEvent( ConfigurationEvent.CM_DELETED, pid, null, true, ++eventCount );
+        }
+        finally
+        {
+            if ( config != null )
+            {
+                try
+                {
+                    config.delete();
+                }
+                catch ( IOException ioe )
+                {
+                    // ignore
+                }
+            }
+
+            listener.unregister();
+        }
+    }
+
+
+    @Test
+    public void test_sync_listener() throws IOException
+    {
+        final String pid = "test_listener";
+        Configuration config = configure( pid, null, false );
+        final TestListener testListener = new SynchronousTestListener();
+        final ServiceRegistration listener = this.bundleContext.registerService( ConfigurationListener.class.getName(),
+            testListener, null );
+        int eventCount = 0;
+        try
+        {
+            delay();
+            testListener.assertNoEvent();
+
+            config.update( new Hashtable<String, Object>()
+            {
+                {
+                    put( "x", "x" );
+                }
+            } );
+            delay();
+            testListener.assertEvent( ConfigurationEvent.CM_UPDATED, pid, null, false, ++eventCount );
+
+            config.update( new Hashtable<String, Object>()
+            {
+                {
+                    put( "x", "x" );
+                }
+            } );
+            delay();
+            testListener.assertEvent( ConfigurationEvent.CM_UPDATED, pid, null, false, ++eventCount );
+
+            config.setBundleLocation( "new_Location" );
+            delay();
+            testListener.assertEvent( ConfigurationEvent.CM_LOCATION_CHANGED, pid, null, false, ++eventCount );
+
+            config.update();
+            testListener.assertNoEvent();
+
+            config.delete();
+            config = null;
+            delay();
+            testListener.assertEvent( ConfigurationEvent.CM_DELETED, pid, null, false, ++eventCount );
+        }
+        finally
+        {
+            if ( config != null )
+            {
+                try
+                {
+                    config.delete();
+                }
+                catch ( IOException ioe )
+                {
+                    // ignore
+                }
+            }
+
+            listener.unregister();
+        }
+    }
+}
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/FELIX2813_ConfigurationAdminStartupTest.java b/configadmin/src/test/java/org/apache/felix/cm/integration/FELIX2813_ConfigurationAdminStartupTest.java
index ea7deef..988f192 100644
--- a/configadmin/src/test/java/org/apache/felix/cm/integration/FELIX2813_ConfigurationAdminStartupTest.java
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/FELIX2813_ConfigurationAdminStartupTest.java
@@ -23,8 +23,8 @@
 import java.util.ArrayList;
 import java.util.Hashtable;
 import java.util.List;
-import junit.framework.TestCase;
-
+import org.apache.felix.cm.integration.helper.SynchronousTestListener;
+import org.apache.felix.cm.integration.helper.TestListener;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.ops4j.pax.exam.junit.JUnit4TestRunner;
@@ -41,14 +41,9 @@
 
 
 @RunWith(JUnit4TestRunner.class)
-public class FELIX2813_ConfigurationAdminStartupTest extends ConfigurationTestBase implements ServiceListener,
-    ConfigurationListener
+public class FELIX2813_ConfigurationAdminStartupTest extends ConfigurationTestBase implements ServiceListener
 {
 
-    private Object lock = new Object();
-    private boolean eventSeen;
-
-
     @Test
     public void testAddConfigurationWhenConfigurationAdminStarts() throws InvalidSyntaxException, BundleException
     {
@@ -64,13 +59,13 @@
             }
         }
 
-        bundleContext.registerService( ConfigurationListener.class.getName(), this, null );
+        final TestListener listener = new TestListener();
+        bundleContext.registerService( ConfigurationListener.class.getName(), listener, null );
+        final TestListener syncListener = new SynchronousTestListener();
+        bundleContext.registerService( ConfigurationListener.class.getName(), syncListener, null );
         bundleContext.addServiceListener( this, "(" + Constants.OBJECTCLASS + "=" + ConfigurationAdmin.class.getName()
             + ")" );
 
-        // ensure we do not have a false positive below
-        eventSeen = false;
-
         for ( Bundle bundle : bundles )
         {
             bundle.start();
@@ -97,26 +92,9 @@
          * assumed to have failed. This will rather generate false negatives
          * (on slow machines) than false positives.
          */
-        synchronized ( lock )
-        {
-            if ( !eventSeen )
-            {
-                try
-                {
-                    lock.wait( 2000 );
-                }
-                catch ( InterruptedException ie )
-                {
-                    // don't care ...
-                }
-            }
-
-            if ( !eventSeen )
-            {
-                TestCase.fail( "ConfigurationEvent not received within 2 seconds since bundle start" );
-            }
-        }
-
+        delay();
+        listener.assertEvent( ConfigurationEvent.CM_UPDATED, "test", null, true, 1 );
+        syncListener.assertEvent( ConfigurationEvent.CM_UPDATED, "test", null, false, 1 );
     }
 
 
@@ -138,17 +116,4 @@
             }
         }
     }
-
-
-    public void configurationEvent( ConfigurationEvent event )
-    {
-        if ( "test".equals( event.getPid() ) )
-        {
-            synchronized ( lock )
-            {
-                eventSeen = true;
-                lock.notifyAll();
-            }
-        }
-    }
 }
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/SynchronousTestListener.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/SynchronousTestListener.java
new file mode 100644
index 0000000..baec499
--- /dev/null
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/SynchronousTestListener.java
@@ -0,0 +1,27 @@
+/*
+ * 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.cm.integration.helper;
+
+
+import org.osgi.service.cm.SynchronousConfigurationListener;
+
+
+public class SynchronousTestListener extends TestListener implements SynchronousConfigurationListener
+{
+}
\ No newline at end of file
diff --git a/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestListener.java b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestListener.java
new file mode 100644
index 0000000..0ea544d
--- /dev/null
+++ b/configadmin/src/test/java/org/apache/felix/cm/integration/helper/TestListener.java
@@ -0,0 +1,124 @@
+/*
+ * 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.cm.integration.helper;
+
+
+import junit.framework.TestCase;
+
+import org.osgi.service.cm.ConfigurationEvent;
+import org.osgi.service.cm.ConfigurationListener;
+
+
+public class TestListener implements ConfigurationListener
+{
+
+    private final Thread mainThread;
+
+    private ConfigurationEvent event;
+
+    private Thread eventThread;
+
+    private int numberOfEvents;
+
+    {
+        this.mainThread = Thread.currentThread();
+        this.numberOfEvents = 0;
+    }
+
+
+    public void configurationEvent( final ConfigurationEvent event )
+    {
+        this.numberOfEvents++;
+
+        if ( this.event != null )
+        {
+            throw new IllegalStateException( "Untested event to be replaced: " + this.event.getType() + "/"
+                + this.event.getPid() );
+        }
+
+        this.event = event;
+        this.eventThread = Thread.currentThread();
+    }
+
+
+    void resetNumberOfEvents()
+    {
+        this.numberOfEvents = 0;
+    }
+
+
+    /**
+     * Asserts an expected event has arrived since the last call to
+     * {@link #assertEvent(int, String, String, boolean, int)} and
+     * {@link #assertNoEvent()}.
+     *
+     * @param type The expected event type
+     * @param pid The expected PID of the event
+     * @param factoryPid The expected factory PID of the event or
+     *      <code>null</code> if no factory PID is expected
+     * @param expectAsync Whether the event is expected to have been
+     *      provided asynchronously
+     * @param numberOfEvents The number of events to have arrived in total
+     */
+    public void assertEvent( final int type, final String pid, final String factoryPid, final boolean expectAsync,
+        final int numberOfEvents )
+    {
+        try
+        {
+            TestCase.assertNotNull( "Expecting an event", this.event );
+            TestCase.assertEquals( "Expecting event type " + type, type, this.event.getType() );
+            TestCase.assertEquals( "Expecting pid " + pid, pid, this.event.getPid() );
+            if ( factoryPid == null )
+            {
+                TestCase.assertNull( "Expecting no factoryPid", this.event.getFactoryPid() );
+            }
+            else
+            {
+                TestCase.assertEquals( "Expecting factory pid " + factoryPid, factoryPid, this.event.getFactoryPid() );
+            }
+
+            TestCase.assertEquals( "Expecting " + numberOfEvents + " events", numberOfEvents, this.numberOfEvents );
+
+            if ( expectAsync )
+            {
+                TestCase.assertNotSame( "Expecting asynchronous event", this.mainThread, this.eventThread );
+            }
+            else
+            {
+                TestCase.assertSame( "Expecting synchronous event", this.mainThread, this.eventThread );
+            }
+        }
+        finally
+        {
+            this.event = null;
+            this.eventThread = null;
+        }
+    }
+
+
+    /**
+     * Fails if an event has been received since the last call to
+     * {@link #assertEvent(int, String, String, boolean, int)} or
+     * {@link #assertNoEvent()}.
+     */
+    public void assertNoEvent()
+    {
+        TestCase.assertNull( this.event );
+    }
+}
\ No newline at end of file