FELIX-1784: add an option to control the behavior in case of a failure in a batched installation

git-svn-id: https://svn.apache.org/repos/asf/felix/trunk@827999 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java
index 6432541..d8cc113 100644
--- a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/FeaturesService.java
@@ -29,6 +29,7 @@
         NoCleanIfFailure,
         PrintBundlesToRefresh,
         NoAutoRefreshBundles,
+        ContinueBatchOnFailure
     }
 
     void addRepository(URI url) throws Exception;
diff --git a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java
index 8fd093c..5a4da58 100644
--- a/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java
+++ b/karaf/features/core/src/main/java/org/apache/felix/karaf/features/internal/FeaturesServiceImpl.java
@@ -234,13 +234,25 @@
 
     public void installFeatures(Set<Feature> features, EnumSet<Option> options) throws Exception {
         InstallationState state = new InstallationState();
+        InstallationState failure = new InstallationState();
         try {
             // Install everything
             for (Feature f : features) {
+                InstallationState s = new InstallationState();
             	try {
-            		doInstallFeature(state, f);
+                    doInstallFeature(s, f);
+                    state.bundles.addAll(s.bundles);
+                    state.features.putAll(s.features);
+                    state.installed.addAll(s.installed);
             	} catch (Exception e) {
-            		LOGGER.error("can't install Feature with name " + f.getName() + " for " + e.getMessage());
+                    failure.bundles.addAll(s.bundles);
+                    failure.features.putAll(s.features);
+                    failure.installed.addAll(s.installed);
+                    if (options.contains(Option.ContinueBatchOnFailure)) {
+                        LOGGER.info("Error when installing feature {}: {}", f.getName(), e);
+                    } else {
+                        throw e;
+                    }
             	}
             }
             // Find bundles to refresh
@@ -284,6 +296,17 @@
                     }
                 }
             }
+            // Clean up for batch
+            if (!options.contains(Option.NoCleanIfFailure)) {
+                failure.installed.removeAll(state.bundles);
+                for (Bundle b : failure.installed) {
+                    try {
+                        b.uninstall();
+                    } catch (Exception e2) {
+                        // Ignore
+                    }
+                }
+            }
         } catch (Exception e) {
             // cleanup on error
             if (!options.contains(Option.NoCleanIfFailure)) {
@@ -295,6 +318,13 @@
                         // Ignore
                     }
                 }
+                for (Bundle b : failure.installed) {
+                    try {
+                        b.uninstall();
+                    } catch (Exception e2) {
+                        // Ignore
+                    }
+                }
             } else {
                 // Force start of bundles so that they are flagged as persistently started
                 for (Bundle b : state.installed) {
@@ -630,7 +660,7 @@
                         }
                     }
                     try {
-                        installFeatures(features, EnumSet.of(Option.NoCleanIfFailure));
+                        installFeatures(features, EnumSet.of(Option.NoCleanIfFailure, Option.ContinueBatchOnFailure));
                     } catch (Exception e) {
                         LOGGER.error("Error installing boot features", e);
                     }
diff --git a/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java b/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java
index 008fb13..d6bcbc0 100644
--- a/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java
+++ b/karaf/features/core/src/test/java/org/apache/felix/karaf/features/FeaturesServiceTest.java
@@ -20,14 +20,20 @@
 import java.io.FileWriter;
 import java.io.InputStream;
 import java.io.PrintWriter;
+import java.net.MalformedURLException;
 import java.net.URI;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.EnumSet;
 import java.util.Hashtable;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArraySet;
 
 import junit.framework.TestCase;
 import org.apache.felix.karaf.features.internal.FeaturesServiceImpl;
 import org.apache.felix.karaf.features.internal.FeatureImpl;
 import org.easymock.EasyMock;
+import static org.easymock.EasyMock.eq;
 import static org.easymock.EasyMock.expect;
 import static org.easymock.EasyMock.isA;
 import static org.easymock.EasyMock.replay;
@@ -42,10 +48,7 @@
 
     public void testInstallFeature() throws Exception {
 
-        String name = Bundle.class.getName();
-        name = name.replace(".", "/")  + ".class";
-        name = getClass().getClassLoader().getResource(name).toString();
-        name = name.substring("jar:".length(), name.indexOf('!'));
+        String name = getJarUrl(Bundle.class);
 
         File tmp = File.createTempFile("smx", ".feature");
         PrintWriter pw = new PrintWriter(new FileWriter(tmp));
@@ -130,11 +133,8 @@
     }
 
     public void testUninstallFeature() throws Exception {
-    	
-        String name = Bundle.class.getName();
-        name = name.replace(".", "/")  + ".class";
-        name = getClass().getClassLoader().getResource(name).toString();
-        name = name.substring("jar:".length(), name.indexOf('!'));
+
+        String name = getJarUrl(Bundle.class);
 
         File tmp = File.createTempFile("smx", ".feature");
         PrintWriter pw = new PrintWriter(new FileWriter(tmp));
@@ -272,12 +272,9 @@
     }    
     
     // Tests Add and Remove Repository
-    public void testAddAndRemoveRepository() throws Exception {        
+    public void testAddAndRemoveRepository() throws Exception {
 
-    	String name = Bundle.class.getName();
-        name = name.replace(".", "/")  + ".class";
-        name = getClass().getClassLoader().getResource(name).toString();
-        name = name.substring("jar:".length(), name.indexOf('!'));        
+        String name = getJarUrl(Bundle.class);
 
         File tmp = File.createTempFile("smx", ".feature");
         PrintWriter pw = new PrintWriter(new FileWriter(tmp));
@@ -349,12 +346,9 @@
 
     // Tests installing all features in a repo and uninstalling
     // all features in a repo
-    public void testInstallUninstallAllFeatures() throws Exception {        
+    public void testInstallUninstallAllFeatures() throws Exception {
 
-    	String name = Bundle.class.getName();
-        name = name.replace(".", "/")  + ".class";
-        name = getClass().getClassLoader().getResource(name).toString();
-        name = name.substring("jar:".length(), name.indexOf('!'));        
+        String name = getJarUrl(Bundle.class);
 
         File tmp = File.createTempFile("smx", ".feature");
         PrintWriter pw = new PrintWriter(new FileWriter(tmp));
@@ -556,12 +550,9 @@
     // with a feature dependency  
     // The dependant feature is in the same repository 
     // Tests uninstall of features
-    public void testInstallFeatureWithDependantFeatures() throws Exception {          
+    public void testInstallFeatureWithDependantFeatures() throws Exception {
 
-    	String name = Bundle.class.getName();
-        name = name.replace(".", "/")  + ".class";
-        name = getClass().getClassLoader().getResource(name).toString();
-        name = name.substring("jar:".length(), name.indexOf('!'));        
+        String name = getJarUrl(Bundle.class);
 
         File tmp = File.createTempFile("smx", ".feature");
         PrintWriter pw = new PrintWriter(new FileWriter(tmp));
@@ -673,11 +664,11 @@
         expect(prefs.node("repositories")).andReturn(repositoriesNode);
         repositoriesNode.clear();
         repositoriesNode.putInt("count", 0);
-        repositoriesNode.put("item.0", uri.toString());        
+        repositoriesNode.put("item.0", uri.toString());
         expect(prefs.node("features")).andReturn(featuresNode);
-        featuresNode.clear();        
+        featuresNode.clear();
         prefs.putBoolean("bootFeaturesInstalled", false);
-        prefs.flush();                        
+        prefs.flush();
         
         replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle);
 
@@ -694,4 +685,302 @@
         
     }
 
+    public void testInstallBatchFeatureWithContinueOnFailureNoClean() throws Exception {
+        String bundle1 = getJarUrl(Bundle.class);
+        String bundle2 = getJarUrl(PreferencesService.class);
+
+        File tmp = File.createTempFile("smx", ".feature");
+        PrintWriter pw = new PrintWriter(new FileWriter(tmp));
+        pw.println("<features>");
+        pw.println("  <feature name='f1'>");
+        pw.println("    <bundle>" + bundle1 + "</bundle>");
+        pw.println("    <bundle>" + "zfs:unknown" + "</bundle>");
+        pw.println("  </feature>");
+        pw.println("  <feature name='f2'>");
+        pw.println("    <bundle>" + bundle2 + "</bundle>");
+        pw.println("  </feature>");
+        pw.println("</features>");
+        pw.close();
+
+        URI uri = tmp.toURI();
+
+        // loads the state
+        Preferences prefs = EasyMock.createMock(Preferences.class);
+        PreferencesService preferencesService = EasyMock.createMock(PreferencesService.class);
+        Preferences repositoriesNode = EasyMock.createMock(Preferences.class);
+        Preferences repositoriesAvailableNode = EasyMock.createMock(Preferences.class);
+        Preferences featuresNode = EasyMock.createMock(Preferences.class);
+        BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
+        Bundle installedBundle1 = EasyMock.createMock(Bundle.class);
+        Bundle installedBundle2 = EasyMock.createMock(Bundle.class);
+
+        // savestate from addRepository
+        expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+        expect(prefs.node("repositories")).andReturn(repositoriesNode);
+        repositoriesNode.clear();
+        repositoriesNode.putInt("count", 1);
+        repositoriesNode.put("item.0", uri.toString());
+        expect(prefs.node("features")).andReturn(featuresNode);
+        featuresNode.clear();
+        prefs.putBoolean("bootFeaturesInstalled", false);
+        prefs.flush();
+
+        // Installs feature f1 and f2
+        expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+        expect(bundleContext.installBundle(eq(bundle1), isA(InputStream.class))).andReturn(installedBundle1);
+        expect(installedBundle1.getBundleId()).andReturn(12345L);
+
+        expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+        expect(bundleContext.installBundle(eq(bundle2), isA(InputStream.class))).andReturn(installedBundle2);
+        expect(installedBundle2.getBundleId()).andReturn(54321L);
+        expect(installedBundle2.getHeaders()).andReturn(new Hashtable());
+        installedBundle2.start();
+
+        expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+        expect(prefs.node("repositories")).andReturn(repositoriesNode);
+        repositoriesNode.clear();
+        repositoriesNode.putInt("count", 1);
+        repositoriesNode.put("item.0", uri.toString());
+        expect(prefs.node("features")).andReturn(featuresNode);
+        featuresNode.clear();
+        featuresNode.put("f2" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.0.0", "54321");
+        prefs.putBoolean("bootFeaturesInstalled", false);
+        prefs.flush();
+
+        replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle1, installedBundle2);
+
+        FeaturesServiceImpl svc = new FeaturesServiceImpl();
+        svc.setPreferences(preferencesService);
+        svc.setBundleContext(bundleContext);
+        svc.addRepository(uri);
+
+        svc.installFeatures(new CopyOnWriteArraySet<Feature>(Arrays.asList(svc.listFeatures())),
+                            EnumSet.of(FeaturesService.Option.ContinueBatchOnFailure, FeaturesService.Option.NoCleanIfFailure));
+
+        verify(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle1, installedBundle2);
+    }
+
+    public void testInstallBatchFeatureWithContinueOnFailureClean() throws Exception {
+        String bundle1 = getJarUrl(Bundle.class);
+        String bundle2 = getJarUrl(PreferencesService.class);
+
+        File tmp = File.createTempFile("smx", ".feature");
+        PrintWriter pw = new PrintWriter(new FileWriter(tmp));
+        pw.println("<features>");
+        pw.println("  <feature name='f1'>");
+        pw.println("    <bundle>" + bundle1 + "</bundle>");
+        pw.println("    <bundle>" + "zfs:unknown" + "</bundle>");
+        pw.println("  </feature>");
+        pw.println("  <feature name='f2'>");
+        pw.println("    <bundle>" + bundle2 + "</bundle>");
+        pw.println("  </feature>");
+        pw.println("</features>");
+        pw.close();
+
+        URI uri = tmp.toURI();
+
+        // loads the state
+        Preferences prefs = EasyMock.createMock(Preferences.class);
+        PreferencesService preferencesService = EasyMock.createMock(PreferencesService.class);
+        Preferences repositoriesNode = EasyMock.createMock(Preferences.class);
+        Preferences repositoriesAvailableNode = EasyMock.createMock(Preferences.class);
+        Preferences featuresNode = EasyMock.createMock(Preferences.class);
+        BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
+        Bundle installedBundle1 = EasyMock.createMock(Bundle.class);
+        Bundle installedBundle2 = EasyMock.createMock(Bundle.class);
+
+        // savestate from addRepository
+        expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+        expect(prefs.node("repositories")).andReturn(repositoriesNode);
+        repositoriesNode.clear();
+        repositoriesNode.putInt("count", 1);
+        repositoriesNode.put("item.0", uri.toString());
+        expect(prefs.node("features")).andReturn(featuresNode);
+        featuresNode.clear();
+        prefs.putBoolean("bootFeaturesInstalled", false);
+        prefs.flush();
+
+        // Installs feature f1 and f2
+        expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+        expect(bundleContext.installBundle(eq(bundle1), isA(InputStream.class))).andReturn(installedBundle1);
+        expect(installedBundle1.getBundleId()).andReturn(12345L);
+        installedBundle1.uninstall();
+
+        expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+        expect(bundleContext.installBundle(eq(bundle2), isA(InputStream.class))).andReturn(installedBundle2);
+        expect(installedBundle2.getBundleId()).andReturn(54321L);
+        expect(installedBundle2.getHeaders()).andReturn(new Hashtable());
+        installedBundle2.start();
+
+        expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+        expect(prefs.node("repositories")).andReturn(repositoriesNode);
+        repositoriesNode.clear();
+        repositoriesNode.putInt("count", 1);
+        repositoriesNode.put("item.0", uri.toString());
+        expect(prefs.node("features")).andReturn(featuresNode);
+        featuresNode.clear();
+        featuresNode.put("f2" + FeatureImpl.SPLIT_FOR_NAME_AND_VERSION + "0.0.0", "54321");
+        prefs.putBoolean("bootFeaturesInstalled", false);
+        prefs.flush();
+
+        replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle1, installedBundle2);
+
+        FeaturesServiceImpl svc = new FeaturesServiceImpl();
+        svc.setPreferences(preferencesService);
+        svc.setBundleContext(bundleContext);
+        svc.addRepository(uri);
+
+        svc.installFeatures(new CopyOnWriteArraySet<Feature>(Arrays.asList(svc.listFeatures())),
+                            EnumSet.of(FeaturesService.Option.ContinueBatchOnFailure));
+
+        verify(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle1, installedBundle2);
+    }
+
+    public void testInstallBatchFeatureWithoutContinueOnFailureNoClean() throws Exception {
+        String bundle1 = getJarUrl(Bundle.class);
+        String bundle2 = getJarUrl(PreferencesService.class);
+
+        File tmp = File.createTempFile("smx", ".feature");
+        PrintWriter pw = new PrintWriter(new FileWriter(tmp));
+        pw.println("<features>");
+        pw.println("  <feature name='f1'>");
+        pw.println("    <bundle>" + bundle1 + "</bundle>");
+        pw.println("    <bundle>" + "zfs:unknown" + "</bundle>");
+        pw.println("  </feature>");
+        pw.println("  <feature name='f2'>");
+        pw.println("    <bundle>" + bundle2 + "</bundle>");
+        pw.println("  </feature>");
+        pw.println("</features>");
+        pw.close();
+
+        URI uri = tmp.toURI();
+
+        // loads the state
+        Preferences prefs = EasyMock.createMock(Preferences.class);
+        PreferencesService preferencesService = EasyMock.createMock(PreferencesService.class);
+        Preferences repositoriesNode = EasyMock.createMock(Preferences.class);
+        Preferences repositoriesAvailableNode = EasyMock.createMock(Preferences.class);
+        Preferences featuresNode = EasyMock.createMock(Preferences.class);
+        BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
+        Bundle installedBundle1 = EasyMock.createMock(Bundle.class);
+        Bundle installedBundle2 = EasyMock.createMock(Bundle.class);
+
+        // savestate from addRepository
+        expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+        expect(prefs.node("repositories")).andReturn(repositoriesNode);
+        repositoriesNode.clear();
+        repositoriesNode.putInt("count", 1);
+        repositoriesNode.put("item.0", uri.toString());
+        expect(prefs.node("features")).andReturn(featuresNode);
+        featuresNode.clear();
+        prefs.putBoolean("bootFeaturesInstalled", false);
+        prefs.flush();
+
+        // Installs feature f1 and f2
+        expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+        expect(bundleContext.installBundle(eq(bundle1), isA(InputStream.class))).andReturn(installedBundle1);
+        expect(installedBundle1.getBundleId()).andReturn(12345L);
+
+        expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+        expect(bundleContext.installBundle(eq(bundle2), isA(InputStream.class))).andReturn(installedBundle2);
+        expect(installedBundle2.getBundleId()).andReturn(54321L);
+        installedBundle2.start();
+
+        replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle1, installedBundle2);
+
+        FeaturesServiceImpl svc = new FeaturesServiceImpl();
+        svc.setPreferences(preferencesService);
+        svc.setBundleContext(bundleContext);
+        svc.addRepository(uri);
+
+        try {
+            List<Feature> features = Arrays.asList(svc.listFeatures());
+            Collections.reverse(features);
+            svc.installFeatures(new CopyOnWriteArraySet<Feature>(features),
+                                EnumSet.of(FeaturesService.Option.NoCleanIfFailure));
+            fail("Call should have thrown an exception");
+        } catch (MalformedURLException e) {
+        }
+
+        verify(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle1, installedBundle2);
+    }
+
+    public void testInstallBatchFeatureWithoutContinueOnFailureClean() throws Exception {
+        String bundle1 = getJarUrl(Bundle.class);
+        String bundle2 = getJarUrl(PreferencesService.class);
+
+        File tmp = File.createTempFile("smx", ".feature");
+        PrintWriter pw = new PrintWriter(new FileWriter(tmp));
+        pw.println("<features>");
+        pw.println("  <feature name='f1'>");
+        pw.println("    <bundle>" + bundle1 + "</bundle>");
+        pw.println("    <bundle>" + "zfs:unknown" + "</bundle>");
+        pw.println("  </feature>");
+        pw.println("  <feature name='f2'>");
+        pw.println("    <bundle>" + bundle2 + "</bundle>");
+        pw.println("  </feature>");
+        pw.println("</features>");
+        pw.close();
+
+        URI uri = tmp.toURI();
+
+        // loads the state
+        Preferences prefs = EasyMock.createMock(Preferences.class);
+        PreferencesService preferencesService = EasyMock.createMock(PreferencesService.class);
+        Preferences repositoriesNode = EasyMock.createMock(Preferences.class);
+        Preferences repositoriesAvailableNode = EasyMock.createMock(Preferences.class);
+        Preferences featuresNode = EasyMock.createMock(Preferences.class);
+        BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
+        Bundle installedBundle1 = EasyMock.createMock(Bundle.class);
+        Bundle installedBundle2 = EasyMock.createMock(Bundle.class);
+
+        // savestate from addRepository
+        expect(preferencesService.getUserPreferences("FeaturesServiceState")).andStubReturn(prefs);
+        expect(prefs.node("repositories")).andReturn(repositoriesNode);
+        repositoriesNode.clear();
+        repositoriesNode.putInt("count", 1);
+        repositoriesNode.put("item.0", uri.toString());
+        expect(prefs.node("features")).andReturn(featuresNode);
+        featuresNode.clear();
+        prefs.putBoolean("bootFeaturesInstalled", false);
+        prefs.flush();
+
+        // Installs feature f1 and f2
+        expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+        expect(bundleContext.installBundle(eq(bundle1), isA(InputStream.class))).andReturn(installedBundle1);
+        expect(installedBundle1.getBundleId()).andReturn(12345L);
+        installedBundle1.uninstall();
+
+        expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
+        expect(bundleContext.installBundle(eq(bundle2), isA(InputStream.class))).andReturn(installedBundle2);
+        expect(installedBundle2.getBundleId()).andReturn(54321L);
+        installedBundle2.uninstall();
+
+        replay(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle1, installedBundle2);
+
+        FeaturesServiceImpl svc = new FeaturesServiceImpl();
+        svc.setPreferences(preferencesService);
+        svc.setBundleContext(bundleContext);
+        svc.addRepository(uri);
+
+        try {
+            List<Feature> features = Arrays.asList(svc.listFeatures());
+            Collections.reverse(features);
+            svc.installFeatures(new CopyOnWriteArraySet<Feature>(features),
+                                EnumSet.noneOf(FeaturesService.Option.class));
+            fail("Call should have thrown an exception");
+        } catch (MalformedURLException e) {
+        }
+
+        verify(preferencesService, prefs, repositoriesNode, featuresNode, bundleContext, installedBundle1, installedBundle2);
+    }
+
+    private String getJarUrl(Class cl) {
+        String name = cl.getName();
+        name = name.replace(".", "/")  + ".class";
+        name = getClass().getClassLoader().getResource(name).toString();
+        name = name.substring("jar:".length(), name.indexOf('!'));
+        return name;
+    }
+
 }