SLING-10538 : Support merging of configurations
diff --git a/pom.xml b/pom.xml
index 5740253..09b4e1c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -90,7 +90,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.installer.core</artifactId>
-            <version>3.9.0</version>
+            <version>3.11.5-SNAPSHOT</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/Activator.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/Activator.java
index d09a6c5..cdf0092 100644
--- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/Activator.java
+++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/Activator.java
@@ -18,6 +18,9 @@
  */
 package org.apache.sling.installer.factories.configuration.impl;
 
+import java.util.Arrays;
+import java.util.List;
+
 import org.osgi.annotation.bundle.Header;
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
@@ -32,23 +35,30 @@
     /** Property for bundle location default. */
     private static final String PROP_LOCATION_DEFAULT = "sling.installer.config.useMulti";
 
+    /** Property for configuration merge schemes. */
+    private static final String PROP_MERGE_SCHEMES = "sling.installer.config.mergeSchemes";
+
     /** Services listener. */
     private ServicesListener listener;
 
     public static String DEFAULT_LOCATION;
 
+    public static List<String> MERGE_SCHEMES;
+
+
     /**
      * @see org.osgi.framework.BundleActivator#start(org.osgi.framework.BundleContext)
      */
     public void start(final BundleContext context) throws Exception {
-        String locationDefault = null;
         if ( context.getProperty(PROP_LOCATION_DEFAULT) != null ) {
             final Boolean bool = Boolean.valueOf(context.getProperty(PROP_LOCATION_DEFAULT).toString());
             if ( bool.booleanValue() ) {
-                locationDefault = "?";
+                DEFAULT_LOCATION = "?";
             }
         }
-        DEFAULT_LOCATION = locationDefault;
+        if ( context.getProperty(PROP_MERGE_SCHEMES) != null ) {
+            MERGE_SCHEMES = Arrays.asList(context.getProperty(PROP_MERGE_SCHEMES).split(","));
+        }
         this.listener = new ServicesListener(context);
     }
 
diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigInstallTask.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigInstallTask.java
index 7285e86..152633e 100644
--- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigInstallTask.java
+++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigInstallTask.java
@@ -20,9 +20,17 @@
 
 import java.io.IOException;
 import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
 
 import org.apache.sling.installer.api.tasks.InstallationContext;
 import org.apache.sling.installer.api.tasks.ResourceState;
+import org.apache.sling.installer.api.tasks.TaskResource;
 import org.apache.sling.installer.api.tasks.TaskResourceGroup;
 import org.apache.sling.installer.factories.configuration.ConfigurationConstants;
 import org.apache.sling.installer.factories.configuration.impl.Coordinator.Operation;
@@ -36,8 +44,8 @@
 
     private static final String CONFIG_INSTALL_ORDER = "20-";
 
-    public ConfigInstallTask(final TaskResourceGroup r, final ConfigurationAdmin configAdmin) {
-        super(r, configAdmin);
+    public ConfigInstallTask(final TaskResourceGroup group, final ConfigurationAdmin configAdmin) {
+        super(group, configAdmin);
     }
 
     @Override
@@ -46,13 +54,40 @@
     }
 
 	@Override
+    protected Dictionary<String, Object> getDictionary() {
+        Dictionary<String, Object> properties = super.getDictionary();
+
+        if ( Activator.MERGE_SCHEMES != null ) {
+            final List<Dictionary<String, Object>> propertiesList = new ArrayList<>();
+            propertiesList.add(properties);
+            final Iterator<TaskResource> iter = this.getResourceGroup().getActiveResourceIterator();
+            if ( iter != null ) {
+                // skip first active resource
+                iter.next();
+                while ( iter.hasNext()) {
+                    final TaskResource rsrc = iter.next();
+                    
+                    if ( Activator.MERGE_SCHEMES.contains(rsrc.getScheme())) {
+                        propertiesList.add(rsrc.getDictionary());
+                    }
+                }
+            }
+            if ( propertiesList.size() > 1 ) {
+                properties = ConfigUtil.mergeReverseOrder(propertiesList);
+            }
+        }
+        return properties;
+    }
+
+    @Override
     public void execute(final InstallationContext ctx) {
         synchronized ( Coordinator.SHARED ) {
             // Get or create configuration, but do not
             // update if the new one has the same values.
+            final Dictionary<String, Object> properties = this.getDictionary();
             boolean created = false;
             try {
-                String location = (String)this.getResource().getDictionary().get(ConfigurationConstants.PROPERTY_BUNDLE_LOCATION);
+                String location = (String)properties.get(ConfigurationConstants.PROPERTY_BUNDLE_LOCATION);
                 if ( location == null ) {
                     location = Activator.DEFAULT_LOCATION; // default
                 } else if ( location.length() == 0 ) {
@@ -65,7 +100,7 @@
                     config = ConfigUtil.createConfiguration(this.getConfigurationAdmin(), this.factoryPid, this.configPid, location);
                     created = true;
                 } else {
-        			if (ConfigUtil.isSameData(config.getProperties(), getResource().getDictionary())) {
+        			if (ConfigUtil.isSameData(config.getProperties(), properties)) {
         			    this.getLogger().debug("Configuration {} already installed with same data, update request ignored: {}",
         	                        config.getPid(), getResource());
         				config = null;
@@ -75,7 +110,7 @@
                 }
 
                 if (config != null) {
-                    config.update(getDictionary());
+                    config.update(properties);
                     ctx.log("Installed configuration {} from resource {}", config.getPid(), getResource());
                     this.getLogger().debug("Configuration " + config.getPid()
                                 + " " + (created ? "created" : "updated")
diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java
index 92d824e..6b7559b 100644
--- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java
+++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigTaskCreator.java
@@ -18,8 +18,10 @@
  */
 package org.apache.sling.installer.factories.configuration.impl;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Dictionary;
+import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.Hashtable;
 import java.util.List;
@@ -27,6 +29,9 @@
 
 import org.apache.sling.installer.api.InstallableResource;
 import org.apache.sling.installer.api.ResourceChangeListener;
+import org.apache.sling.installer.api.info.InfoProvider;
+import org.apache.sling.installer.api.info.Resource;
+import org.apache.sling.installer.api.info.ResourceGroup;
 import org.apache.sling.installer.api.tasks.ChangeStateTask;
 import org.apache.sling.installer.api.tasks.InstallTask;
 import org.apache.sling.installer.api.tasks.InstallTaskFactory;
@@ -62,10 +67,15 @@
     /** Resource change listener. */
     private final ResourceChangeListener changeListener;
 
+    /** Info Provider */
+    private final InfoProvider infoProvider;
+
     public ConfigTaskCreator(final ResourceChangeListener listener,
-            final ConfigurationAdmin configAdmin) {
+            final ConfigurationAdmin configAdmin,
+            final InfoProvider infoProvider) {
         this.changeListener = listener;
         this.configAdmin = configAdmin;
+        this.infoProvider = infoProvider;
     }
 
     public ServiceRegistration<?> register(final BundleContext bundleContext) {
@@ -80,7 +90,10 @@
                 ConfigurationListener.class.getName(),
                 ResourceTransformer.class.getName()
         };
-        return bundleContext.registerService(serviceInterfaces, this, props);
+        final ServiceRegistration<?> reg = bundleContext.registerService(serviceInterfaces, this, props);
+        this.logger.info("OSGi Configuration support for OSGi installer active, default location={}, merge schemes={}", 
+                Activator.DEFAULT_LOCATION, Activator.MERGE_SCHEMES);
+        return reg;
     }
 
     /**
@@ -151,6 +164,7 @@
                             attrs.put(ConfigurationAdmin.SERVICE_FACTORYPID, event.getFactoryPid());
                         }
 
+                        removeDefaultProperties(event.getPid(), dict);
                         this.changeListener.resourceAddedOrUpdated(InstallableResource.TYPE_CONFIG, event.getPid(), null, dict, attrs);
 
                     } else {
@@ -163,6 +177,40 @@
         }
     }
 
+    private void removeDefaultProperties(final String pid, final Dictionary<String, Object> dict) {
+        if ( Activator.MERGE_SCHEMES != null ) {
+            final List<Dictionary<String, Object>> propertiesList = new ArrayList<>();
+            final String entityId = InstallableResource.TYPE_CONFIG.concat(":").concat(pid);
+            boolean done = false;
+            for(final ResourceGroup group : this.infoProvider.getInstallationState().getInstalledResources()) {
+                for(final Resource rsrc : group.getResources()) {
+                    if ( rsrc.getEntityId().equals(entityId) ) {
+                        done = true;
+                        if ( Activator.MERGE_SCHEMES.contains(rsrc.getScheme()) ) {
+                            propertiesList.add(rsrc.getDictionary());
+                        }
+                    }
+                }
+                if ( done ) {
+                    break;
+                }
+            }
+            if ( !propertiesList.isEmpty() ) {
+                final Dictionary<String, Object> defaultProps = ConfigUtil.mergeReverseOrder(propertiesList);
+                final Enumeration<String> keyEnum = defaultProps.keys();
+                while ( keyEnum.hasMoreElements() ) {
+                    final String key = keyEnum.nextElement();
+                    final Object value = defaultProps.get(key);
+
+                    final Object newValue = dict.get(key);
+                    if ( newValue != null && ConfigUtil.isSameValue(newValue, value)) {
+                        dict.remove(key);
+                    }
+                }
+            }
+        }
+    }
+
     /**
      * @see org.apache.sling.installer.api.tasks.ResourceTransformer#transform(org.apache.sling.installer.api.tasks.RegisteredResource)
      */
diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigUtil.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigUtil.java
index 1e42ade..30f3f50 100644
--- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigUtil.java
+++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ConfigUtil.java
@@ -20,10 +20,12 @@
 
 import java.io.IOException;
 import java.lang.reflect.Array;
+import java.util.Collections;
 import java.util.Dictionary;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.Hashtable;
+import java.util.List;
 import java.util.Set;
 
 import org.osgi.framework.Constants;
@@ -98,28 +100,8 @@
                 for(final String key : keysA ) {
                     final Object valA = a.get(key);
                     final Object valB = b.get(key);
-                    if ( valA.getClass().isArray() && valB.getClass().isArray()) {
-                        final Object[] arrA = convertToObjectArray(valA);
-                        final Object[] arrB = convertToObjectArray(valB);
 
-                        if ( arrA.length != arrB.length ) {
-                            result = false;
-                            break;
-                        }
-                        for(int i=0; i<arrA.length; i++) {
-                            if ( !(String.valueOf(arrA[i]).equals(String.valueOf(arrB[i]))) ) {
-                                result = false;
-                                break;
-                            }
-                        }
-                    } else if (!valA.getClass().isArray() && !valB.getClass().isArray()) {
-                        // if no arrays do a string comparison
-                        if ( !(String.valueOf(valA).equals(String.valueOf(valB))) ) {
-                            result = false;
-                            break;
-                        }
-                    } else {
-                        // one value is array the other is not!
+                    if ( !isSameValue(valA, valB) ) {
                         result = false;
                         break;
                     }
@@ -129,6 +111,31 @@
         return result;
     }
 
+    public static boolean isSameValue(final Object valA, final Object valB) {
+        if ( valA.getClass().isArray() && valB.getClass().isArray()) {
+            final Object[] arrA = convertToObjectArray(valA);
+            final Object[] arrB = convertToObjectArray(valB);
+
+            if ( arrA.length != arrB.length ) {
+                return false;
+            }
+            for(int i=0; i<arrA.length; i++) {
+                if ( !(String.valueOf(arrA[i]).equals(String.valueOf(arrB[i]))) ) {
+                    return false;
+                }
+            }
+        } else if (!valA.getClass().isArray() && !valB.getClass().isArray()) {
+            // if no arrays do a string comparison
+            if ( !(String.valueOf(valA).equals(String.valueOf(valB))) ) {
+                return false;
+            }
+        } else {
+            // one value is array the other is not!
+            return false;
+        }
+        return true;
+    }
+
     /**
      * Remove all ignored properties
      */
@@ -278,4 +285,31 @@
     public static String getPIDOfFactoryPID(final String factoryPID, final String name) {
         return factoryPID.concat("~").concat(name);
     }
+
+    /**
+     * Merge all dictionaries into a single dictionary in reverse order
+     * @param propertiesList The list of dictionaries
+     * @return The merged dictionary
+     */
+    public static Dictionary<String, Object> mergeReverseOrder(final List<Dictionary<String, Object>> propertiesList) {
+        Collections.reverse(propertiesList);
+        final Dictionary<String, Object> properties = new Hashtable<>();
+        for(final Dictionary<String, Object> dict : propertiesList) {
+            merge(properties, dict);
+        }
+        return properties;
+    }
+
+    /**
+     * Merge one dictionary into the other
+     * @param base Base dictionary
+     * @param props Overwriting dictionary
+     */
+    private static void merge(final Dictionary<String, Object> base, final Dictionary<String, Object> props) {
+        final Enumeration<String> keyIter = props.keys();
+        while (keyIter.hasMoreElements() ) {
+            final String key = keyIter.nextElement();
+            base.put(key, props.get(key));
+        }
+    }
 }
diff --git a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ServicesListener.java b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ServicesListener.java
index 526bdfe..41e37f9 100644
--- a/src/main/java/org/apache/sling/installer/factories/configuration/impl/ServicesListener.java
+++ b/src/main/java/org/apache/sling/installer/factories/configuration/impl/ServicesListener.java
@@ -21,6 +21,7 @@
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.apache.sling.installer.api.ResourceChangeListener;
+import org.apache.sling.installer.api.info.InfoProvider;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.Constants;
 import org.osgi.framework.InvalidSyntaxException;
@@ -49,6 +50,9 @@
     /** The listener for the configuration admin. */
     private final Listener configAdminListener;
 
+    /** The listener for the installer info service. */
+    private final Listener infoServiceListener;
+
     /** Registration the service. */
     private volatile ServiceRegistration<?> configTaskCreatorRegistration;
 
@@ -60,6 +64,8 @@
         this.bundleContext = bundleContext;
         this.changeHandlerListener = new Listener(ResourceChangeListener.class.getName());
         this.configAdminListener = new Listener(ConfigurationAdmin.class.getName());
+        this.infoServiceListener = new Listener(InfoProvider.class.getName());
+        this.infoServiceListener.start();
         this.changeHandlerListener.start();
         this.configAdminListener.start();
     }
@@ -68,12 +74,13 @@
         // check if all services are available
         final ResourceChangeListener listener = (ResourceChangeListener)this.changeHandlerListener.getService();
         final ConfigurationAdmin configAdmin = (ConfigurationAdmin)this.configAdminListener.getService();
+        final InfoProvider infoProvider = (InfoProvider)this.infoServiceListener.getService();
 
-        if ( configAdmin != null && listener != null ) {
+        if ( configAdmin != null && listener != null && infoProvider != null ) {
             if ( configTaskCreator == null ) {
                 active.set(true);
                 // start and register osgi installer service
-                this.configTaskCreator = new ConfigTaskCreator(listener, configAdmin);
+                this.configTaskCreator = new ConfigTaskCreator(listener, configAdmin, infoProvider);
                 final ConfigUpdateHandler handler = new ConfigUpdateHandler(configAdmin, this);
                 configTaskCreatorRegistration = handler.register(this.bundleContext);
             }
@@ -107,6 +114,7 @@
      * Deactivate this listener.
      */
     public void deactivate() {
+        this.infoServiceListener.deactivate();
         this.changeHandlerListener.deactivate();
         this.configAdminListener.deactivate();
         this.stop();