[FELIX-2571] Have fileinstall listen for configuration changes and write them back to the config files

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1027254 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/fileinstall/pom.xml b/fileinstall/pom.xml
index 9ba034a..b5b54c9 100644
--- a/fileinstall/pom.xml
+++ b/fileinstall/pom.xml
@@ -45,7 +45,13 @@
     <dependency>
       <groupId>org.apache.felix</groupId>
       <artifactId>org.apache.felix.configadmin</artifactId>
-      <version>1.2.4</version>
+      <version>1.2.8</version>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.utils</artifactId>
+      <version>1.0.1-SNAPSHOT</version>
       <scope>provided</scope>
     </dependency>
   </dependencies>
@@ -54,7 +60,7 @@
       <plugin>
         <groupId>org.apache.felix</groupId>
         <artifactId>maven-bundle-plugin</artifactId>
-        <version>2.0.0</version>
+        <version>2.1.0</version>
         <extensions>true</extensions>
         <configuration>
           <instructions>
@@ -62,20 +68,27 @@
                 org.apache.felix.fileinstall;version=${project.version}
             </Export-Package>
             <Private-Package>
-                org.apache.felix.fileinstall.internal
+                org.apache.felix.fileinstall.internal,
+                org.apache.felix.utils.properties,
             </Private-Package>
             <Import-Package>
                 org.osgi.service.log;resolution:=optional,
                 org.osgi.service.cm;resolution:=optional,
                 *
             </Import-Package>
-            <Include-Resource>src/main/resources,META-INF/LICENSE=LICENSE,META-INF/NOTICE=NOTICE,META-INF/DEPENDENCIES=DEPENDENCIES</Include-Resource>
+            <Include-Resource>
+                src/main/resources,
+                META-INF/LICENSE=LICENSE,
+                META-INF/NOTICE=NOTICE,
+                META-INF/DEPENDENCIES=DEPENDENCIES
+            </Include-Resource>
             <Bundle-Activator>org.apache.felix.fileinstall.internal.FileInstall</Bundle-Activator>
             <Bundle-SymbolicName>${pom.artifactId}</Bundle-SymbolicName>
             <Bundle-Vendor>The Apache Software Foundation</Bundle-Vendor>
             <_versionpolicy>[$(version;==;$(@)),$(version;+;$(@)))</_versionpolicy>
             <Embed-Dependency>
-                org.apache.felix.configadmin;inline="org/apache/felix/cm/file/ConfigurationHandler.*"
+                org.apache.felix.configadmin;inline="org/apache/felix/cm/file/ConfigurationHandler.*",
+                org.apache.felix.utils;inline="org/apache/felix/utils/collections/DictionaryAsMap*.*"
             </Embed-Dependency>
           </instructions>
         </configuration>
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java
index 73e45c8..e386d6f 100644
--- a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java
@@ -18,24 +18,39 @@
  */
 package org.apache.felix.fileinstall.internal;
 
-import java.io.*;
-import java.util.*;
-
 import org.apache.felix.cm.file.ConfigurationHandler;
 import org.apache.felix.fileinstall.ArtifactInstaller;
 import org.apache.felix.fileinstall.internal.Util.Logger;
+import org.apache.felix.utils.properties.InterpolationHelper;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
 import org.osgi.service.cm.Configuration;
 import org.osgi.service.cm.ConfigurationAdmin;
+import org.osgi.service.cm.ConfigurationEvent;
+import org.osgi.service.cm.ConfigurationListener;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Properties;
 
 /**
  * ArtifactInstaller for configurations.
  * TODO: This service lifecycle should be bound to the ConfigurationAdmin service lifecycle.
  */
-public class ConfigInstaller implements ArtifactInstaller
+public class ConfigInstaller implements ArtifactInstaller, ConfigurationListener
 {
     private final BundleContext context;
     private final ConfigurationAdmin configAdmin;
+    private ServiceRegistration registration;
 
     ConfigInstaller(BundleContext context, ConfigurationAdmin configAdmin)
     {
@@ -43,6 +58,24 @@
         this.configAdmin = configAdmin;
     }
 
+    public void init()
+    {
+        if (registration == null)
+        {
+            Properties props = new Properties();
+            registration = this.context.registerService(ConfigurationListener.class.getName(), this, props);
+        }
+    }
+
+    public void destroy()
+    {
+        if (registration != null)
+        {
+            registration.unregister();
+            registration = null;
+        }
+    }
+
     public boolean canHandle(File artifact)
     {
         return artifact.getName().endsWith(".cfg")
@@ -64,6 +97,54 @@
         deleteConfig(artifact);
     }
 
+    public void configurationEvent(ConfigurationEvent configurationEvent)
+    {
+        if (configurationEvent.getType() == ConfigurationEvent.CM_UPDATED)
+        {
+            try
+            {
+                Configuration config = getConfigurationAdmin().getConfiguration(
+                                            configurationEvent.getPid(),
+                                            configurationEvent.getFactoryPid());
+                Dictionary dict = config.getProperties();
+                String fileName = (String) dict.get( DirectoryWatcher.FILENAME );
+                File file = fileName != null ? new File(fileName) : null;
+                if( file != null && file.isFile()   ) {
+                    if( fileName.endsWith( ".cfg" ) )
+                    {
+                        org.apache.felix.utils.properties.Properties props = new org.apache.felix.utils.properties.Properties( file );
+                        for( Enumeration e  = dict.keys(); e.hasMoreElements(); )
+                        {
+                            String key = e.nextElement().toString();
+                            if( !Constants.SERVICE_PID.equals(key) && !DirectoryWatcher.FILENAME.equals(key) )
+                            {
+                                String val = dict.get( key ).toString();
+                                props.put( key, val );
+                            }
+                        }
+                        props.save();
+                    }
+                    else if( fileName.endsWith( ".config" ) )
+                    {
+                        OutputStream fos = new FileOutputStream( file );
+                        try
+                        {
+                            ConfigurationHandler.write( fos, dict );
+                        }
+                        finally
+                        {
+                            fos.close();
+                        }
+                    }
+                }
+            }
+            catch (Exception e)
+            {
+                Util.log( context, Util.getGlobalLogLevel(context), Logger.LOG_INFO, "Unable to save configuration", e );
+            }
+        }
+    }
+
     ConfigurationAdmin getConfigurationAdmin()
     {
         return configAdmin;
@@ -83,7 +164,7 @@
         final InputStream in = new BufferedInputStream(new FileInputStream(f));
         try
         {
-            if ( f.getName().endsWith(".cfg") )
+            if ( f.getName().endsWith( ".cfg" ) )
             {
                 final Properties p = new Properties();
                 in.mark(1);
@@ -94,10 +175,10 @@
                 } else {
                     p.load(in);
                 }
-                Util.performSubstitution(p);
+                InterpolationHelper.performSubstitution((Map) p);
                 ht.putAll(p);
             }
-            else
+            else if ( f.getName().endsWith( ".config" ) )
             {
                 final Dictionary config = ConfigurationHandler.read(in);
                 final Enumeration i = config.keys();
@@ -114,8 +195,8 @@
         }
 
         String pid[] = parsePid(f.getName());
-        ht.put(DirectoryWatcher.FILENAME, f.getName());
-        Configuration config = getConfiguration(pid[0], pid[1]);
+        ht.put(DirectoryWatcher.FILENAME, f.getAbsolutePath());
+        Configuration config = getConfiguration(f.getAbsolutePath(), pid[0], pid[1]);
         if (config.getBundleLocation() != null)
         {
             config.setBundleLocation(null);
@@ -135,7 +216,7 @@
     boolean deleteConfig(File f) throws Exception
     {
         String pid[] = parsePid(f.getName());
-        Configuration config = getConfiguration(pid[0], pid[1]);
+        Configuration config = getConfiguration(f.getAbsolutePath(), pid[0], pid[1]);
         config.delete();
         return true;
     }
@@ -162,10 +243,10 @@
         }
     }
 
-    Configuration getConfiguration(String pid, String factoryPid)
+    Configuration getConfiguration(String fileName, String pid, String factoryPid)
         throws Exception
     {
-        Configuration oldConfiguration = findExistingConfiguration(pid, factoryPid);
+        Configuration oldConfiguration = findExistingConfiguration(fileName);
         if (oldConfiguration != null)
         {
             Util.log(context, Util.getGlobalLogLevel(context),
@@ -188,11 +269,9 @@
         }
     }
 
-    Configuration findExistingConfiguration(String pid, String factoryPid) throws Exception
+    Configuration findExistingConfiguration(String fileName) throws Exception
     {
-        String suffix = factoryPid == null ? ".cfg" : "-" + factoryPid + ".cfg";
-
-        String filter = "(" + DirectoryWatcher.FILENAME + "=" + pid + suffix + ")";
+        String filter = "(" + DirectoryWatcher.FILENAME + "=" + fileName + ")";
         Configuration[] configurations = getConfigurationAdmin().listConfigurations(filter);
         if (configurations != null && configurations.length > 0)
         {
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java
index 0dd3b28..aa9818d 100644
--- a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java
@@ -22,6 +22,8 @@
 
 import org.apache.felix.fileinstall.*;
 import org.apache.felix.fileinstall.internal.Util.Logger;
+import org.apache.felix.utils.collections.DictionaryAsMap;
+import org.apache.felix.utils.properties.InterpolationHelper;
 import org.osgi.framework.*;
 import org.osgi.service.cm.*;
 import org.osgi.service.packageadmin.PackageAdmin;
@@ -185,7 +187,7 @@
 
     public void updated(String pid, Dictionary properties)
     {
-        Util.performSubstitution(properties);
+        InterpolationHelper.performSubstitution(new DictionaryAsMap(properties));
         DirectoryWatcher watcher = null;
         synchronized (watchers)
         {
@@ -334,14 +336,19 @@
             {
                 ConfigurationAdmin cm = (ConfigurationAdmin) super.addingService(serviceReference);
                 configInstaller = new ConfigInstaller(context, cm);
+                configInstaller.init();
                 fileInstall.addListener(configInstaller);
                 return cm;
             }
 
             public void removedService(ServiceReference serviceReference, Object o)
             {
-                configInstaller = null;
-                fileInstall.removeListener(configInstaller);
+                if (configInstaller != null)
+                {
+                    configInstaller.destroy();
+                    fileInstall.removeListener(configInstaller);
+                    configInstaller = null;
+                }
                 super.removedService(serviceReference, o);
             }
         }
diff --git a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java
index e712a26..e6a7713 100644
--- a/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java
+++ b/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java
@@ -44,154 +44,8 @@
 
 public class Util
 {
-    private static final char   ESCAPE_CHAR = '\\';
-    private static final String DELIM_START = "${";
-    private static final String DELIM_STOP = "}";
-
     private static final String CHECKSUM_SUFFIX = ".checksum";
 
-    /**
-     * Perform substitution on a property set
-     *
-     * @param properties the property set to perform substitution on
-     */
-    public static void performSubstitution(Dictionary properties)
-    {
-        for (Enumeration e = properties.keys(); e.hasMoreElements(); )
-        {
-            String name = (String) e.nextElement();
-            Object value = properties.get(name);
-            properties.put(name,
-                value instanceof String
-                    ? Util.substVars((String) value, name, null, properties)
-                    : value);
-        }
-    }
-
-    /**
-     * <p>
-     * This method performs property variable substitution on the
-     * specified value. If the specified value contains the syntax
-     * <tt>${&lt;prop-name&gt;}</tt>, where <tt>&lt;prop-name&gt;</tt>
-     * refers to either a configuration property or a system property,
-     * then the corresponding property value is substituted for the variable
-     * placeholder. Multiple variable placeholders may exist in the
-     * specified value as well as nested variable placeholders, which
-     * are substituted from inner most to outer most. Configuration
-     * properties override system properties.
-     * </p>
-     * @param val The string on which to perform property substitution.
-     * @param currentKey The key of the property being evaluated used to
-     *        detect cycles.
-     * @param cycleMap Map of variable references used to detect nested cycles.
-     * @param configProps Set of configuration properties.
-     * @return The value of the specified string after system property substitution.
-     * @throws IllegalArgumentException If there was a syntax error in the
-     *         property placeholder syntax or a recursive variable reference.
-     **/
-    public static String substVars(String val, String currentKey, Map cycleMap, Dictionary configProps)
-        throws IllegalArgumentException
-    {
-        if (cycleMap == null)
-        {
-            cycleMap = new HashMap();
-        }
-
-        // Put the current key in the cycle map.
-        cycleMap.put(currentKey, currentKey);
-
-        // Assume we have a value that is something like:
-        // "leading ${foo.${bar}} middle ${baz} trailing"
-
-        // Find the first ending '}' variable delimiter, which
-        // will correspond to the first deepest nested variable
-        // placeholder.
-        int stopDelim = val.indexOf(DELIM_STOP);
-        while (stopDelim > 0 && val.charAt(stopDelim - 1) == ESCAPE_CHAR)
-        {
-            stopDelim = val.indexOf(DELIM_STOP, stopDelim + 1);
-        }
-
-        // Find the matching starting "${" variable delimiter
-        // by looping until we find a start delimiter that is
-        // greater than the stop delimiter we have found.
-        int startDelim = val.indexOf(DELIM_START);
-        while (stopDelim >= 0)
-        {
-            int idx = val.indexOf(DELIM_START, startDelim + DELIM_START.length());
-            if ((idx < 0) || (idx > stopDelim))
-            {
-                break;
-            }
-            else if (idx < stopDelim)
-            {
-                startDelim = idx;
-            }
-        }
-
-        // If we do not have a start or stop delimiter, then just
-        // return the existing value.
-        if ((startDelim < 0) || (stopDelim < 0))
-        {
-            return unescape(val);
-        }
-
-        // At this point, we have found a variable placeholder so
-        // we must perform a variable substitution on it.
-        // Using the start and stop delimiter indices, extract
-        // the first, deepest nested variable placeholder.
-        String variable = val.substring(startDelim + DELIM_START.length(), stopDelim);
-
-        // Verify that this is not a recursive variable reference.
-        if (cycleMap.get(variable) != null)
-        {
-            throw new IllegalArgumentException("recursive variable reference: " + variable);
-        }
-
-        // Get the value of the deepest nested variable placeholder.
-        // Try to configuration properties first.
-        String substValue = (String) ((configProps != null) ? configProps.get(variable) : null);
-        if (substValue == null)
-        {
-            // Ignore unknown property values.
-            substValue = variable.length() > 0 ? System.getProperty(variable, "") : "";
-        }
-
-        // Remove the found variable from the cycle map, since
-        // it may appear more than once in the value and we don't
-        // want such situations to appear as a recursive reference.
-        cycleMap.remove(variable);
-
-        // Append the leading characters, the substituted value of
-        // the variable, and the trailing characters to get the new
-        // value.
-        val = val.substring(0, startDelim) + substValue + val.substring(stopDelim + DELIM_STOP.length(), val.length());
-
-        // Now perform substitution again, since there could still
-        // be substitutions to make.
-        val = substVars(val, currentKey, cycleMap, configProps);
-
-        // Remove escape characters preceding {, } and \
-        val = unescape(val);
-
-        // Return the value.
-        return val;
-    }
-
-    private static String unescape(String val) {
-        int escape = val.indexOf(ESCAPE_CHAR);
-        while (escape >= 0 && escape < val.length() - 1)
-        {
-            char c = val.charAt(escape + 1);
-            if (c == '{' || c == '}' || c == ESCAPE_CHAR)
-            {
-                val = val.substring(0, escape) + val.substring(escape + 1);
-            }
-            escape = val.indexOf(ESCAPE_CHAR, escape + 1);
-        }
-        return val;
-    }
-
     public static int getGlobalLogLevel(BundleContext context)
     {
         String s = (String) context.getProperty(DirectoryWatcher.LOG_LEVEL);
diff --git a/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java b/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java
index 9794744..6d0095f 100644
--- a/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java
+++ b/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java
@@ -94,7 +94,7 @@
 
         ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin );
 
-        assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid", "factoryPid" ) );
+        assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid-factoryPid.cfg", "pid", "factoryPid" ) );
 
         mockConfigurationAdminControl.verify();
         mockConfigurationControl.verify();
@@ -115,7 +115,7 @@
 
         ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin );
 
-        assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid", "factoryPid" ) );
+        assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid-factoryPid.cfg","pid", "factoryPid" ) );
 
         mockConfigurationAdminControl.verify();
         mockConfigurationControl.verify();
@@ -136,7 +136,7 @@
 
         ConfigInstaller ci = new ConfigInstaller( mockBundleContext, mockConfigurationAdmin );
 
-        assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid", null ) );
+        assertEquals( "Factory configuration retrieved", mockConfiguration, ci.getConfiguration( "pid.cfg", "pid", null ) );
 
         mockConfigurationAdminControl.verify();
         mockConfigurationControl.verify();
diff --git a/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/UtilTest.java b/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/UtilTest.java
deleted file mode 100644
index 343380d..0000000
--- a/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/UtilTest.java
+++ /dev/null
@@ -1,84 +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.felix.fileinstall.internal;
-
-import java.util.Dictionary;
-import java.util.Enumeration;
-import java.util.Hashtable;
-
-import junit.framework.TestCase;
-import org.apache.felix.fileinstall.internal.Util;
-
-public class UtilTest extends TestCase
-{
-    public void testBasicSubstitution()
-    {
-        System.setProperty("value1", "sub_value1");
-        Dictionary props = new Hashtable();
-        props.put("key0", "value0");
-        props.put("key1", "${value1}");
-        props.put("key2", "${value2}");
-
-        for (Enumeration e = props.keys(); e.hasMoreElements();)
-        {
-            String name = (String) e.nextElement();
-            props.put(name,
-                Util.substVars((String) props.get(name), name, null, props));
-        }
-
-        assertEquals("value0", props.get("key0"));
-        assertEquals("sub_value1", props.get("key1"));
-        assertEquals("", props.get("key2"));
-
-    }
-
-    public void testSubstitutionFailures()
-    {
-        assertEquals("a}", Util.substVars("a}", "b", null, new Hashtable()));
-        assertEquals("${a", Util.substVars("${a", "b", null, new Hashtable()));
-    }
-
-    public void testEmptyVariable() {
-        assertEquals("", Util.substVars("${}", "b", null, new Hashtable()));
-    }
-
-    public void testInnerSubst() {
-        Dictionary props = new Hashtable();
-        props.put("a", "b");
-        props.put("b", "c");
-        assertEquals("c", Util.substVars("${${a}}", "z", null, props));
-    }
-
-    public void testSubstLoop() {
-        try {
-            Util.substVars("${a}", "a", null, new Hashtable());
-            fail("Should have thrown an exception");
-        } catch (IllegalArgumentException e) {
-            // expected
-        }
-    }
-
-    public void testSubstitutionEscape()
-    {
-        assertEquals("${a}", Util.substVars("$\\{a${#}\\}", "b", null, new Hashtable()));
-        assertEquals("${a}", Util.substVars("$\\{a\\}${#}", "b", null, new Hashtable()));
-        assertEquals("${a}", Util.substVars("$\\{a\\}", "b", null, new Hashtable()));
-    }
-
-}
\ No newline at end of file