You are viewing a plain text version of this content. The canonical link for it is here.
Posted to oak-commits@jackrabbit.apache.org by ch...@apache.org on 2014/05/27 08:14:30 UTC

svn commit: r1597707 - in /jackrabbit/oak/trunk/oak-pojosr: ./ src/main/java/org/apache/jackrabbit/oak/run/osgi/ src/test/groovy/ src/test/groovy/org/ src/test/groovy/org/apache/ src/test/groovy/org/apache/jackrabbit/ src/test/groovy/org/apache/jackrab...

Author: chetanm
Date: Tue May 27 06:14:29 2014
New Revision: 1597707

URL: http://svn.apache.org/r1597707
Log:
OAK-1856 - Enable specifying of OSGi config via JSON file and in memory map

Added required support and also added Groovy based testcase

Added:
    jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/ConfigTracker.java   (with props)
    jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/
    jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/
    jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/apache/
    jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/apache/jackrabbit/
    jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/
    jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/
    jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/osgi/
    jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/osgi/ConfigTest.groovy
Modified:
    jackrabbit/oak/trunk/oak-pojosr/pom.xml
    jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/OakOSGiRepositoryFactory.java

Modified: jackrabbit/oak/trunk/oak-pojosr/pom.xml
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-pojosr/pom.xml?rev=1597707&r1=1597706&r2=1597707&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-pojosr/pom.xml (original)
+++ jackrabbit/oak/trunk/oak-pojosr/pom.xml Tue May 27 06:14:29 2014
@@ -32,9 +32,37 @@
 
   <properties>
     <skip.deployment>true</skip.deployment>
+    <groovy.version>2.3.1</groovy.version>
   </properties>
 
   <build>
+    <plugins>
+      <plugin>
+        <groupId>org.codehaus.gmaven</groupId>
+        <artifactId>gmaven-plugin</artifactId>
+        <version>1.4</version>
+        <!--suppress MavenModelInspection -->
+        <configuration>
+          <providerSelection>2.0</providerSelection>
+          <sourceEncoding>UTF-8</sourceEncoding>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>generateTestStubs</goal>
+              <goal>testCompile</goal>
+            </goals>
+          </execution>
+        </executions>
+        <dependencies>
+          <dependency>
+            <groupId>org.codehaus.groovy</groupId>
+            <artifactId>groovy-all</artifactId>
+            <version>${groovy.version}</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+    </plugins>
     <pluginManagement>
       <plugins>
         <plugin>
@@ -107,18 +135,16 @@
     </dependency>
 
     <!-- Pojo SR -->
-
+    <dependency>
+      <groupId>org.osgi</groupId>
+      <artifactId>org.osgi.core</artifactId>
+      <version>4.3.1</version>
+    </dependency>
     <dependency>
       <groupId>com.googlecode.pojosr</groupId>
       <artifactId>de.kalpatec.pojosr.framework.bare</artifactId>
       <version>0.2.1</version>
     </dependency>
-
-    <dependency>
-      <groupId>org.osgi</groupId>
-      <artifactId>org.osgi.core</artifactId>
-      <version>4.3.0</version>
-    </dependency>
     <dependency>
       <groupId>org.apache.felix</groupId>
       <artifactId>org.apache.felix.scr</artifactId>
@@ -139,6 +165,11 @@
       <artifactId>org.apache.felix.jaas</artifactId>
       <version>0.0.2</version>
     </dependency>
+    <dependency>
+      <groupId>com.googlecode.json-simple</groupId>
+      <artifactId>json-simple</artifactId>
+      <version>1.1.1</version>
+    </dependency>
 
     <!-- Test dependencies -->
     <dependency>
@@ -146,5 +177,11 @@
       <artifactId>junit</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.codehaus.groovy</groupId>
+      <artifactId>groovy-all</artifactId>
+      <version>${groovy.version}</version>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 </project>

Added: jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/ConfigTracker.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/ConfigTracker.java?rev=1597707&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/ConfigTracker.java (added)
+++ jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/ConfigTracker.java Tue May 27 06:14:29 2014
@@ -0,0 +1,260 @@
+/*
+ * 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.jackrabbit.oak.run.osgi;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Dictionary;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.io.Files;
+import org.apache.felix.utils.collections.DictionaryAsMap;
+import org.apache.felix.utils.properties.InterpolationHelper;
+import org.json.simple.JSONObject;
+import org.json.simple.JSONValue;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Installs config obtained from JSON Config file or passed as part of
+ * startup
+ */
+class ConfigTracker extends ServiceTracker {
+    private static final String MARKER_NAME = "oak.configinstall.name";
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private ConfigurationAdmin cm;
+    private final Map config;
+    private final BundleContext bundleContext;
+
+    public ConfigTracker(Map config, BundleContext bundleContext) {
+        super(bundleContext, ConfigurationAdmin.class.getName(), null);
+        this.config = config;
+        this.bundleContext = bundleContext;
+        open();
+    }
+
+    @Override
+    public Object addingService(ServiceReference reference) {
+        cm = (ConfigurationAdmin) super.addingService(reference);
+        try {
+            synchronizeConfigs();
+        } catch (Exception e) {
+            log.error("Error occurred while installing configs", e);
+            throw new RuntimeException(e);
+        }
+        return cm;
+    }
+
+    /**
+     * Synchronizes the configs. All config added by this class is also kept in sync with re runs
+     * i.e. if a config was added in first run and say later removed then that would also be removed
+     * from the ConfigurationAdmin
+     */
+    private void synchronizeConfigs() throws Exception {
+        Set<String> existingPids = determineExistingConfigs();
+        Set<String> processedPids = Sets.newHashSet();
+
+        Map<String, Map<String, Object>> configs = Maps.newHashMap();
+
+        Map<String, Map<String, Object>> configFromFile =
+                parseJSONConfig((String) config.get(OakOSGiRepositoryFactory.REPOSITORY_CONFIG_FILE));
+        configs.putAll(configFromFile);
+
+        @SuppressWarnings("unchecked")
+        Map<String, Map<String, Object>> runtimeConfig =
+                (Map<String, Map<String, Object>>) config.get(OakOSGiRepositoryFactory.REPOSITORY_CONFIG);
+        if (runtimeConfig != null) {
+            configs.putAll(runtimeConfig);
+        }
+
+        installConfigs(configs, processedPids);
+        Set<String> pidsToBeRemoved = Sets.difference(existingPids, processedPids);
+        removeConfigs(pidsToBeRemoved);
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, Map<String, Object>> parseJSONConfig(String jsonFilePath) throws IOException {
+        Map<String, Map<String, Object>> configs = Maps.newHashMap();
+        if (jsonFilePath == null) {
+            return configs;
+        }
+
+        List<String> files = Splitter.on(',').trimResults().omitEmptyStrings().splitToList(jsonFilePath);
+        for (String filePath : files) {
+            File jsonFile = new File(filePath);
+            if (!jsonFile.exists()) {
+                log.warn("No file found at path {}. Ignoring the file entry", jsonFile.getAbsolutePath());
+                continue;
+            }
+
+            String content = Files.toString(jsonFile, Charsets.UTF_8);
+            JSONObject json = (JSONObject) JSONValue.parse(content);
+            configs.putAll(json);
+        }
+
+        return configs;
+    }
+
+    private void removeConfigs(Set<String> pidsToBeRemoved) throws Exception {
+        for (String pidString : pidsToBeRemoved) {
+            String[] pid = parsePid(pidString);
+            Configuration config = getConfiguration(pidString, pid[0], pid[1]);
+            config.delete();
+        }
+
+        if (!pidsToBeRemoved.isEmpty()) {
+            log.info("Configuration belonging to following pids have been removed ", pidsToBeRemoved);
+        }
+    }
+
+    private void installConfigs(Map<String, Map<String, Object>> osgiConfig, Set<String> processedPids)
+            throws Exception {
+        for (Map.Entry<String, Map<String, Object>> pidEntry : osgiConfig.entrySet()) {
+            final String pidString = pidEntry.getKey();
+
+            final Hashtable<String, Object> current = new Hashtable<String, Object>();
+            current.putAll(pidEntry.getValue());
+            performSubstitution(current);
+
+            String[] pid = parsePid(pidString);
+            Configuration config = getConfiguration(pidString, pid[0], pid[1]);
+
+            @SuppressWarnings("unchecked") Dictionary<String, Object> props = config.getProperties();
+            Hashtable<String, Object> old = props != null ?
+                    new Hashtable<String, Object>(new DictionaryAsMap<String, Object>(props)) : null;
+            if (old != null) {
+                old.remove(MARKER_NAME);
+                old.remove(Constants.SERVICE_PID);
+                old.remove(ConfigurationAdmin.SERVICE_FACTORYPID);
+            }
+
+            if (!current.equals(old)) {
+                current.put(MARKER_NAME, pidString);
+                if (config.getBundleLocation() != null) {
+                    config.setBundleLocation(null);
+                }
+                if (old == null) {
+                    log.info("Creating configuration from {}", pidString);
+                } else {
+                    log.info("Updating configuration from {}", pidString);
+                }
+                config.update(current);
+                processedPids.add(pidString);
+            }
+        }
+    }
+
+    private void performSubstitution(Hashtable<String, Object> current) {
+        Map<String, String> simpleConfig = Maps.newHashMap();
+
+        for (Map.Entry<String, Object> e : current.entrySet()) {
+            if (e.getValue() instanceof String) {
+                simpleConfig.put(e.getKey(), (String) e.getValue());
+            }
+        }
+
+        InterpolationHelper.performSubstitution(simpleConfig, bundleContext);
+
+        for (Map.Entry<String, String> e : simpleConfig.entrySet()) {
+            current.put(e.getKey(), e.getValue());
+        }
+    }
+
+    /**
+     * Determines the existing configs which are installed by ConfigInstaller
+     *
+     * @return set of pid strings
+     */
+    private Set<String> determineExistingConfigs() throws IOException, InvalidSyntaxException {
+        Set<String> pids = Sets.newHashSet();
+        String filter = "(" + MARKER_NAME + "=" + "*" + ")";
+        Configuration[] configurations = cm.listConfigurations(filter);
+        if (configurations != null) {
+            for (Configuration cfg : configurations) {
+                pids.add((String) cfg.getProperties().get(MARKER_NAME));
+            }
+        }
+        return pids;
+    }
+
+    private Configuration getConfiguration(String pidString, String pid, String factoryPid)
+            throws Exception {
+        Configuration oldConfiguration = findExistingConfiguration(pidString);
+        if (oldConfiguration != null) {
+            return oldConfiguration;
+        } else {
+            Configuration newConfiguration;
+            if (factoryPid != null) {
+                newConfiguration = cm.createFactoryConfiguration(pid, null);
+            } else {
+                newConfiguration = cm.getConfiguration(pid, null);
+            }
+            return newConfiguration;
+        }
+    }
+
+    private Configuration findExistingConfiguration(String pidString) throws Exception {
+        String filter = "(" + MARKER_NAME + "=" + escapeFilterValue(pidString) + ")";
+        Configuration[] configurations = cm.listConfigurations(filter);
+        if (configurations != null && configurations.length > 0) {
+            return configurations[0];
+        } else {
+            return null;
+        }
+    }
+
+    private static String escapeFilterValue(String s) {
+        return s.replaceAll("[(]", "\\\\(").
+                replaceAll("[)]", "\\\\)").
+                replaceAll("[=]", "\\\\=").
+                replaceAll("[\\*]", "\\\\*");
+    }
+
+    private static String[] parsePid(String pid) {
+        int n = pid.indexOf('-');
+        if (n > 0) {
+            String factoryPid = pid.substring(n + 1);
+            pid = pid.substring(0, n);
+            return new String[]
+                    {
+                            pid, factoryPid
+                    };
+        } else {
+            return new String[]
+                    {
+                            pid, null
+                    };
+        }
+    }
+}

Propchange: jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/ConfigTracker.java
------------------------------------------------------------------------------
    svn:eol-style = native

Modified: jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/OakOSGiRepositoryFactory.java
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/OakOSGiRepositoryFactory.java?rev=1597707&r1=1597706&r2=1597707&view=diff
==============================================================================
--- jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/OakOSGiRepositoryFactory.java (original)
+++ jackrabbit/oak/trunk/oak-pojosr/src/main/java/org/apache/jackrabbit/oak/run/osgi/OakOSGiRepositoryFactory.java Tue May 27 06:14:29 2014
@@ -37,6 +37,7 @@ import javax.jcr.RepositoryException;
 import javax.jcr.RepositoryFactory;
 
 import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
 import com.google.common.util.concurrent.SettableFuture;
 import de.kalpatec.pojosr.framework.launch.BundleDescriptor;
 import de.kalpatec.pojosr.framework.launch.ClasspathScanner;
@@ -56,6 +57,7 @@ import static com.google.common.base.Pre
 public class OakOSGiRepositoryFactory implements RepositoryFactory {
 
     private static Logger log = LoggerFactory.getLogger(OakOSGiRepositoryFactory.class);
+
     /**
      * Name of the repository home parameter.
      */
@@ -63,7 +65,19 @@ public class OakOSGiRepositoryFactory im
             = "org.apache.jackrabbit.repository.home";
 
     public static final String REPOSITORY_STARTUP_TIMEOUT
-            = "org.apache.jackrabbit.repository.startupTimeOut";
+            = "org.apache.jackrabbit.oak.repository.startupTimeOut";
+
+    /**
+     * Config key which refers to the map of config where key in that map refers to OSGi
+     * config
+     */
+    public static final String REPOSITORY_CONFIG = "org.apache.jackrabbit.oak.repository.config";
+
+    /**
+     * Comma separated list of file names which referred to config stored in form of JSON. The
+     * JSON content consist of pid as the key and config map as the value
+     */
+    public static final String REPOSITORY_CONFIG_FILE = "org.apache.jackrabbit.oak.repository.configFile";
 
     /**
      * Default timeout for repository creation
@@ -75,26 +89,11 @@ public class OakOSGiRepositoryFactory im
         Map config = new HashMap();
         config.putAll(parameters);
 
-        //TODO Add support for passing config as map of PID -> Dictionary
-        //as part of parameters and hook it up with Felix ConfigAdmin
-        //Say via custom InMemory PersistenceManager or programatically
-        //registering it with using ConfigAdmin API
-        //For later part we would need to implement some sort of Start Level
-        //support such that
-        // 1. Some base bundles like ConfigAdmin get start first
-        // 2. We register the user provided config
-        // 3. Other bundles get started
-
         //TODO With OSGi Whiteboard we need to provide support for handling
         //execution and JMX support as so far they were provided by Sling bundles
         //in OSGi env
 
-        processConfig(config);
-
-        PojoServiceRegistry registry = createServiceRegistry(config);
-        preProcessRegistry(registry);
-        startBundles(registry);
-        postProcessRegistry(registry);
+        PojoServiceRegistry registry = initializeServiceRegistry(config);
 
         //Future which would be used to notify when repository is ready
         // to be used
@@ -126,6 +125,19 @@ public class OakOSGiRepositoryFactory im
         }
     }
 
+    @SuppressWarnings("unchecked")
+    PojoServiceRegistry initializeServiceRegistry(Map config) {
+        processConfig(config);
+
+        PojoServiceRegistry registry = createServiceRegistry(config);
+        startConfigTracker(registry, config);
+        preProcessRegistry(registry);
+        startBundles(registry);
+        postProcessRegistry(registry);
+
+        return registry;
+    }
+
     /**
      * Enables pre processing of service registry by sub classes. This can be
      * used to register services before any bundle gets started
@@ -146,13 +158,7 @@ public class OakOSGiRepositoryFactory im
 
     }
 
-    /**
-     * @param descriptors
-     * @return the bundle descriptors
-     */
     protected List<BundleDescriptor> processDescriptors(List<BundleDescriptor> descriptors) {
-        //If required sort the bundle descriptors such that configuration admin and file install bundle
-        //gets started before SCR
         Collections.sort(descriptors, new BundleDescriptorComparator());
         return descriptors;
     }
@@ -163,6 +169,10 @@ public class OakOSGiRepositoryFactory im
         }
     }
 
+    private static void startConfigTracker(PojoServiceRegistry registry, Map config) {
+        new ConfigTracker(config, registry.getBundleContext());
+    }
+
     private static int getTimeoutInSeconds(Map config) {
         Integer timeout = (Integer) config.get(REPOSITORY_STARTUP_TIMEOUT);
         if (timeout == null) {
@@ -171,6 +181,7 @@ public class OakOSGiRepositoryFactory im
         return timeout;
     }
 
+    @SuppressWarnings("unchecked")
     private static void processConfig(Map config) {
         String home = (String) config.get(REPOSITORY_HOME);
         checkNotNull(home, "Repository home not defined via [%s]", REPOSITORY_HOME);
@@ -193,12 +204,12 @@ public class OakOSGiRepositoryFactory im
         //and not in a different thread
         config.put("felix.fileinstall.noInitialDelay", "true");
 
-        //Directory used by Felix File Install to watch for configs
         config.put("repository.home", FilenameUtils.concat(home, "repository"));
 
         copyConfigToSystemProps(config);
     }
 
+    @SuppressWarnings("unchecked")
     private static void copyConfigToSystemProps(Map config) {
         //TODO This is a temporary workaround as the current release version
         //of PojoSR reads value from System properties. Trunk version reads from
@@ -225,6 +236,7 @@ public class OakOSGiRepositoryFactory im
     private void startBundles(PojoServiceRegistry registry) {
         try {
             List<BundleDescriptor> descriptors = new ClasspathScanner().scanForBundles();
+            descriptors = Lists.newArrayList(descriptors);
             descriptors = processDescriptors(descriptors);
             registry.startBundles(descriptors);
         } catch (Exception e) {

Added: jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/osgi/ConfigTest.groovy
URL: http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/osgi/ConfigTest.groovy?rev=1597707&view=auto
==============================================================================
--- jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/osgi/ConfigTest.groovy (added)
+++ jackrabbit/oak/trunk/oak-pojosr/src/test/groovy/org/apache/jackrabbit/oak/run/osgi/ConfigTest.groovy Tue May 27 06:14:29 2014
@@ -0,0 +1,143 @@
+/*
+ * 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.jackrabbit.oak.run.osgi
+
+import de.kalpatec.pojosr.framework.launch.BundleDescriptor
+import de.kalpatec.pojosr.framework.launch.PojoServiceRegistry
+import groovy.json.JsonOutput
+import org.apache.commons.io.FileUtils
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.osgi.framework.Constants
+import org.osgi.service.cm.Configuration
+import org.osgi.service.cm.ConfigurationAdmin
+
+import static org.apache.jackrabbit.oak.run.osgi.OakOSGiRepositoryFactory.*
+
+class ConfigTest {
+    TestRepositoryFactory factory = new TestRepositoryFactory()
+    Map config
+    File workDir
+    PojoServiceRegistry registry
+    ConfigurationAdmin cm
+
+    @Before
+    void setUp(){
+        workDir = new File("target", "ConfigTest");
+        config  = [
+                (REPOSITORY_HOME) : workDir.absolutePath,
+                'magic.spell' : 'Alohomora'
+        ]
+    }
+
+    @After
+    void tearDown(){
+        if(workDir.exists()) {
+            FileUtils.cleanDirectory(workDir);
+        }
+    }
+
+    @Test
+    void testRuntimeConfig(){
+        config[REPOSITORY_CONFIG] = createConfigMap()
+
+        initRegistry(config)
+
+        assertConfig()
+    }
+
+    @Test
+    void testFileConfig(){
+        def jf1 = new File(workDir, "config1.json")
+        def jf2 = new File(workDir, "config2.json")
+        jf1 << JsonOutput.toJson(createConfigMap())
+        jf2 << JsonOutput.toJson([bar : [a:'a3', b:4]])
+        config[REPOSITORY_CONFIG_FILE] = "${jf1.absolutePath},${jf2.absolutePath}" as String
+
+        initRegistry(config)
+
+        assertConfig()
+
+        Configuration c1 = cm.getConfiguration('bar', null)
+        assert c1.properties
+        assert c1.properties.get('a') == 'a3'
+        assert c1.properties.get('b') == 4
+    }
+
+    @Test
+    void testConfigSync(){
+        config[REPOSITORY_CONFIG] = [
+                foo : [a:'a', b:1],
+                bar : [a:'a1', b:2]
+        ]
+        initRegistry(config)
+        Configuration c = cm.getConfiguration('baz')
+        c.update(new Hashtable([a :'a2']))
+
+        assert cm.getConfiguration('baz').properties.get('a') == 'a2'
+        assert cm.getConfiguration('foo').properties.get('a') == 'a'
+        assert cm.getConfiguration('bar').properties.get('a') == 'a1'
+
+        //Now re init and remove the pid bar
+        config[REPOSITORY_CONFIG] = [
+                foo : [a:'a-new', b:1],
+        ]
+        initRegistry(config)
+
+        assert cm.getConfiguration('baz').properties.get('a') == 'a2'
+        assert cm.getConfiguration('foo').properties.get('a') == 'a-new'
+        assert cm.getConfiguration('bar').properties == null
+    }
+
+    private static Map createConfigMap() {
+        [
+                'foo'            : [a: 'a', b: 1, c:'${magic.spell}'],
+                'foo.bar-default': [a: 'a1', b: 2],
+                'foo.bar-simple' : [a: 'a2', b: 3],
+        ]
+    }
+
+    private void assertConfig() {
+        Configuration c1 = cm.getConfiguration('foo', null)
+        assert c1.properties
+        assert c1.properties.get('a') == 'a'
+        assert c1.properties.get('b') == 1
+        assert c1.properties.get('c') == 'Alohomora'
+
+        Configuration[] fcs = cm.listConfigurations('(service.factoryPid=foo.bar)')
+        assert fcs.size() == 2
+    }
+
+    private void initRegistry(Map config){
+        registry = factory.initializeServiceRegistry(config)
+        cm = registry.getService(registry.getServiceReference(ConfigurationAdmin.class.name)) as ConfigurationAdmin
+    }
+
+    private static class TestRepositoryFactory extends OakOSGiRepositoryFactory {
+        @Override
+        protected List<BundleDescriptor> processDescriptors(List<BundleDescriptor> descriptors) {
+            //skip the oak bundles to prevent repository initialization
+            return super.processDescriptors(descriptors).findAll {BundleDescriptor bd ->
+                !bd.headers[Constants.BUNDLE_SYMBOLICNAME]?.startsWith('org.apache.jackrabbit')
+            }
+        }
+    }
+}