UNOMI-106 Infinite loop on startup
- Refactored ConfigSharingService to use a single one across the whole application. It also supports listeners if needed.

Signed-off-by: Serge Huber <shuber@apache.org>
diff --git a/api/src/main/java/org/apache/unomi/api/services/ConfigSharingService.java b/api/src/main/java/org/apache/unomi/api/services/ConfigSharingService.java
index 1bfb050..c1d91af 100644
--- a/api/src/main/java/org/apache/unomi/api/services/ConfigSharingService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/ConfigSharingService.java
@@ -16,17 +16,54 @@
  */
 package org.apache.unomi.api.services;
 
+import java.util.Set;
+
 /**
- * A service to share cfg properties with other bundles.
+ * A service to share configuration properties with other bundles. It also support listeners that will be called whenever
+ * a property is added/updated/removed. Simply register a service with the @link ConfigSharingServiceConfigChangeListener interface and it will
+ * be automatically picked up.
  */
 public interface ConfigSharingService {
 
-    String getOneshotImportUploadDir();
+    Object getProperty(String name);
+    Object setProperty(String name, Object value);
+    boolean hasProperty(String name);
+    Object removeProperty(String name);
+    Set<String> getPropertyNames();
 
-    void setOneshotImportUploadDir(String oneshotImportUploadDir);
+    class ConfigChangeEvent {
+        public enum ConfigChangeEventType { ADDED, UPDATED, REMOVED };
+        private ConfigChangeEventType eventType;
+        private String name;
+        private Object oldValue;
+        private Object newValue;
 
-    String getInternalServerPort();
+        public ConfigChangeEvent(ConfigChangeEventType eventType, String name, Object oldValue, Object newValue) {
+            this.eventType = eventType;
+            this.name = name;
+            this.oldValue = oldValue;
+            this.newValue = newValue;
+        }
 
-    void setInternalServerPort(String internalServerPort);
+        public ConfigChangeEventType getEventType() {
+            return eventType;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public Object getOldValue() {
+            return oldValue;
+        }
+
+        public Object getNewValue() {
+            return newValue;
+        }
+    }
+
+    interface ConfigChangeListener {
+        void configChanged(ConfigChangeEvent configChangeEvent);
+    }
 
 }
diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/config/ConfigSharingServiceImpl.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/config/ConfigSharingServiceImpl.java
deleted file mode 100644
index 6aa357e..0000000
--- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/config/ConfigSharingServiceImpl.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * 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.unomi.router.core.config;
-
-import org.apache.unomi.api.services.ConfigSharingService;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.BundleEvent;
-import org.osgi.framework.SynchronousBundleListener;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Created by amidani on 27/06/2017.
- */
-public class ConfigSharingServiceImpl implements ConfigSharingService, SynchronousBundleListener {
-
-    private static final Logger logger = LoggerFactory.getLogger(ConfigSharingServiceImpl.class);
-
-    private String oneshotImportUploadDir;
-    private BundleContext bundleContext;
-
-    public void setBundleContext(BundleContext bundleContext) {
-        this.bundleContext = bundleContext;
-    }
-
-    @Override
-    public String getOneshotImportUploadDir() {
-        return oneshotImportUploadDir;
-    }
-
-    @Override
-    public void setOneshotImportUploadDir(String oneshotImportUploadDir) {
-        this.oneshotImportUploadDir = oneshotImportUploadDir;
-    }
-
-    /** Methods below not used in router bundle implementation of the ConfigSharingService **/
-
-    @Override
-    public String getInternalServerPort() {
-        return null;
-    }
-
-    @Override
-    public void setInternalServerPort(String internalServerPort) { }
-
-
-    public void preDestroy() throws Exception {
-        bundleContext.removeBundleListener(this);
-        logger.info("Config sharing service for Router is shutdown.");
-    }
-
-    private void processBundleStartup(BundleContext bundleContext) {
-        if (bundleContext == null) {
-            return;
-        }
-    }
-
-    @Override
-    public void bundleChanged(BundleEvent bundleEvent) {
-
-    }
-
-}
diff --git a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java
index 5a26a15..32ceba8 100644
--- a/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java
+++ b/extensions/router/router-core/src/main/java/org/apache/unomi/router/core/context/RouterCamelContext.java
@@ -21,6 +21,7 @@
 import org.apache.camel.component.jackson.JacksonDataFormat;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.model.RouteDefinition;
+import org.apache.unomi.api.services.ConfigSharingService;
 import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.router.api.ExportConfiguration;
 import org.apache.unomi.router.api.ImportConfiguration;
@@ -63,13 +64,21 @@
     private String configType;
     private String allowedEndpoints;
     private BundleContext bundleContext;
+    private ConfigSharingService configSharingService;
 
     public void setBundleContext(BundleContext bundleContext) {
         this.bundleContext = bundleContext;
     }
 
+    public void setConfigSharingService(ConfigSharingService configSharingService) {
+        this.configSharingService = configSharingService;
+    }
+
     public void initCamelContext() throws Exception {
         logger.info("Initialize Camel Context...");
+
+        configSharingService.setProperty("oneshotImportUploadDir", uploadDir);
+
         camelContext = new DefaultCamelContext();
 
         //--IMPORT ROUTES
diff --git a/extensions/router/router-core/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/extensions/router/router-core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index aa39b15..5ae1e9c 100644
--- a/extensions/router/router-core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/extensions/router/router-core/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -39,17 +39,6 @@
         </cm:default-properties>
     </cm:property-placeholder>
 
-    <bean id="configSharingServiceRB" class="org.apache.unomi.router.core.config.ConfigSharingServiceImpl" destroy-method="preDestroy">
-        <property name="oneshotImportUploadDir" value="${import.oneshot.uploadDir}"/>
-        <property name="bundleContext" ref="blueprintBundleContext"/>
-    </bean>
-
-    <service id="configSharingServiceRBE" ref="configSharingServiceRB" auto-export="interfaces">
-        <service-properties>
-            <entry key="bundleDiscriminator" value="ROUTER"/>
-        </service-properties>
-    </service>
-
     <bean id="unomiStorageProcessor" class="org.apache.unomi.router.core.processor.UnomiStorageProcessor">
         <property name="profileImportService" ref="profileImportService"/>
     </bean>
@@ -116,6 +105,7 @@
         <property name="persistenceService" ref="persistenceService"/>
         <property name="jacksonDataFormat" ref="jacksonDataFormat"/>
         <property name="bundleContext" ref="blueprintBundleContext"/>
+        <property name="configSharingService" ref="configSharingService" />
     </bean>
 
     <camel:camelContext id="httpEndpoint" xmlns="http://camel.apache.org/schema/blueprint">
@@ -126,6 +116,7 @@
         <property name="routerCamelContext" ref="camelContext"/>
     </bean>
 
+    <reference id="configSharingService" interface="org.apache.unomi.api.services.ConfigSharingService" />
     <reference id="httpService" interface="org.osgi.service.http.HttpService"/>
     <reference id="profileImportService" interface="org.apache.unomi.router.api.services.ProfileImportService"/>
     <reference id="persistenceService" interface="org.apache.unomi.persistence.spi.PersistenceService"/>
diff --git a/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/AbstractConfigurationServiceEndpoint.java b/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/AbstractConfigurationServiceEndpoint.java
index 195cd68..d7abbb2 100644
--- a/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/AbstractConfigurationServiceEndpoint.java
+++ b/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/AbstractConfigurationServiceEndpoint.java
@@ -30,17 +30,11 @@
 public abstract class AbstractConfigurationServiceEndpoint<T> {
 
     protected ImportExportConfigurationService<T> configurationService;
-    protected ConfigSharingService routerConfigSharingService;
-    protected ConfigSharingService clusterConfigSharingService;
+    protected ConfigSharingService configSharingService;
 
     @WebMethod(exclude = true)
-    public void setRouterConfigSharingService(ConfigSharingService routerConfigSharingService) {
-        this.routerConfigSharingService = routerConfigSharingService;
-    }
-
-    @WebMethod(exclude = true)
-    public void setClusterConfigSharingService(ConfigSharingService clusterConfigSharingService) {
-        this.clusterConfigSharingService = clusterConfigSharingService;
+    public void setConfigSharingService(ConfigSharingService configSharingService) {
+        this.configSharingService = configSharingService;
     }
 
     /**
diff --git a/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ExportConfigurationServiceEndPoint.java b/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ExportConfigurationServiceEndPoint.java
index d5130ba..b3bcf65 100644
--- a/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ExportConfigurationServiceEndPoint.java
+++ b/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ExportConfigurationServiceEndPoint.java
@@ -65,7 +65,7 @@
         if (RouterConstants.IMPORT_EXPORT_CONFIG_TYPE_RECURRENT.equals(exportConfigSaved.getConfigType())) {
             CloseableHttpClient httpClient = HttpClients.createDefault();
             try {
-                HttpPut httpPut = new HttpPut("http://localhost:" + clusterConfigSharingService.getInternalServerPort() + "/configUpdate/exportConfigAdmin");
+                HttpPut httpPut = new HttpPut("http://localhost:" + configSharingService.getProperty("internalServerPort") + "/configUpdate/exportConfigAdmin");
                 StringEntity input = new StringEntity(new ObjectMapper().writeValueAsString(exportConfigSaved));
                 input.setContentType(MediaType.APPLICATION_JSON);
                 httpPut.setEntity(input);
diff --git a/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ImportConfigurationServiceEndPoint.java b/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ImportConfigurationServiceEndPoint.java
index e05167f..25ab70d 100644
--- a/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ImportConfigurationServiceEndPoint.java
+++ b/extensions/router/router-rest/src/main/java/org/apache/unomi/router/rest/ImportConfigurationServiceEndPoint.java
@@ -75,7 +75,7 @@
         if (RouterConstants.IMPORT_EXPORT_CONFIG_TYPE_RECURRENT.equals(importConfigSaved.getConfigType())) {
             CloseableHttpClient httpClient = HttpClients.createDefault();
             try {
-                HttpPut httpPut = new HttpPut("http://localhost:" + clusterConfigSharingService.getInternalServerPort() + "/configUpdate/importConfigAdmin");
+                HttpPut httpPut = new HttpPut("http://localhost:" + configSharingService.getProperty("internalServerPort") + "/configUpdate/importConfigAdmin");
                 StringEntity input = new StringEntity(new ObjectMapper().writeValueAsString(importConfigSaved));
                 input.setContentType(MediaType.APPLICATION_JSON);
                 httpPut.setEntity(input);
@@ -105,7 +105,7 @@
     @Produces(MediaType.APPLICATION_JSON)
     public Response processOneshotImportConfigurationCSV(@Multipart(value = "importConfigId") String importConfigId, @Multipart(value = "file") Attachment file) {
         try {
-            java.nio.file.Path path = Paths.get(routerConfigSharingService.getOneshotImportUploadDir() + importConfigId + ".csv");
+            java.nio.file.Path path = Paths.get(configSharingService.getProperty("oneshotImportUploadDir") + importConfigId + ".csv");
             Files.deleteIfExists(path);
             InputStream in = file.getObject(InputStream.class);
 
diff --git a/extensions/router/router-rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/extensions/router/router-rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 8cd4948..6c3c367 100644
--- a/extensions/router/router-rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/extensions/router/router-rest/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -17,13 +17,10 @@
   -->
 
 <blueprint xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-           xmlns:cm="http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.1.0"
            xmlns:jaxrs="http://cxf.apache.org/blueprint/jaxrs"
            xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
            xsi:schemaLocation="http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd
-                              http://cxf.apache.org/blueprint/jaxrs http://cxf.apache.org/schemas/blueprint/jaxrs.xsd
-
-                              http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.1.0 http://aries.apache.org/schemas/blueprint-cm/blueprint-cm-1.1.0.xsd">
+                              http://cxf.apache.org/blueprint/jaxrs http://cxf.apache.org/schemas/blueprint/jaxrs.xsd">
 
     <bean id="cors-filter" class="org.apache.cxf.rs.security.cors.CrossOriginResourceSharingFilter"/>
     <bean id="jacksonMapper" class="com.fasterxml.jackson.databind.ObjectMapper"/>
@@ -69,22 +66,16 @@
     <reference id="exportConfigurationService" interface="org.apache.unomi.router.api.services.ImportExportConfigurationService"
                filter="(configDiscriminator=EXPORT)"/>
 
-    <reference id="configSharingServiceRouter" interface="org.apache.unomi.api.services.ConfigSharingService"
-               filter="(bundleDiscriminator=ROUTER)"/>
-
-    <reference id="configSharingServiceCluster" interface="org.apache.unomi.api.services.ConfigSharingService"
-               filter="(bundleDiscriminator=SERVICES)"/>
+    <reference id="configSharingService" interface="org.apache.unomi.api.services.ConfigSharingService"/>
 
     <bean id="importConfigurationServiceEndPoint" class="org.apache.unomi.router.rest.ImportConfigurationServiceEndPoint">
         <property name="importConfigurationService" ref="importConfigurationService"/>
-        <property name="routerConfigSharingService" ref="configSharingServiceRouter"/>
-        <property name="clusterConfigSharingService" ref="configSharingServiceCluster"/>
+        <property name="configSharingService" ref="configSharingService"/>
     </bean>
 
     <bean id="exportConfigurationServiceEndPoint" class="org.apache.unomi.router.rest.ExportConfigurationServiceEndPoint">
         <property name="exportConfigurationService" ref="exportConfigurationService"/>
-        <property name="routerConfigSharingService" ref="configSharingServiceRouter"/>
-        <property name="clusterConfigSharingService" ref="configSharingServiceCluster"/>
+        <property name="configSharingService" ref="configSharingService"/>
     </bean>
 
 </blueprint>
diff --git a/services/src/main/java/org/apache/unomi/services/services/ConfigSharingServiceImpl.java b/services/src/main/java/org/apache/unomi/services/services/ConfigSharingServiceImpl.java
index 2b7da8a..6583910 100644
--- a/services/src/main/java/org/apache/unomi/services/services/ConfigSharingServiceImpl.java
+++ b/services/src/main/java/org/apache/unomi/services/services/ConfigSharingServiceImpl.java
@@ -17,48 +17,76 @@
 package org.apache.unomi.services.services;
 
 import org.apache.unomi.api.services.ConfigSharingService;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.BundleEvent;
-import org.osgi.framework.SynchronousBundleListener;
+import org.osgi.framework.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
 /**
- * Created by amidani on 27/06/2017.
+ * An implementation of the ConfigSharingService that supports listeners that will be called when a property is added,
+ * updated or removed. The properties are stored in a ConcurrentHashMap so access should be thread-safe.
  */
 public class ConfigSharingServiceImpl implements ConfigSharingService, SynchronousBundleListener {
 
     private static final Logger logger = LoggerFactory.getLogger(ConfigSharingServiceImpl.class);
 
-    private String internalServerPort;
-
     private BundleContext bundleContext;
+    private Map<String,Object> configProperties = new ConcurrentHashMap<String,Object>();
 
     public void setBundleContext(BundleContext bundleContext) {
         this.bundleContext = bundleContext;
     }
 
-    @Override
-    public String getInternalServerPort() {
-        return this.internalServerPort;
+    public void setConfigProperties(Map<String, Object> configProperties) {
+        this.configProperties = configProperties;
     }
 
     @Override
-    public void setInternalServerPort(String internalServerPort) {
-        this.internalServerPort = internalServerPort;
-    }
-
-    /**
-     * Methods below not used in services bundle implementation of the ConfigSharingService
-     **/
-
-    @Override
-    public String getOneshotImportUploadDir() {
-        return null;
+    public Object getProperty(String name) {
+        return configProperties.get(name);
     }
 
     @Override
-    public void setOneshotImportUploadDir(String oneshotImportUploadDir) {
+    public Object setProperty(String name, Object newValue) {
+        boolean existed = false;
+        if (configProperties.containsKey(name)) {
+            existed = true;
+        }
+        Object oldValue = configProperties.put(name, newValue);
+        if (existed) {
+            firePropertyUpdatedEvent(name, oldValue, newValue);
+        } else {
+            firePropertyAddedEvent(name, newValue);
+        }
+        return oldValue;
+    }
+
+    @Override
+    public boolean hasProperty(String name) {
+        return configProperties.containsKey(name);
+    }
+
+    @Override
+    public Object removeProperty(String name) {
+        boolean existed = false;
+        if (configProperties.containsKey(name)) {
+            existed = true;
+        }
+        Object oldValue = configProperties.remove(name);
+        if (existed) {
+            firePropertyRemovedEvent(name, oldValue);
+        }
+        return oldValue;
+    }
+
+    @Override
+    public Set<String> getPropertyNames() {
+        return configProperties.keySet();
     }
 
     public void preDestroy() throws Exception {
@@ -77,4 +105,47 @@
 
     }
 
+    private void firePropertyAddedEvent(String name, Object newValue) {
+        fireConfigChangeEvent(new ConfigChangeEvent(ConfigChangeEvent.ConfigChangeEventType.ADDED, name, null, newValue));
+    }
+
+    private void firePropertyUpdatedEvent(String name, Object oldValue, Object newValue) {
+        fireConfigChangeEvent(new ConfigChangeEvent(ConfigChangeEvent.ConfigChangeEventType.UPDATED, name, oldValue, newValue));
+    }
+
+    private void firePropertyRemovedEvent(String name, Object oldValue) {
+        fireConfigChangeEvent(new ConfigChangeEvent(ConfigChangeEvent.ConfigChangeEventType.REMOVED, name, oldValue, null));
+    }
+
+    private void fireConfigChangeEvent(ConfigChangeEvent configChangeEvent) {
+        List<ConfigChangeListener> listeners = getListeners();
+        for (ConfigChangeListener configChangeListener : listeners) {
+            configChangeListener.configChanged(configChangeEvent);
+        }
+    }
+
+    /**
+     * This method is called a lot because this list may change at any time as listeners may come and go in OSGi
+     * @return a list of ConfigChangeListeners that will be used to listen to property changes
+     */
+    private List<ConfigChangeListener> getListeners() {
+        List<ConfigChangeListener> listeners = new ArrayList<>();
+        try {
+            ServiceReference<?>[] allListenerReferences = bundleContext.getAllServiceReferences(ConfigChangeListener.class.getName(), null);
+            if (allListenerReferences == null) {
+                return listeners;
+            }
+            for (ServiceReference<?> listenerReference : allListenerReferences) {
+                ConfigChangeListener configChangeListener = (ConfigChangeListener) bundleContext.getService(listenerReference);
+                if (configChangeListener != null) {
+                    listeners.add(configChangeListener);
+                }
+            }
+        } catch (InvalidSyntaxException e) {
+            e.printStackTrace();
+            return listeners;
+        }
+        return listeners;
+    }
+
 }
diff --git a/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index b6c4f6c..8bbdcc6 100644
--- a/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -235,15 +235,16 @@
         </bean>
     </service>
 
-    <bean id="configSharingServiceCB" class="org.apache.unomi.services.services.ConfigSharingServiceImpl" destroy-method="preDestroy">
-        <property name="internalServerPort" value="${cluster.contextserver.port}"/>
+    <bean id="configSharingServiceImpl" class="org.apache.unomi.services.services.ConfigSharingServiceImpl" destroy-method="preDestroy">
+        <property name="configProperties">
+            <map>
+                <entry key="internalServerPort" value="${cluster.contextserver.port}" />
+            </map>
+        </property>
         <property name="bundleContext" ref="blueprintBundleContext"/>
     </bean>
 
-    <service id="configSharingServiceCBE" ref="configSharingServiceCB" auto-export="interfaces">
-        <service-properties>
-            <entry key="bundleDiscriminator" value="SERVICES"/>
-        </service-properties>
+    <service id="configSharingService" ref="configSharingServiceImpl" auto-export="interfaces">
     </service>
 
 </blueprint>