FELIX-5177: Applied the patch from Jan Willem, and slightly modified it in order to check when callbackInstance is null.
(when callbackInstance is null, the callback must be invoked on the instantiated component).

Also, added more signatures for the new callback methods.
Fixed the ConfigurationDependencyTest, in order to ensure that updated() is invoked before init() in the ConfigurationConsumerWithTypeSafeConfiguration class.


git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1728564 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ConfigurationDependencyTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ConfigurationDependencyTest.java
index add1a5f..1c70881 100644
--- a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ConfigurationDependencyTest.java
+++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ConfigurationDependencyTest.java
@@ -42,6 +42,26 @@
 public class ConfigurationDependencyTest extends TestBase {
     final static String PID = "ConfigurationDependencyTest.pid";
     
+    /**
+     * Tests that we can provision a type-safe configuration to a component.
+     */
+    public void testComponentWithRequiredConfigurationWithTypeSafeConfiguration() {
+        DependencyManager m = getDM();
+        // helper class that ensures certain steps get executed in sequence
+        Ensure e = new Ensure();
+        // create a service provider and consumer
+        Component s1 = m.createComponent().setImplementation(new ConfigurationConsumerWithTypeSafeConfiguration(e)).add(m.createConfigurationDependency().setCallback(null, "updated", MyConfig.class).setPid(PID).setPropagate(true));
+        Component s2 = m.createComponent().setImplementation(new ConfigurationCreator(e, PID)).add(m.createServiceDependency().setService(ConfigurationAdmin.class).setRequired(true));
+        m.add(s1);
+        m.add(s2);
+        e.waitForStep(3, 5000); // component called in updated(), then in init()
+        m.remove(s1);
+        m.remove(s2);
+        // ensure we executed all steps inside the component instance
+        e.waitForStep(3, 5000); // conf creator is called in destroy and is about to delete the conf
+        e.waitForStep(4, 5000); // type safe conf consumer is destroyed
+    }
+    
     public void testComponentWithRequiredConfigurationAndServicePropertyPropagation() {
         DependencyManager m = getDM();
         // helper class that ensures certain steps get executed in sequence
@@ -53,7 +73,7 @@
         m.add(s1);
         m.add(s2);
         m.add(s3);
-        e.waitForStep(4, 50000000);
+        e.waitForStep(4, 5000);
         m.remove(s1);
         m.remove(s2);
         m.remove(s3);
@@ -342,4 +362,35 @@
             m_runnable.run();
         }
     }
+    
+    static interface MyConfig {
+        String getTestkey();
+    }
+
+    static class ConfigurationConsumerWithTypeSafeConfiguration {
+        private final Ensure m_ensure;
+
+        public ConfigurationConsumerWithTypeSafeConfiguration(Ensure e) {
+            m_ensure = e;
+        }
+
+        // configuration updates is always the first invoked callback (before init).
+        public void updated(Component component, MyConfig cfg) throws ConfigurationException {
+            Assert.assertNotNull(component);
+            Assert.assertNotNull(cfg);
+            m_ensure.step(2);
+            if (!"testvalue".equals(cfg.getTestkey())) {
+                Assert.fail("Could not find the configured property.");
+            }
+        }
+
+        // called after configuration has been injected.
+        public void init() {
+            m_ensure.step(3); 
+        }
+        
+        public void destroy() {
+            m_ensure.step(4); 
+        }        
+    }
 }
diff --git a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceTest.java b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceTest.java
index fd2785d..b1c0ed1 100644
--- a/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceTest.java
+++ b/dependencymanager/org.apache.felix.dependencymanager.itest/src/org/apache/felix/dm/itest/api/ServiceRaceTest.java
@@ -47,7 +47,7 @@
 @SuppressWarnings({"unchecked", "rawtypes"})
 public class ServiceRaceTest extends TestBase {
     volatile ConfigurationAdmin m_cm;
-    final static int STEP_WAIT = 5000;
+    final static int STEP_WAIT = 15000;
     final static int DEPENDENCIES = 10;
     final static int LOOPS = 3000;
     final Ensure m_done = new Ensure(true);
diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ConfigurationDependency.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ConfigurationDependency.java
index 4c58059..a4a97fc 100644
--- a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ConfigurationDependency.java
+++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/ConfigurationDependency.java
@@ -59,8 +59,8 @@
      * is available. The contract for this method is identical to that of
      * <code>ManagedService.updated(Dictionary) throws ConfigurationException</code>.
      * By default, if this method is not called, the callback name is "updated".
-     * The callback is always invoked with an already instantiated component (the component implementation class(es) are
-     * always instantiated before the updated callback is invoked).
+     * 
+     * <p> The callback is invoked on the instantiated component.
      * 
      * @param callback the name of the callback method
      */
@@ -70,10 +70,11 @@
      * Sets the name of the callback method that should be invoked when a configuration
      * is available. The contract for this method is identical to that of
      * <code>ManagedService.updated(Dictionary) throws ConfigurationException</code>.
-     * The callback is called with a component that is not yet instantiated. This allows factory objects to get
-     * injected with a configuration before its <code>create</code> method is called.
      * 
-     * @param instance the instance to call the callbacks on
+     * <p> the callback is invoked on the callback instance, and the component is not 
+     * yet instantiated at the time the callback is invoked.
+     * 
+     * @param instance the object to invoke the callback on
      * @param callback the name of the callback method
      */
     ConfigurationDependency setCallback(Object instance, String callback);
@@ -82,18 +83,61 @@
      * Sets the name of the callback method that should be invoked when a configuration
      * is available. The contract for this method is identical to that of
      * <code>ManagedService.updated(Dictionary) throws ConfigurationException</code>.
-     * The component instance is instantiated before the callback is invoked only the the <code>needsInstance</code> parameter is set to true.
      * 
-     * @param instance the instance to call the callback on
+     * <p> the callback is invoked on the callback instance, and if <code>needsInstance</code> is true, 
+     * the component is instantiated at the time the callback is invoked 
+     * 
+     * @param instance the object to invoke the callback on.
      * @param callback the name of the callback method
-     * @param needsInstance true if the component implementation class(es) must be created before the
-     *        callback instance is invoked, else false.
+     * @param needsInstance true if the component must be instantiated before the callback is invoked on the callback instance.
      */
     ConfigurationDependency setCallback(Object instance, String callback, boolean needsInstance);
-    
+
     /**
-     * Sets the <code>service.pid</code> of the configuration you are depending
-     * on.
+     * Sets the name of the callback method that should be invoked when a configuration
+     * is available. The contract for this method is identical to that of
+     * <code>ManagedService.updated(Dictionary) throws ConfigurationException</code> with the difference that 
+     * instead of a Dictionary it accepts an interface of the given configuration type.<br>
+     * 
+     * <p>The callback is invoked on the instantiated component.
+     * 
+     * @param callback the name of the callback method
+     * @param configType the configuration type that the callback method accepts.
+     */
+    ConfigurationDependency setCallback(String callback, Class<?> configType);
+
+    /**
+     * Sets the name of the callback method that should be invoked when a configuration
+     * is available. The contract for this method is identical to that of
+     * <code>ManagedService.updated(Dictionary) throws ConfigurationException</code> with the difference that 
+     * instead of a Dictionary it accepts an interface of the given configuration type.<br>
+     * 
+     * <p> The callback is invoked on the callback instance, and at this point the component is not yet instantiated.
+     * 
+     * @param instance the object to invoke the callback on.
+     * @param callback the name of the callback method
+     * @param configType the configuration type that the callback method accepts.
+     */
+    ConfigurationDependency setCallback(Object instance, String callback, Class<?> configType);
+
+    /**
+     * Sets the name of the callback method that should be invoked when a configuration
+     * is available. The contract for this method is identical to that of
+     * <code>ManagedService.updated(Dictionary) throws ConfigurationException</code> with the difference that 
+     * instead of a Dictionary it accepts an interface of the given configuration type.<br>
+     * 
+     * <p> the callback is invoked on the callback instance, and if <code>needsInstance</code> is true, 
+     * the component is instantiated at the time the callback is invoked 
+     * 
+     * @param instance the object to invoke the callback on.
+     * @param callback the name of the callback method
+     * @param configType the configuration type that the callback method accepts.
+     * @param needsInstance true if the component must be instantiated before the callback is invoked on the callback instance.
+     */
+    ConfigurationDependency setCallback(Object instance, String callback, Class<?> configType, boolean needsInstance);
+
+    /**
+     * Sets the <code>service.pid</code> of the configuration you are depending on.
      */
 	ConfigurationDependency setPid(String pid);
 
diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/Configurable.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/Configurable.java
new file mode 100644
index 0000000..e05e985
--- /dev/null
+++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/Configurable.java
@@ -0,0 +1,450 @@
+/*
+ * 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.dm.impl;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Proxy;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/**
+ * Provides a way for creating type-safe configurations from a {@link Map} or {@link Dictionary}.
+ * <p>
+ * This class takes a map or dictionary along with a class, the configuration-type, and returns a proxy that converts
+ * method calls from the configuration-type to lookups in the map or dictionary. The results of these lookups are then
+ * converted to the expected return type of the invoked configuration method.<br>
+ * As proxies are returned, no implementations of the desired configuration-type are necessary!
+ * </p>
+ * <p>
+ * The lookups performed are based on the name of the method called on the configuration type. The method names are
+ * "mangled" to the following form: <tt>[lower case letter] [any valid character]*</tt>. Method names starting with
+ * <tt>get</tt> or <tt>is</tt> (JavaBean convention) are stripped from these prefixes. For example: given a dictionary
+ * with the key <tt>"foo"</tt> can be accessed from a configuration-type using the following method names:
+ * <tt>foo()</tt>, <tt>getFoo()</tt> and <tt>isFoo()</tt>.
+ * </p>
+ * <p>
+ * The return values supported are: primitive types (or their object wrappers), strings, enums, arrays of
+ * primitives/strings, {@link Collection} types, {@link Map} types, {@link Class}es and interfaces. When an interface is
+ * returned, it is treated equally to a configuration type, that is, it is returned as a proxy.
+ * </p>
+ * <p>
+ * Arrays can be represented either as comma-separated values, optionally enclosed in square brackets. For example:
+ * <tt>[ a, b, c ]</tt> and <tt>a, b,c</tt> are both considered an array of length 3 with the values "a", "b" and "c".
+ * Alternatively, you can append the array index to the key in the dictionary to obtain the same: a dictionary with
+ * "arr.0" =&gt; "a", "arr.1" =&gt; "b", "arr.2" =&gt; "c" would result in the same array as the earlier examples.
+ * </p>
+ * <p>
+ * Maps can be represented as single string values similarly as arrays, each value consisting of both the key and value
+ * separated by a dot. Optionally, the value can be enclosed in curly brackets. Similar to array, you can use the same
+ * dot notation using the keys. For example, a dictionary with <tt>"map" => "{key1.value1, key2.value2}"</tt> and a
+ * dictionary with <tt>"map.key1" => "value1", "map2.key2" => "value2"</tt> result in the same map being returned.
+ * Instead of a map, you could also define an interface with the methods <tt>getKey1()</tt> and <tt>getKey2</tt> and use
+ * that interface as return type instead of a {@link Map}.
+ * </p>
+ * <p>
+ * In case a lookup does not yield a value from the underlying map or dictionary, the following rules are applied:
+ * <ol>
+ * <li>primitive types yield their default value, as defined by the Java Specification;
+ * <li>string, {@link Class}es and enum values yield <code>null</code>;
+ * <li>for arrays, collections and maps, an empty array/collection/map is returned;
+ * <li>for other interface types that are treated as configuration type a null-object is returned.
+ * </ol>
+ * </p>
+ */
+public final class Configurable {
+
+    static class ConfigHandler implements InvocationHandler {
+        private final ClassLoader m_cl;
+        private final Map<?, ?> m_config;
+
+        public ConfigHandler(ClassLoader cl, Map<?, ?> config) {
+            m_cl = cl;
+            m_config = config;
+        }
+
+        @Override
+        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+            String name = getPropertyName(method.getName());
+
+            Object result = convert(method.getGenericReturnType(), name, m_config.get(name), false /* useImplicitDefault */);
+            if (result == null) {
+                Object defaultValue = getDefaultValue(method, name);
+                if (defaultValue != null) {
+                    return defaultValue;
+                }
+            }
+
+            return result;
+        }
+
+        private Object convert(ParameterizedType type, String key, Object value) throws Exception {
+            Class<?> resultType = (Class<?>) type.getRawType();
+            if (Class.class.isAssignableFrom(resultType)) {
+                if (value == null) {
+                    return null;
+                }
+                return m_cl.loadClass(value.toString());
+            }
+            else if (Collection.class.isAssignableFrom(resultType)) {
+                Collection<?> input = toCollection(key, value);
+
+                if (resultType == Collection.class || resultType == List.class) {
+                    resultType = ArrayList.class;
+                }
+                else if (resultType == Set.class || resultType == SortedSet.class) {
+                    resultType = TreeSet.class;
+                }
+                else if (resultType == Queue.class) {
+                    resultType = LinkedList.class;
+                }
+                else if (resultType.isInterface()) {
+                    throw new RuntimeException("Unknown collection interface: " + resultType);
+                }
+
+                Collection<Object> result = (Collection<Object>) resultType.newInstance();
+                if (input != null) {
+                    Type componentType = type.getActualTypeArguments()[0];
+                    for (Object i : input) {
+                        result.add(convert(componentType, key, i, false /* useImplicitDefault */));
+                    }
+                }
+                return result;
+            }
+            else if (Map.class.isAssignableFrom(resultType)) {
+                Map<?, ?> input = toMap(key, value);
+
+                if (resultType == SortedMap.class) {
+                    resultType = TreeMap.class;
+                }
+                else if (resultType == Map.class) {
+                    resultType = LinkedHashMap.class;
+                }
+                else if (resultType.isInterface()) {
+                    throw new RuntimeException("Unknown map interface: " + resultType);
+                }
+
+                Map<Object, Object> result = (Map<Object, Object>) resultType.newInstance();
+                Type keyType = type.getActualTypeArguments()[0];
+                Type valueType = type.getActualTypeArguments()[1];
+
+                for (Map.Entry<?, ?> entry : input.entrySet()) {
+                    result.put(convert(keyType, key, entry.getKey(), false /* useImplicitDefault */), convert(valueType, key, entry.getValue(), false /* useImplicitDefault */));
+                }
+                return result;
+            }
+
+            throw new RuntimeException("Unhandled type: " + type);
+        }
+
+        private Object convert(Type type, String key, Object value, boolean useImplicitDefault) throws Exception {
+            if (type instanceof ParameterizedType) {
+                return convert((ParameterizedType) type, key, value);
+            }
+            if (type instanceof GenericArrayType) {
+                return convertArray(((GenericArrayType) type).getGenericComponentType(), key, value);
+            }
+            Class<?> resultType = (Class<?>) type;
+            if (resultType.isArray()) {
+                return convertArray(resultType.getComponentType(), key, value);
+            }
+            if (resultType.isInstance(value)) {
+                return value;
+            }
+
+            if (Boolean.class.equals(resultType) || Boolean.TYPE.equals(resultType)) {
+                if (value == null) {
+                    return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_BOOLEAN : null;
+                }
+                return Boolean.valueOf(value.toString());
+            }
+            else if (Byte.class.equals(resultType) || Byte.TYPE.equals(resultType)) {
+                if (value == null) {
+                    return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_BYTE : null;
+                }
+                if (value instanceof Number) {
+                    return ((Number) value).byteValue();
+                }
+                return Byte.valueOf(value.toString());
+            }
+            else if (Short.class.equals(resultType) || Short.TYPE.equals(resultType)) {
+                if (value == null) {
+                    return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_SHORT : null;
+                }
+                if (value instanceof Number) {
+                    return ((Number) value).shortValue();
+                }
+                return Short.valueOf(value.toString());
+            }
+            else if (Integer.class.equals(resultType) || Integer.TYPE.equals(resultType)) {
+                if (value == null) {
+                    return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_INT : null;
+                }
+                if (value instanceof Number) {
+                    return ((Number) value).intValue();
+                }
+                return Integer.valueOf(value.toString());
+            }
+            else if (Long.class.equals(resultType) || Long.TYPE.equals(resultType)) {
+                if (value == null) {
+                    return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_LONG : null;
+                }
+                if (value instanceof Number) {
+                    return ((Number) value).longValue();
+                }
+                return Long.valueOf(value.toString());
+            }
+            else if (Float.class.equals(resultType) || Float.TYPE.equals(resultType)) {
+                if (value == null) {
+                    return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_FLOAT : null;
+                }
+                if (value instanceof Number) {
+                    return ((Number) value).floatValue();
+                }
+                return Float.valueOf(value.toString());
+            }
+            else if (Double.class.equals(resultType) || Double.TYPE.equals(resultType)) {
+                if (value == null) {
+                    return useImplicitDefault && resultType.isPrimitive() ? DEFAULT_DOUBLE : null;
+                }
+                if (value instanceof Number) {
+                    return ((Number) value).doubleValue();
+                }
+                return Double.valueOf(value.toString());
+            }
+            else if (Number.class.equals(resultType)) {
+                if (value == null) {
+                    return null;
+                }
+                String numStr = value.toString();
+                if (numStr.indexOf('.') > 0) {
+                    return Double.valueOf(numStr);
+                }
+                return Long.valueOf(numStr);
+            }
+            else if (String.class.isAssignableFrom(resultType)) {
+                return value == null ? null : value.toString();
+            }
+            else if (Enum.class.isAssignableFrom(resultType)) {
+                if (value == null) {
+                    return null;
+                }
+                Class<Enum> enumType = (Class<Enum>) resultType;
+                return Enum.valueOf(enumType, value.toString().toUpperCase());
+            }
+            else if (resultType.isInterface()) {
+                Map<?, ?> map = toMap(key, value);
+                return create(resultType, map);
+            }
+
+            throw new RuntimeException("Unhandled type: " + type);
+        }
+
+        private Object convertArray(Type type, String key, Object value) throws Exception {
+            if (value instanceof String) {
+                String str = (String) value;
+                if (type == Byte.class || type == byte.class) {
+                    return str.getBytes("UTF-8");
+                }
+                if (type == Character.class || type == char.class) {
+                    return str.toCharArray();
+                }
+            }
+
+            Collection<?> input = toCollection(key, value);
+            if (input == null) {
+                return null;
+            }
+
+            Class<?> componentClass = getRawClass(type);
+            Object array = Array.newInstance(componentClass, input.size());
+
+            int i = 0;
+            for (Object next : input) {
+                Array.set(array, i++, convert(type, key, next, false /* useImplicitDefault */));
+            }
+            return array;
+        }
+
+        private Object getDefaultValue(Method method, String key) throws Exception {
+            return convert(method.getGenericReturnType(), key, null, true /* useImplicitDefault */);
+        }
+
+        private Class<?> getRawClass(Type type) {
+            if (type instanceof ParameterizedType) {
+                return (Class<?>) ((ParameterizedType) type).getRawType();
+            }
+            if (type instanceof Class) {
+                return (Class<?>) type;
+            }
+            throw new RuntimeException("Unhandled type: " + type);
+        }
+
+        private Collection<?> toCollection(String prefix, Object value) {
+            if (value instanceof Collection) {
+                return (Collection<?>) value;
+            }
+
+            if (value == null) {
+                List<Object> result = new ArrayList<>();
+
+                String needle = prefix.concat(".");
+                for (Map.Entry<?, ?> entry : m_config.entrySet()) {
+                    String key = entry.getKey().toString();
+                    if (!key.startsWith(needle)) {
+                        continue;
+                    }
+
+                    int idx = 0;
+                    try {
+                        idx = Integer.parseInt(key.substring(needle.length()));
+                    }
+                    catch (NumberFormatException e) {
+                        // Ignore
+                    }
+
+                    result.add(Math.min(result.size(), idx), entry.getValue());
+                }
+
+                return result;
+            }
+
+            if (value.getClass().isArray()) {
+                if (value.getClass().getComponentType().isPrimitive()) {
+                    int length = Array.getLength(value);
+                    List<Object> result = new ArrayList<Object>(length);
+                    for (int i = 0; i < length; i++) {
+                        result.add(Array.get(value, i));
+                    }
+                    return result;
+                }
+                return Arrays.asList((Object[]) value);
+            }
+
+            if (value instanceof String) {
+                String str = (String) value;
+                if (str.startsWith("[") && str.endsWith("]")) {
+                    str = str.substring(1, str.length() - 1);
+                }
+                return Arrays.asList(str.split("\\s*,\\s*"));
+            }
+
+            return Arrays.asList(value);
+        }
+
+        private Map<?, ?> toMap(String prefix, Object value) {
+            if (value instanceof Map) {
+                return (Map<?, ?>) value;
+            }
+
+            Map<String, Object> result = new HashMap<>();
+            if (value == null) {
+                String needle = prefix.concat(".");
+                for (Map.Entry<?, ?> entry : m_config.entrySet()) {
+                    String key = entry.getKey().toString();
+                    if (key.startsWith(needle)) {
+                        result.put(key.substring(needle.length()), entry.getValue());
+                    }
+                }
+            }
+            else if (value instanceof String) {
+                String str = (String) value;
+                if (str.startsWith("{") && str.endsWith("}")) {
+                    str = str.substring(1, str.length() - 1);
+                }
+                for (String entry : str.split("\\s*,\\s*")) {
+                    String[] pair = entry.split("\\s*\\.\\s*", 2);
+                    result.put(pair[0], pair[1]);
+                }
+            }
+
+            return result;
+        }
+
+        private String getPropertyName(String id) {
+            StringBuilder sb = new StringBuilder(id);
+            if (id.startsWith("get")) {
+                sb.delete(0, 3);
+            }
+            else if (id.startsWith("is")) {
+                sb.delete(0, 2);
+            }
+            char c = sb.charAt(0);
+            if (Character.isUpperCase(c)) {
+                sb.setCharAt(0, Character.toLowerCase(c));
+            }
+            return sb.toString();
+        }
+    }
+
+    private static final Boolean DEFAULT_BOOLEAN = Boolean.FALSE;
+    private static final Byte DEFAULT_BYTE = new Byte((byte) 0);
+    private static final Short DEFAULT_SHORT = new Short((short) 0);
+    private static final Integer DEFAULT_INT = new Integer(0);
+    private static final Long DEFAULT_LONG = new Long(0);
+    private static final Float DEFAULT_FLOAT = new Float(0.0f);
+    private static final Double DEFAULT_DOUBLE = new Double(0.0);
+
+    /**
+     * Creates a configuration for a given type backed by a given dictionary.
+     * 
+     * @param type the configuration class, cannot be <code>null</code>;
+     * @param config the configuration to wrap, cannot be <code>null</code>.
+     * @return an instance of the given type that wraps the given configuration.
+     */
+    public static <T> T create(Class<T> type, Dictionary<?, ?> config) {
+        Map<Object, Object> map = new HashMap<Object, Object>();
+        for (Enumeration<?> e = config.keys(); e.hasMoreElements();) {
+            Object key = e.nextElement();
+            map.put(key, config.get(key));
+        }
+        return create(type, map);
+    }
+
+    /**
+     * Creates a configuration for a given type backed by a given map.
+     * 
+     * @param type the configuration class, cannot be <code>null</code>;
+     * @param config the configuration to wrap, cannot be <code>null</code>.
+     * @return an instance of the given type that wraps the given configuration.
+     */
+    public static <T> T create(Class<T> type, Map<?, ?> config) {
+        ClassLoader cl = type.getClassLoader();
+        Object result = Proxy.newProxyInstance(cl, new Class<?>[] { type }, new ConfigHandler(cl, config));
+        return type.cast(result);
+    }
+}
diff --git a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ConfigurationDependencyImpl.java b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ConfigurationDependencyImpl.java
index 50f7acf..bf03b8a 100644
--- a/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ConfigurationDependencyImpl.java
+++ b/dependencymanager/org.apache.felix.dependencymanager/src/org/apache/felix/dm/impl/ConfigurationDependencyImpl.java
@@ -49,9 +49,11 @@
  * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
  */
 public class ConfigurationDependencyImpl extends AbstractDependency<ConfigurationDependency> implements ConfigurationDependency, ManagedService {
+    // Our fields are not volatile because they are "safely published" using the DM thread model (based on a Concurrent queue).
     private Dictionary<String, Object> m_settings;
 	private String m_pid;
 	private ServiceRegistration m_registration;
+	private Class<?> m_configType;
     private MetaTypeProviderImpl m_metaType;
 	private final AtomicBoolean m_updateInvokedCache = new AtomicBoolean();
 	private final Logger m_logger;
@@ -77,6 +79,7 @@
 	    m_logger = prototype.m_logger;
         m_metaType = prototype.m_metaType != null ? new MetaTypeProviderImpl(prototype.m_metaType, this, null) : null;
         m_needsInstance = prototype.needsInstance();
+        m_configType = prototype.m_configType;
 	}
 	
     @Override
@@ -89,21 +92,64 @@
 	    return new ConfigurationDependencyImpl(this);
 	}
 
+	/**
+	 * Sets a callback method invoked on the instantiated component.
+	 */
     public ConfigurationDependencyImpl setCallback(String callback) {
         super.setCallbacks(callback, null);
         return this;
     }
     
+    /**
+     * Sets a callback method on an external callback instance object.
+     * The component is not yet instantiated at the time the callback is invoked.
+     * We check if callback instance is null, in this case, the callback will be invoked on the instantiated component.
+     */
     public ConfigurationDependencyImpl setCallback(Object instance, String callback) {
-    	return setCallback(instance, callback, false);
+        boolean needsInstantiatedComponent = (instance == null);
+    	return setCallback(instance, callback, needsInstantiatedComponent);
     }
 
+    /**
+     * Sets a callback method on an external callback instance object.
+     * If needsInstance == true, the component is instantiated at the time the callback is invoked.
+     * We check if callback instance is null, in this case, the callback will be invoked on the instantiated component.
+     */
     public ConfigurationDependencyImpl setCallback(Object instance, String callback, boolean needsInstance) {
         super.setCallbacks(instance, callback, null);
         m_needsInstance = needsInstance;
         return this;
     }
+        
+    /**
+     * Sets a type-safe callback method invoked on the instantiated component.
+     */
+    public ConfigurationDependency setCallback(String callback, Class<?> configType) {
+        setCallback(callback);
+        m_configType = configType;
+        return this;
+    }
 
+    /**
+     * Sets a type-safe callback method on an external callback instance object.
+     * The component is not yet instantiated at the time the callback is invoked.
+     */
+    public ConfigurationDependency setCallback(Object instance, String callback, Class<?> configType) {
+        setCallback(instance, callback);
+        m_configType = configType;
+        return this;
+    }
+    
+    /**
+     * Sets a type-safe callback method on an external callback instance object.
+     * If needsInstance == true, the component is instantiated at the time the callback is invoked.
+     */
+    public ConfigurationDependencyImpl setCallback(Object instance, String callback, Class<?> configType, boolean needsInstance) {
+        setCallback(instance, callback, needsInstance);
+        m_configType = configType;
+        return this;
+    }
+    
     @Override
     public boolean needsInstance() {
         return m_needsInstance;
@@ -295,48 +341,54 @@
         }
     }
     
-    private void invokeUpdated(Dictionary<?,?> settings) throws ConfigurationException {
-    	if (m_updateInvokedCache.compareAndSet(false, true)) {
-			Object[] instances = super.getInstances(); // either the callback instance or the component instances
-			if (instances != null) {
-				for (int i = 0; i < instances.length; i++) {
-					try {
-						InvocationUtil.invokeCallbackMethod(instances[i],
-								m_add, new Class[][] {
-										{ Dictionary.class },
-										{ Component.class, Dictionary.class },
-										{} },
-								new Object[][] { 
-						            { settings }, 
-						            { m_component, settings },
-						            {} });
-					}
+    private void invokeUpdated(Dictionary<?, ?> settings) throws ConfigurationException {
+        if (m_updateInvokedCache.compareAndSet(false, true)) {
+            Object[] instances = super.getInstances(); // either the callback instance or the component instances
+            if (instances == null) {
+                return;
+            }
 
-					catch (InvocationTargetException e) {
-						// The component has thrown an exception during it's
-						// callback invocation.
-						if (e.getTargetException() instanceof ConfigurationException) {
-							// the callback threw an OSGi
-							// ConfigurationException: just re-throw it.
-							throw (ConfigurationException) e
-									.getTargetException();
-						} else {
-							// wrap the callback exception into a
-							// ConfigurationException.
-							throw new ConfigurationException(null,
-									"Configuration update failed",
-									e.getTargetException());
-						}
-					} catch (NoSuchMethodException e) {
-						// if the method does not exist, ignore it
-					} catch (Throwable t) {
-						// wrap any other exception as a ConfigurationException.
-						throw new ConfigurationException(null,
-								"Configuration update failed", t);
-					}
-				}
-			}
-    	}
+            Class<?>[][] sigs = new Class[][] { { Dictionary.class }, { Component.class, Dictionary.class }, {} };
+            Object[][] args = new Object[][] { { settings }, { m_component, settings }, {} };
+
+            if (m_configType != null) {
+                Object configurable;
+                try {
+                    configurable = Configurable.create(m_configType, settings);
+
+                    sigs = new Class[][] { { Dictionary.class }, { Component.class, Dictionary.class }, { Component.class, m_configType }, { m_configType }, {} };
+                    args = new Object[][] { { settings }, { m_component, settings }, { m_component, configurable }, { configurable }, {} };
+                }
+                catch (Exception e) {
+                    // This is not something we can recover from, use the defaults above...
+                    m_component.getLogger().warn("Failed to create configurable for method %s and configuration type %s!", e, m_add, m_configType);
+                }
+            }
+
+            for (int i = 0; i < instances.length; i++) {
+                try {
+                    InvocationUtil.invokeCallbackMethod(instances[i], m_add, sigs, args);
+                }
+                catch (InvocationTargetException e) {
+                    // The component has thrown an exception during it's callback invocation.
+                    if (e.getTargetException() instanceof ConfigurationException) {
+                        // the callback threw an OSGi ConfigurationException: just re-throw it.
+                        throw (ConfigurationException) e.getTargetException();
+                    }
+                    else {
+                        // wrap the callback exception into a ConfigurationException.
+                        throw new ConfigurationException(null, "Configuration update failed", e.getTargetException());
+                    }
+                }
+                catch (NoSuchMethodException e) {
+                    // if the method does not exist, ignore it
+                }
+                catch (Throwable t) {
+                    // wrap any other exception as a ConfigurationException.
+                    throw new ConfigurationException(null, "Configuration update failed", t);
+                }
+            }
+        }
     }
     
     private synchronized void createMetaTypeImpl() {
diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurableTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurableTest.java
new file mode 100644
index 0000000..36cb606
--- /dev/null
+++ b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurableTest.java
@@ -0,0 +1,259 @@
+/*
+ * 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.dm.impl;
+
+import static org.apache.felix.dm.impl.Configurable.*;
+import static org.junit.Assert.*;
+
+import org.apache.felix.dm.impl.Configurable;
+import org.junit.Test;
+
+import java.util.*;
+
+/**
+ * Test cases for {@link Configurable}.
+ */
+public class ConfigurableTest {
+    private static final double ERROR_MARGIN = 0.0001;
+
+    static enum EnumValue {
+        ONE, TWO, THREE;
+    }
+
+    static interface IfaceA {
+        int getInt();
+
+        double getDouble();
+        
+        Byte getByte();
+
+        String getString();
+
+        int[] getArray();
+        
+        boolean isBoolean();
+
+        List<String> getList();
+
+        SortedMap<String, Object> getMap();
+
+        EnumValue getEnum();
+        
+        Map<String, List<Number>> getMapStringList();
+
+        List<String>[] getArrayOfList();
+    }
+
+    static interface IfaceB {
+        Stack<Integer> getA();
+
+        Set<Integer> getB();
+
+        Queue<String> getC();
+    }
+
+    static interface IfaceC {
+        IfaceA getIfaceA();
+        
+        Class<?> getType();
+    }
+
+    @Test
+    public void testHandleImplicitDefaultValues() {
+        IfaceA ifaceA = create(IfaceA.class, createMap());
+        assertNotNull(ifaceA);
+
+        assertEquals(0, ifaceA.getInt());
+        assertEquals(0.0, ifaceA.getDouble(), ERROR_MARGIN);
+        assertEquals(null, ifaceA.getString());
+        assertArrayEquals(new int[0], ifaceA.getArray());
+        assertEquals(Collections.emptyList(), ifaceA.getList());
+        assertEquals(Collections.emptyMap(), ifaceA.getMap());
+        assertEquals(null, ifaceA.getEnum());
+
+        IfaceB ifaceB = create(IfaceB.class, createMap());
+        assertNotNull(ifaceB);
+        
+        assertEquals(new Stack<>(), ifaceB.getA());
+        assertEquals(Collections.emptySet(), ifaceB.getB());
+        assertEquals(new LinkedList<>(), ifaceB.getC());
+
+        IfaceC ifaceC = create(IfaceC.class, createMap());
+        assertNotNull(ifaceC);
+
+        assertNotNull(ifaceC.getIfaceA());
+        assertNull(ifaceC.getType());
+    }
+
+    @Test
+    public void testHandleInt() {
+        IfaceA cfg = create(IfaceA.class, createMap("int", 41));
+        assertNotNull(cfg);
+
+        assertEquals(41, cfg.getInt());
+    }
+
+    @Test
+    public void testHandleDouble() {
+        IfaceA cfg = create(IfaceA.class, createMap("double", 3.141));
+        assertNotNull(cfg);
+
+        assertEquals(3.141, cfg.getDouble(), ERROR_MARGIN);
+    }
+
+    @Test
+    public void testHandleString() {
+        IfaceA cfg = create(IfaceA.class, createMap("string", "hello"));
+        assertNotNull(cfg);
+
+        assertEquals("hello", cfg.getString());
+    }
+
+    @Test
+    public void testHandleEnum() {
+        IfaceA cfg = create(IfaceA.class, createMap("enum", "two"));
+        assertNotNull(cfg);
+
+        assertEquals(EnumValue.TWO, cfg.getEnum());
+    }
+
+    @Test
+    public void testHandleMapFromString() {
+        IfaceA cfg = create(IfaceA.class, createMap("map.a", "1", "map.b", "2", "map.c", "hello world", "map.d", "[4, 5, 6]"));
+        assertNotNull(cfg);
+
+        Map<String, Object> map = cfg.getMap();
+        assertEquals("1", map.get("a"));
+        assertEquals("2", map.get("b"));
+        assertEquals("hello world", map.get("c"));
+        assertEquals("[4, 5, 6]", map.get("d"));
+        
+        cfg = create(IfaceA.class, createMap("map", "{a.1, b.2, c.3}"));
+        assertNotNull(cfg);
+
+        map = cfg.getMap();
+        assertEquals("1", map.get("a"));
+        assertEquals("2", map.get("b"));
+        assertEquals("3", map.get("c"));
+    }
+
+    @Test
+    public void testHandleObjectFromString() {
+        IfaceC cfg = create(IfaceC.class, createMap(
+            "ifaceA.array", "[4, 5, 6]", 
+            "ifaceA.arrayOfList.0", "[foo, bar]",
+            "ifaceA.arrayOfList.1", "[qux]",
+            "ifaceA.byte", "127",
+            "ifaceA.double", "3.141",
+            "ifaceA.enum", "THREE", 
+            "ifaceA.int", "123",
+            "ifaceA.list", "[qux, quu]",
+            "ifaceA.map.c", "d",
+            "ifaceA.map.a", "b",
+            "ifaceA.mapStringList.x", "[1, 2, 3]",
+            "ifaceA.mapStringList.y", "[4, 5, 6]",
+            "ifaceA.mapStringList.z", "[7, 8, 9]",
+            "ifaceA.string", "hello world", 
+            "ifaceA.boolean", "true",
+            "type", "java.lang.String"));
+        assertNotNull(cfg);
+        
+        SortedMap<String, Object> sortedMap = new TreeMap<>();
+        sortedMap.put("a", "b");
+        sortedMap.put("c", "d");
+        
+        Map<String, List<Number>> mapStringList = new HashMap<>();
+        mapStringList.put("x", Arrays.<Number> asList(1L, 2L, 3L));
+        mapStringList.put("y", Arrays.<Number> asList(4L, 5L, 6L));
+        mapStringList.put("z", Arrays.<Number> asList(7L, 8L, 9L));
+
+        assertEquals(String.class, cfg.getType());
+
+        IfaceA ifaceA = cfg.getIfaceA();
+        assertNotNull(ifaceA);
+
+        assertArrayEquals(new int[] { 4, 5, 6 }, ifaceA.getArray());
+        assertArrayEquals(new List[] { Arrays.asList("foo", "bar"), Arrays.asList("qux") }, ifaceA.getArrayOfList());
+        assertEquals(Byte.valueOf("127"), ifaceA.getByte());
+        assertEquals(3.141, ifaceA.getDouble(), ERROR_MARGIN);
+        assertEquals(EnumValue.THREE, ifaceA.getEnum());
+        assertEquals(123, ifaceA.getInt());
+        assertEquals(Arrays.asList("qux", "quu"), ifaceA.getList());
+        assertEquals(sortedMap, ifaceA.getMap());
+        assertEquals(mapStringList, ifaceA.getMapStringList());
+        assertEquals("hello world", ifaceA.getString());
+        assertEquals(true, ifaceA.isBoolean());
+    }
+
+    @Test
+    public void testHandleArrayDirect() {
+        IfaceA cfg = create(IfaceA.class, createMap("array", new int[] { 2, 3, 4, 5 }));
+        assertNotNull(cfg);
+
+        int[] vals = cfg.getArray();
+        assertEquals(2, vals[0]);
+        assertEquals(3, vals[1]);
+        assertEquals(4, vals[2]);
+        assertEquals(5, vals[3]);
+    }
+
+    @Test
+    public void testHandleArrayFromString() {
+        IfaceA cfg = create(IfaceA.class, createMap("array", "[2,3,4,5]"));
+        assertNotNull(cfg);
+
+        int[] vals = cfg.getArray();
+        assertEquals(2, vals[0]);
+        assertEquals(3, vals[1]);
+        assertEquals(4, vals[2]);
+        assertEquals(5, vals[3]);
+    }
+
+    @Test
+    public void testHandleListDirect() {
+        IfaceA cfg = create(IfaceA.class, createMap("list", Arrays.asList("2", "3", "4", "5")));
+        assertNotNull(cfg);
+
+        List<String> list = cfg.getList();
+        assertEquals("2", list.get(0));
+        assertEquals("3", list.get(1));
+        assertEquals("4", list.get(2));
+        assertEquals("5", list.get(3));
+    }
+
+    @Test
+    public void testHandleListFromString() {
+        IfaceA cfg = create(IfaceA.class, createMap("list", "[2,3,4,5]"));
+        assertNotNull(cfg);
+
+        List<String> list = cfg.getList();
+        assertEquals("2", list.get(0));
+        assertEquals("3", list.get(1));
+        assertEquals("4", list.get(2));
+        assertEquals("5", list.get(3));
+    }
+
+    private static Map<?, ?> createMap(Object... vals) {
+        Map<String, Object> result = new HashMap<>();
+        for (int i = 0; i < vals.length; i += 2) {
+            result.put(vals[i].toString(), vals[i + 1]);
+        }
+        return result;
+    }
+}
diff --git a/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurationDependencyImplTest.java b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurationDependencyImplTest.java
new file mode 100644
index 0000000..c455a77
--- /dev/null
+++ b/dependencymanager/org.apache.felix.dependencymanager/test/org/apache/felix/dm/impl/ConfigurationDependencyImplTest.java
@@ -0,0 +1,198 @@
+/*
+ * 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.dm.impl;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.felix.dm.Logger;
+import org.apache.felix.dm.context.ComponentContext;
+import org.junit.Test;
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+
+/**
+ * @author <a href="mailto:dev@felix.apache.org">Felix Project Team</a>
+ */
+public class ConfigurationDependencyImplTest {
+    public static interface MyMap {
+        String getFoo();
+
+        int getQux();
+
+        String[] getQuu();
+    }
+
+    public static interface MyConfiguration {
+        boolean isTrue();
+
+        int getValue();
+
+        long getLongValue();
+
+        double getPi();
+
+        String[] getArgArray();
+
+        List<String> getArgList();
+
+        MyMap getMap();
+
+        String getMessage();
+    }
+
+    static class PlainService {
+        final CountDownLatch m_latch = new CountDownLatch(1);
+
+        public void updated(Dictionary config) throws ConfigurationException {
+            if (config != null) {
+                m_latch.countDown();
+            }
+            assertConfiguration(config);
+        }
+
+        private void assertConfiguration(Dictionary cfg) {
+            assertEquals("isTrue", "true", cfg.get("true"));
+            assertEquals("getValue", "42", cfg.get("value"));
+            assertEquals("getLongValue", "1234567890", cfg.get("longValue"));
+            assertEquals("getPi", "3.141", cfg.get("pi"));
+            assertEquals("getArgArray", "[a, b, c]", cfg.get("argArray"));
+            assertEquals("getArgList", "[d, e, f]", cfg.get("argList"));
+            assertEquals("getMap.foo", "bar", cfg.get("map.foo"));
+            assertEquals("getMap.qux", "123", cfg.get("map.qux"));
+            assertEquals("getMap.quu", "[x, y, z]", cfg.get("map.quu"));
+            assertEquals("getMessage", "hello world!", cfg.get("message"));
+        }
+    }
+
+    static class AManagedService extends PlainService implements ManagedService {
+        @Override
+        public void updated(Dictionary config) throws ConfigurationException {
+            super.updated(config);
+        }
+    }
+
+    static class FancyService {
+        final CountDownLatch m_latch = new CountDownLatch(1);
+
+        public void updated(MyConfiguration config) throws ConfigurationException {
+            if (config != null) {
+                m_latch.countDown();
+            }
+            assertConfiguration(config);
+        }
+
+        private void assertConfiguration(MyConfiguration cfg) {
+            assertEquals("isTrue", true, cfg.isTrue());
+            assertEquals("getValue", 42, cfg.getValue());
+            assertEquals("getLongValue", 1234567890L, cfg.getLongValue());
+            assertEquals("getPi", 3.141, cfg.getPi(), 0.001);
+            assertArrayEquals("getArgArray", new String[] { "a", "b", "c" }, cfg.getArgArray());
+            assertEquals("getArgList", Arrays.asList("d", "e", "f"), cfg.getArgList());
+            assertEquals("getMessage", "hello world!", cfg.getMessage());
+
+            MyMap map = cfg.getMap();
+            assertEquals("getMap.getFoo", "bar", map.getFoo());
+            assertEquals("getMap.getQux", 123, map.getQux());
+            assertArrayEquals("getMap.getQuu", new String[] { "x", "y", "z" }, map.getQuu());
+        }
+    }
+
+    @Test
+    public void testInvokeManagedServiceUpdatedMethodOk() throws Exception {
+        AManagedService service = new AManagedService();
+
+        ConfigurationDependencyImpl cdi = createConfigurationDependency(service);
+        cdi.updated(createDictionary());
+
+        assertTrue(service.m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    @Test
+    public void testInvokePlainUpdatedMethodOk() throws Exception {
+        PlainService service = new PlainService();
+
+        ConfigurationDependencyImpl cdi = createConfigurationDependency(service);
+        cdi.updated(createDictionary());
+
+        assertTrue(service.m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    @Test
+    public void testInvokeFancyUpdatedMethodOk() throws Exception {
+        FancyService service = new FancyService();
+
+        ConfigurationDependencyImpl cdi = createConfigurationDependency(service);
+        cdi.setCallback(service, "updated", MyConfiguration.class);
+        cdi.updated(createDictionary());
+
+        assertTrue(service.m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    @Test
+    public void testDoNotInvokeFancyUpdatedMethodWithWrongSignatureOk() throws Exception {
+        FancyService service = new FancyService();
+
+        ConfigurationDependencyImpl cdi = createConfigurationDependency(service);
+        cdi.setCallback(service, "updated", Dictionary.class);
+        cdi.updated(createDictionary());
+
+        assertFalse(service.m_latch.await(1, TimeUnit.SECONDS));
+    }
+
+    private Dictionary createDictionary() {
+        Dictionary<String, Object> result = new Hashtable<>();
+        result.put("true", "true");
+        result.put("value", "42");
+        result.put("longValue", "1234567890");
+        result.put("pi", "3.141");
+        result.put("argArray", "[a, b, c]");
+        result.put("argList", "[d, e, f]");
+        result.put("map.foo", "bar");
+        result.put("map.qux", "123");
+        result.put("map.quu", "[x, y, z]");
+        result.put("message", "hello world!");
+        return result;
+    }
+
+    private ConfigurationDependencyImpl createConfigurationDependency(Object service) {
+        Logger mockLogger = mock(Logger.class);
+
+        ComponentContext component = mock(ComponentContext.class);
+        when(component.getExecutor()).thenReturn(Executors.newSingleThreadExecutor());
+        when(component.getLogger()).thenReturn(mockLogger);
+
+        ConfigurationDependencyImpl result = new ConfigurationDependencyImpl();
+        result.setCallback(service, "updated").setPid("does.not.matter");
+        result.setComponentContext(component);
+        return result;
+    }
+}