FELIX-4844 : Store configuration data in a diff-tool friendly way. Apply patch from Balazs Zsoldos

This closes #16

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@1678871 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/configadmin/changelog.txt b/configadmin/changelog.txt
index 622da53..e17efbe 100644
--- a/configadmin/changelog.txt
+++ b/configadmin/changelog.txt
@@ -4,6 +4,9 @@
 ** Bug
     * [FELIX-4884] - listConfigurations should return null if no configuration is found
 
+** Improvement
+    * [FELIX-4844] - Store configuration data in a diff-tool friendly way
+
 
 Changes from 1.8.2 to 1.8.4
 ---------------------------
diff --git a/configadmin/src/main/java/org/apache/felix/cm/file/ConfigurationHandler.java b/configadmin/src/main/java/org/apache/felix/cm/file/ConfigurationHandler.java
index 1d53ad2..d1a54a9 100644
--- a/configadmin/src/main/java/org/apache/felix/cm/file/ConfigurationHandler.java
+++ b/configadmin/src/main/java/org/apache/felix/cm/file/ConfigurationHandler.java
@@ -30,8 +30,10 @@
 import java.io.Writer;
 import java.lang.reflect.Array;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.BitSet;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Dictionary;
 import java.util.Enumeration;
 import java.util.HashMap;
@@ -95,6 +97,8 @@
     protected static final int TOKEN_PRIMITIVE_BOOLEAN = 'b';
 
     protected static final String CRLF = "\r\n";
+    protected static final String INDENT = "  ";
+    protected static final String COLLECTION_LINE_BREAK = " \\\r\n";
 
     protected static final Map code2Type;
     protected static final Map type2Code;
@@ -200,7 +204,7 @@
     {
         BufferedWriter bw = new BufferedWriter( new OutputStreamWriter( out, ENCODING ) );
 
-        for ( Enumeration ce = properties.keys(); ce.hasMoreElements(); )
+        for ( Enumeration ce = orderedKeys(properties); ce.hasMoreElements(); )
         {
             String key = ( String ) ce.nextElement();
 
@@ -214,6 +218,28 @@
         bw.flush();
     }
 
+    /**
+     * Generates an <code>Enumeration</code> for the given
+     * <code>Dictionary</code> where the keys of the <code>Dictionary</code>
+     * are provided in sorted order.
+     *
+     * @param properties
+     *                   The <code>Dictionary</code> that keys are sorted.
+     * @return An <code>Enumeration</code> that provides the keys of
+     *         properties in an ordered manner.
+     */
+    private static Enumeration orderedKeys(Dictionary properties) {
+        String[] keyArray = new String[properties.size()];
+        int i = 0;
+        for ( Enumeration ce = properties.keys(); ce.hasMoreElements(); )
+        {
+            keyArray[i] = ( String ) ce.nextElement();
+            i++;
+        }
+        Arrays.sort(keyArray);
+        return Collections.enumeration( Arrays.asList( keyArray ) );
+    }
+
 
     /**
      * Reads configuration data from the given <code>InputStream</code> and
@@ -336,7 +362,7 @@
         List list = new ArrayList();
         for ( ;; )
         {
-            int c = read(pr);
+            int c = ignorablePageBreakAndWhiteSpace( pr );
             if ( c == TOKEN_VAL_OPEN )
             {
                 Object value = readSimple( typeCode, pr );
@@ -350,7 +376,7 @@
 
                 list.add( value );
 
-                c = read( pr );
+                c = ignorablePageBreakAndWhiteSpace( pr );
             }
 
             if ( c == TOKEN_ARR_CLOS )
@@ -380,7 +406,7 @@
         Collection collection = new ArrayList();
         for ( ;; )
         {
-            int c = read( pr );
+            int c = ignorablePageBreakAndWhiteSpace( pr );
             if ( c == TOKEN_VAL_OPEN )
             {
                 Object value = readSimple( typeCode, pr );
@@ -394,7 +420,7 @@
 
                 collection.add( value );
 
-                c = read( pr );
+                c = ignorablePageBreakAndWhiteSpace( pr );
             }
 
             if ( c == TOKEN_VEC_CLOS )
@@ -480,23 +506,6 @@
     }
 
 
-    private boolean checkNext( PushbackReader pr, int expected ) throws IOException
-    {
-        int next = read( pr );
-        if ( next < 0 )
-        {
-            return false;
-        }
-
-        if ( next == expected )
-        {
-            return true;
-        }
-
-        return false;
-    }
-
-
     private String readQuoted( PushbackReader pr ) throws IOException
     {
         StringBuffer buf = new StringBuffer();
@@ -599,6 +608,28 @@
     }
 
 
+    private int ignorablePageBreakAndWhiteSpace( PushbackReader pr ) throws IOException
+    {
+        int c = ignorableWhiteSpace( pr );
+        for ( ;; )
+        {
+            if ( c != '\\' )
+            {
+                break;
+            }
+            int c1 = pr.read();
+            if ( c1 == '\r' || c1 == '\n' )
+            {
+                c = ignorableWhiteSpace( pr );
+            } else {
+                pr.unread(c1);
+                break;
+            }
+        }
+        return c;
+    }
+
+
     private int read( PushbackReader pr ) throws IOException
     {
         int c = pr.read();
@@ -678,12 +709,12 @@
         int size = Array.getLength( arrayValue );
         writeType( out, arrayValue.getClass().getComponentType() );
         out.write( TOKEN_ARR_OPEN );
+        out.write( COLLECTION_LINE_BREAK );
         for ( int i = 0; i < size; i++ )
         {
-            if ( i > 0 )
-                out.write( TOKEN_COMMA );
-            writeSimple( out, Array.get( arrayValue, i ) );
+            writeCollectionElement(out, Array.get( arrayValue, i ));
         }
+        out.write( INDENT );
         out.write( TOKEN_ARR_CLOS );
     }
 
@@ -693,6 +724,7 @@
         if ( collection.isEmpty() )
         {
             out.write( TOKEN_VEC_OPEN );
+            out.write( COLLECTION_LINE_BREAK );
             out.write( TOKEN_VEC_CLOS );
         }
         else
@@ -702,18 +734,27 @@
 
             writeType( out, firstElement.getClass() );
             out.write( TOKEN_VEC_OPEN );
-            writeSimple( out, firstElement );
+            out.write( COLLECTION_LINE_BREAK );
+
+            writeCollectionElement( out, firstElement );
 
             while ( ci.hasNext() )
             {
-                out.write( TOKEN_COMMA );
-                writeSimple( out, ci.next() );
+                writeCollectionElement( out, ci.next() );
             }
             out.write( TOKEN_VEC_CLOS );
         }
     }
 
 
+    private static void writeCollectionElement(Writer out, Object element) throws IOException {
+        out.write( INDENT );
+        writeSimple( out, element );
+        out.write( TOKEN_COMMA );
+        out.write(COLLECTION_LINE_BREAK);
+    }
+
+
     private static void writeType( Writer out, Class valueType ) throws IOException
     {
         Integer code = ( Integer ) type2Code.get( valueType );
diff --git a/configadmin/src/test/java/org/apache/felix/cm/file/FilePersistenceManagerTest.java b/configadmin/src/test/java/org/apache/felix/cm/file/FilePersistenceManagerTest.java
index b8a03a1..ad41462 100644
--- a/configadmin/src/test/java/org/apache/felix/cm/file/FilePersistenceManagerTest.java
+++ b/configadmin/src/test/java/org/apache/felix/cm/file/FilePersistenceManagerTest.java
@@ -19,7 +19,9 @@
 package org.apache.felix.cm.file;
 
 
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileReader;
 import java.io.IOException;
 import java.lang.reflect.Array;
 import java.util.ArrayList;
@@ -29,6 +31,7 @@
 import java.util.Hashtable;
 import java.util.Vector;
 
+import junit.framework.Assert;
 import junit.framework.TestCase;
 
 
@@ -253,6 +256,37 @@
         check( "LPT1", "lpt1" );
     }
 
+    public void testKeyOrderInFile() throws IOException
+    {
+        Dictionary props = new Hashtable();
+        // The following keys are stored as "c, a, b" in HashTable based
+        // due to their hash code
+        props.put( "a_first", "a" );
+        props.put( "b_second", "b" );
+        props.put( "c_third", "c" );
+
+        String pid = "keyOrderInFile";
+        fpm.store( pid, props );
+        File configFile = new File( file, fpm.encodePid( pid ) + ".config" );
+        FileReader reader = new FileReader( configFile );
+        BufferedReader breader = new BufferedReader(reader);
+        try
+        {
+            String previousLine = breader.readLine();
+            while ( previousLine != null) 
+            {
+                String line = breader.readLine();
+                if (line != null) {
+                    Assert.assertTrue( previousLine.compareTo( line ) < 0 );
+                }
+                previousLine = line;
+            }
+        }
+        finally
+        {
+            breader.close();
+        }
+    }
 
     private void check( String name, Object value ) throws IOException
     {