You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by gn...@apache.org on 2010/10/25 22:19:05 UTC

svn commit: r1027254 - in /felix/trunk: fileinstall/ fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ utils/ utils/src/main/java/org/apache/felix/utils/collections/ utils/...

Author: gnodet
Date: Mon Oct 25 20:19:04 2010
New Revision: 1027254

URL: http://svn.apache.org/viewvc?rev=1027254&view=rev
Log:
[FELIX-2571] Have fileinstall listen for configuration changes and write them back to the config files

Added:
    felix/trunk/utils/src/main/java/org/apache/felix/utils/collections/DictionaryAsMap.java
    felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/
    felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java
    felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/Properties.java
    felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/
    felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java
    felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/PropertiesTest.java
    felix/trunk/utils/src/test/resources/
    felix/trunk/utils/src/test/resources/test.properties
Removed:
    felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/UtilTest.java
Modified:
    felix/trunk/fileinstall/pom.xml
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java
    felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java
    felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java
    felix/trunk/utils/pom.xml

Modified: felix/trunk/fileinstall/pom.xml
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/pom.xml?rev=1027254&r1=1027253&r2=1027254&view=diff
==============================================================================
--- felix/trunk/fileinstall/pom.xml (original)
+++ felix/trunk/fileinstall/pom.xml Mon Oct 25 20:19:04 2010
@@ -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>

Modified: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java?rev=1027254&r1=1027253&r2=1027254&view=diff
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java (original)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/ConfigInstaller.java Mon Oct 25 20:19:04 2010
@@ -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 @@ public class ConfigInstaller implements 
         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 @@ public class ConfigInstaller implements 
         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 @@ public class ConfigInstaller implements 
         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 @@ public class ConfigInstaller implements 
                 } 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 @@ public class ConfigInstaller implements 
         }
 
         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 @@ public class ConfigInstaller implements 
     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 @@ public class ConfigInstaller implements 
         }
     }
 
-    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 @@ public class ConfigInstaller implements 
         }
     }
 
-    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)
         {

Modified: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java?rev=1027254&r1=1027253&r2=1027254&view=diff
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java (original)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/FileInstall.java Mon Oct 25 20:19:04 2010
@@ -22,6 +22,8 @@ import java.util.*;
 
 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 class FileInstall implements Bund
 
     public void updated(String pid, Dictionary properties)
     {
-        Util.performSubstitution(properties);
+        InterpolationHelper.performSubstitution(new DictionaryAsMap(properties));
         DirectoryWatcher watcher = null;
         synchronized (watchers)
         {
@@ -334,14 +336,19 @@ public class FileInstall implements Bund
             {
                 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);
             }
         }

Modified: felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java?rev=1027254&r1=1027253&r2=1027254&view=diff
==============================================================================
--- felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java (original)
+++ felix/trunk/fileinstall/src/main/java/org/apache/felix/fileinstall/internal/Util.java Mon Oct 25 20:19:04 2010
@@ -44,154 +44,8 @@ import org.osgi.service.log.LogService;
 
 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);

Modified: felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java?rev=1027254&r1=1027253&r2=1027254&view=diff
==============================================================================
--- felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java (original)
+++ felix/trunk/fileinstall/src/test/java/org/apache/felix/fileinstall/internal/ConfigInstallerTest.java Mon Oct 25 20:19:04 2010
@@ -94,7 +94,7 @@ public class ConfigInstallerTest extends
 
         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 @@ public class ConfigInstallerTest extends
 
         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 @@ public class ConfigInstallerTest extends
 
         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();

Modified: felix/trunk/utils/pom.xml
URL: http://svn.apache.org/viewvc/felix/trunk/utils/pom.xml?rev=1027254&r1=1027253&r2=1027254&view=diff
==============================================================================
--- felix/trunk/utils/pom.xml (original)
+++ felix/trunk/utils/pom.xml Mon Oct 25 20:19:04 2010
@@ -45,8 +45,8 @@
         <plugin>
             <artifactId>maven-compiler-plugin</artifactId>
             <configuration>
-                <source>1.4</source>
-                <target>1.4</target>
+                <source>1.5</source>
+                <target>jsr14</target>
             </configuration>
         </plugin>
        <plugin>

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/collections/DictionaryAsMap.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/collections/DictionaryAsMap.java?rev=1027254&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/collections/DictionaryAsMap.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/collections/DictionaryAsMap.java Mon Oct 25 20:19:04 2010
@@ -0,0 +1,110 @@
+/*
+ * 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.utils.collections;
+
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A wrapper around a dictionary access it as a Map
+ */
+public class DictionaryAsMap<U, V> extends AbstractMap<U, V>
+{
+
+    private Dictionary<U, V> dict;
+
+    public DictionaryAsMap(Dictionary<U, V> dict)
+    {
+        this.dict = dict;
+    }
+
+    @Override
+    public Set<Entry<U, V>> entrySet()
+    {
+        return new AbstractSet<Entry<U, V>>()
+        {
+            @Override
+            public Iterator<Entry<U, V>> iterator()
+            {
+                final Enumeration<U> e = dict.keys();
+                return new Iterator<Entry<U, V>>()
+                {
+                    private U key;
+                    public boolean hasNext()
+                    {
+                        return e.hasMoreElements();
+                    }
+
+                    public Entry<U, V> next()
+                    {
+                        key = e.nextElement();
+                        return new KeyEntry(key);
+                    }
+
+                    public void remove()
+                    {
+                        if (key == null)
+                        {
+                            throw new IllegalStateException();
+                        }
+                        dict.remove(key);
+                    }
+                };
+            }
+
+            @Override
+            public int size()
+            {
+                return dict.size();
+            }
+        };
+    }
+
+    @Override
+    public V put(U key, V value) {
+        return dict.put(key, value);
+    }
+
+    class KeyEntry implements Map.Entry<U,V> {
+
+        private final U key;
+
+        KeyEntry(U key) {
+            this.key = key;
+        }
+
+        public U getKey() {
+            return key;
+        }
+
+        public V getValue() {
+            return dict.get(key);
+        }
+
+        public V setValue(V value) {
+            return DictionaryAsMap.this.put(key, value);
+        }
+    }
+
+}
\ No newline at end of file

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java?rev=1027254&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/InterpolationHelper.java Mon Oct 25 20:19:04 2010
@@ -0,0 +1,189 @@
+/*
+ * 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.utils.properties;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.*;
+
+/**
+ * <p>
+ * Enhancement of the standard <code>Properties</code>
+ * managing the maintain of comments, etc.
+ * </p>
+ *
+ * @author gnodet, jbonofre
+ */
+public class InterpolationHelper {
+
+    private InterpolationHelper() {
+    }
+
+    private static final char   ESCAPE_CHAR = '\\';
+    private static final String DELIM_START = "${";
+    private static final String DELIM_STOP = "}";
+
+    /**
+     * Perform substitution on a property set
+     *
+     * @param properties the property set to perform substitution on
+     */
+    public static void performSubstitution(Map<String,String> properties)
+    {
+        for (String name : properties.keySet())
+        {
+            String value = properties.get(name);
+            properties.put(name, substVars(value, name, null, properties));
+        }
+    }
+
+    /**
+     * <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<String,String> cycleMap, Map<String,String> configProps)
+        throws IllegalArgumentException
+    {
+        if (cycleMap == null)
+        {
+            cycleMap = new HashMap<String,String>();
+        }
+
+        // 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;
+    }
+
+}
\ No newline at end of file

Added: felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/Properties.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/Properties.java?rev=1027254&view=auto
==============================================================================
--- felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/Properties.java (added)
+++ felix/trunk/utils/src/main/java/org/apache/felix/utils/properties/Properties.java Mon Oct 25 20:19:04 2010
@@ -0,0 +1,928 @@
+/*
+ * 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.utils.properties;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.net.URL;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * <p>
+ * Enhancement of the standard <code>Properties</code>
+ * managing the maintain of comments, etc.
+ * </p>
+ *
+ * @author gnodet, jbonofre
+ */
+public class Properties extends AbstractMap<String, String> {
+
+    /** Constant for the supported comment characters.*/
+    private static final String COMMENT_CHARS = "#!";
+
+    /** The list of possible key/value separators */
+    private static final char[] SEPARATORS = new char[] {'=', ':'};
+
+    /** The white space characters used as key/value separators. */
+    private static final char[] WHITE_SPACE = new char[] {' ', '\t', '\f'};
+
+    /**
+     * The default encoding (ISO-8859-1 as specified by
+     * http://java.sun.com/j2se/1.5.0/docs/api/java/util/Properties.html)
+     */
+    private static final String DEFAULT_ENCODING = "ISO-8859-1";
+
+    /** Constant for the platform specific line separator.*/
+    private static final String LINE_SEPARATOR = System.getProperty("line.separator");
+
+    /** Constant for the radix of hex numbers.*/
+    private static final int HEX_RADIX = 16;
+
+    /** Constant for the length of a unicode literal.*/
+    private static final int UNICODE_LEN = 4;
+
+    private Map<String,String> storage = new LinkedHashMap<String,String>();
+    private Map<String,Layout> layout = new LinkedHashMap<String,Layout>();
+    private List<String> header;
+    private List<String> footer;
+    private File location;
+
+    public Properties() {
+    }
+
+    public Properties(File location) throws IOException {
+        this.location = location;
+        if(location.exists())
+            load(location);
+    }
+
+    public void load(File location) throws IOException {
+        InputStream is = new FileInputStream(location);
+        try {
+            load(is);
+        } finally {
+            is.close();
+        }
+    }
+
+    public void load(URL location) throws IOException {
+        InputStream is = location.openStream();
+        try {
+            load(is);
+        } finally {
+            is.close();
+        }
+    }
+
+    public void load(InputStream is) throws IOException {
+        load(new InputStreamReader(is, DEFAULT_ENCODING));
+    }
+
+    public void load(Reader reader) throws IOException {
+        loadLayout(reader);
+    }
+
+    public void save() throws IOException {
+        save(this.location);
+    }
+
+    public void save(File location) throws IOException {
+        OutputStream os = new FileOutputStream(location);
+        try {
+            save(os);
+        } finally {
+            os.close();
+        }
+    }
+
+    public void save(OutputStream os) throws IOException {
+        save(new OutputStreamWriter(os, DEFAULT_ENCODING));
+    }
+
+    public void save(Writer writer) throws IOException {
+        saveLayout(writer);
+    }
+
+    @Override
+    public Set<Entry<String, String>> entrySet() {
+        return storage.entrySet();
+    }
+
+    @Override
+    public String put(String key, String value) {
+        String old = storage.put(key, value);
+        if (old == null || !old.equals(value)) {
+            Layout l = layout.get(key);
+            if (l != null) {
+                l.clearValue();
+            }
+        }
+        return old;
+    }
+
+    @Override
+    public String remove(Object key) {
+        Layout l = layout.get(key);
+        if (l != null) {
+            l.clearValue();
+        }
+        return storage.remove(key);
+    }
+
+    @Override
+    public void clear() {
+        for (Layout l : layout.values()) {
+            l.clearValue();
+        }
+        storage.clear();
+    }
+
+    /**
+     * Return the comment header.
+     *
+     * @return the comment header
+     */
+    public List<String> getHeader()
+    {
+        return header;
+    }
+
+    /**
+     * Set the comment header.
+     *
+     * @param header the header to use
+     */
+    public void setHeader(List<String> header)
+    {
+        this.header = header;
+    }
+
+    /**
+     * Return the comment footer.
+     *
+     * @return the comment footer
+     */
+    public List<String> getFooter()
+    {
+        return footer;
+    }
+
+    /**
+     * Set the comment footer.
+     *
+     * @param footer the footer to use
+     */
+    public void setFooter(List<String> footer)
+    {
+        this.footer = footer;
+    }
+
+    /**
+     * Reads a properties file and stores its internal structure. The found
+     * properties will be added to the associated configuration object.
+     *
+     * @param in the reader to the properties file
+     * @throws java.io.IOException if an error occurs
+     */
+    protected void loadLayout(Reader in) throws IOException
+    {
+        PropertiesReader reader = new PropertiesReader(in);
+        while (reader.nextProperty())
+        {
+            storage.put(reader.getPropertyName(), reader.getPropertyValue());
+            int idx = checkHeaderComment(reader.getCommentLines());
+            layout.put(reader.getPropertyName(),
+                    new Layout(idx < reader.getCommentLines().size() ?
+                                    new ArrayList<String>(reader.getCommentLines().subList(idx, reader.getCommentLines().size())) :
+                                    null,
+                               new ArrayList<String>(reader.getValueLines())));
+        }
+        footer = new ArrayList<String>(reader.getCommentLines());
+        InterpolationHelper.performSubstitution(storage);
+    }
+
+    /**
+     * Writes the properties file to the given writer, preserving as much of its
+     * structure as possible.
+     *
+     * @param out the writer
+     * @throws java.io.IOException if an error occurs
+     */
+    protected void saveLayout(Writer out) throws IOException
+    {
+        PropertiesWriter writer = new PropertiesWriter(out);
+        if (header != null)
+        {
+            for (String s : header)
+            {
+                writer.writeln(s);
+            }
+        }
+
+        for (String key : storage.keySet())
+        {
+            Layout l = layout.get(key);
+            if (l != null && l.getCommentLines() != null)
+            {
+                for (String s : l.getCommentLines())
+                {
+                    writer.writeln(s);
+                }
+            }
+            if (l != null && l.getValueLines() != null)
+            {
+                for (String s : l.getValueLines())
+                {
+                    writer.writeln(s);
+                }
+            }
+            else
+            {
+                writer.writeProperty(key, storage.get(key));
+            }
+        }
+        if (footer != null)
+        {
+            for (String s : footer)
+            {
+                writer.writeln(s);
+            }
+        }
+        writer.flush();
+    }
+
+    /**
+     * Checks if parts of the passed in comment can be used as header comment.
+     * This method checks whether a header comment can be defined (i.e. whether
+     * this is the first comment in the loaded file). If this is the case, it is
+     * searched for the lates blank line. This line will mark the end of the
+     * header comment. The return value is the index of the first line in the
+     * passed in list, which does not belong to the header comment.
+     *
+     * @param commentLines the comment lines
+     * @return the index of the next line after the header comment
+     */
+    private int checkHeaderComment(List<String> commentLines)
+    {
+        if (getHeader() == null && layout.isEmpty())
+        {
+            // This is the first comment. Search for blank lines.
+            int index = commentLines.size() - 1;
+            while (index >= 0 && commentLines.get(index).length() > 0)
+            {
+                index--;
+            }
+            setHeader(new ArrayList<String>(commentLines.subList(0, index + 1)));
+            return index + 1;
+        }
+        else
+        {
+            return 0;
+        }
+    }
+
+    /**
+     * Tests whether a line is a comment, i.e. whether it starts with a comment
+     * character.
+     *
+     * @param line the line
+     * @return a flag if this is a comment line
+     */
+    static boolean isCommentLine(String line) {
+        String s = line.trim();
+        // blank lines are also treated as comment lines
+        return s.length() < 1 || COMMENT_CHARS.indexOf(s.charAt(0)) >= 0;
+    }
+
+    /**
+     * <p>Unescapes any Java literals found in the <code>String</code> to a
+     * <code>Writer</code>.</p> This is a slightly modified version of the
+     * StringEscapeUtils.unescapeJava() function in commons-lang that doesn't
+     * drop escaped separators (i.e '\,').
+     *
+     * @param str  the <code>String</code> to unescape, may be null
+     * @return the processed string
+     * @throws IllegalArgumentException if the Writer is <code>null</code>
+     */
+    protected static String unescapeJava(String str) {
+        if (str == null) {
+            return null;
+        }
+        int sz = str.length();
+        StringBuffer out = new StringBuffer(sz);
+        StringBuffer unicode = new StringBuffer(UNICODE_LEN);
+        boolean hadSlash = false;
+        boolean inUnicode = false;
+        for (int i = 0; i < sz; i++) {
+            char ch = str.charAt(i);
+            if (inUnicode) {
+                // if in unicode, then we're reading unicode
+                // values in somehow
+                unicode.append(ch);
+                if (unicode.length() == UNICODE_LEN) {
+                    // unicode now contains the four hex digits
+                    // which represents our unicode character
+                    try {
+                        int value = Integer.parseInt(unicode.toString(), HEX_RADIX);
+                        out.append((char) value);
+                        unicode.setLength(0);
+                        inUnicode = false;
+                        hadSlash = false;
+                    } catch (NumberFormatException nfe) {
+                        throw new IllegalArgumentException("Unable to parse unicode value: " + unicode, nfe);
+                    }
+                }
+                continue;
+            }
+
+            if (hadSlash) {
+                // handle an escaped value
+                hadSlash = false;
+                switch (ch) {
+                    case '\\' :
+                        out.append('\\');
+                        break;
+                    case '\'' :
+                        out.append('\'');
+                        break;
+                    case '\"' :
+                        out.append('"');
+                        break;
+                    case 'r' :
+                        out.append('\r');
+                        break;
+                    case 'f' :
+                        out.append('\f');
+                        break;
+                    case 't' :
+                        out.append('\t');
+                        break;
+                    case 'n' :
+                        out.append('\n');
+                        break;
+                    case 'b' :
+                        out.append('\b');
+                        break;
+                    case 'u' :
+                        // uh-oh, we're in unicode country....
+                        inUnicode = true;
+                        break;
+                    default :
+                        out.append(ch);
+                        break;
+                }
+                continue;
+            } else if (ch == '\\') {
+                hadSlash = true;
+                continue;
+            }
+            out.append(ch);
+        }
+
+        if (hadSlash) {
+            // then we're in the weird case of a \ at the end of the
+            // string, let's output it anyway.
+            out.append('\\');
+        }
+
+        return out.toString();
+    }
+
+    /**
+     * <p>Escapes the characters in a <code>String</code> using Java String rules.</p>
+     *
+     * <p>Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.) </p>
+     *
+     * <p>So a tab becomes the characters <code>'\\'</code> and
+     * <code>'t'</code>.</p>
+     *
+     * <p>The only difference between Java strings and JavaScript strings
+     * is that in JavaScript, a single quote must be escaped.</p>
+     *
+     * <p>Example:
+     * <pre>
+     * input string: He didn't say, "Stop!"
+     * output string: He didn't say, \"Stop!\"
+     * </pre>
+     * </p>
+     *
+     * @param str  String to escape values in, may be null
+     * @return String with escaped values, <code>null</code> if null string input
+     */
+    protected static String escapeJava(String str) {
+        if (str == null) {
+            return null;
+        }
+        int sz = str.length();
+        StringBuffer out = new StringBuffer(sz * 2);
+        for (int i = 0; i < sz; i++) {
+            char ch = str.charAt(i);
+            // handle unicode
+            if (ch > 0xfff) {
+                out.append("\\u").append(hex(ch));
+            } else if (ch > 0xff) {
+                out.append("\\u0").append(hex(ch));
+            } else if (ch > 0x7f) {
+                out.append("\\u00").append(hex(ch));
+            } else if (ch < 32) {
+                switch (ch) {
+                    case '\b' :
+                        out.append('\\');
+                        out.append('b');
+                        break;
+                    case '\n' :
+                        out.append('\\');
+                        out.append('n');
+                        break;
+                    case '\t' :
+                        out.append('\\');
+                        out.append('t');
+                        break;
+                    case '\f' :
+                        out.append('\\');
+                        out.append('f');
+                        break;
+                    case '\r' :
+                        out.append('\\');
+                        out.append('r');
+                        break;
+                    default :
+                        if (ch > 0xf) {
+                            out.append("\\u00").append(hex(ch));
+                        } else {
+                            out.append("\\u000").append(hex(ch));
+                        }
+                        break;
+                }
+            } else {
+                switch (ch) {
+                    case '"' :
+                        out.append('\\');
+                        out.append('"');
+                        break;
+                    case '\\' :
+                        out.append('\\');
+                        out.append('\\');
+                        break;
+                    default :
+                        out.append(ch);
+                        break;
+                }
+            }
+        }
+        return out.toString();
+    }
+
+    /**
+     * <p>Returns an upper case hexadecimal <code>String</code> for the given
+     * character.</p>
+     *
+     * @param ch The character to convert.
+     * @return An upper case hexadecimal <code>String</code>
+     */
+    protected static String hex(char ch) {
+        return Integer.toHexString(ch).toUpperCase(Locale.ENGLISH);
+    }
+
+    /**
+     * <p>Checks if the value is in the given array.</p>
+     *
+     * <p>The method returns <code>false</code> if a <code>null</code> array is passed in.</p>
+     *
+     * @param array  the array to search through
+     * @param valueToFind  the value to find
+     * @return <code>true</code> if the array contains the object
+     */
+    public static boolean contains(char[] array, char valueToFind) {
+        if (array == null) {
+            return false;
+        }
+        for (int i = 0; i < array.length; i++) {
+            if (valueToFind == array[i]) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * This class is used to read properties lines. These lines do
+     * not terminate with new-line chars but rather when there is no
+     * backslash sign a the end of the line.  This is used to
+     * concatenate multiple lines for readability.
+     */
+    public static class PropertiesReader extends LineNumberReader
+    {
+        /** Stores the comment lines for the currently processed property.*/
+        private List<String> commentLines;
+
+        /** Stores the value lines for the currently processed property.*/
+        private List<String> valueLines;
+
+        /** Stores the name of the last read property.*/
+        private String propertyName;
+
+        /** Stores the value of the last read property.*/
+        private String propertyValue;
+
+        /**
+         * Creates a new instance of <code>PropertiesReader</code> and sets
+         * the underlaying reader and the list delimiter.
+         *
+         * @param reader the reader
+         */
+        public PropertiesReader(Reader reader)
+        {
+            super(reader);
+            commentLines = new ArrayList<String>();
+            valueLines = new ArrayList<String>();
+        }
+
+        /**
+         * Reads a property line. Returns null if Stream is
+         * at EOF. Concatenates lines ending with "\".
+         * Skips lines beginning with "#" or "!" and empty lines.
+         * The return value is a property definition (<code>&lt;name&gt;</code>
+         * = <code>&lt;value&gt;</code>)
+         *
+         * @return A string containing a property value or null
+         *
+         * @throws java.io.IOException in case of an I/O error
+         */
+        public String readProperty() throws IOException
+        {
+            commentLines.clear();
+            valueLines.clear();
+            StringBuffer buffer = new StringBuffer();
+
+            while (true)
+            {
+                String line = readLine();
+                if (line == null)
+                {
+                    // EOF
+                    return null;
+                }
+
+                if (isCommentLine(line))
+                {
+                    commentLines.add(line);
+                    continue;
+                }
+
+                valueLines.add(line);
+                line = line.trim();
+
+                if (checkCombineLines(line))
+                {
+                    line = line.substring(0, line.length() - 1);
+                    buffer.append(line);
+                }
+                else
+                {
+                    buffer.append(line);
+                    break;
+                }
+            }
+            return buffer.toString();
+        }
+
+        /**
+         * Parses the next property from the input stream and stores the found
+         * name and value in internal fields. These fields can be obtained using
+         * the provided getter methods. The return value indicates whether EOF
+         * was reached (<b>false</b>) or whether further properties are
+         * available (<b>true</b>).
+         *
+         * @return a flag if further properties are available
+         * @throws java.io.IOException if an error occurs
+         */
+        public boolean nextProperty() throws IOException
+        {
+            String line = readProperty();
+
+            if (line == null)
+            {
+                return false; // EOF
+            }
+
+            // parse the line
+            String[] property = parseProperty(line);
+            propertyName = unescapeJava(property[0]);
+            propertyValue = unescapeJava(property[1]);
+            return true;
+        }
+
+        /**
+         * Returns the comment lines that have been read for the last property.
+         *
+         * @return the comment lines for the last property returned by
+         * <code>readProperty()</code>
+         */
+        public List<String> getCommentLines()
+        {
+            return commentLines;
+        }
+
+        /**
+         * Returns the value lines that have been read for the last property.
+         *
+         * @return the raw value lines for the last property returned by
+         * <code>readProperty()</code>
+         */
+        public List<String> getValueLines()
+        {
+            return valueLines;
+        }
+
+        /**
+         * Returns the name of the last read property. This method can be called
+         * after <code>{@link #nextProperty()}</code> was invoked and its
+         * return value was <b>true</b>.
+         *
+         * @return the name of the last read property
+         */
+        public String getPropertyName()
+        {
+            return propertyName;
+        }
+
+        /**
+         * Returns the value of the last read property. This method can be
+         * called after <code>{@link #nextProperty()}</code> was invoked and
+         * its return value was <b>true</b>.
+         *
+         * @return the value of the last read property
+         */
+        public String getPropertyValue()
+        {
+            return propertyValue;
+        }
+
+        /**
+         * Checks if the passed in line should be combined with the following.
+         * This is true, if the line ends with an odd number of backslashes.
+         *
+         * @param line the line
+         * @return a flag if the lines should be combined
+         */
+        private static boolean checkCombineLines(String line)
+        {
+            int bsCount = 0;
+            for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--)
+            {
+                bsCount++;
+            }
+
+            return bsCount % 2 != 0;
+        }
+
+        /**
+         * Parse a property line and return the key and the value in an array.
+         *
+         * @param line the line to parse
+         * @return an array with the property's key and value
+         */
+        private static String[] parseProperty(String line)
+        {
+            // sorry for this spaghetti code, please replace it as soon as
+            // possible with a regexp when the Java 1.3 requirement is dropped
+
+            String[] result = new String[2];
+            StringBuffer key = new StringBuffer();
+            StringBuffer value = new StringBuffer();
+
+            // state of the automaton:
+            // 0: key parsing
+            // 1: antislash found while parsing the key
+            // 2: separator crossing
+            // 3: value parsing
+            int state = 0;
+
+            for (int pos = 0; pos < line.length(); pos++)
+            {
+                char c = line.charAt(pos);
+
+                switch (state)
+                {
+                    case 0:
+                        if (c == '\\')
+                        {
+                            state = 1;
+                        }
+                        else if (contains(WHITE_SPACE, c))
+                        {
+                            // switch to the separator crossing state
+                            state = 2;
+                        }
+                        else if (contains(SEPARATORS, c))
+                        {
+                            // switch to the value parsing state
+                            state = 3;
+                        }
+                        else
+                        {
+                            key.append(c);
+                        }
+
+                        break;
+
+                    case 1:
+                        if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c))
+                        {
+                            // this is an escaped separator or white space
+                            key.append(c);
+                        }
+                        else
+                        {
+                            // another escaped character, the '\' is preserved
+                            key.append('\\');
+                            key.append(c);
+                        }
+
+                        // return to the key parsing state
+                        state = 0;
+
+                        break;
+
+                    case 2:
+                        if (contains(WHITE_SPACE, c))
+                        {
+                            // do nothing, eat all white spaces
+                            state = 2;
+                        }
+                        else if (contains(SEPARATORS, c))
+                        {
+                            // switch to the value parsing state
+                            state = 3;
+                        }
+                        else
+                        {
+                            // any other character indicates we encoutered the beginning of the value
+                            value.append(c);
+
+                            // switch to the value parsing state
+                            state = 3;
+                        }
+
+                        break;
+
+                    case 3:
+                        value.append(c);
+                        break;
+                }
+            }
+
+            result[0] = key.toString().trim();
+            result[1] = value.toString().trim();
+
+            return result;
+        }
+    } // class PropertiesReader
+
+    /**
+     * This class is used to write properties lines.
+     */
+    public static class PropertiesWriter extends FilterWriter
+    {
+        /**
+         * Constructor.
+         *
+         * @param writer a Writer object providing the underlying stream
+         */
+        public PropertiesWriter(Writer writer)
+        {
+            super(writer);
+        }
+
+        /**
+         * Writes the given property and its value.
+         *
+         * @param key the property key
+         * @param value the property value
+         * @throws java.io.IOException if an error occurs
+         */
+        public void writeProperty(String key, String value) throws IOException
+        {
+            write(escapeKey(key));
+            write(" = ");
+            write(escapeJava(value));
+            writeln(null);
+        }
+
+        /**
+         * Escape the separators in the key.
+         *
+         * @param key the key
+         * @return the escaped key
+         */
+        private String escapeKey(String key)
+        {
+            StringBuffer newkey = new StringBuffer();
+
+            for (int i = 0; i < key.length(); i++)
+            {
+                char c = key.charAt(i);
+
+                if (contains(SEPARATORS, c) || contains(WHITE_SPACE, c))
+                {
+                    // escape the separator
+                    newkey.append('\\');
+                    newkey.append(c);
+                }
+                else
+                {
+                    newkey.append(c);
+                }
+            }
+
+            return newkey.toString();
+        }
+
+        /**
+         * Helper method for writing a line with the platform specific line
+         * ending.
+         *
+         * @param s the content of the line (may be <b>null</b>)
+         * @throws java.io.IOException if an error occurs
+         */
+        public void writeln(String s) throws IOException
+        {
+            if (s != null)
+            {
+                write(s);
+            }
+            write(LINE_SEPARATOR);
+        }
+
+    } // class PropertiesWriter
+
+    /**
+     * TODO
+     */
+    protected static class Layout {
+
+        private List<String> commentLines;
+        private List<String> valueLines;
+
+        public Layout() {
+        }
+
+        public Layout(List<String> commentLines, List<String> valueLines) {
+            this.commentLines = commentLines;
+            this.valueLines = valueLines;
+        }
+
+        public List<String> getCommentLines() {
+            return commentLines;
+        }
+
+        public void setCommentLines(List<String> commentLines) {
+            this.commentLines = commentLines;
+        }
+
+        public List<String> getValueLines() {
+            return valueLines;
+        }
+
+        public void setValueLines(List<String> valueLines) {
+            this.valueLines = valueLines;
+        }
+
+        public void clearValue() {
+            this.valueLines = null;
+        }
+
+    } // class Layout
+
+}
\ No newline at end of file

Added: felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java?rev=1027254&view=auto
==============================================================================
--- felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java (added)
+++ felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/InterpolationHelperTest.java Mon Oct 25 20:19:04 2010
@@ -0,0 +1,79 @@
+/*
+ * 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.utils.properties;
+
+import junit.framework.TestCase;
+
+import java.util.Enumeration;
+import java.util.Hashtable;
+
+public class InterpolationHelperTest extends TestCase {
+
+    public void testBasicSubstitution()
+    {
+        System.setProperty("value1", "sub_value1");
+        Hashtable 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, InterpolationHelper.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}", InterpolationHelper.substVars("a}", "b", null, new Hashtable()));
+        assertEquals("${a", InterpolationHelper.substVars("${a", "b", null, new Hashtable()));
+    }
+
+    public void testEmptyVariable() {
+        assertEquals("", InterpolationHelper.substVars("${}", "b", null, new Hashtable()));
+    }
+
+    public void testInnerSubst() {
+        Hashtable props = new Hashtable();
+        props.put("a", "b");
+        props.put("b", "c");
+        assertEquals("c", InterpolationHelper.substVars("${${a}}", "z", null, props));
+    }
+
+    public void testSubstLoop() {
+        try {
+            InterpolationHelper.substVars("${a}", "a", null, new Hashtable());
+            fail("Should have thrown an exception");
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    public void testSubstitutionEscape()
+    {
+        assertEquals("${a}", InterpolationHelper.substVars("$\\{a${#}\\}", "b", null, new Hashtable()));
+        assertEquals("${a}", InterpolationHelper.substVars("$\\{a\\}${#}", "b", null, new Hashtable()));
+        assertEquals("${a}", InterpolationHelper.substVars("$\\{a\\}", "b", null, new Hashtable()));
+    }
+
+}

Added: felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/PropertiesTest.java
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/PropertiesTest.java?rev=1027254&view=auto
==============================================================================
--- felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/PropertiesTest.java (added)
+++ felix/trunk/utils/src/test/java/org/apache/felix/utils/properties/PropertiesTest.java Mon Oct 25 20:19:04 2010
@@ -0,0 +1,91 @@
+/*
+ * 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.utils.properties;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
+
+import junit.framework.TestCase;
+
+/**
+ * <p>
+ * Unit tests on <code>Properties</code>.
+ * </p>
+ *
+ * @author jbonofre
+ */
+public class PropertiesTest extends TestCase {
+
+    private final static String TEST_PROPERTIES_FILE = "test.properties";
+
+    private Properties properties;
+
+    /*
+     * (non-Javadoc)
+     * @see junit.framework.TestCase#setUp()
+     */
+    public void setUp() throws Exception {
+        properties = new Properties();
+        properties.load(this.getClass().getClassLoader().getResourceAsStream(TEST_PROPERTIES_FILE));
+    }
+
+    /**
+     * <p>
+     * Test getting property.
+     * </p>
+     *
+     * @throws Exception
+     */
+    public void testGettingProperty() throws Exception {
+        assertEquals("test", properties.get("test"));
+    }
+
+    public void testLoadSave() throws IOException {
+        StringWriter sw = new StringWriter();
+        PrintWriter pw = new PrintWriter(sw);
+        pw.println("# ");
+        pw.println("# The Main  ");
+        pw.println("# ");
+        pw.println("# Comment ");
+        pw.println("# ");
+        pw.println("");
+        pw.println("# Another comment");
+        pw.println("");
+        pw.println("# A value comment");
+        pw.println("key1 = val1");
+        pw.println("");
+        pw.println("# Another value comment");
+        pw.println("key2 = ${key1}/foo");
+        pw.println("");
+        pw.println("# A third comment");
+        pw.println("key3 = val3");
+        pw.println("");
+
+
+        Properties props = new Properties();
+        props.load(new StringReader(sw.toString()));
+        props.save(System.err);
+        System.err.println("=====");
+
+        props.put("key2", props.get("key2"));
+        props.put("key3", "foo");
+        props.save(System.err);
+        System.err.println("=====");
+    }
+}

Added: felix/trunk/utils/src/test/resources/test.properties
URL: http://svn.apache.org/viewvc/felix/trunk/utils/src/test/resources/test.properties?rev=1027254&view=auto
==============================================================================
--- felix/trunk/utils/src/test/resources/test.properties (added)
+++ felix/trunk/utils/src/test/resources/test.properties Mon Oct 25 20:19:04 2010
@@ -0,0 +1,21 @@
+##---------------------------------------------------------------------------
+##  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.
+##---------------------------------------------------------------------------
+#
+# test.properties
+# Used in the PropertiesTest
+#
+test=test
\ No newline at end of file