SLING-3737 : Instance Sling Identifier may be randomly reset on restart. Apply patch from Timothee Maret

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1608798 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index 349146e..2a767db 100644
--- a/pom.xml
+++ b/pom.xml
@@ -117,5 +117,17 @@
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-simple</artifactId>
         </dependency>
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.4</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <version>1.8.2</version>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/settings/impl/SlingSettingsServiceImpl.java b/src/main/java/org/apache/sling/settings/impl/SlingSettingsServiceImpl.java
index 5fadb1e..1281495 100644
--- a/src/main/java/org/apache/sling/settings/impl/SlingSettingsServiceImpl.java
+++ b/src/main/java/org/apache/sling/settings/impl/SlingSettingsServiceImpl.java
@@ -18,6 +18,8 @@
  */
 package org.apache.sling.settings.impl;
 
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
@@ -77,6 +79,9 @@
     /** The name of the data file holding install run mode options */
     private static final String OPTIONS_FILE = "sling.options.file";
 
+    /** The length in bytes of a sling identifier */
+    private static final int SLING_ID_LENGTH = 36;
+
     /** The properties for name, description. */
     private final Map<String, String> slingProps = new HashMap<String, String>();
 
@@ -124,7 +129,7 @@
             // the osgi framework does not support storing something in the file system
             throw new RuntimeException("Unable to read from bundle data file.");
         }
-        this.slingId = this.readSlingId(idFile);
+        this.slingId = this.readSlingId(idFile, SLING_ID_LENGTH);
 
         // no sling id yet or failure to read file: create an id and store
         if (slingId == null) {
@@ -147,7 +152,7 @@
         }
     }
 
-    private static final class Options implements Serializable {
+    static final class Options implements Serializable {
         private static final long serialVersionUID = 1L;
         String[] modes;
         String   selected;
@@ -263,7 +268,7 @@
         return optionsList;
     }
 
-    private void writeOptions(final BundleContext context, final List<Options> optionsList) {
+    void writeOptions(final BundleContext context, final List<Options> optionsList) {
         final File file = context.getDataFile(OPTIONS_FILE);
         FileOutputStream fos = null;
         ObjectOutputStream oos = null;
@@ -286,30 +291,28 @@
     /**
      * Read the id from a file.
      */
-    private String readSlingId(final File idFile) {
-        if (idFile.exists() && idFile.length() >= 36) {
-            FileInputStream fin = null;
+    String readSlingId(final File idFile, int maxLength) {
+        if (idFile.exists() && idFile.length() >= maxLength) {
+            DataInputStream dis = null;
             try {
-                fin = new FileInputStream(idFile);
-                final byte[] rawBytes = new byte[36];
-                if (fin.read(rawBytes) == 36) {
-                    final String rawString = new String(rawBytes, "ISO-8859-1");
+                final byte[] rawBytes = new byte[maxLength];
+                dis = new DataInputStream(new FileInputStream(idFile));
+                dis.readFully(rawBytes);
+                final String rawString = new String(rawBytes, "ISO-8859-1");
 
-                    // roundtrip to ensure correct format of UUID value
-                    final String id = UUID.fromString(rawString).toString();
-                    logger.debug("Got Sling ID {} from file {}", id, idFile);
+                // roundtrip to ensure correct format of UUID value
+                final String id = UUID.fromString(rawString).toString();
+                logger.debug("Got Sling ID {} from file {}", id, idFile);
 
-                    return id;
-                }
+                return id;
             } catch (final Throwable t) {
                 logger.error("Failed reading UUID from id file " + idFile
                         + ", creating new id", t);
             } finally {
-                if (fin != null) {
+                if (dis != null) {
                     try {
-                        fin.close();
-                    } catch (IOException ignore) {
-                    }
+                        dis.close();
+                    } catch (IOException ignore){}
                 }
             }
         }
@@ -319,22 +322,22 @@
     /**
      * Write the sling id file.
      */
-    private void writeSlingId(final File idFile, final String id) {
+    void writeSlingId(final File idFile, final String id) {
         idFile.delete();
         idFile.getParentFile().mkdirs();
-        FileOutputStream fout = null;
+        DataOutputStream dos = null;
         try {
-            fout = new FileOutputStream(idFile);
-            fout.write(slingId.getBytes("ISO-8859-1"));
-            fout.flush();
+            final byte[] rawBytes = id.getBytes("ISO-8859-1");
+            dos = new DataOutputStream(new FileOutputStream(idFile));
+            dos.write(rawBytes, 0, rawBytes.length);
+            dos.flush();
         } catch (final Throwable t) {
             logger.error("Failed writing UUID to id file " + idFile, t);
         } finally {
-            if (fout != null) {
+            if (dos != null) {
                 try {
-                    fout.close();
-                } catch (IOException ignore) {
-                }
+                    dos.close();
+                } catch (IOException ignore) {}
             }
         }
     }
diff --git a/src/test/java/org/apache/sling/settings/impl/SlingSettingsServiceImplTest.java b/src/test/java/org/apache/sling/settings/impl/SlingSettingsServiceImplTest.java
new file mode 100644
index 0000000..81e3dd1
--- /dev/null
+++ b/src/test/java/org/apache/sling/settings/impl/SlingSettingsServiceImplTest.java
@@ -0,0 +1,156 @@
+/*
+ * 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.sling.settings.impl;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.commons.lang.RandomStringUtils;
+import org.junit.After;
+import org.junit.Assert;
+import org.apache.sling.launchpad.api.StartupHandler;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.BundleContext;
+
+public class SlingSettingsServiceImplTest {
+
+    private static final String SLING_ID_FILE_NAME = "sling.id.file";
+
+    private static final String OPTIONS_FILE_NAME = "sling.options.file";
+
+    private static final String SLING_ID = "097bae9b-bf60-45a2-ad8c-ccdd374dd9b0";
+
+    private File slingIdFile = null;
+
+    private File optionsFile = null;
+
+    @Before
+    public void before() throws IOException {
+        slingIdFile = File.createTempFile(
+                SLING_ID_FILE_NAME, "");
+        optionsFile = File.createTempFile(
+                OPTIONS_FILE_NAME, "");
+    }
+
+    @After
+    public void after() throws IOException {
+        if (slingIdFile != null ) {
+            slingIdFile.delete();
+            slingIdFile = null;
+        }
+        if (optionsFile != null) {
+            optionsFile.delete();
+            optionsFile = null;
+        }
+    }
+
+    @Test
+    public void testGenerateSlingId()
+            throws IOException {
+        String slingId =  readSlingId(slingIdFile, optionsFile, SLING_ID.length());
+        Assert.assertNotNull(slingId);
+    }
+
+    @Test
+    public void testGetSlingId()
+            throws IOException {
+        writeSlingId(slingIdFile, optionsFile, SLING_ID);
+        String generated =  readSlingId(slingIdFile, optionsFile, SLING_ID.length());
+        Assert.assertNotNull(generated);
+        Assert.assertEquals(SLING_ID, generated);
+        String slingId = readSlingId(slingIdFile, optionsFile, SLING_ID.length());
+        Assert.assertNotNull(slingId);
+        Assert.assertEquals(generated, slingId);
+    }
+
+    @Test
+    public void testGetLongSlingIdFromTooLargeData()
+            throws IOException {
+        String data = SLING_ID + RandomStringUtils.randomAscii(1024 * 1024); // 1MB long random String
+        writeSlingId(slingIdFile, optionsFile, data);
+        String slingId =  readSlingId(slingIdFile, optionsFile, SLING_ID.length());
+        Assert.assertNotNull(slingId);
+        Assert.assertEquals(SLING_ID, slingId);
+    }
+
+    @Test
+    public void testGetSlingIdFromTooShortData()
+            throws IOException {
+        String data = RandomStringUtils.randomAscii(8); // 8 byte long string
+        writeSlingId(slingIdFile, optionsFile, data);
+        String slingId =  readSlingId(slingIdFile, optionsFile, SLING_ID.length());
+        Assert.assertNotNull(slingId);
+        Assert.assertNotEquals(SLING_ID, slingId);
+    }
+
+    private String readSlingId(File slingIdFile, File optionsFile, int maxLength)
+            throws IOException {
+        SlingSettingsServiceImpl settings = getSlingSettings(slingIdFile, optionsFile);
+        return settings.readSlingId(slingIdFile, maxLength);
+    }
+
+    private void writeSlingId(File slingIdFile, File optionsFile, String slingId)
+            throws IOException {
+        SlingSettingsServiceImpl settings = getSlingSettings(slingIdFile, optionsFile);
+        settings.writeSlingId(slingIdFile, slingId);
+    }
+
+    private SlingSettingsServiceImpl getSlingSettings(File slingIdFile, File optionsFile)
+            throws IOException {
+        BundleContext context = Mockito.mock(BundleContext.class);
+        Mockito.when(context.getDataFile(SLING_ID_FILE_NAME))
+                .thenReturn(slingIdFile);
+        Mockito.when(context.getDataFile(OPTIONS_FILE_NAME))
+                .thenReturn(optionsFile);
+        StartupHandler handler = Mockito.mock(StartupHandler.class);
+        // write options
+        List<SlingSettingsServiceImpl.Options> options = new ArrayList<SlingSettingsServiceImpl.Options>();
+        FileOutputStream fos = null;
+        ObjectOutputStream oos = null;
+        try {
+            fos = new FileOutputStream(optionsFile);
+            oos = new ObjectOutputStream(fos);
+            oos.writeObject(options);
+        } catch ( final IOException ioe ) {
+            throw new RuntimeException("Unable to write to options data file.", ioe);
+        } finally {
+            if ( oos != null ) {
+                try {
+                    oos.close();
+                } catch (IOException ignore) {
+                    // ...
+                }
+            }
+            if ( fos != null ) {
+                try {
+                    fos.close();
+                } catch (IOException ignore) {
+                    // ...
+                }
+            }
+        }
+        return new SlingSettingsServiceImpl(context, handler);
+    }
+}