Add the ability to rollback bundle updates.

git-svn-id: https://svn.apache.org/repos/asf/incubator/felix/trunk@423447 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/org.apache.felix.framework/src/main/java/org/apache/felix/framework/Felix.java b/org.apache.felix.framework/src/main/java/org/apache/felix/framework/Felix.java
index aa0391b..d623b9b 100644
--- a/org.apache.felix.framework/src/main/java/org/apache/felix/framework/Felix.java
+++ b/org.apache.felix.framework/src/main/java/org/apache/felix/framework/Felix.java
@@ -1523,12 +1523,30 @@
                 // Create a module for the new revision; the revision is
                 // base zero, so subtract one from the revision count to
                 // get the revision of the new update.
-                IModule module = createModule(
-                    info.getBundleId(),
-                    archive.getRevisionCount() - 1,
-                    info.getCurrentHeader());
-                // Add module to bundle info.
-                info.addModule(module);
+                try
+                {
+                    IModule module = createModule(
+                        info.getBundleId(),
+                        archive.getRevisionCount() - 1,
+                        info.getCurrentHeader());
+                    // Add module to bundle info.
+                    info.addModule(module);
+                } 
+                catch (Exception ex)
+                {
+                    rethrow = ex;
+                    
+                    try 
+                    {
+                        archive.undoRevise();
+                    }
+                    catch (Exception busted)
+                    {
+                        m_logger.log(Logger.LOG_ERROR, "Unable to rollback.", busted);
+                    }
+                    
+                    m_logger.log(Logger.LOG_ERROR, "Unable to update the bundle.", ex);
+                }
             }
             catch (Exception ex)
             {
@@ -1536,12 +1554,13 @@
                 rethrow = ex;
             }
 
-            info.setState(Bundle.INSTALLED);
-            info.setLastModified(System.currentTimeMillis());
-
-            // Fire updated event if successful.
+            // Set new state, mark as needing a refresh, and fire updated event 
+            // if successful.
             if (rethrow == null)
             {
+                info.setState(Bundle.INSTALLED);
+                info.setLastModified(System.currentTimeMillis());
+                
                 // Mark previous the bundle's old module for removal since
                 // it can no longer be used to resolve other modules per the spec.
                 IModule module = info.getModules()[info.getModules().length - 2];
diff --git a/org.apache.felix.framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java b/org.apache.felix.framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java
index 5c1d24c..9cd4e81 100644
--- a/org.apache.felix.framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java
+++ b/org.apache.felix.framework/src/main/java/org/apache/felix/framework/cache/BundleArchive.java
@@ -70,6 +70,7 @@
     private static final transient String BUNDLE_ID_FILE = "bundle.id";
     private static final transient String BUNDLE_LOCATION_FILE = "bundle.location";
     private static final transient String CURRENT_LOCATION_FILE = "current.location";
+    private static final transient String REVISION_LOCATION_FILE = "revision.location";
     private static final transient String BUNDLE_STATE_FILE = "bundle.state";
     private static final transient String BUNDLE_START_LEVEL_FILE = "bundle.startlevel";
     private static final transient String REFRESH_COUNTER_FILE = "refresh.counter";
@@ -79,7 +80,7 @@
     private static final transient String ACTIVE_STATE = "active";
     private static final transient String INSTALLED_STATE = "installed";
     private static final transient String UNINSTALLED_STATE = "uninstalled";
-
+    
     private Logger m_logger = null;
     private long m_id = -1;
     private File m_archiveRootDir = null;
@@ -136,7 +137,7 @@
         initialize();
 
         // Add a revision for the content.
-        revise(getCurrentLocation(), is);
+        revise(m_originalLocation, is);
     }
 
     /**
@@ -190,8 +191,21 @@
             m_revisions = new BundleRevision[revisionCount - 1];
         }
 
-        // Add the revision object for the most recent revision.
-        revise(getCurrentLocation(), null);
+        // Add the revision object for the most recent revision. We first try to read
+        // the location from the current revision - if that fails we likely have 
+        // an old bundle cache and read the location the old way. The next 
+        // revision will update the bundle cache.
+        // TODO: FRAMEWORK - This try catch block can eventually be deleted when we decide to remove
+        // support for the old way, then we only need the first call to revise().
+        try 
+        {
+            revise(getRevisionLocation(revisionCount - 1), null);
+        }
+        catch (Exception ex)
+        {
+            m_logger.log(Logger.LOG_WARNING, getClass().getName() + ": Updating old bundle cache format.");
+            revise(getCurrentLocation(), null);
+        }
     }
 
     /**
@@ -598,6 +612,7 @@
      * This method adds a revision to the archive. The revision is created
      * based on the specified location and/or input stream.
      * </p>
+     * @param location the location string associated with the revision.
      * @throws Exception if any error occurs.
     **/
     public synchronized void revise(String location, InputStream is)
@@ -618,8 +633,12 @@
         }
 
         // Set the current revision location to match.
+        // TODO: FRAMEWORK - This can eventually be deleted when we removed
+        // support for the old way of doing things.
         setCurrentLocation(location);
 
+        setRevisionLocation(location, (m_revisions == null) ? 0 : m_revisions.length);
+        
         // Add new revision to revision array.
         if (m_revisions == null)
         {
@@ -636,6 +655,97 @@
 
     /**
      * <p>
+     * This method undoes the previous revision to the archive; this method will
+     * remove the latest revision from the archive. This method is only called
+     * when there are problems during an update after the revision has been
+     * created, such as errors in the update bundle's manifest. This method
+     * can only be called if there is more than one revision, otherwise there
+     * is nothing to undo.
+     * </p>
+     * @return true if the undo was a success false if there is no previous revision
+     * @throws Exception if any error occurs.
+     */
+    public synchronized boolean undoRevise() throws Exception
+    {
+        // Can only undo the revision if there is more than one.
+        if (getRevisionCount() <= 1)
+        {
+            return false;
+        }
+        
+        String location = getRevisionLocation(m_revisions.length - 2);
+
+        // TODO: FRAMEWORK - This can eventually be deleted when we removed
+        // support for the old way of doing things.
+        setCurrentLocation(location);
+        
+        try
+        {
+            m_revisions[m_revisions.length - 1].dispose();
+        } 
+        catch(Exception ex)
+        {
+           m_logger.log(Logger.LOG_ERROR, getClass().getName() + 
+               ": Unable to dispose latest revision", ex); 
+        }
+
+        File revisionDir = new File(m_archiveRootDir, REVISION_DIRECTORY + 
+            getRefreshCount() + "." + (m_revisions.length - 1));
+        
+        if (BundleCache.getSecureAction().fileExists(revisionDir))
+        {
+            BundleCache.deleteDirectoryTree(revisionDir);
+        }
+        
+        BundleRevision[] tmp = new BundleRevision[m_revisions.length - 1];
+        System.arraycopy(m_revisions, 0, tmp, 0, m_revisions.length - 1);
+        
+        return true;
+    }
+    
+    private synchronized String getRevisionLocation(int revision) throws Exception
+    {   
+        InputStream is = null;
+        BufferedReader br = null;
+        try
+        {
+            is = BundleCache.getSecureAction().getFileInputStream(new File(
+                new File(m_archiveRootDir, REVISION_DIRECTORY + 
+                getRefreshCount() + "." + revision), REVISION_LOCATION_FILE));
+            
+            br = new BufferedReader(new InputStreamReader(is));
+            return br.readLine();
+        }
+        finally
+        {
+            if (br != null) br.close();
+            if (is != null) is.close();
+        }
+    }
+    
+    private synchronized void setRevisionLocation(String location, int revision) throws Exception
+    {
+        // Save current revision location.
+        OutputStream os = null;
+        BufferedWriter bw = null;
+        try
+        {
+            os = BundleCache.getSecureAction()
+                .getFileOutputStream(new File(
+                    new File(m_archiveRootDir, REVISION_DIRECTORY + 
+                    getRefreshCount() + "." + revision), REVISION_LOCATION_FILE));
+            bw = new BufferedWriter(new OutputStreamWriter(os));
+            bw.write(location, 0, location.length());
+        }
+        finally
+        {
+            if (bw != null) bw.close();
+            if (os != null) os.close();
+        }
+    }
+    
+    /**
+     * <p>
      * This method removes all old revisions associated with the archive
      * and keeps only the current revision.
      * </p>
@@ -672,9 +782,13 @@
         // to the new refresh level.
         m_revisions[count - 1].dispose();
 
+        // Save the current revision location for use later when
+        // we recreate the revision.
+        String location = getRevisionLocation(count -1);
+        
         // Increment the refresh count.
         setRefreshCount(refreshCount + 1);
-
+        
         // Rename the current revision directory to be the zero revision
         // of the new refresh level.
         File currentDir = new File(m_archiveRootDir, REVISION_DIRECTORY + (refreshCount + 1) + ".0");
@@ -684,7 +798,7 @@
         // Null the revision array since they are all invalid now.
         m_revisions = null;
         // Finally, recreate the revision for the current location.
-        BundleRevision revision = createRevisionFromLocation(getCurrentLocation(), null);
+        BundleRevision revision = createRevisionFromLocation(location, null);
         // Create new revision array.
         m_revisions = new BundleRevision[] { revision };
     }