You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@karaf.apache.org by gn...@apache.org on 2014/04/30 20:04:53 UTC

[1/7] git commit: [KARAF-2888] Add a few unit tests for the deployer, more needed

Repository: karaf
Updated Branches:
  refs/heads/master 703c62f97 -> bd9167729


[KARAF-2888] Add a few unit tests for the deployer, more needed


Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/e334faed
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/e334faed
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/e334faed

Branch: refs/heads/master
Commit: e334faed45eff1b7a6b961dac809aa006123b238
Parents: c082817
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Wed Apr 30 18:02:58 2014 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Wed Apr 30 20:04:27 2014 +0200

----------------------------------------------------------------------
 .../internal/resolver/ResourceBuilder.java      |  13 +-
 .../internal/resolver/ResourceImpl.java         |   4 +-
 .../features/internal/region/SubsystemTest.java |  75 +----
 .../features/internal/service/DeployerTest.java | 299 +++++++++++++++++++
 .../features/internal/support/TestBundle.java   | 262 ++++++++++++++++
 .../internal/support/TestDownloadManager.java   |  88 ++++++
 .../features/internal/service/data1/a100.mf     |   6 +
 .../features/internal/service/data1/a101.mf     |   6 +
 .../internal/service/data1/features.xml         |  29 ++
 9 files changed, 710 insertions(+), 72 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/e334faed/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java
index 9ad1012..2c17732 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceBuilder.java
@@ -60,7 +60,10 @@ public final class ResourceBuilder {
     }
 
     public static ResourceImpl build(String uri, Map<String, String> headerMap) throws BundleException {
+        return build(new ResourceImpl(), uri, headerMap);
+    }
 
+    public static ResourceImpl build(ResourceImpl resource, String uri, Map<String, String> headerMap) throws BundleException {
         // Verify that only manifest version 2 is specified.
         String manifestVersion = getManifestVersion(headerMap);
         if (manifestVersion == null || !manifestVersion.equals("2")) {
@@ -89,7 +92,15 @@ public final class ResourceBuilder {
 
         // Now that we have symbolic name and version, create the resource
         String type = headerMap.get(Constants.FRAGMENT_HOST) == null ? IdentityNamespace.TYPE_BUNDLE : IdentityNamespace.TYPE_FRAGMENT;
-        ResourceImpl resource = new ResourceImpl(bundleSymbolicName, type, bundleVersion);
+        {
+            Map<String, String> dirs = new HashMap<>();
+            Map<String, Object> attrs = new HashMap<>();
+            attrs.put(IdentityNamespace.IDENTITY_NAMESPACE, bundleSymbolicName);
+            attrs.put(IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE, type);
+            attrs.put(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE, bundleVersion);
+            CapabilityImpl identity = new CapabilityImpl(resource, IdentityNamespace.IDENTITY_NAMESPACE, dirs, attrs);
+            resource.addCapability(identity);
+        }
         if (uri != null) {
             Map<String, Object> attrs = new HashMap<>();
             attrs.put(ContentNamespace.CAPABILITY_URL_ATTRIBUTE, uri);

http://git-wip-us.apache.org/repos/asf/karaf/blob/e334faed/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java
index 99ffb52..756c440 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/resolver/ResourceImpl.java
@@ -31,8 +31,8 @@ import org.osgi.resource.Resource;
  */
 public class ResourceImpl implements Resource {
 
-    private final List<Capability> caps;
-    private final List<Requirement> reqs;
+    protected final List<Capability> caps;
+    protected final List<Requirement> reqs;
 
     /**
      * CAUTION: This constructor does not ensure that the resource

http://git-wip-us.apache.org/repos/asf/karaf/blob/e334faed/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
----------------------------------------------------------------------
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
index dc371a1..42c66d7 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
@@ -16,11 +16,6 @@
  */
 package org.apache.karaf.features.internal.region;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.MalformedURLException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -29,13 +24,10 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.jar.JarOutputStream;
-import java.util.jar.Manifest;
 
 import org.apache.karaf.features.FeaturesService;
-import org.apache.karaf.features.internal.download.StreamProvider;
 import org.apache.karaf.features.internal.service.RepositoryImpl;
-import org.apache.karaf.features.internal.download.simple.SimpleDownloader;
+import org.apache.karaf.features.internal.support.TestDownloadManager;
 import org.junit.Test;
 import org.osgi.framework.namespace.IdentityNamespace;
 import org.osgi.framework.wiring.BundleRevision;
@@ -61,7 +53,7 @@ public class SubsystemTest {
         addToMapSet(expected, "root", "c/1.0.0");
         addToMapSet(expected, "root/apps1", "b/1.0.0");
 
-        SubsystemResolver resolver = new SubsystemResolver(new TestDownloadManager("data1"));
+        SubsystemResolver resolver = new SubsystemResolver(new TestDownloadManager(getClass(), "data1"));
         resolver.resolve(Arrays.asList(repo.getFeatures()),
                          features,
                          Collections.<String, Set<BundleRevision>>emptyMap(),
@@ -92,7 +84,7 @@ public class SubsystemTest {
         addToMapSet(expected, "root/apps2", "c/1.0.0");
         addToMapSet(expected, "root/apps2#f1", "a/1.0.0");
 
-        SubsystemResolver resolver = new SubsystemResolver(new TestDownloadManager("data2"));
+        SubsystemResolver resolver = new SubsystemResolver(new TestDownloadManager(getClass(), "data2"));
         resolver.resolve(Arrays.asList(repo.getFeatures()),
                          features,
                          Collections.<String, Set<BundleRevision>>emptyMap(),
@@ -113,7 +105,7 @@ public class SubsystemTest {
         Map<String, Set<String>> expected = new HashMap<String, Set<String>>();
         addToMapSet(expected, "root/apps1", "a/1.0.1");
 
-        SubsystemResolver resolver = new SubsystemResolver(new TestDownloadManager("data3"));
+        SubsystemResolver resolver = new SubsystemResolver(new TestDownloadManager(getClass(), "data3"));
         resolver.resolve(Arrays.asList(repo.getFeatures()),
                          features,
                          Collections.<String, Set<BundleRevision>>emptyMap(),
@@ -133,7 +125,7 @@ public class SubsystemTest {
         Map<String, Set<String>> expected = new HashMap<String, Set<String>>();
         addToMapSet(expected, "root/apps1", "a/1.0.0");
 
-        SubsystemResolver resolver = new SubsystemResolver(new TestDownloadManager("data4"));
+        SubsystemResolver resolver = new SubsystemResolver(new TestDownloadManager(getClass(), "data4"));
         resolver.resolve(Arrays.asList(repo.getFeatures()),
                 features,
                 Collections.<String, Set<BundleRevision>>emptyMap(),
@@ -155,7 +147,7 @@ public class SubsystemTest {
         addToMapSet(expected, "root/apps1", "a/1.0.0");
         addToMapSet(expected, "root/apps1", "b/1.0.0");
 
-        SubsystemResolver resolver = new SubsystemResolver(new TestDownloadManager("data4"));
+        SubsystemResolver resolver = new SubsystemResolver(new TestDownloadManager(getClass(), "data4"));
         resolver.resolve(Arrays.asList(repo.getFeatures()),
                 features,
                 Collections.<String, Set<BundleRevision>>emptyMap(),
@@ -223,59 +215,4 @@ public class SubsystemTest {
                 + cap.getAttributes().get(IdentityNamespace.CAPABILITY_VERSION_ATTRIBUTE);
     }
 
-    class TestDownloadManager extends SimpleDownloader {
-
-        private final String dir;
-
-        TestDownloadManager(String dir) {
-            this.dir = dir;
-        }
-
-        @Override
-        protected StreamProvider createProvider(String location) throws MalformedURLException {
-            return new TestProvider(location);
-        }
-
-        class TestProvider implements StreamProvider {
-            private final IOException exception;
-            private final Map<String, String> headers;
-            private final byte[] data;
-
-            TestProvider(String location) {
-                Map<String, String> headers = null;
-                byte[] data = null;
-                IOException exception = null;
-                try {
-                    Manifest man = new Manifest(getClass().getResourceAsStream(dir +"/" + location + ".mf"));
-                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                    JarOutputStream jos = new JarOutputStream(baos, man);
-                    jos.close();
-                    data = baos.toByteArray();
-                    headers = new HashMap<String, String>();
-                    for (Map.Entry attr : man.getMainAttributes().entrySet()) {
-                        headers.put(attr.getKey().toString(), attr.getValue().toString());
-                    }
-                } catch (IOException e) {
-                    exception = e;
-                }
-                this.headers = headers;
-                this.data = data;
-                this.exception = exception;
-            }
-
-            @Override
-            public InputStream open() throws IOException {
-                if (exception != null)
-                    throw exception;
-                return new ByteArrayInputStream(data);
-            }
-
-            @Override
-            public Map<String, String> getMetadata() throws IOException {
-                if (exception != null)
-                    throw exception;
-                return headers;
-            }
-        }
-    }
 }

http://git-wip-us.apache.org/repos/asf/karaf/blob/e334faed/features/core/src/test/java/org/apache/karaf/features/internal/service/DeployerTest.java
----------------------------------------------------------------------
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/DeployerTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/DeployerTest.java
new file mode 100644
index 0000000..42c7c49
--- /dev/null
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/DeployerTest.java
@@ -0,0 +1,299 @@
+/*
+ * 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.karaf.features.internal.service;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.Manifest;
+
+import org.apache.felix.utils.version.VersionRange;
+import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeatureEvent;
+import org.apache.karaf.features.internal.support.TestBundle;
+import org.apache.karaf.features.internal.support.TestDownloadManager;
+import org.easymock.EasyMock;
+import org.easymock.IAnswer;
+import org.easymock.IArgumentMatcher;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+
+import static org.apache.karaf.features.FeaturesService.*;
+import static org.apache.karaf.features.internal.util.MapUtils.addToMapSet;
+import static org.easymock.EasyMock.anyInt;
+import static org.easymock.EasyMock.anyObject;
+
+public class DeployerTest {
+
+    @Test
+    public void testInstallSimpleFeature() throws Exception {
+
+        String dataDir = "data1";
+
+        TestDownloadManager manager = new TestDownloadManager(getClass(), dataDir);
+
+        RepositoryImpl repo = new RepositoryImpl(getClass().getResource(dataDir + "/features.xml").toURI());
+        repo.load(true);
+        Feature f100 = repo.getFeatures()[0];
+        Feature f101 = repo.getFeatures()[1];
+
+        Deployer.DeployCallback callback = EasyMock.createMock(Deployer.DeployCallback.class);
+        Deployer deployer = new Deployer(manager, callback);
+
+        callback.print(EasyMock.anyString(), EasyMock.anyBoolean());
+        EasyMock.expectLastCall().anyTimes();
+        callback.replaceDigraph(EasyMock.<Map<String, Map<String, Map<String, Set<String>>>>>anyObject(),
+                EasyMock.<Map<String, Set<Long>>>anyObject());
+        EasyMock.expectLastCall();
+        callback.saveState(EasyMock.<State>anyObject());
+        EasyMock.expectLastCall();
+        callback.installFeatureConfigs(f100);
+        EasyMock.expectLastCall();
+        callback.resolveBundles(EasyMock.<Set<Bundle>>anyObject());
+        EasyMock.expectLastCall();
+        callback.callListeners(EasyMock.<FeatureEvent>anyObject());
+        EasyMock.expectLastCall();
+
+        Bundle bundleA = createTestBundle(1, Bundle.ACTIVE, dataDir, "a100");
+        EasyMock.expect(callback.installBundle(EasyMock.eq(ROOT_REGION), EasyMock.eq("a100"), EasyMock.<InputStream>anyObject()))
+                .andReturn(bundleA);
+
+        EasyMock.replay(callback);
+
+        Deployer.DeploymentState dstate = new Deployer.DeploymentState();
+        dstate.state = new State();
+        dstate.bundles = new HashMap<>();
+        dstate.bundlesPerRegion = new HashMap<>();
+        dstate.features = new HashMap<>();
+        dstate.features.put(f100.getId(), f100);
+        dstate.features.put(f101.getId(), f101);
+        dstate.filtersPerRegion = new HashMap<>();
+        dstate.filtersPerRegion.put(ROOT_REGION, new HashMap<String, Map<String, Set<String>>>());
+
+        Deployer.DeploymentRequest request = new Deployer.DeploymentRequest();
+        request.bundleUpdateRange = DEFAULT_BUNDLE_UPDATE_RANGE;
+        request.featureResolutionRange = DEFAULT_FEATURE_RESOLUTION_RANGE;
+        request.globalRepository = null;
+        request.options = EnumSet.noneOf(Option.class);
+        request.overrides = Collections.emptySet();
+        request.stateChanges = Collections.emptyMap();
+        request.updateSnaphots = UPDATE_SNAPSHOTS_NONE;
+        request.requestedFeatures = new HashMap<>();
+        addToMapSet(request.requestedFeatures, ROOT_REGION, f100.getName() + "/" + new VersionRange(f100.getVersion(), true));
+
+        deployer.deploy(dstate, request);
+
+        EasyMock.verify(callback);
+    }
+
+    @Test
+    public void testUpdateSimpleFeature() throws Exception {
+
+        final String dataDir = "data1";
+
+        TestDownloadManager manager = new TestDownloadManager(getClass(), dataDir);
+
+        RepositoryImpl repo = new RepositoryImpl(getClass().getResource(dataDir + "/features.xml").toURI());
+        repo.load(true);
+        Feature f100 = repo.getFeatures()[0];
+        Feature f101 = repo.getFeatures()[1];
+
+        Deployer.DeployCallback callback = EasyMock.createMock(Deployer.DeployCallback.class);
+        Deployer deployer = new Deployer(manager, callback);
+
+        final TestBundle bundleA = createTestBundle(1L, Bundle.ACTIVE, dataDir, "a100");
+
+        callback.print(EasyMock.anyString(), EasyMock.anyBoolean());
+        EasyMock.expectLastCall().anyTimes();
+
+        callback.stopBundle(EasyMock.eq(bundleA), anyInt());
+        EasyMock.expectLastCall().andStubAnswer(new IAnswer<Object>() {
+            @Override
+            public Object answer() throws Throwable {
+                bundleA.state = Bundle.RESOLVED;
+                return null;
+            }
+        });
+        callback.updateBundle(EasyMock.eq(bundleA), EasyMock.<InputStream>anyObject());
+        EasyMock.expectLastCall().andStubAnswer(new IAnswer<Object>() {
+            @Override
+            public Object answer() throws Throwable {
+                URL loc = getClass().getResource(dataDir + "/" + "a101" + ".mf");
+                Manifest man = new Manifest(loc.openStream());
+                Hashtable<String, String> headers = new Hashtable<>();
+                for (Map.Entry attr : man.getMainAttributes().entrySet()) {
+                    headers.put(attr.getKey().toString(), attr.getValue().toString());
+                }
+                bundleA.update(headers);
+                return null;
+            }
+        });
+        callback.startBundle(EasyMock.eq(bundleA));
+        EasyMock.expectLastCall();
+
+        callback.replaceDigraph(EasyMock.<Map<String, Map<String, Map<String, Set<String>>>>>anyObject(),
+                EasyMock.<Map<String, Set<Long>>>anyObject());
+        EasyMock.expectLastCall();
+        callback.saveState(EasyMock.<State>anyObject());
+        EasyMock.expectLastCall();
+        callback.installFeatureConfigs(f101);
+        EasyMock.expectLastCall();
+        callback.resolveBundles(EasyMock.eq(Collections.<Bundle>singleton(bundleA)));
+        EasyMock.expectLastCall();
+        callback.refreshPackages(EasyMock.eq(Collections.<Bundle>singleton(bundleA)));
+        EasyMock.expectLastCall();
+        callback.callListeners(FeatureEventMatcher.eq(new FeatureEvent(f100, FeatureEvent.EventType.FeatureUninstalled, false)));
+        EasyMock.expectLastCall();
+        callback.callListeners(FeatureEventMatcher.eq(new FeatureEvent(f101, FeatureEvent.EventType.FeatureInstalled, false)));
+        EasyMock.expectLastCall();
+
+        EasyMock.replay(callback);
+
+        Deployer.DeploymentState dstate = new Deployer.DeploymentState();
+        dstate.state = new State();
+        addToMapSet(dstate.state.installedFeatures, ROOT_REGION, f100.getId());
+        addToMapSet(dstate.state.managedBundles, ROOT_REGION, 1L);
+        dstate.bundles = new HashMap<>();
+        dstate.bundles.put(1L, bundleA);
+        dstate.bundlesPerRegion = new HashMap<>();
+        addToMapSet(dstate.bundlesPerRegion, ROOT_REGION, 1L);
+        dstate.features = new HashMap<>();
+        dstate.features.put(f100.getId(), f100);
+        dstate.features.put(f101.getId(), f101);
+        dstate.filtersPerRegion = new HashMap<>();
+        dstate.filtersPerRegion.put(ROOT_REGION, new HashMap<String, Map<String, Set<String>>>());
+
+        Deployer.DeploymentRequest request = new Deployer.DeploymentRequest();
+        request.bundleUpdateRange = DEFAULT_BUNDLE_UPDATE_RANGE;
+        request.featureResolutionRange = DEFAULT_FEATURE_RESOLUTION_RANGE;
+        request.globalRepository = null;
+        request.options = EnumSet.noneOf(Option.class);
+        request.overrides = Collections.emptySet();
+        request.stateChanges = Collections.emptyMap();
+        request.updateSnaphots = UPDATE_SNAPSHOTS_NONE;
+        request.requestedFeatures = new HashMap<>();
+        addToMapSet(request.requestedFeatures, ROOT_REGION, f101.getName() + "/" + new VersionRange(f101.getVersion(), true));
+
+        deployer.deploy(dstate, request);
+
+        EasyMock.verify(callback);
+    }
+
+    @Test
+    public void testUpdateServiceBundle() throws Exception {
+
+        String dataDir = "data1";
+
+        TestDownloadManager manager = new TestDownloadManager(getClass(), dataDir);
+
+        RepositoryImpl repo = new RepositoryImpl(getClass().getResource(dataDir + "/features.xml").toURI());
+        repo.load(true);
+        Feature f1 = repo.getFeatures()[0];
+
+        Bundle serviceBundle = createTestBundle(1, Bundle.ACTIVE, dataDir, "a100");
+
+        Deployer.DeployCallback callback = EasyMock.createMock(Deployer.DeployCallback.class);
+        Deployer deployer = new Deployer(manager, callback);
+
+        callback.print(EasyMock.anyString(), EasyMock.anyBoolean());
+        EasyMock.expectLastCall().anyTimes();
+        callback.replaceDigraph(EasyMock.<Map<String, Map<String, Map<String, Set<String>>>>>anyObject(),
+                EasyMock.<Map<String, Set<Long>>>anyObject());
+        EasyMock.expectLastCall();
+        callback.saveState(EasyMock.<State>anyObject());
+        EasyMock.expectLastCall();
+        callback.installFeatureConfigs(f1);
+        EasyMock.expectLastCall();
+        callback.resolveBundles(EasyMock.<Set<Bundle>>anyObject());
+        EasyMock.expectLastCall();
+        callback.callListeners(EasyMock.<FeatureEvent>anyObject());
+        EasyMock.expectLastCall();
+
+        EasyMock.replay(callback);
+
+        Deployer.DeploymentState dstate = new Deployer.DeploymentState();
+        dstate.state = new State();
+        dstate.bundles = new HashMap<>();
+        dstate.bundles.put(serviceBundle.getBundleId(), serviceBundle);
+        dstate.bundlesPerRegion = new HashMap<>();
+        addToMapSet(dstate.bundlesPerRegion, ROOT_REGION, serviceBundle.getBundleId());
+        dstate.features = Collections.singletonMap(f1.getId(), f1);
+        dstate.filtersPerRegion = new HashMap<>();
+        dstate.filtersPerRegion.put(ROOT_REGION, new HashMap<String, Map<String, Set<String>>>());
+
+        Deployer.DeploymentRequest request = new Deployer.DeploymentRequest();
+        request.bundleUpdateRange = DEFAULT_BUNDLE_UPDATE_RANGE;
+        request.featureResolutionRange = DEFAULT_FEATURE_RESOLUTION_RANGE;
+        request.globalRepository = null;
+        request.options = EnumSet.noneOf(Option.class);
+        request.overrides = Collections.emptySet();
+        request.stateChanges = Collections.emptyMap();
+        request.updateSnaphots = UPDATE_SNAPSHOTS_NONE;
+        request.requestedFeatures = new HashMap<>();
+        addToMapSet(request.requestedFeatures, ROOT_REGION, f1.getName());
+
+        deployer.deploy(dstate, request);
+
+        EasyMock.verify(callback);
+    }
+
+    private TestBundle createTestBundle(long bundleId, int state, String dir, String name) throws IOException, BundleException {
+        URL loc = getClass().getResource(dir + "/" + name + ".mf");
+        Manifest man = new Manifest(loc.openStream());
+        Hashtable<String, String> headers = new Hashtable<>();
+        for (Map.Entry attr : man.getMainAttributes().entrySet()) {
+            headers.put(attr.getKey().toString(), attr.getValue().toString());
+        }
+        return new TestBundle(bundleId, name, state, headers);
+    }
+
+    static class FeatureEventMatcher implements IArgumentMatcher {
+        final FeatureEvent expected;
+
+        FeatureEventMatcher(FeatureEvent expected) {
+            this.expected = expected;
+        }
+
+        public static FeatureEvent eq(FeatureEvent expected) {
+            EasyMock.reportMatcher(new FeatureEventMatcher(expected));
+            return null;
+        }
+
+        public boolean matches(Object argument) {
+            if (!(argument instanceof FeatureEvent)) {
+                return false;
+            }
+            FeatureEvent arg = (FeatureEvent) argument;
+            return arg.getFeature() == expected.getFeature()
+                    && arg.getType() == expected.getType()
+                    && arg.isReplay() == expected.isReplay();
+        }
+
+        public void appendTo(StringBuffer buffer) {
+
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e334faed/features/core/src/test/java/org/apache/karaf/features/internal/support/TestBundle.java
----------------------------------------------------------------------
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/support/TestBundle.java b/features/core/src/test/java/org/apache/karaf/features/internal/support/TestBundle.java
new file mode 100644
index 0000000..6f71731
--- /dev/null
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/support/TestBundle.java
@@ -0,0 +1,262 @@
+/*
+ * 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.karaf.features.internal.support;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.cert.X509Certificate;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.felix.resolver.Util;
+import org.apache.karaf.features.internal.resolver.ResourceBuilder;
+import org.apache.karaf.features.internal.resolver.ResourceImpl;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.Version;
+import org.osgi.framework.startlevel.BundleStartLevel;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleRequirement;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.framework.wiring.BundleWiring;
+
+import static org.apache.karaf.features.internal.resolver.ResourceUtils.getUri;
+
+public class TestBundle extends ResourceImpl implements BundleRevision, Bundle, BundleStartLevel {
+
+    private final long bundleId;
+    private final String location;
+    private final Hashtable<String, String> headers = new Hashtable<>();
+    public int state;
+
+    public TestBundle(long bundleId, String location, int state, Hashtable<String, String> headers) throws BundleException {
+        this.bundleId = bundleId;
+        this.location = location;
+        this.state = state;
+        update(headers);
+    }
+
+    public void update(Hashtable<String, String> headers) throws BundleException {
+        this.headers.clear();
+        this.headers.putAll(headers);
+        this.caps.clear();
+        this.reqs.clear();
+        ResourceBuilder.build(this, location, headers);
+    }
+
+    @Override
+    public int getState() {
+        return state;
+    }
+
+    @Override
+    public void start(int options) throws BundleException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void start() throws BundleException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void stop(int options) throws BundleException {
+        throw new UnsupportedOperationException();
+   }
+
+    @Override
+    public void stop() throws BundleException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void update(InputStream input) throws BundleException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void update() throws BundleException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void uninstall() throws BundleException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Dictionary<String, String> getHeaders() {
+        return headers;
+    }
+
+    @Override
+    public long getBundleId() {
+        return bundleId;
+    }
+
+    @Override
+    public String getLocation() {
+        return getUri(this);
+    }
+
+    @Override
+    public ServiceReference<?>[] getRegisteredServices() {
+        return new ServiceReference<?>[0];
+    }
+
+    @Override
+    public ServiceReference<?>[] getServicesInUse() {
+        return new ServiceReference<?>[0];
+    }
+
+    @Override
+    public boolean hasPermission(Object permission) {
+        return true;
+    }
+
+    @Override
+    public URL getResource(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Dictionary<String, String> getHeaders(String locale) {
+        return getHeaders();
+    }
+
+    @Override
+    public Class<?> loadClass(String name) throws ClassNotFoundException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Enumeration<URL> getResources(String name) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Enumeration<String> getEntryPaths(String path) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public URL getEntry(String path) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public long getLastModified() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Enumeration<URL> findEntries(String path, String filePattern, boolean recurse) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public BundleContext getBundleContext() {
+        return null;
+    }
+
+    @Override
+    public Map<X509Certificate, List<X509Certificate>> getSignerCertificates(int signersType) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public <A> A adapt(Class<A> type) {
+        if (type.isInstance(this)) {
+            return type.cast(this);
+        }
+        return null;
+    }
+
+    @Override
+    public File getDataFile(String filename) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String getSymbolicName() {
+        return Util.getSymbolicName(this);
+    }
+
+    @Override
+    public Version getVersion() {
+        return Util.getVersion(this);
+    }
+
+    @Override
+    public List<BundleCapability> getDeclaredCapabilities(String namespace) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public List<BundleRequirement> getDeclaredRequirements(String namespace) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int getTypes() {
+        return 0;
+    }
+
+    @Override
+    public BundleWiring getWiring() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Bundle getBundle() {
+        return this;
+    }
+
+    @Override
+    public int compareTo(Bundle o) {
+        long thisBundleId = this.getBundleId();
+        long thatBundleId = o.getBundleId();
+        return (thisBundleId < thatBundleId ? -1 : (thisBundleId == thatBundleId ? 0 : 1));
+    }
+
+    @Override
+    public int getStartLevel() {
+        return 0;
+    }
+
+    @Override
+    public void setStartLevel(int startlevel) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean isPersistentlyStarted() {
+        return false;
+    }
+
+    @Override
+    public boolean isActivationPolicyUsed() {
+        return false;
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e334faed/features/core/src/test/java/org/apache/karaf/features/internal/support/TestDownloadManager.java
----------------------------------------------------------------------
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/support/TestDownloadManager.java b/features/core/src/test/java/org/apache/karaf/features/internal/support/TestDownloadManager.java
new file mode 100644
index 0000000..999c212
--- /dev/null
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/support/TestDownloadManager.java
@@ -0,0 +1,88 @@
+/*
+ * 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.karaf.features.internal.support;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import org.apache.karaf.features.internal.download.StreamProvider;
+import org.apache.karaf.features.internal.download.simple.SimpleDownloader;
+
+public class TestDownloadManager extends SimpleDownloader {
+
+    private final Class loader;
+    private final String dir;
+
+    public TestDownloadManager(Class loader, String dir) {
+        this.loader = loader;
+        this.dir = dir;
+    }
+
+    @Override
+    protected StreamProvider createProvider(String location) throws MalformedURLException {
+        return new TestProvider(location);
+    }
+
+    class TestProvider implements StreamProvider {
+        private final IOException exception;
+        private final Map<String, String> headers;
+        private final byte[] data;
+
+        TestProvider(String location) {
+            Map<String, String> headers = null;
+            byte[] data = null;
+            IOException exception = null;
+            try {
+                Manifest man = new Manifest(loader.getResourceAsStream(dir + "/" + location + ".mf"));
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                JarOutputStream jos = new JarOutputStream(baos, man);
+                jos.close();
+                data = baos.toByteArray();
+                headers = new HashMap<>();
+                for (Map.Entry attr : man.getMainAttributes().entrySet()) {
+                    headers.put(attr.getKey().toString(), attr.getValue().toString());
+                }
+            } catch (IOException e) {
+                exception = e;
+            }
+            this.headers = headers;
+            this.data = data;
+            this.exception = exception;
+        }
+
+        @Override
+        public InputStream open() throws IOException {
+            if (exception != null)
+                throw exception;
+            return new ByteArrayInputStream(data);
+        }
+
+        @Override
+        public Map<String, String> getMetadata() throws IOException {
+            if (exception != null)
+                throw exception;
+            return headers;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/e334faed/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/a100.mf
----------------------------------------------------------------------
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/a100.mf b/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/a100.mf
new file mode 100644
index 0000000..b64f9c1
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/a100.mf
@@ -0,0 +1,6 @@
+Manifest-Version: 1
+Bundle-ManifestVersion: 2
+Bundle-SymbolicName: a
+Bundle-Version: 1.0.0
+Provide-Capability: ns;ns=a
+

http://git-wip-us.apache.org/repos/asf/karaf/blob/e334faed/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/a101.mf
----------------------------------------------------------------------
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/a101.mf b/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/a101.mf
new file mode 100644
index 0000000..20ad3d0
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/a101.mf
@@ -0,0 +1,6 @@
+Manifest-Version: 1
+Bundle-ManifestVersion: 2
+Bundle-SymbolicName: a
+Bundle-Version: 1.0.1
+Provide-Capability: ns;ns=a
+

http://git-wip-us.apache.org/repos/asf/karaf/blob/e334faed/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/features.xml
----------------------------------------------------------------------
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/features.xml b/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/features.xml
new file mode 100644
index 0000000..653d930
--- /dev/null
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/data1/features.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+
+-->
+<features name="test" xmlns="http://karaf.apache.org/xmlns/features/v1.3.0">
+
+    <feature name="f" version="1.0.0">
+        <bundle>a100</bundle>
+    </feature>
+
+    <feature name="f" version="1.0.1">
+        <bundle>a101</bundle>
+    </feature>
+
+</features>
\ No newline at end of file


[4/7] [KARAF-2888] Extract the deployment for easier unit testing

Posted by gn...@apache.org.
http://git-wip-us.apache.org/repos/asf/karaf/blob/9e2ada3c/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
index d902a0e..099d175 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
@@ -25,17 +25,13 @@ import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
 import java.util.TreeSet;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
@@ -45,21 +41,14 @@ import java.util.concurrent.Executors;
 
 import org.apache.felix.utils.version.VersionRange;
 import org.apache.felix.utils.version.VersionTable;
-import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.Feature;
 import org.apache.karaf.features.FeatureEvent;
 import org.apache.karaf.features.FeaturesListener;
 import org.apache.karaf.features.FeaturesService;
 import org.apache.karaf.features.Repository;
 import org.apache.karaf.features.RepositoryEvent;
-import org.apache.karaf.features.internal.download.StreamProvider;
-import org.apache.karaf.features.internal.region.SubsystemResolver;
-import org.apache.karaf.features.internal.util.ChecksumUtils;
 import org.apache.karaf.features.internal.util.JsonReader;
 import org.apache.karaf.features.internal.util.JsonWriter;
-import org.apache.karaf.features.internal.util.Macro;
-import org.apache.karaf.features.internal.util.MapUtils;
-import org.apache.karaf.features.internal.util.MultiException;
 import org.apache.karaf.util.collections.CopyOnWriteArrayIdentityList;
 import org.eclipse.equinox.region.Region;
 import org.eclipse.equinox.region.RegionDigraph;
@@ -67,63 +56,26 @@ import org.eclipse.equinox.region.RegionFilterBuilder;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleException;
-import org.osgi.framework.Constants;
 import org.osgi.framework.FrameworkEvent;
 import org.osgi.framework.FrameworkListener;
-import org.osgi.framework.ServiceReference;
+import org.osgi.framework.InvalidSyntaxException;
 import org.osgi.framework.Version;
 import org.osgi.framework.startlevel.BundleStartLevel;
 import org.osgi.framework.startlevel.FrameworkStartLevel;
-import org.osgi.framework.wiring.BundleRevision;
-import org.osgi.framework.wiring.BundleWire;
-import org.osgi.framework.wiring.BundleWiring;
 import org.osgi.framework.wiring.FrameworkWiring;
-import org.osgi.resource.Capability;
-import org.osgi.resource.Resource;
-import org.osgi.resource.Wire;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static org.apache.felix.resolver.Util.getSymbolicName;
-import static org.apache.felix.resolver.Util.getVersion;
-import static org.apache.karaf.features.internal.resolver.ResourceUtils.TYPE_SUBSYSTEM;
-import static org.apache.karaf.features.internal.resolver.ResourceUtils.getFeatureId;
-import static org.apache.karaf.features.internal.resolver.ResourceUtils.getUri;
 import static org.apache.karaf.features.internal.service.StateStorage.toStringStringSetMap;
 import static org.apache.karaf.features.internal.util.MapUtils.addToMapSet;
-import static org.apache.karaf.features.internal.util.MapUtils.apply;
 import static org.apache.karaf.features.internal.util.MapUtils.copy;
-import static org.apache.karaf.features.internal.util.MapUtils.diff;
-import static org.apache.karaf.features.internal.util.MapUtils.flatten;
-import static org.apache.karaf.features.internal.util.MapUtils.map;
-import static org.apache.karaf.features.internal.util.MapUtils.removeFromMapSet;
-import static org.osgi.framework.Bundle.ACTIVE;
-import static org.osgi.framework.Bundle.RESOLVED;
-import static org.osgi.framework.Bundle.STARTING;
-import static org.osgi.framework.Bundle.STOPPING;
-import static org.osgi.framework.Bundle.STOP_TRANSIENT;
-import static org.osgi.framework.Bundle.UNINSTALLED;
-import static org.osgi.framework.namespace.IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE;
-import static org.osgi.framework.namespace.IdentityNamespace.IDENTITY_NAMESPACE;
-import static org.osgi.resource.Namespace.CAPABILITY_EFFECTIVE_DIRECTIVE;
-import static org.osgi.resource.Namespace.EFFECTIVE_ACTIVE;
 
 /**
  *
  */
-public class FeaturesServiceImpl implements FeaturesService {
-
-    public static final String UPDATE_SNAPSHOTS_NONE = "none";
-    public static final String UPDATE_SNAPSHOTS_CRC = "crc";
-    public static final String UPDATE_SNAPSHOTS_ALWAYS = "always";
-    public static final String DEFAULT_UPDATE_SNAPSHOTS = UPDATE_SNAPSHOTS_CRC;
-
-    public static final String DEFAULT_FEATURE_RESOLUTION_RANGE = "${range;[====,====]}";
-    public static final String DEFAULT_BUNDLE_UPDATE_RANGE = "${range;[==,=+)}";
+public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCallback {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(FeaturesServiceImpl.class);
-    private static final String SNAPSHOT = "SNAPSHOT";
-    private static final String MAVEN = "mvn:";
 
     /**
      * Our bundle.
@@ -149,13 +101,13 @@ public class FeaturesServiceImpl implements FeaturesService {
     private final String overrides;
     /**
      * Range to use when a version is specified on a feature dependency.
-     * The default is {@link FeaturesServiceImpl#DEFAULT_FEATURE_RESOLUTION_RANGE}
+     * The default is {@link org.apache.karaf.features.FeaturesService#DEFAULT_FEATURE_RESOLUTION_RANGE}
      */
     private final String featureResolutionRange;
     /**
      * Range to use when verifying if a bundle should be updated or
      * new bundle installed.
-     * The default is {@link FeaturesServiceImpl#DEFAULT_BUNDLE_UPDATE_RANGE}
+     * The default is {@link org.apache.karaf.features.FeaturesService#DEFAULT_BUNDLE_UPDATE_RANGE}
      */
     private final String bundleUpdateRange;
     /**
@@ -331,7 +283,7 @@ public class FeaturesServiceImpl implements FeaturesService {
         listeners.remove(listener);
     }
 
-    protected void callListeners(FeatureEvent event) {
+    public void callListeners(FeatureEvent event) {
         if (eventAdminListener != null) {
             eventAdminListener.featureEvent(event);
         }
@@ -885,36 +837,37 @@ public class FeaturesServiceImpl implements FeaturesService {
         }
     }
 
-    static class DeploymentState {
-        Map<Long, Bundle> bundles;
-        Map<String, Feature> features;
-        Map<String, Set<Long>> bundlesPerRegion;
-        Map<String, Map<String, Map<String, Set<String>>>> filtersPerRegion;
-    }
-
-    protected DeploymentState getDeploymentState() throws Exception {
-        DeploymentState state = new DeploymentState();
+    protected Deployer.DeploymentState getDeploymentState(State state) throws Exception {
+        Deployer.DeploymentState dstate = new Deployer.DeploymentState();
+        // State
+        dstate.state = state;
+        // Service bundle
+        dstate.serviceBundle = bundle;
+        // Start level
+        FrameworkStartLevel fsl = systemBundleContext.getBundle().adapt(FrameworkStartLevel.class);
+        dstate.initialBundleStartLevel = fsl.getInitialBundleStartLevel();
+        dstate.currentStartLevel = fsl.getStartLevel();
         // Bundles
-        state.bundles = new HashMap<>();
+        dstate.bundles = new HashMap<>();
         for (Bundle bundle : systemBundleContext.getBundles()) {
-            state.bundles.put(bundle.getBundleId(), bundle);
+            dstate.bundles.put(bundle.getBundleId(), bundle);
         }
         // Features
-        state.features = new HashMap<>();
+        dstate.features = new HashMap<>();
         for (Map<String, Feature> m : getFeatures().values()) {
             for (Feature feature : m.values()) {
                 String id = feature.getName() + "/" + VersionTable.getVersion(feature.getVersion());
-                state.features.put(id, feature);
+                dstate.features.put(id, feature);
             }
         }
         // Region -> bundles mapping
         // Region -> policy mapping
-        state.bundlesPerRegion = new HashMap<>();
-        state.filtersPerRegion = new HashMap<>();
+        dstate.bundlesPerRegion = new HashMap<>();
+        dstate.filtersPerRegion = new HashMap<>();
         RegionDigraph clone = digraph.copy();
         for (Region region : clone.getRegions()) {
             // Get bundles
-            state.bundlesPerRegion.put(region.getName(), new HashSet<>(region.getBundleIds()));
+            dstate.bundlesPerRegion.put(region.getName(), new HashSet<>(region.getBundleIds()));
             // Get policies
             Map<String, Map<String, Set<String>>> edges = new HashMap<>();
             for (RegionDigraph.FilteredRegion fr : clone.getEdges(region)) {
@@ -927,1030 +880,150 @@ public class FeaturesServiceImpl implements FeaturesService {
                 }
                 edges.put(fr.getRegion().getName(), policy);
             }
-            state.filtersPerRegion.put(region.getName(), edges);
+            dstate.filtersPerRegion.put(region.getName(), edges);
         }
         // Return
-        return state;
+        return dstate;
     }
 
+    private Deployer.DeploymentRequest getDeploymentRequest(Map<String, Set<String>> requestedFeatures, Map<String, Map<String, RequestedState>> stateChanges, EnumSet<Option> options) {
+        Deployer.DeploymentRequest request = new Deployer.DeploymentRequest();
+        request.bundleUpdateRange = bundleUpdateRange;
+        request.featureResolutionRange = featureResolutionRange;
+        request.globalRepository = globalRepository;
+        request.overrides = overrides;
+        request.requestedFeatures = requestedFeatures;
+        request.stateChanges = stateChanges;
+        request.options = options;
+        return request;
+    }
+
+
+
     public void doInstallFeatures(Map<String, Set<String>> requestedFeatures,            // all request features
                                   Map<String, Map<String, RequestedState>> stateChanges, // features state changes
                                   State state,                                           // current state
                                   EnumSet<Option> options                                // installation options
     ) throws Exception {
 
-        boolean noRefreshUnmanaged = options.contains(Option.NoAutoRefreshUnmanagedBundles);
-        boolean noRefreshManaged = options.contains(Option.NoAutoRefreshManagedBundles);
-        boolean noRefresh = options.contains(Option.NoAutoRefreshBundles);
-        boolean noStart = options.contains(Option.NoAutoStartBundles);
-        boolean verbose = options.contains(Option.Verbose);
-        boolean simulate = options.contains(Option.Simulate);
-        boolean noManageBundles = options.contains(Option.NoAutoManageBundles);
-
-        // TODO: add an option to unmanage bundles instead of uninstalling those
-
-        DeploymentState dstate = getDeploymentState();
-
-        Map<String, Set<Long>> managedBundles = copy(state.managedBundles);
-
-        Map<String, Set<Bundle>> unmanagedBundles = apply(diff(dstate.bundlesPerRegion, state.managedBundles),
-                map(dstate.bundles));
-
-        // Resolve
-        // TODO: requirements
-        // TODO: bundles
-
-        SubsystemResolver resolver = new SubsystemResolver();
-        resolver.resolve(
-                dstate.features.values(),
-                requestedFeatures,
-                apply(unmanagedBundles, adapt(BundleRevision.class)),
-                Overrides.loadOverrides(this.overrides),
-                featureResolutionRange,
-                globalRepository);
-
-        Map<String, StreamProvider> providers = resolver.getProviders();
-        Map<String, Set<Resource>> featuresPerRegion = resolver.getFeaturesPerRegions();
-        Map<String, Set<String>> installedFeatures = apply(featuresPerRegion, featureId());
-        Map<String, Set<String>> newFeatures = diff(installedFeatures, state.installedFeatures);
-        Map<String, Set<String>> delFeatures = diff(state.installedFeatures, installedFeatures);
-
-        //
-        // Compute requested features state
-        //
-        Map<String, Map<String, String>> stateFeatures = copy(state.stateFeatures);
-        for (Map.Entry<String, Set<String>> entry : delFeatures.entrySet()) {
-            Map<String, String> map = stateFeatures.get(entry.getKey());
-            if (map != null) {
-                map.entrySet().removeAll(entry.getValue());
-                if (map.isEmpty()) {
-                    stateFeatures.remove(entry.getKey());
-                }
-            }
-        }
-        for (Map.Entry<String, Map<String, RequestedState>> entry1 : stateChanges.entrySet()) {
-            String region = entry1.getKey();
-            Map<String, String> regionStates = stateFeatures.get(region);
-            if (regionStates != null) {
-                for (Map.Entry<String, RequestedState> entry2 : entry1.getValue().entrySet()) {
-                    String feature = entry2.getKey();
-                    if (regionStates.containsKey(feature)) {
-                        regionStates.put(feature, entry2.getValue().name());
-                    }
-                }
-            }
-        }
-        for (Map.Entry<String, Set<String>> entry : newFeatures.entrySet()) {
-            for (String feature : entry.getValue()) {
-                Map<String, String> map = stateFeatures.get(entry.getKey());
-                if (map == null) {
-                    map = new HashMap<>();
-                    stateFeatures.put(entry.getKey(), map);
-                }
-                map.put(feature, noStart ? RequestedState.Installed.name() : RequestedState.Started.name());
-            }
-        }
-
-        // Compute information for each bundle
-        Map<String, Map<String, BundleInfo>> bundleInfos = resolver.getBundleInfos();
-
-        //
-        // Compute deployment
-        //
-        Deployment deployment = computeDeployment(dstate, resolver, state);
-
-        //
-        // Compute the set of bundles to refresh
-        //
-        Set<Bundle> toRefresh = new TreeSet<>(new BundleComparator()); // sort is only used for display
-        for (RegionDeployment regionDeployment : deployment.regions.values()) {
-            toRefresh.addAll(regionDeployment.toDelete);
-            toRefresh.addAll(regionDeployment.toUpdate.keySet());
-        }
-        if (!noRefreshManaged) {
-            computeBundlesToRefresh(toRefresh, dstate.bundles.values(), deployment.resToBnd, resolver.getWiring());
-        }
-        if (noRefreshUnmanaged) {
-            toRefresh.removeAll(flatten(unmanagedBundles));
-        }
-
-        // Automatically turn unmanaged bundles into managed bundles
-        // if they are required by a feature and no other unmanaged
-        // bundles have a requirement on it
-        Set<Bundle> toManage = new TreeSet<>(new BundleComparator()); // sort is only used for display
-        if (!noManageBundles) {
-            Set<Resource> features = resolver.getFeatures().keySet();
-            Set<? extends Resource> unmanaged = apply(flatten(unmanagedBundles), adapt(BundleRevision.class));
-            Set<Resource> requested = new HashSet<>();
-            // Gather bundles required by a feature
-            for (List<Wire> wires : resolver.getWiring().values()) {
-                for (Wire wire : wires) {
-                    if (features.contains(wire.getRequirer()) && unmanaged.contains(wire.getProvider())) {
-                        requested.add(wire.getProvider());
-                    }
-                }
-            }
-            // Now, we know which bundles are completely unmanaged
-            unmanaged.removeAll(requested);
-            // Check if bundles have wires from really unmanaged bundles
-            for (List<Wire> wires : resolver.getWiring().values()) {
-                for (Wire wire : wires) {
-                    if (requested.contains(wire.getProvider()) && unmanaged.contains(wire.getRequirer())) {
-                        requested.remove(wire.getProvider());
-                    }
-                }
-            }
-            if (!requested.isEmpty()) {
-                Map<Long, String> bundleToRegion = new HashMap<>();
-                for (Map.Entry<String, Set<Long>> entry : dstate.bundlesPerRegion.entrySet()) {
-                    for (long id : entry.getValue()) {
-                        bundleToRegion.put(id, entry.getKey());
-                    }
-                }
-                for (Resource rev : requested) {
-                    Bundle bundle = ((BundleRevision) rev).getBundle();
-                    long id = bundle.getBundleId();
-                    addToMapSet(managedBundles, bundleToRegion.get(id), id);
-                    toManage.add(bundle);
-                }
-            }
-        }
-
-        //
-        // Log deployment
-        //
-        logDeployment(deployment, verbose);
-
-        if (simulate) {
-            if (!noRefresh && !toRefresh.isEmpty()) {
-                print("  Bundles to refresh:", verbose);
-                for (Bundle bundle : toRefresh) {
-                    print("    " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
-                }
-            }
-            if (!toManage.isEmpty()) {
-                print("  Managing bundle:", verbose);
-                for (Bundle bundle : toManage) {
-                    print("    " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
-                }
-            }
-            return;
-        }
-
-        Set<Bundle> toStart = new HashSet<>();
-        Set<Bundle> toResolve = new HashSet<>();
-        Set<Bundle> toStop = new HashSet<>();
-
-        //
-        // Execute deployment
-        //
-        // #1: stop bundles that needs to be updated or uninstalled in order
-        // #2: uninstall needed bundles
-        // #3: update regions
-        // #4: update bundles
-        // #5: install bundles
-        // #6: save state
-        // #7: install configuration
-        // #8: refresh bundles
-        // #9: start bundles in order
-        // #10: send events
-        //
-
-        //
-        // Compute bundle states
-        //
-        Map<Resource, RequestedState> states = new HashMap<>();
-        for (Map.Entry<String, Set<Resource>> entry : resolver.getFeaturesPerRegions().entrySet()) {
-            String region = entry.getKey();
-            Map<String, String> fss = stateFeatures.get(region);
-            for (Resource feature : entry.getValue()) {
-                String fs = fss.get(getFeatureId(feature));
-                propagateState(states, feature, RequestedState.valueOf(fs), resolver);
-            }
-        }
-        states.keySet().retainAll(resolver.getBundles().keySet());
-        //
-        // Compute bundles to start, stop and resolve
-        //
-        for (Map.Entry<Resource, RequestedState> entry : states.entrySet()) {
-            Bundle bundle = deployment.resToBnd.get(entry.getKey());
-            if (bundle != null) {
-                switch (entry.getValue()) {
-                case Started:
-                    toResolve.add(bundle);
-                    toStart.add(bundle);
-                    break;
-                case Resolved:
-                    toResolve.add(bundle);
-                    toStop.add(bundle);
-                    break;
-                }
-            }
-        }
-        //
-        // Compute bundle all start levels and start levels to update
-        //
-        FrameworkStartLevel fsl = systemBundleContext.getBundle().adapt(FrameworkStartLevel.class);
-        int initialBundleStartLevel = fsl.getInitialBundleStartLevel();
-        int currentStartLevel = fsl.getStartLevel();
-        Map<Resource, Integer> startLevels = new HashMap<>();
-        Map<Bundle, Integer> toUpdateStartLevel = new HashMap<>();
-        for (Map.Entry<String, Set<Resource>> entry : resolver.getBundlesPerRegions().entrySet()) {
-            String region = entry.getKey();
-            for (Resource resource : entry.getValue()) {
-                BundleInfo bi = bundleInfos.get(region).get(getUri(resource));
-                if (bi != null) {
-                    int sl = bi.getStartLevel() > 0 ? bi.getStartLevel() : initialBundleStartLevel;
-                    startLevels.put(resource, sl);
-                    Bundle bundle = deployment.resToBnd.get(resource);
-                    if (bundle != null) {
-                        int curSl = bundle.adapt(BundleStartLevel.class).getStartLevel();
-                        if (sl != curSl) {
-                            toUpdateStartLevel.put(bundle, sl);
-                            if (sl > currentStartLevel) {
-                                toStop.add(bundle);
-                            }
-                        }
-                    }
-                }
-            }
-        }
-
-
-        //
-        // Handle updates on the FeaturesService bundle
-        //
-        RegionDeployment rootRegionDeployment = deployment.regions.get(ROOT_REGION);
-        // We don't support uninstalling the bundle
-        if (rootRegionDeployment != null && rootRegionDeployment.toDelete.contains(bundle)) {
-            throw new UnsupportedOperationException("Uninstalling the FeaturesService bundle is not supported");
-        }
-        // If the bundle needs to be updated, do the following:
-        //  - create flag files to indicate the resolution must be continued after restart
-        //  - update the checksum and save the state
-        //  - compute bundles wired to the FeaturesService bundle that will be refreshed
-        //  - stop the bundle
-        //  - update the bundle
-        //  - refresh wired bundles
-        //  - start the bundle
-        //  - exit
-        // When restarting, the resolution will be attempted again
-        if (rootRegionDeployment != null && rootRegionDeployment.toUpdate.containsKey(bundle)) {
-            writeResolve(requestedFeatures, options);
-            // If the bundle is updated because of a different checksum,
-            // save the new checksum persistently
-            if (deployment.bundleChecksums.containsKey(bundle.getBundleId())) {
-                synchronized (lock) {
-                    this.state.bundleChecksums.put(bundle.getBundleId(), deployment.bundleChecksums.get(bundle.getBundleId()));
-                    saveState();
-                }
-            }
-            Resource resource = rootRegionDeployment.toUpdate.get(bundle);
-            String uri = getUri(resource);
-            print("The FeaturesService bundle needs is being updated with " + uri, verbose);
-            toRefresh.clear();
-            toRefresh.add(bundle);
-            computeBundlesToRefresh(toRefresh,
-                    dstate.bundles.values(),
-                    Collections.<Resource, Bundle>emptyMap(),
-                    Collections.<Resource, List<Wire>>emptyMap());
-            bundle.stop(STOP_TRANSIENT);
-            try (
-                    InputStream is = getBundleInputStream(resource, providers)
-            ) {
-                bundle.update(is);
-            }
-            refreshPackages(toRefresh);
-            bundle.start();
-            return;
-        }
-
-        //
-        // Perform bundle operations
-        //
-
-        //
-        // Stop bundles by chunks
-        //
-        for (RegionDeployment regionDeployment : deployment.regions.values()) {
-            toStop.addAll(regionDeployment.toUpdate.keySet());
-            toStop.addAll(regionDeployment.toDelete);
-        }
-        removeFragmentsAndBundlesInState(toStop, UNINSTALLED | RESOLVED | STOPPING);
-        if (!toStop.isEmpty()) {
-            print("Stopping bundles:", verbose);
-            while (!toStop.isEmpty()) {
-                List<Bundle> bs = getBundlesToStop(toStop);
-                for (Bundle bundle : bs) {
-                    print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
-                    // If the bundle start level will be changed, stop it persistently to
-                    // avoid a restart when the start level is actually changed
-                    bundle.stop(toUpdateStartLevel.containsKey(bundle) ? 0 : STOP_TRANSIENT);
-                    toStop.remove(bundle);
-                }
-            }
-        }
-
-        //
-        // Delete bundles
-        //
-        boolean hasToDelete = false;
-        for (RegionDeployment regionDeployment : deployment.regions.values()) {
-            if (hasToDelete = !regionDeployment.toDelete.isEmpty()) {
-                break;
-            }
-        }
-        if (hasToDelete) {
-            print("Uninstalling bundles:", verbose);
-            for (Map.Entry<String, RegionDeployment> entry : deployment.regions.entrySet()) {
-                String name = entry.getKey();
-                RegionDeployment regionDeployment = entry.getValue();
-                for (Bundle bundle : regionDeployment.toDelete) {
-                    print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
-                    bundle.uninstall();
-                    removeFromMapSet(managedBundles, name, bundle.getBundleId());
-                }
-            }
-        }
+        Deployer.DeploymentState dstate = getDeploymentState(state);
+        Deployer.DeploymentRequest request = getDeploymentRequest(requestedFeatures, stateChanges, options);
+        new Deployer(this).deploy(dstate, request);
+    }
 
-        //
-        // Update regions
-        //
-        {
-            RegionDigraph clone = digraph.copy();
-            RegionDigraph computedDigraph = resolver.getFlatDigraph();
-            Map<String, Map<String, Map<String, Set<String>>>> policies = copy(dstate.filtersPerRegion);
-            // Iterate through previously managed regions and
-            // delete those that do not contain any bundles anymore
-            for (String name : state.managedBundles.keySet()) {
-                if (!managedBundles.containsKey(name) && !unmanagedBundles.containsKey(name)) {
-                    dstate.filtersPerRegion.remove(name);
-                }
-            }
-            // Fix broken filters
-            for (String name : policies.keySet()) {
-                policies.get(name).keySet().retainAll(policies.keySet());
-            }
-            // Update managed regions
-            for (Region computedRegion : computedDigraph.getRegions()) {
-                String name = computedRegion.getName();
-                Map<String, Map<String, Set<String>>> policy = policies.get(name);
-                if (policy == null) {
-                    policy = new HashMap<>();
-                    policies.put(name, policy);
-                }
-                for (RegionDigraph.FilteredRegion fr : computedDigraph.getEdges(computedRegion)) {
-                    String r2 = fr.getRegion().getName();
-                    Map<String, Set<String>> filters = new HashMap<>();
-                    Map<String, Collection<String>> current = fr.getFilter().getSharingPolicy();
-                    for (String ns : current.keySet()) {
-                        for (String f : current.get(ns)) {
-                            addToMapSet(filters, ns, f);
-                        }
-                    }
-                    policy.put(r2, filters);
-                }
-            }
-            // Apply all changes
-            for (Region region : clone.getRegions()) {
-                clone.removeRegion(region);
-            }
-            for (String name : policies.keySet()) {
-                clone.createRegion(name);
-            }
-            for (String r1Name : policies.keySet()) {
-                Region r1 = clone.getRegion(r1Name);
-                Map<String, Map<String, Set<String>>> policy = policies.get(r1Name);
-                for (String r2Name : policy.keySet()) {
-                    Region r2 = clone.getRegion(r2Name);
-                    RegionFilterBuilder rfb = clone.createRegionFilterBuilder();
-                    for (String ns : policy.get(r2Name).keySet()) {
-                        for (String f : policy.get(r2Name).get(ns)) {
-                            rfb.allow(ns, f);
-                        }
-                    }
-                    clone.connect(r1, rfb.build(), r2);
-                }
-                // Dispatch bundles
-                if (unmanagedBundles.containsKey(r1Name)) {
-                    for (Bundle bundle : unmanagedBundles.get(r1Name)) {
-                        r1.addBundle(bundle);
-                    }
-                }
-                if (managedBundles.containsKey(r1Name)) {
-                    for (long id : managedBundles.get(r1Name)) {
-                        r1.addBundle(id);
-                    }
-                }
-            }
-            this.digraph.replace(clone);
+    public void print(String message, boolean verbose) {
+        LOGGER.info(message);
+        if (verbose) {
+            System.out.println(message);
         }
+    }
 
-
-        //
-        // Update bundles
-        //
-        boolean hasToUpdate = false;
-        for (RegionDeployment regionDeployment : deployment.regions.values()) {
-            if (hasToUpdate = !regionDeployment.toUpdate.isEmpty()) {
-                break;
-            }
-        }
-        if (hasToUpdate) {
-            print("Updating bundles:", verbose);
-            for (Map.Entry<String, RegionDeployment> rde : deployment.regions.entrySet()) {
-                for (Map.Entry<Bundle, Resource> entry : rde.getValue().toUpdate.entrySet()) {
-                    Bundle bundle = entry.getKey();
-                    Resource resource = entry.getValue();
-                    String uri = getUri(resource);
-                    print("  " + uri, verbose);
-                    try (
-                            InputStream is = getBundleInputStream(resource, providers)
-                    ) {
-                        bundle.update(is);
-                    }
-                    toStart.add(bundle);
-                }
-            }
-        }
-        //
-        // Update start levels
-        //
-        for (Map.Entry<Bundle, Integer> entry : toUpdateStartLevel.entrySet()) {
-            Bundle bundle = entry.getKey();
-            int sl = entry.getValue();
-            bundle.adapt(BundleStartLevel.class).setStartLevel(sl);
-        }
-        //
-        // Install bundles
-        //
-        boolean hasToInstall = false;
-        for (RegionDeployment regionDeployment : deployment.regions.values()) {
-            if (hasToInstall = !regionDeployment.toInstall.isEmpty()) {
-                break;
-            }
-        }
-        if (hasToInstall) {
-            print("Installing bundles:", verbose);
-            for (Map.Entry<String, RegionDeployment> entry : deployment.regions.entrySet()) {
-                String name = entry.getKey();
-                Region region = digraph.getRegion(name);
-                RegionDeployment regionDeployment = entry.getValue();
-                for (Resource resource : regionDeployment.toInstall) {
-                    String uri = getUri(resource);
-                    print("  " + uri, verbose);
-                    Bundle bundle;
-                    long crc;
-                    try (
-                            ChecksumUtils.CRCInputStream is = new ChecksumUtils.CRCInputStream(getBundleInputStream(resource, providers))
-                    ) {
-                        if (ROOT_REGION.equals(name)) {
-                            bundle = region.installBundleAtLocation(uri, is);
-                        } else {
-                            bundle = region.installBundle(uri, is);
-                        }
-                        crc = is.getCRC();
-                    }
-                    addToMapSet(managedBundles, name, bundle.getBundleId());
-                    deployment.resToBnd.put(resource, bundle);
-                    // save a checksum of installed snapshot bundle
-                    if (UPDATE_SNAPSHOTS_CRC.equals(updateSnaphots)
-                            && isUpdateable(resource) && !deployment.bundleChecksums.containsKey(bundle.getBundleId())) {
-                        deployment.bundleChecksums.put(bundle.getBundleId(), crc);
-                    }
-                    int startLevel = startLevels.get(resource);
-                    bundle.adapt(BundleStartLevel.class).setStartLevel(startLevel);
-                    RequestedState reqState = states.get(resource);
-                    switch (reqState) {
-                    case Started:
-                        toResolve.add(bundle);
-                        toStart.add(bundle);
-                        break;
-                    case Resolved:
-                        toResolve.add(bundle);
-                        break;
-                    }
+    public void refreshPackages(Collection<Bundle> bundles) throws InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        FrameworkWiring fw = systemBundleContext.getBundle().adapt(FrameworkWiring.class);
+        fw.refreshBundles(bundles, new FrameworkListener() {
+            @Override
+            public void frameworkEvent(FrameworkEvent event) {
+                if (event.getType() == FrameworkEvent.ERROR) {
+                    LOGGER.error("Framework error", event.getThrowable());
                 }
+                latch.countDown();
             }
-        }
+        });
+        latch.await();
+    }
 
-        //
-        // Update and save state
-        //
+    @Override
+    public void saveState(State state) {
         synchronized (lock) {
-            this.state.bundleChecksums.clear();
-            this.state.bundleChecksums.putAll(deployment.bundleChecksums);
-            this.state.requestedFeatures.clear();
-            this.state.requestedFeatures.putAll(requestedFeatures);
-            this.state.installedFeatures.clear();
-            this.state.installedFeatures.putAll(installedFeatures);
-            this.state.stateFeatures.clear();
-            this.state.stateFeatures.putAll(stateFeatures);
-            this.state.managedBundles.clear();
-            this.state.managedBundles.putAll(managedBundles);
+            this.state.replace(state);
             saveState();
         }
-
-        //
-        // Install configurations
-        //
-        if (configInstaller != null && !newFeatures.isEmpty()) {
-            Set<Feature> set = apply(flatten(newFeatures), map(dstate.features));
-            for (Feature feature : set) {
-                configInstaller.installFeatureConfigs(feature);
-            }
-        }
-
-        // TODO: remove this hack, but it avoids loading the class after the bundle is refreshed
-        new CopyOnWriteArrayIdentityList().iterator();
-        RequirementSort.sort(Collections.<Resource>emptyList());
-
-        if (!noRefresh) {
-            toStop = new HashSet<>();
-            toStop.addAll(toRefresh);
-            removeFragmentsAndBundlesInState(toStop, UNINSTALLED | RESOLVED | STOPPING);
-            if (!toStop.isEmpty()) {
-                print("Stopping bundles:", verbose);
-                while (!toStop.isEmpty()) {
-                    List<Bundle> bs = getBundlesToStop(toStop);
-                    for (Bundle bundle : bs) {
-                        print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
-                        bundle.stop(STOP_TRANSIENT);
-                        toStop.remove(bundle);
-                        toStart.add(bundle);
-                    }
-                }
-            }
-
-            if (!toRefresh.isEmpty()) {
-                print("Refreshing bundles:", verbose);
-                for (Bundle bundle : toRefresh) {
-                    print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
-                }
-                if (!toRefresh.isEmpty()) {
-                    refreshPackages(toRefresh);
-                }
-            }
-        }
-
-        // Resolve bundles
-        toResolve.addAll(toStart);
-        toResolve.addAll(toRefresh);
-        removeFragmentsAndBundlesInState(toResolve, UNINSTALLED);
-        systemBundleContext.getBundle().adapt(FrameworkWiring.class).resolveBundles(toResolve);
-
-        // Compute bundles to start
-        removeFragmentsAndBundlesInState(toStart, UNINSTALLED | ACTIVE | STARTING);
-        if (!toStart.isEmpty()) {
-            // Compute correct start order
-            List<Exception> exceptions = new ArrayList<>();
-            print("Starting bundles:", verbose);
-            while (!toStart.isEmpty()) {
-                List<Bundle> bs = getBundlesToStart(toStart);
-                for (Bundle bundle : bs) {
-                    print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
-                    try {
-                        bundle.start();
-                    } catch (BundleException e) {
-                        exceptions.add(e);
-                    }
-                    toStart.remove(bundle);
-                }
-            }
-            if (!exceptions.isEmpty()) {
-                throw new MultiException("Error restarting bundles", exceptions);
-            }
-        }
-
-        // Call listeners
-        // TODO: add region information and avoid flattening
-        for (Feature feature : apply(flatten(delFeatures), map(dstate.features))) {
-            callListeners(new FeatureEvent(feature, FeatureEvent.EventType.FeatureUninstalled, false));
-        }
-        for (Feature feature : apply(flatten(newFeatures), map(dstate.features))) {
-            callListeners(new FeatureEvent(feature, FeatureEvent.EventType.FeatureInstalled, false));
-        }
-
-        print("Done.", verbose);
-    }
-
-    private void propagateState(Map<Resource, RequestedState> states, Resource resource, RequestedState state, SubsystemResolver resolver) {
-        if (!isSubsystem(resource)) {
-            RequestedState reqState = mergeStates(state, states.get(resource));
-            if (reqState != states.get(resource)) {
-                states.put(resource, reqState);
-                for (Wire wire : resolver.getWiring().get(resource)) {
-                    Resource provider = wire.getProvider();
-                    RequestedState stateToMerge;
-                    String region = resolver.getBundles().get(provider);
-                    BundleInfo bi = region != null ? resolver.getBundleInfos().get(region).get(getUri(provider)) : null;
-                    if (reqState == RequestedState.Started) {
-                        String effective = wire.getCapability().getDirectives().get(CAPABILITY_EFFECTIVE_DIRECTIVE);
-                        // If there is an active effective capability or a requirement from the feature
-                        // and if the bundle is flagged as to start, start it
-                        if ((EFFECTIVE_ACTIVE.equals(effective) || IDENTITY_NAMESPACE.equals(wire.getCapability().getNamespace()))
-                                && (bi == null || bi.isStart())) {
-                            stateToMerge = RequestedState.Started;
-                        } else {
-                            stateToMerge = RequestedState.Resolved;
-                        }
-                    } else {
-                        stateToMerge = reqState;
-                    }
-                    propagateState(states, provider, stateToMerge, resolver);
-                }
-            }
-        }
     }
 
-    private boolean isSubsystem(Resource resource) {
-        for (Capability cap : resource.getCapabilities(null)) {
-            if (cap.getNamespace().equals(IDENTITY_NAMESPACE)) {
-                String type = (String) cap.getAttributes().get(CAPABILITY_TYPE_ATTRIBUTE);
-                return (type != null) && type.equals(TYPE_SUBSYSTEM);
-            }
-        }
-        return false;
-    }
-
-    private RequestedState mergeStates(RequestedState s1, RequestedState s2) {
-        if (s1 == RequestedState.Started || s2 == RequestedState.Started) {
-            return RequestedState.Started;
-        }
-        if (s1 == RequestedState.Resolved || s2 == RequestedState.Resolved) {
-            return RequestedState.Resolved;
-        }
-        return RequestedState.Installed;
-    }
-
-    private void computeBundlesToRefresh(Set<Bundle> toRefresh, Collection<Bundle> bundles, Map<Resource, Bundle> resources, Map<Resource, List<Wire>> resolution) {
-        int size;
-        do {
-            size = toRefresh.size();
-            for (Bundle bundle : bundles) {
-                // Continue if we already know about this bundle
-                if (toRefresh.contains(bundle)) {
-                    continue;
-                }
-                // Ignore non resolved bundle
-                BundleWiring wiring = bundle.adapt(BundleWiring.class);
-                if (wiring == null) {
-                    continue;
-                }
-                // Get through the old resolution and flag this bundle
-                // if it was wired to a bundle to be refreshed
-                for (BundleWire wire : wiring.getRequiredWires(null)) {
-                    if (toRefresh.contains(wire.getProvider().getBundle())) {
-                        toRefresh.add(bundle);
-                        break;
-                    }
-                }
-                // Get through the new resolution and flag this bundle
-                // if it's wired to any new bundle
-                List<Wire> newWires = resolution.get(wiring.getRevision());
-                if (newWires != null) {
-                    for (Wire wire : newWires) {
-                        Bundle b;
-                        if (wire.getProvider() instanceof BundleRevision) {
-                            b = ((BundleRevision) wire.getProvider()).getBundle();
-                        } else {
-                            b = resources.get(wire.getProvider());
-                        }
-                        if (b == null || toRefresh.contains(b)) {
-                            toRefresh.add(bundle);
-                            break;
-                        }
-                    }
-                }
-            }
-        } while (toRefresh.size() > size);
+    @Override
+    public void persistResolveRequest(Deployer.DeploymentRequest request) throws IOException {
+        writeResolve(request.requestedFeatures, request.options);
     }
 
-    private void print(String message, boolean verbose) {
-        LOGGER.info(message);
-        if (verbose) {
-            System.out.println(message);
+    @Override
+    public void installFeatureConfigs(Feature feature) throws IOException, InvalidSyntaxException {
+        if (configInstaller != null) {
+            configInstaller.installFeatureConfigs(feature);
         }
     }
 
-    private void removeFragmentsAndBundlesInState(Collection<Bundle> bundles, int state) {
-        for (Iterator<Bundle> iterator = bundles.iterator(); iterator.hasNext();) {
-            Bundle bundle = iterator.next();
-            if ((bundle.getState() & state) != 0
-                    || bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null) {
-                iterator.remove();
-            }
+    @Override
+    public Bundle installBundle(String region, String uri, InputStream is) throws BundleException {
+        if (ROOT_REGION.equals(region)) {
+            return digraph.getRegion(region).installBundleAtLocation(uri, is);
+        } else {
+            return digraph.getRegion(region).installBundle(uri, is);
         }
     }
 
-    protected void logDeployment(Deployment overallDeployment, boolean verbose) {
-        if (overallDeployment.regions.isEmpty()) {
-            print("No deployment change.", verbose);
-            return;
-        }
-        print("Changes to perform:", verbose);
-        for (Map.Entry<String, RegionDeployment> region : overallDeployment.regions.entrySet()) {
-            RegionDeployment deployment = region.getValue();
-            print("  Region: " + region.getKey(), verbose);
-            if (!deployment.toDelete.isEmpty()) {
-                print("    Bundles to uninstall:", verbose);
-                for (Bundle bundle : deployment.toDelete) {
-                    print("      " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
-                }
-            }
-            if (!deployment.toUpdate.isEmpty()) {
-                print("    Bundles to update:", verbose);
-                for (Map.Entry<Bundle, Resource> entry : deployment.toUpdate.entrySet()) {
-                    print("      " + entry.getKey().getSymbolicName() + " / " + entry.getKey().getVersion() + " with " + getUri(entry.getValue()), verbose);
-                }
-            }
-            if (!deployment.toInstall.isEmpty()) {
-                print("    Bundles to install:", verbose);
-                for (Resource resource : deployment.toInstall) {
-                    print("      " + getUri(resource), verbose);
-                }
-            }
-        }
+    @Override
+    public void updateBundle(Bundle bundle, InputStream is) throws BundleException {
+        bundle.update(is);
     }
 
-    protected Deployment computeDeployment(
-            DeploymentState dstate,
-            SubsystemResolver resolver,
-            State state) throws IOException {
-
-        Deployment result = new Deployment();
-
-        Map<String, Set<Resource>> bundlesPerRegions = resolver.getBundlesPerRegions();
-
-        // Gather all regions, including old ones and new ones
-        Set<String> regions = new HashSet<>();
-        regions.addAll(state.managedBundles.keySet());
-        regions.addAll(bundlesPerRegions.keySet());
-
-        for (String region : regions) {
-
-            RegionDeployment deployment = new RegionDeployment();
-
-            // Get the list of bundles currently assigned in the region
-            Set<Long> managed = state.managedBundles.get(region);
-            if (managed == null) {
-                managed = Collections.emptySet();
-            }
-
-            // Compute the list of resources to deploy in the region
-            Set<Resource> bundlesInRegion = bundlesPerRegions.get(region);
-            List<Resource> toDeploy = bundlesInRegion != null
-                    ? new ArrayList<>(bundlesInRegion) : new ArrayList<Resource>();
-
-            // First pass: go through all installed bundles and mark them
-            // as either to ignore or delete
-            for (long bundleId : managed) {
-                // Look for the installed bundle
-                Bundle bundle = dstate.bundles.get(bundleId);
-                // Bundle has been manually uninstalled ?
-                if (bundle != null) {
-                    // Look for a matching resource
-                    Resource resource = null;
-                    for (Resource res : toDeploy) {
-                        if (bundle.getSymbolicName().equals(getSymbolicName(res))
-                                && bundle.getVersion().equals(getVersion(res))) {
-                            resource = res;
-                            break;
-                        }
-                    }
-                    // We found a matching bundle
-                    if (resource != null) {
-                        // In case of snapshots, check if the snapshot is out of date
-                        // and flag it as to update
-                        if (isUpdateable(resource)) {
-                            // Always update snapshots
-                            if (UPDATE_SNAPSHOTS_ALWAYS.equalsIgnoreCase(updateSnaphots)) {
-                                LOGGER.debug("Update snapshot for " + bundle.getLocation());
-                                deployment.toUpdate.put(bundle, resource);
-                            } else if (UPDATE_SNAPSHOTS_CRC.equalsIgnoreCase(updateSnaphots)) {
-                                // if the checksum are different
-                                try (
-                                        InputStream is = getBundleInputStream(resource, resolver.getProviders())
-                                ) {
-                                    long newCrc = ChecksumUtils.checksum(is);
-                                    long oldCrc = state.bundleChecksums.containsKey(bundle.getBundleId()) ? state.bundleChecksums.get(bundle.getBundleId()) : 0L;
-                                    if (newCrc != oldCrc) {
-                                        LOGGER.debug("New snapshot available for " + bundle.getLocation());
-                                        deployment.toUpdate.put(bundle, resource);
-                                    }
-                                    result.bundleChecksums.put(bundle.getBundleId(), newCrc);
-                                }
-                            }
-                        }
-                        // We're done for this resource
-                        toDeploy.remove(resource);
-                        result.resToBnd.put(resource, bundle);
-                        // There's no matching resource
-                        // If the bundle is managed, we need to delete it
-                    } else if (managed.contains(bundle.getBundleId())) {
-                        deployment.toDelete.add(bundle);
-                    }
-                }
-            }
-
-            // Second pass on remaining resources
-            for (Resource resource : toDeploy) {
-                TreeMap<Version, Bundle> matching = new TreeMap<>();
-                VersionRange range = new VersionRange(Macro.transform(bundleUpdateRange, getVersion(resource).toString()));
-                for (Bundle bundle : deployment.toDelete) {
-                    if (bundle.getSymbolicName().equals(getSymbolicName(resource)) && range.contains(bundle.getVersion())) {
-                        matching.put(bundle.getVersion(), bundle);
-                    }
-                }
-                if (!matching.isEmpty()) {
-                    Bundle bundle = matching.lastEntry().getValue();
-                    deployment.toUpdate.put(bundle, resource);
-                    deployment.toDelete.remove(bundle);
-                    result.resToBnd.put(resource, bundle);
-                } else {
-                    deployment.toInstall.add(resource);
-                }
-            }
-            Collections.sort(deployment.toInstall, new ResourceComparator());
-
-            // Add this region if there is something to do
-            if (!deployment.toDelete.isEmpty()
-                    || !deployment.toUpdate.isEmpty()
-                    || !deployment.toInstall.isEmpty()) {
-                result.regions.put(region, deployment);
-            }
-        }
-
-        return result;
+    @Override
+    public void uninstall(Bundle bundle) throws BundleException {
+        bundle.uninstall();
     }
 
-    protected <T> MapUtils.Function<Bundle, T> adapt(final Class<T> clazz) {
-        return new MapUtils.Function<Bundle, T>() {
-            @Override
-            public T apply(Bundle bundle) {
-                return bundle.adapt(clazz);
-            }
-        };
+    @Override
+    public void startBundle(Bundle bundle) throws BundleException {
+        bundle.start();
     }
 
-    protected MapUtils.Function<Resource, String> featureId() {
-        return new MapUtils.Function<Resource, String>() {
-            @Override
-            public String apply(Resource resource) {
-                return getFeatureId(resource);
-            }
-        };
+    @Override
+    public void stopBundle(Bundle bundle, int options) throws BundleException {
+        bundle.stop(options);
     }
 
-
-    protected boolean isUpdateable(Resource resource) {
-        String uri = getUri(resource);
-        return getVersion(resource).getQualifier().endsWith(SNAPSHOT)
-                || uri.contains(SNAPSHOT)
-                || !uri.contains(MAVEN);
+    @Override
+    public void setBundleStartLevel(Bundle bundle, int startLevel) {
+        bundle.adapt(BundleStartLevel.class).setStartLevel(startLevel);
     }
 
-    protected List<Bundle> getBundlesToStart(Collection<Bundle> bundles) {
-        // Restart the features service last, regardless of any other consideration
-        // so that we don't end up with the service trying to do stuff before we're done
-        boolean restart = false;
-
-        SortedMap<Integer, Set<Bundle>> bundlesPerStartLevel = new TreeMap<>();
-        for (Bundle bundle : bundles) {
-            if (bundle == this.bundle) {
-                restart = true;
-            } else {
-                int sl = bundle.adapt(BundleStartLevel.class).getStartLevel();
-                addToMapSet(bundlesPerStartLevel, sl, bundle);
-            }
-        }
-        if (bundlesPerStartLevel.isEmpty()) {
-            bundles = Collections.emptyList();
-        } else {
-            bundles = bundlesPerStartLevel.remove(bundlesPerStartLevel.firstKey());
-        }
-
-        // We hit FELIX-2949 if we don't use the correct order as Felix resolver isn't greedy.
-        // In order to minimize that, we make sure we resolve the bundles in the order they
-        // are given back by the resolution, meaning that all root bundles (i.e. those that were
-        // not flagged as dependencies in features) are started before the others.   This should
-        // make sure those important bundles are started first and minimize the problem.
-        List<BundleRevision> revs = new ArrayList<>();
-        for (Bundle bundle : bundles) {
-            revs.add(bundle.adapt(BundleRevision.class));
-        }
-        List<Bundle> sorted = new ArrayList<>();
-        for (BundleRevision rev : RequirementSort.sort(revs)) {
-            sorted.add(rev.getBundle());
-        }
-        if (sorted.isEmpty() && restart) {
-            sorted.add(bundle);
-        }
-        return sorted;
+    @Override
+    public void resolveBundles(Set<Bundle> bundles) {
+        systemBundleContext.getBundle().adapt(FrameworkWiring.class).resolveBundles(bundles);
     }
 
-    protected List<Bundle> getBundlesToStop(Collection<Bundle> bundles) {
-        SortedMap<Integer, Set<Bundle>> bundlesPerStartLevel = new TreeMap<>();
-        for (Bundle bundle : bundles) {
-            int sl = bundle.adapt(BundleStartLevel.class).getStartLevel();
-            addToMapSet(bundlesPerStartLevel, sl, bundle);
-        }
-        bundles = bundlesPerStartLevel.get(bundlesPerStartLevel.lastKey());
-
-        List<Bundle> bundlesToDestroy = new ArrayList<>();
-        for (Bundle bundle : bundles) {
-            ServiceReference[] references = bundle.getRegisteredServices();
-            int usage = 0;
-            if (references != null) {
-                for (ServiceReference reference : references) {
-                    usage += getServiceUsage(reference, bundles);
-                }
-            }
-            LOGGER.debug("Usage for bundle {} is {}", bundle, usage);
-            if (usage == 0) {
-                bundlesToDestroy.add(bundle);
-            }
-        }
-        if (!bundlesToDestroy.isEmpty()) {
-            Collections.sort(bundlesToDestroy, new Comparator<Bundle>() {
-                public int compare(Bundle b1, Bundle b2) {
-                    return (int) (b2.getLastModified() - b1.getLastModified());
-                }
-            });
-            LOGGER.debug("Selected bundles {} for destroy (no services in use)", bundlesToDestroy);
-        } else {
-            ServiceReference ref = null;
-            for (Bundle bundle : bundles) {
-                ServiceReference[] references = bundle.getRegisteredServices();
-                for (ServiceReference reference : references) {
-                    if (getServiceUsage(reference, bundles) == 0) {
-                        continue;
-                    }
-                    if (ref == null || reference.compareTo(ref) < 0) {
-                        LOGGER.debug("Currently selecting bundle {} for destroy (with reference {})", bundle, reference);
-                        ref = reference;
+    @Override
+    public void replaceDigraph(Map<String, Map<String, Map<String, Set<String>>>> policies, Map<String, Set<Long>> bundles) throws BundleException, InvalidSyntaxException {
+        RegionDigraph temp = digraph.copy();
+        // Remove everything
+        for (Region region : temp.getRegions()) {
+            temp.removeRegion(region);
+        }
+        // Re-create regions
+        for (String name : policies.keySet()) {
+            temp.createRegion(name);
+        }
+        // Dispatch bundles
+        for (Map.Entry<String, Set<Long>> entry : bundles.entrySet()) {
+            Region region = temp.getRegion(entry.getKey());
+            for (long bundleId : entry.getValue()) {
+                region.addBundle(bundleId);
+            }
+        }
+        // Add policies
+        for (Map.Entry<String, Map<String, Map<String, Set<String>>>> entry1 : policies.entrySet()) {
+            Region region1 = temp.getRegion(entry1.getKey());
+            for (Map.Entry<String, Map<String, Set<String>>> entry2 : entry1.getValue().entrySet()) {
+                Region region2 = temp.getRegion(entry2.getKey());
+                RegionFilterBuilder rfb = temp.createRegionFilterBuilder();
+                for (Map.Entry<String, Set<String>> entry3 : entry2.getValue().entrySet()) {
+                    for (String flt : entry3.getValue()) {
+                        rfb.allow(entry3.getKey(), flt);
                     }
                 }
-            }
-            if (ref != null) {
-                bundlesToDestroy.add(ref.getBundle());
-            }
-            LOGGER.debug("Selected bundle {} for destroy (lowest ranking service)", bundlesToDestroy);
-        }
-        return bundlesToDestroy;
-    }
-
-    private static int getServiceUsage(ServiceReference ref, Collection<Bundle> bundles) {
-        Bundle[] usingBundles = ref.getUsingBundles();
-        int nb = 0;
-        if (usingBundles != null) {
-            for (Bundle bundle : usingBundles) {
-                if (bundles.contains(bundle)) {
-                    nb++;
-                }
+                region1.connectRegion(region2, rfb.build());
             }
         }
-        return nb;
+        digraph.replace(temp);
     }
-
-    protected InputStream getBundleInputStream(Resource resource, Map<String, StreamProvider> providers) throws IOException {
-        String uri = getUri(resource);
-        if (uri == null) {
-            throw new IllegalStateException("Resource has no uri");
-        }
-        StreamProvider provider = providers.get(uri);
-        if (provider == null) {
-            throw new IllegalStateException("Resource " + uri + " has no StreamProvider");
-        }
-        return provider.open();
-    }
-
-    protected void refreshPackages(Collection<Bundle> bundles) throws InterruptedException {
-        final CountDownLatch latch = new CountDownLatch(1);
-        FrameworkWiring fw = systemBundleContext.getBundle().adapt(FrameworkWiring.class);
-        fw.refreshBundles(bundles, new FrameworkListener() {
-            @Override
-            public void frameworkEvent(FrameworkEvent event) {
-                if (event.getType() == FrameworkEvent.ERROR) {
-                    LOGGER.error("Framework error", event.getThrowable());
-                }
-                latch.countDown();
-            }
-        });
-        latch.await();
-    }
-
-
-    static class Deployment {
-        Map<Long, Long> bundleChecksums = new HashMap<>();
-        Map<Resource, Bundle> resToBnd = new HashMap<>();
-        Map<String, RegionDeployment> regions = new HashMap<>();
-    }
-
-    static class RegionDeployment {
-        List<Resource> toInstall = new ArrayList<>();
-        List<Bundle> toDelete = new ArrayList<>();
-        Map<Bundle, Resource> toUpdate = new HashMap<>();
-    }
-
 }

http://git-wip-us.apache.org/repos/asf/karaf/blob/9e2ada3c/features/core/src/main/java/org/apache/karaf/features/internal/service/State.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/State.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/State.java
index fa1fd18..b1a3ccd 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/State.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/State.java
@@ -36,14 +36,30 @@ public class State {
 
     public State copy() {
         State state = new State();
-        state.bootDone.set(bootDone.get());
-        MapUtils.copy(repositories, state.repositories);
-        MapUtils.copy(requestedFeatures, state.requestedFeatures);
-        MapUtils.copy(installedFeatures, state.installedFeatures);
-        MapUtils.copy(stateFeatures, state.stateFeatures);
-        MapUtils.copy(managedBundles, state.managedBundles);
-        MapUtils.copy(bundleChecksums, state.bundleChecksums);
+        copy(this, state, false);
         return state;
     }
 
+    public void replace(State state) {
+        copy(state, this, true);
+    }
+
+    private static void copy(State from, State to, boolean clear) {
+        if (clear) {
+            to.repositories.clear();
+            to.requestedFeatures.clear();
+            to.installedFeatures.clear();
+            to.stateFeatures.clear();
+            to.managedBundles.clear();
+            to.bundleChecksums.clear();
+        }
+        to.bootDone.set(from.bootDone.get());
+        MapUtils.copy(from.repositories, to.repositories);
+        MapUtils.copy(from.requestedFeatures, to.requestedFeatures);
+        MapUtils.copy(from.installedFeatures, to.installedFeatures);
+        MapUtils.copy(from.stateFeatures, to.stateFeatures);
+        MapUtils.copy(from.managedBundles, to.managedBundles);
+        MapUtils.copy(from.bundleChecksums, to.bundleChecksums);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/karaf/blob/9e2ada3c/features/core/src/test/java/org/apache/karaf/features/FeaturesServiceTest.java
----------------------------------------------------------------------
diff --git a/features/core/src/test/java/org/apache/karaf/features/FeaturesServiceTest.java b/features/core/src/test/java/org/apache/karaf/features/FeaturesServiceTest.java
index 2609c8d..3a2f581 100644
--- a/features/core/src/test/java/org/apache/karaf/features/FeaturesServiceTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/FeaturesServiceTest.java
@@ -50,6 +50,7 @@ import org.junit.Before;
 import org.junit.Test;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
+import org.osgi.framework.startlevel.FrameworkStartLevel;
 
 public class FeaturesServiceTest extends TestBase {
     private static final String FEATURE_WITH_INVALID_BUNDLE = "<features name='test' xmlns='http://karaf.apache.org/xmlns/features/v1.0.0'>"
@@ -363,8 +364,14 @@ public class FeaturesServiceTest extends TestBase {
         URI uri = createTempRepo(FEATURE_WITH_INVALID_BUNDLE, bundle1Uri, bundle2Uri);
         
         BundleContext bundleContext = EasyMock.createMock(BundleContext.class);
+        Bundle bundle = EasyMock.createMock(Bundle.class);
+        FrameworkStartLevel fsl = EasyMock.createMock(FrameworkStartLevel.class);
         expect(bundleContext.getBundles()).andReturn(new Bundle[0]);
-        replay(bundleContext);
+        expect(bundleContext.getBundle()).andReturn(bundle);
+        expect(bundle.adapt(FrameworkStartLevel.class)).andReturn(fsl);
+        expect(fsl.getInitialBundleStartLevel()).andReturn(50);
+        expect(fsl.getStartLevel()).andReturn(100);
+        replay(bundleContext, bundle, fsl);
 
         FeaturesServiceImpl svc = new FeaturesServiceImpl(null, bundleContext, new Storage(), null, null, null, null, null, null, null, null, null);
         svc.addRepository(uri);

http://git-wip-us.apache.org/repos/asf/karaf/blob/9e2ada3c/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
----------------------------------------------------------------------
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
index 5aabcb7..dc371a1 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/region/SubsystemTest.java
@@ -32,9 +32,8 @@ import java.util.Set;
 import java.util.jar.JarOutputStream;
 import java.util.jar.Manifest;
 
-import org.apache.karaf.features.Repository;
+import org.apache.karaf.features.FeaturesService;
 import org.apache.karaf.features.internal.download.StreamProvider;
-import org.apache.karaf.features.internal.service.FeaturesServiceImpl;
 import org.apache.karaf.features.internal.service.RepositoryImpl;
 import org.apache.karaf.features.internal.download.simple.SimpleDownloader;
 import org.junit.Test;
@@ -67,7 +66,7 @@ public class SubsystemTest {
                          features,
                          Collections.<String, Set<BundleRevision>>emptyMap(),
                          Collections.<String>emptySet(),
-                         FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE,
+                         FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                          null);
 
         verify(resolver, expected);
@@ -98,7 +97,7 @@ public class SubsystemTest {
                          features,
                          Collections.<String, Set<BundleRevision>>emptyMap(),
                          Collections.<String>emptySet(),
-                         FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE,
+                         FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                          null);
 
         verify(resolver, expected);
@@ -119,7 +118,7 @@ public class SubsystemTest {
                          features,
                          Collections.<String, Set<BundleRevision>>emptyMap(),
                          Collections.singleton("b"),
-                         FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE,
+                         FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                          null);
 
         verify(resolver, expected);
@@ -139,7 +138,7 @@ public class SubsystemTest {
                 features,
                 Collections.<String, Set<BundleRevision>>emptyMap(),
                 Collections.<String>emptySet(),
-                FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE,
+                FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                 null);
 
         verify(resolver, expected);
@@ -161,7 +160,7 @@ public class SubsystemTest {
                 features,
                 Collections.<String, Set<BundleRevision>>emptyMap(),
                 Collections.<String>emptySet(),
-                FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE,
+                FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE,
                 null);
 
         verify(resolver, expected);


[5/7] git commit: [KARAF-2888] Extract the deployment for easier unit testing

Posted by gn...@apache.org.
[KARAF-2888] Extract the deployment for easier unit testing


Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/9e2ada3c
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/9e2ada3c
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/9e2ada3c

Branch: refs/heads/master
Commit: 9e2ada3cc655e9ed245e3bf0c97fc3723358c6f0
Parents: 3d20404
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Tue Apr 29 22:32:54 2014 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Wed Apr 30 20:04:27 2014 +0200

----------------------------------------------------------------------
 .../apache/karaf/features/FeaturesService.java  |   12 +-
 .../karaf/features/internal/osgi/Activator.java |    6 +-
 .../features/internal/service/Deployer.java     | 1112 +++++++++++++++++
 .../internal/service/FeaturesServiceImpl.java   | 1171 ++----------------
 .../karaf/features/internal/service/State.java  |   30 +-
 .../karaf/features/FeaturesServiceTest.java     |    9 +-
 .../features/internal/region/SubsystemTest.java |   13 +-
 7 files changed, 1285 insertions(+), 1068 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/9e2ada3c/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java b/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
index 4366036..e266402 100644
--- a/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
+++ b/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
@@ -26,7 +26,17 @@ import java.util.Set;
  */
 public interface FeaturesService {
 
-    public static final String ROOT_REGION = "root";
+    String ROOT_REGION = "root";
+
+    String UPDATE_SNAPSHOTS_NONE = "none";
+    String UPDATE_SNAPSHOTS_CRC = "crc";
+    String DEFAULT_UPDATE_SNAPSHOTS = UPDATE_SNAPSHOTS_CRC;
+    String UPDATE_SNAPSHOTS_ALWAYS = "always";
+
+    String DEFAULT_FEATURE_RESOLUTION_RANGE = "${range;[====,====]}";
+    String DEFAULT_BUNDLE_UPDATE_RANGE = "${range;[==,=+)}";
+
+    String UPDATEABLE_URIS = "mvn:.*SNAPSHOT|(?!mvn:).*";
 
     enum Option {
         NoFailOnFeatureNotFound,

http://git-wip-us.apache.org/repos/asf/karaf/blob/9e2ada3c/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
index 1be87fc..5db6204 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
@@ -159,9 +159,9 @@ public class Activator extends BaseActivator {
 
         FeatureConfigInstaller configInstaller = new FeatureConfigInstaller(configurationAdmin);
         String overrides = getString("overrides", new File(System.getProperty("karaf.etc"), "overrides.properties").toURI().toString());
-        String featureResolutionRange = getString("featureResolutionRange", FeaturesServiceImpl.DEFAULT_FEATURE_RESOLUTION_RANGE);
-        String bundleUpdateRange = getString("bundleUpdateRange", FeaturesServiceImpl.DEFAULT_BUNDLE_UPDATE_RANGE);
-        String updateSnapshots = getString("updateSnapshots", FeaturesServiceImpl.DEFAULT_UPDATE_SNAPSHOTS);
+        String featureResolutionRange = getString("featureResolutionRange", FeaturesService.DEFAULT_FEATURE_RESOLUTION_RANGE);
+        String bundleUpdateRange = getString("bundleUpdateRange", FeaturesService.DEFAULT_BUNDLE_UPDATE_RANGE);
+        String updateSnapshots = getString("updateSnapshots", FeaturesService.DEFAULT_UPDATE_SNAPSHOTS);
         StateStorage stateStorage = new StateStorage() {
             @Override
             protected InputStream getInputStream() throws IOException {

http://git-wip-us.apache.org/repos/asf/karaf/blob/9e2ada3c/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
new file mode 100644
index 0000000..e26a07a
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
@@ -0,0 +1,1112 @@
+/*
+ * 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.karaf.features.internal.service;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+import org.apache.felix.utils.version.VersionRange;
+import org.apache.karaf.features.BundleInfo;
+import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeatureEvent;
+import org.apache.karaf.features.FeaturesService;
+import org.apache.karaf.features.internal.download.StreamProvider;
+import org.apache.karaf.features.internal.region.SubsystemResolver;
+import org.apache.karaf.features.internal.util.ChecksumUtils;
+import org.apache.karaf.features.internal.util.Macro;
+import org.apache.karaf.features.internal.util.MapUtils;
+import org.apache.karaf.features.internal.util.MultiException;
+import org.apache.karaf.util.collections.CopyOnWriteArrayIdentityList;
+import org.eclipse.equinox.region.Region;
+import org.eclipse.equinox.region.RegionDigraph;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.Version;
+import org.osgi.framework.startlevel.BundleStartLevel;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Resource;
+import org.osgi.resource.Wire;
+import org.osgi.service.repository.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.felix.resolver.Util.getSymbolicName;
+import static org.apache.felix.resolver.Util.getVersion;
+import static org.apache.karaf.features.FeaturesService.ROOT_REGION;
+import static org.apache.karaf.features.internal.resolver.ResourceUtils.TYPE_SUBSYSTEM;
+import static org.apache.karaf.features.internal.resolver.ResourceUtils.getFeatureId;
+import static org.apache.karaf.features.internal.resolver.ResourceUtils.getUri;
+import static org.apache.karaf.features.FeaturesService.UPDATEABLE_URIS;
+import static org.apache.karaf.features.FeaturesService.UPDATE_SNAPSHOTS_ALWAYS;
+import static org.apache.karaf.features.FeaturesService.UPDATE_SNAPSHOTS_CRC;
+import static org.apache.karaf.features.internal.util.MapUtils.add;
+import static org.apache.karaf.features.internal.util.MapUtils.addToMapSet;
+import static org.apache.karaf.features.internal.util.MapUtils.apply;
+import static org.apache.karaf.features.internal.util.MapUtils.copy;
+import static org.apache.karaf.features.internal.util.MapUtils.diff;
+import static org.apache.karaf.features.internal.util.MapUtils.flatten;
+import static org.apache.karaf.features.internal.util.MapUtils.map;
+import static org.apache.karaf.features.internal.util.MapUtils.removeFromMapSet;
+import static org.osgi.framework.Bundle.ACTIVE;
+import static org.osgi.framework.Bundle.RESOLVED;
+import static org.osgi.framework.Bundle.STARTING;
+import static org.osgi.framework.Bundle.STOPPING;
+import static org.osgi.framework.Bundle.STOP_TRANSIENT;
+import static org.osgi.framework.Bundle.UNINSTALLED;
+import static org.osgi.framework.namespace.IdentityNamespace.CAPABILITY_TYPE_ATTRIBUTE;
+import static org.osgi.framework.namespace.IdentityNamespace.IDENTITY_NAMESPACE;
+import static org.osgi.resource.Namespace.CAPABILITY_EFFECTIVE_DIRECTIVE;
+import static org.osgi.resource.Namespace.EFFECTIVE_ACTIVE;
+
+public class Deployer {
+
+    public interface DeployCallback {
+
+        void print(String message, boolean verbose);
+
+        void saveState(State state);
+        void persistResolveRequest(DeploymentRequest request) throws IOException;
+        void installFeatureConfigs(Feature feature) throws IOException, InvalidSyntaxException;
+        void callListeners(FeatureEvent featureEvent);
+
+        Bundle installBundle(String region, String uri, InputStream is) throws BundleException;
+        void updateBundle(Bundle bundle, InputStream is) throws BundleException;
+        void uninstall(Bundle bundle) throws BundleException;
+        void startBundle(Bundle bundle) throws BundleException;
+        void stopBundle(Bundle bundle, int options) throws BundleException;
+        void setBundleStartLevel(Bundle bundle, int startLevel);
+
+        void refreshPackages(Collection<Bundle> bundles) throws InterruptedException;
+        void resolveBundles(Set<Bundle> bundles);
+
+        void replaceDigraph(Map<String, Map<String,Map<String,Set<String>>>> policies, Map<String, Set<Long>> bundles) throws BundleException, InvalidSyntaxException;
+    }
+
+    static class DeploymentState {
+        State state;
+        Bundle serviceBundle;
+        int initialBundleStartLevel;
+        int currentStartLevel;
+        Map<Long, Bundle> bundles;
+        Map<String, Feature> features;
+        Map<String, Set<Long>> bundlesPerRegion;
+        Map<String, Map<String, Map<String, Set<String>>>> filtersPerRegion;
+    }
+
+    static class DeploymentRequest {
+        String overrides;
+        String featureResolutionRange;
+        String bundleUpdateRange;
+        String updateSnaphots;
+        Repository globalRepository;
+
+        Map<String, Set<String>> requestedFeatures;
+        Map<String, Map<String, FeaturesService.RequestedState>> stateChanges;
+        EnumSet<FeaturesService.Option> options;
+    }
+
+    static class Deployment {
+        Map<Long, Long> bundleChecksums = new HashMap<>();
+        Map<Resource, Bundle> resToBnd = new HashMap<>();
+        Map<String, RegionDeployment> regions = new HashMap<>();
+    }
+
+    static class RegionDeployment {
+        List<Resource> toInstall = new ArrayList<>();
+        List<Bundle> toDelete = new ArrayList<>();
+        Map<Bundle, Resource> toUpdate = new HashMap<>();
+    }
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Deployer.class);
+
+    private final DeployCallback callback;
+
+    public Deployer(DeployCallback callback) {
+        this.callback = callback;
+    }
+
+    /**
+     *
+     * @param dstate  deployment state
+     * @param request deployment request
+     * @throws Exception
+     */
+    public void deploy(DeploymentState dstate, DeploymentRequest request) throws Exception {
+
+        boolean noRefreshUnmanaged = request.options.contains(FeaturesService.Option.NoAutoRefreshUnmanagedBundles);
+        boolean noRefreshManaged = request.options.contains(FeaturesService.Option.NoAutoRefreshManagedBundles);
+        boolean noRefresh = request.options.contains(FeaturesService.Option.NoAutoRefreshBundles);
+        boolean noStart = request.options.contains(FeaturesService.Option.NoAutoStartBundles);
+        boolean verbose = request.options.contains(FeaturesService.Option.Verbose);
+        boolean simulate = request.options.contains(FeaturesService.Option.Simulate);
+        boolean noManageBundles = request.options.contains(FeaturesService.Option.NoAutoManageBundles);
+
+        // TODO: add an option to unmanage bundles instead of uninstalling those
+
+        Map<String, Set<Long>> managedBundles = copy(dstate.state.managedBundles);
+
+        Map<String, Set<Bundle>> unmanagedBundles = apply(diff(dstate.bundlesPerRegion, dstate.state.managedBundles),
+                map(dstate.bundles));
+
+        // Resolve
+        // TODO: requirements
+        // TODO: bundles
+
+        SubsystemResolver resolver = new SubsystemResolver();
+        resolver.resolve(
+                dstate.features.values(),
+                request.requestedFeatures,
+                apply(unmanagedBundles, adapt(BundleRevision.class)),
+                Overrides.loadOverrides(request.overrides),
+                request.featureResolutionRange,
+                request.globalRepository);
+
+        Map<String, StreamProvider> providers = resolver.getProviders();
+        Map<String, Set<Resource>> featuresPerRegion = resolver.getFeaturesPerRegions();
+        Map<String, Set<String>> installedFeatures = apply(featuresPerRegion, featureId());
+        Map<String, Set<String>> newFeatures = diff(installedFeatures, dstate.state.installedFeatures);
+        Map<String, Set<String>> delFeatures = diff(dstate.state.installedFeatures, installedFeatures);
+
+        //
+        // Compute requested features state
+        //
+        Map<String, Map<String, String>> stateFeatures = copy(dstate.state.stateFeatures);
+        for (Map.Entry<String, Set<String>> entry : delFeatures.entrySet()) {
+            Map<String, String> map = stateFeatures.get(entry.getKey());
+            if (map != null) {
+                map.entrySet().removeAll(entry.getValue());
+                if (map.isEmpty()) {
+                    stateFeatures.remove(entry.getKey());
+                }
+            }
+        }
+        for (Map.Entry<String, Map<String, FeaturesService.RequestedState>> entry1 : request.stateChanges.entrySet()) {
+            String region = entry1.getKey();
+            Map<String, String> regionStates = stateFeatures.get(region);
+            if (regionStates != null) {
+                for (Map.Entry<String, FeaturesService.RequestedState> entry2 : entry1.getValue().entrySet()) {
+                    String feature = entry2.getKey();
+                    if (regionStates.containsKey(feature)) {
+                        regionStates.put(feature, entry2.getValue().name());
+                    }
+                }
+            }
+        }
+        for (Map.Entry<String, Set<String>> entry : newFeatures.entrySet()) {
+            for (String feature : entry.getValue()) {
+                Map<String, String> map = stateFeatures.get(entry.getKey());
+                if (map == null) {
+                    map = new HashMap<>();
+                    stateFeatures.put(entry.getKey(), map);
+                }
+                map.put(feature, noStart ? FeaturesService.RequestedState.Installed.name() : FeaturesService.RequestedState.Started.name());
+            }
+        }
+
+        // Compute information for each bundle
+        Map<String, Map<String, BundleInfo>> bundleInfos = resolver.getBundleInfos();
+
+        //
+        // Compute deployment
+        //
+        Deployer.Deployment deployment = computeDeployment(dstate, request, resolver);
+
+        //
+        // Compute the set of bundles to refresh
+        //
+        Set<Bundle> toRefresh = new TreeSet<>(new BundleComparator()); // sort is only used for display
+        for (Deployer.RegionDeployment regionDeployment : deployment.regions.values()) {
+            toRefresh.addAll(regionDeployment.toDelete);
+            toRefresh.addAll(regionDeployment.toUpdate.keySet());
+        }
+        if (!noRefreshManaged) {
+            computeBundlesToRefresh(toRefresh, dstate.bundles.values(), deployment.resToBnd, resolver.getWiring());
+        }
+        if (noRefreshUnmanaged) {
+            toRefresh.removeAll(flatten(unmanagedBundles));
+        }
+
+        // Automatically turn unmanaged bundles into managed bundles
+        // if they are required by a feature and no other unmanaged
+        // bundles have a requirement on it
+        Set<Bundle> toManage = new TreeSet<>(new BundleComparator()); // sort is only used for display
+        if (!noManageBundles) {
+            Set<Resource> features = resolver.getFeatures().keySet();
+            Set<? extends Resource> unmanaged = apply(flatten(unmanagedBundles), adapt(BundleRevision.class));
+            Set<Resource> requested = new HashSet<>();
+            // Gather bundles required by a feature
+            for (List<Wire> wires : resolver.getWiring().values()) {
+                for (Wire wire : wires) {
+                    if (features.contains(wire.getRequirer()) && unmanaged.contains(wire.getProvider())) {
+                        requested.add(wire.getProvider());
+                    }
+                }
+            }
+            // Now, we know which bundles are completely unmanaged
+            unmanaged.removeAll(requested);
+            // Check if bundles have wires from really unmanaged bundles
+            for (List<Wire> wires : resolver.getWiring().values()) {
+                for (Wire wire : wires) {
+                    if (requested.contains(wire.getProvider()) && unmanaged.contains(wire.getRequirer())) {
+                        requested.remove(wire.getProvider());
+                    }
+                }
+            }
+            if (!requested.isEmpty()) {
+                Map<Long, String> bundleToRegion = new HashMap<>();
+                for (Map.Entry<String, Set<Long>> entry : dstate.bundlesPerRegion.entrySet()) {
+                    for (long id : entry.getValue()) {
+                        bundleToRegion.put(id, entry.getKey());
+                    }
+                }
+                for (Resource rev : requested) {
+                    Bundle bundle = ((BundleRevision) rev).getBundle();
+                    long id = bundle.getBundleId();
+                    addToMapSet(managedBundles, bundleToRegion.get(id), id);
+                    toManage.add(bundle);
+                }
+            }
+        }
+
+        //
+        // Log deployment
+        //
+        logDeployment(deployment, verbose);
+
+        if (simulate) {
+            if (!noRefresh && !toRefresh.isEmpty()) {
+                print("  Bundles to refresh:", verbose);
+                for (Bundle bundle : toRefresh) {
+                    print("    " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                }
+            }
+            if (!toManage.isEmpty()) {
+                print("  Managing bundle:", verbose);
+                for (Bundle bundle : toManage) {
+                    print("    " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                }
+            }
+            return;
+        }
+
+        Set<Bundle> toStart = new HashSet<>();
+        Set<Bundle> toResolve = new HashSet<>();
+        Set<Bundle> toStop = new HashSet<>();
+
+        //
+        // Execute deployment
+        //
+        // #1: stop bundles that needs to be updated or uninstalled in order
+        // #2: uninstall needed bundles
+        // #3: update regions
+        // #4: update bundles
+        // #5: install bundles
+        // #6: save state
+        // #7: install configuration
+        // #8: refresh bundles
+        // #9: start bundles in order
+        // #10: send events
+        //
+
+        //
+        // Compute bundle states
+        //
+        Map<Resource, FeaturesService.RequestedState> states = new HashMap<>();
+        for (Map.Entry<String, Set<Resource>> entry : resolver.getFeaturesPerRegions().entrySet()) {
+            String region = entry.getKey();
+            Map<String, String> fss = stateFeatures.get(region);
+            for (Resource feature : entry.getValue()) {
+                String fs = fss.get(getFeatureId(feature));
+                propagateState(states, feature, FeaturesService.RequestedState.valueOf(fs), resolver);
+            }
+        }
+        states.keySet().retainAll(resolver.getBundles().keySet());
+        //
+        // Compute bundles to start, stop and resolve
+        //
+        for (Map.Entry<Resource, FeaturesService.RequestedState> entry : states.entrySet()) {
+            Bundle bundle = deployment.resToBnd.get(entry.getKey());
+            if (bundle != null) {
+                switch (entry.getValue()) {
+                case Started:
+                    toResolve.add(bundle);
+                    toStart.add(bundle);
+                    break;
+                case Resolved:
+                    toResolve.add(bundle);
+                    toStop.add(bundle);
+                    break;
+                }
+            }
+        }
+        //
+        // Compute bundle all start levels and start levels to update
+        //
+        Map<Resource, Integer> startLevels = new HashMap<>();
+        Map<Bundle, Integer> toUpdateStartLevel = new HashMap<>();
+        for (Map.Entry<String, Set<Resource>> entry : resolver.getBundlesPerRegions().entrySet()) {
+            String region = entry.getKey();
+            for (Resource resource : entry.getValue()) {
+                BundleInfo bi = bundleInfos.get(region).get(getUri(resource));
+                if (bi != null) {
+                    int sl = bi.getStartLevel() > 0 ? bi.getStartLevel() : dstate.initialBundleStartLevel;
+                    startLevels.put(resource, sl);
+                    Bundle bundle = deployment.resToBnd.get(resource);
+                    if (bundle != null) {
+                        int curSl = bundle.adapt(BundleStartLevel.class).getStartLevel();
+                        if (sl != curSl) {
+                            toUpdateStartLevel.put(bundle, sl);
+                            if (sl > dstate.currentStartLevel) {
+                                toStop.add(bundle);
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+
+        //
+        // Handle updates on the FeaturesService bundle
+        //
+        Deployer.RegionDeployment rootRegionDeployment = deployment.regions.get(ROOT_REGION);
+        // We don't support uninstalling the bundle
+        if (rootRegionDeployment != null && rootRegionDeployment.toDelete.contains(dstate.serviceBundle)) {
+            throw new UnsupportedOperationException("Uninstalling the FeaturesService bundle is not supported");
+        }
+        // If the bundle needs to be updated, do the following:
+        //  - persist the request to indicate the resolution must be continued after restart
+        //  - update the checksum and save the state
+        //  - compute bundles wired to the FeaturesService bundle that will be refreshed
+        //  - stop the bundle
+        //  - update the bundle
+        //  - refresh wired bundles
+        //  - start the bundle
+        //  - exit
+        // When restarting, the resolution will be attempted again
+        if (rootRegionDeployment != null && rootRegionDeployment.toUpdate.containsKey(dstate.serviceBundle)) {
+            callback.persistResolveRequest(request);
+            // If the bundle is updated because of a different checksum,
+            // save the new checksum persistently
+            if (deployment.bundleChecksums.containsKey(dstate.serviceBundle.getBundleId())) {
+                State state = dstate.state.copy();
+                state.bundleChecksums.put(dstate.serviceBundle.getBundleId(),
+                                          deployment.bundleChecksums.get(dstate.serviceBundle.getBundleId()));
+                callback.saveState(state);
+            }
+            Resource resource = rootRegionDeployment.toUpdate.get(dstate.serviceBundle);
+            String uri = getUri(resource);
+            print("The FeaturesService bundle needs is being updated with " + uri, verbose);
+            toRefresh.clear();
+            toRefresh.add(dstate.serviceBundle);
+            computeBundlesToRefresh(toRefresh,
+                    dstate.bundles.values(),
+                    Collections.<Resource, Bundle>emptyMap(),
+                    Collections.<Resource, List<Wire>>emptyMap());
+            callback.stopBundle(dstate.serviceBundle, STOP_TRANSIENT);
+            try (
+                    InputStream is = getBundleInputStream(resource, providers)
+            ) {
+                callback.updateBundle(dstate.serviceBundle, is);
+            }
+            callback.refreshPackages(toRefresh);
+            callback.startBundle(dstate.serviceBundle);
+            return;
+        }
+
+        //
+        // Perform bundle operations
+        //
+
+        //
+        // Stop bundles by chunks
+        //
+        for (Deployer.RegionDeployment regionDeployment : deployment.regions.values()) {
+            toStop.addAll(regionDeployment.toUpdate.keySet());
+            toStop.addAll(regionDeployment.toDelete);
+        }
+        removeFragmentsAndBundlesInState(toStop, UNINSTALLED | RESOLVED | STOPPING);
+        if (!toStop.isEmpty()) {
+            print("Stopping bundles:", verbose);
+            while (!toStop.isEmpty()) {
+                List<Bundle> bs = getBundlesToStop(toStop);
+                for (Bundle bundle : bs) {
+                    print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                    // If the bundle start level will be changed, stop it persistently to
+                    // avoid a restart when the start level is actually changed
+                    callback.stopBundle(bundle, toUpdateStartLevel.containsKey(bundle) ? 0 : STOP_TRANSIENT);
+                    toStop.remove(bundle);
+                }
+            }
+        }
+
+        //
+        // Delete bundles
+        //
+        boolean hasToDelete = false;
+        for (Deployer.RegionDeployment regionDeployment : deployment.regions.values()) {
+            if (hasToDelete = !regionDeployment.toDelete.isEmpty()) {
+                break;
+            }
+        }
+        if (hasToDelete) {
+            print("Uninstalling bundles:", verbose);
+            for (Map.Entry<String, Deployer.RegionDeployment> entry : deployment.regions.entrySet()) {
+                String name = entry.getKey();
+                Deployer.RegionDeployment regionDeployment = entry.getValue();
+                for (Bundle bundle : regionDeployment.toDelete) {
+                    print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                    callback.uninstall(bundle);
+                    removeFromMapSet(managedBundles, name, bundle.getBundleId());
+                }
+            }
+        }
+
+        //
+        // Update regions
+        //
+        {
+            // Add bundles
+            Map<String, Set<Long>> bundles = new HashMap<>();
+            add(bundles, apply(unmanagedBundles, bundleId()));
+            add(bundles, managedBundles);
+            // Compute policies
+            RegionDigraph computedDigraph = resolver.getFlatDigraph();
+            Map<String, Map<String, Map<String, Set<String>>>> policies = copy(dstate.filtersPerRegion);
+            // Only keep regions which still have bundles
+            policies.keySet().retainAll(bundles.keySet());
+            // Fix broken filters
+            for (String name : policies.keySet()) {
+                policies.get(name).keySet().retainAll(policies.keySet());
+            }
+            // Update managed regions
+            for (Region computedRegion : computedDigraph.getRegions()) {
+                String name = computedRegion.getName();
+                Map<String, Map<String, Set<String>>> policy = policies.get(name);
+                if (policy == null) {
+                    policy = new HashMap<>();
+                    policies.put(name, policy);
+                }
+                for (RegionDigraph.FilteredRegion fr : computedRegion.getEdges()) {
+                    String r2 = fr.getRegion().getName();
+                    Map<String, Set<String>> filters = new HashMap<>();
+                    Map<String, Collection<String>> current = fr.getFilter().getSharingPolicy();
+                    for (String ns : current.keySet()) {
+                        for (String f : current.get(ns)) {
+                            addToMapSet(filters, ns, f);
+                        }
+                    }
+                    policy.put(r2, filters);
+                }
+            }
+            // Apply all changes
+            callback.replaceDigraph(policies, bundles);
+        }
+
+
+        //
+        // Update bundles
+        //
+        boolean hasToUpdate = false;
+        for (Deployer.RegionDeployment regionDeployment : deployment.regions.values()) {
+            if (hasToUpdate = !regionDeployment.toUpdate.isEmpty()) {
+                break;
+            }
+        }
+        if (hasToUpdate) {
+            print("Updating bundles:", verbose);
+            for (Map.Entry<String, Deployer.RegionDeployment> rde : deployment.regions.entrySet()) {
+                for (Map.Entry<Bundle, Resource> entry : rde.getValue().toUpdate.entrySet()) {
+                    Bundle bundle = entry.getKey();
+                    Resource resource = entry.getValue();
+                    String uri = getUri(resource);
+                    print("  " + uri, verbose);
+                    try (
+                            InputStream is = getBundleInputStream(resource, providers)
+                    ) {
+                        callback.updateBundle(bundle, is);
+                    }
+                    toStart.add(bundle);
+                }
+            }
+        }
+        //
+        // Update start levels
+        //
+        for (Map.Entry<Bundle, Integer> entry : toUpdateStartLevel.entrySet()) {
+            Bundle bundle = entry.getKey();
+            int sl = entry.getValue();
+            callback.setBundleStartLevel(bundle, sl);
+        }
+        //
+        // Install bundles
+        //
+        boolean hasToInstall = false;
+        for (Deployer.RegionDeployment regionDeployment : deployment.regions.values()) {
+            if (hasToInstall = !regionDeployment.toInstall.isEmpty()) {
+                break;
+            }
+        }
+        if (hasToInstall) {
+            print("Installing bundles:", verbose);
+            for (Map.Entry<String, Deployer.RegionDeployment> entry : deployment.regions.entrySet()) {
+                String name = entry.getKey();
+                Deployer.RegionDeployment regionDeployment = entry.getValue();
+                for (Resource resource : regionDeployment.toInstall) {
+                    String uri = getUri(resource);
+                    print("  " + uri, verbose);
+                    Bundle bundle;
+                    long crc;
+                    try (
+                            ChecksumUtils.CRCInputStream is = new ChecksumUtils.CRCInputStream(getBundleInputStream(resource, providers))
+                    ) {
+                        bundle = callback.installBundle(name, uri, is);
+                        crc = is.getCRC();
+                    }
+                    addToMapSet(managedBundles, name, bundle.getBundleId());
+                    deployment.resToBnd.put(resource, bundle);
+                    // save a checksum of installed snapshot bundle
+                    if (UPDATE_SNAPSHOTS_CRC.equals(request.updateSnaphots)
+                            && isUpdateable(resource) && !deployment.bundleChecksums.containsKey(bundle.getBundleId())) {
+                        deployment.bundleChecksums.put(bundle.getBundleId(), crc);
+                    }
+                    int startLevel = startLevels.get(resource);
+                    callback.setBundleStartLevel(bundle, startLevel);
+                    FeaturesService.RequestedState reqState = states.get(resource);
+                    switch (reqState) {
+                    case Started:
+                        toResolve.add(bundle);
+                        toStart.add(bundle);
+                        break;
+                    case Resolved:
+                        toResolve.add(bundle);
+                        break;
+                    }
+                }
+            }
+        }
+
+        //
+        // Update and save state
+        //
+        State newState = new State();
+        newState.bundleChecksums.putAll(deployment.bundleChecksums);
+        newState.requestedFeatures.putAll(request.requestedFeatures);
+        newState.installedFeatures.putAll(installedFeatures);
+        newState.stateFeatures.putAll(stateFeatures);
+        newState.managedBundles.putAll(managedBundles);
+        callback.saveState(newState);
+
+        //
+        // Install configurations
+        //
+        if (!newFeatures.isEmpty()) {
+            Set<Feature> set = apply(flatten(newFeatures), map(dstate.features));
+            for (Feature feature : set) {
+                callback.installFeatureConfigs(feature);
+            }
+        }
+
+        // TODO: remove this hack, but it avoids loading the class after the bundle is refreshed
+        new CopyOnWriteArrayIdentityList().iterator();
+        RequirementSort.sort(Collections.<Resource>emptyList());
+
+        if (!noRefresh) {
+            toStop = new HashSet<>();
+            toStop.addAll(toRefresh);
+            removeFragmentsAndBundlesInState(toStop, UNINSTALLED | RESOLVED | STOPPING);
+            if (!toStop.isEmpty()) {
+                print("Stopping bundles:", verbose);
+                while (!toStop.isEmpty()) {
+                    List<Bundle> bs = getBundlesToStop(toStop);
+                    for (Bundle bundle : bs) {
+                        print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                        callback.stopBundle(bundle, STOP_TRANSIENT);
+                        toStop.remove(bundle);
+                        toStart.add(bundle);
+                    }
+                }
+            }
+
+            if (!toRefresh.isEmpty()) {
+                print("Refreshing bundles:", verbose);
+                for (Bundle bundle : toRefresh) {
+                    print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                }
+                if (!toRefresh.isEmpty()) {
+                    callback.refreshPackages(toRefresh);
+                }
+            }
+        }
+
+        // Resolve bundles
+        toResolve.addAll(toStart);
+        toResolve.addAll(toRefresh);
+        removeFragmentsAndBundlesInState(toResolve, UNINSTALLED);
+        callback.resolveBundles(toResolve);
+
+        // Compute bundles to start
+        removeFragmentsAndBundlesInState(toStart, UNINSTALLED | ACTIVE | STARTING);
+        if (!toStart.isEmpty()) {
+            // Compute correct start order
+            List<Exception> exceptions = new ArrayList<>();
+            print("Starting bundles:", verbose);
+            while (!toStart.isEmpty()) {
+                List<Bundle> bs = getBundlesToStart(toStart, dstate.serviceBundle);
+                for (Bundle bundle : bs) {
+                    print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                    try {
+                        callback.startBundle(bundle);
+                    } catch (BundleException e) {
+                        exceptions.add(e);
+                    }
+                    toStart.remove(bundle);
+                }
+            }
+            if (!exceptions.isEmpty()) {
+                throw new MultiException("Error restarting bundles", exceptions);
+            }
+        }
+
+        // Call listeners
+        // TODO: add region information and avoid flattening
+        for (Feature feature : apply(flatten(delFeatures), map(dstate.features))) {
+            callback.callListeners(new FeatureEvent(feature, FeatureEvent.EventType.FeatureUninstalled, false));
+        }
+        for (Feature feature : apply(flatten(newFeatures), map(dstate.features))) {
+            callback.callListeners(new FeatureEvent(feature, FeatureEvent.EventType.FeatureInstalled, false));
+        }
+
+        print("Done.", verbose);
+    }
+
+    private void propagateState(Map<Resource, FeaturesService.RequestedState> states, Resource resource, FeaturesService.RequestedState state, SubsystemResolver resolver) {
+        if (!isSubsystem(resource)) {
+            FeaturesService.RequestedState reqState = mergeStates(state, states.get(resource));
+            if (reqState != states.get(resource)) {
+                states.put(resource, reqState);
+                for (Wire wire : resolver.getWiring().get(resource)) {
+                    Resource provider = wire.getProvider();
+                    FeaturesService.RequestedState stateToMerge;
+                    String region = resolver.getBundles().get(provider);
+                    BundleInfo bi = region != null ? resolver.getBundleInfos().get(region).get(getUri(provider)) : null;
+                    if (reqState == FeaturesService.RequestedState.Started) {
+                        String effective = wire.getCapability().getDirectives().get(CAPABILITY_EFFECTIVE_DIRECTIVE);
+                        // If there is an active effective capability or a requirement from the feature
+                        // and if the bundle is flagged as to start, start it
+                        if ((EFFECTIVE_ACTIVE.equals(effective) || IDENTITY_NAMESPACE.equals(wire.getCapability().getNamespace()))
+                                && (bi == null || bi.isStart())) {
+                            stateToMerge = FeaturesService.RequestedState.Started;
+                        } else {
+                            stateToMerge = FeaturesService.RequestedState.Resolved;
+                        }
+                    } else {
+                        stateToMerge = reqState;
+                    }
+                    propagateState(states, provider, stateToMerge, resolver);
+                }
+            }
+        }
+    }
+
+    private boolean isSubsystem(Resource resource) {
+        for (Capability cap : resource.getCapabilities(null)) {
+            if (cap.getNamespace().equals(IDENTITY_NAMESPACE)) {
+                String type = (String) cap.getAttributes().get(CAPABILITY_TYPE_ATTRIBUTE);
+                return (type != null) && type.equals(TYPE_SUBSYSTEM);
+            }
+        }
+        return false;
+    }
+
+    private FeaturesService.RequestedState mergeStates(FeaturesService.RequestedState s1, FeaturesService.RequestedState s2) {
+        if (s1 == FeaturesService.RequestedState.Started || s2 == FeaturesService.RequestedState.Started) {
+            return FeaturesService.RequestedState.Started;
+        }
+        if (s1 == FeaturesService.RequestedState.Resolved || s2 == FeaturesService.RequestedState.Resolved) {
+            return FeaturesService.RequestedState.Resolved;
+        }
+        return FeaturesService.RequestedState.Installed;
+    }
+
+    private void computeBundlesToRefresh(Set<Bundle> toRefresh, Collection<Bundle> bundles, Map<Resource, Bundle> resources, Map<Resource, List<Wire>> resolution) {
+        int size;
+        do {
+            size = toRefresh.size();
+            for (Bundle bundle : bundles) {
+                // Continue if we already know about this bundle
+                if (toRefresh.contains(bundle)) {
+                    continue;
+                }
+                // Ignore non resolved bundle
+                BundleWiring wiring = bundle.adapt(BundleWiring.class);
+                if (wiring == null) {
+                    continue;
+                }
+                // Get through the old resolution and flag this bundle
+                // if it was wired to a bundle to be refreshed
+                for (BundleWire wire : wiring.getRequiredWires(null)) {
+                    if (toRefresh.contains(wire.getProvider().getBundle())) {
+                        toRefresh.add(bundle);
+                        break;
+                    }
+                }
+                // Get through the new resolution and flag this bundle
+                // if it's wired to any new bundle
+                List<Wire> newWires = resolution.get(wiring.getRevision());
+                if (newWires != null) {
+                    for (Wire wire : newWires) {
+                        Bundle b;
+                        if (wire.getProvider() instanceof BundleRevision) {
+                            b = ((BundleRevision) wire.getProvider()).getBundle();
+                        } else {
+                            b = resources.get(wire.getProvider());
+                        }
+                        if (b == null || toRefresh.contains(b)) {
+                            toRefresh.add(bundle);
+                            break;
+                        }
+                    }
+                }
+            }
+        } while (toRefresh.size() > size);
+    }
+
+    private void print(String message, boolean verbose) {
+        callback.print(message, verbose);
+    }
+
+    private void removeFragmentsAndBundlesInState(Collection<Bundle> bundles, int state) {
+        for (Iterator<Bundle> iterator = bundles.iterator(); iterator.hasNext();) {
+            Bundle bundle = iterator.next();
+            if ((bundle.getState() & state) != 0
+                    || bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null) {
+                iterator.remove();
+            }
+        }
+    }
+
+    protected void logDeployment(Deployer.Deployment overallDeployment, boolean verbose) {
+        if (overallDeployment.regions.isEmpty()) {
+            print("No deployment change.", verbose);
+            return;
+        }
+        print("Changes to perform:", verbose);
+        for (Map.Entry<String, Deployer.RegionDeployment> region : overallDeployment.regions.entrySet()) {
+            Deployer.RegionDeployment deployment = region.getValue();
+            print("  Region: " + region.getKey(), verbose);
+            if (!deployment.toDelete.isEmpty()) {
+                print("    Bundles to uninstall:", verbose);
+                for (Bundle bundle : deployment.toDelete) {
+                    print("      " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                }
+            }
+            if (!deployment.toUpdate.isEmpty()) {
+                print("    Bundles to update:", verbose);
+                for (Map.Entry<Bundle, Resource> entry : deployment.toUpdate.entrySet()) {
+                    print("      " + entry.getKey().getSymbolicName() + " / " + entry.getKey().getVersion() + " with " + getUri(entry.getValue()), verbose);
+                }
+            }
+            if (!deployment.toInstall.isEmpty()) {
+                print("    Bundles to install:", verbose);
+                for (Resource resource : deployment.toInstall) {
+                    print("      " + getUri(resource), verbose);
+                }
+            }
+        }
+    }
+
+    protected Deployment computeDeployment(
+                    DeploymentState dstate,
+                    DeploymentRequest request,
+                    SubsystemResolver resolver) throws IOException {
+
+        Deployment result = new Deployment();
+
+        Map<String, Set<Resource>> bundlesPerRegions = resolver.getBundlesPerRegions();
+
+        // Gather all regions, including old ones and new ones
+        Set<String> regions = new HashSet<>();
+        regions.addAll(dstate.state.managedBundles.keySet());
+        regions.addAll(bundlesPerRegions.keySet());
+
+        for (String region : regions) {
+
+            Deployer.RegionDeployment deployment = new Deployer.RegionDeployment();
+
+            // Get the list of bundles currently assigned in the region
+            Set<Long> managed = dstate.state.managedBundles.get(region);
+            if (managed == null) {
+                managed = Collections.emptySet();
+            }
+
+            // Compute the list of resources to deploy in the region
+            Set<Resource> bundlesInRegion = bundlesPerRegions.get(region);
+            List<Resource> toDeploy = bundlesInRegion != null
+                    ? new ArrayList<>(bundlesInRegion) : new ArrayList<Resource>();
+
+            // First pass: go through all installed bundles and mark them
+            // as either to ignore or delete
+            for (long bundleId : managed) {
+                // Look for the installed bundle
+                Bundle bundle = dstate.bundles.get(bundleId);
+                // Bundle has been manually uninstalled ?
+                if (bundle != null) {
+                    // Look for a matching resource
+                    Resource resource = null;
+                    for (Resource res : toDeploy) {
+                        if (bundle.getSymbolicName().equals(getSymbolicName(res))
+                                && bundle.getVersion().equals(getVersion(res))) {
+                            resource = res;
+                            break;
+                        }
+                    }
+                    // We found a matching bundle
+                    if (resource != null) {
+                        // In case of snapshots, check if the snapshot is out of date
+                        // and flag it as to update
+                        if (isUpdateable(resource)) {
+                            // Always update snapshots
+                            if (UPDATE_SNAPSHOTS_ALWAYS.equalsIgnoreCase(request.updateSnaphots)) {
+                                LOGGER.debug("Update snapshot for " + bundle.getLocation());
+                                deployment.toUpdate.put(bundle, resource);
+                            } else if (UPDATE_SNAPSHOTS_CRC.equalsIgnoreCase(request.updateSnaphots)) {
+                                // if the checksum are different
+                                try (
+                                        InputStream is = getBundleInputStream(resource, resolver.getProviders())
+                                ) {
+                                    long newCrc = ChecksumUtils.checksum(is);
+                                    long oldCrc = dstate.state.bundleChecksums.containsKey(bundle.getBundleId())
+                                                    ? dstate.state.bundleChecksums.get(bundle.getBundleId()) : 0L;
+                                    if (newCrc != oldCrc) {
+                                        LOGGER.debug("New snapshot available for " + bundle.getLocation());
+                                        deployment.toUpdate.put(bundle, resource);
+                                    }
+                                    result.bundleChecksums.put(bundle.getBundleId(), newCrc);
+                                }
+                            }
+                        }
+                        // We're done for this resource
+                        toDeploy.remove(resource);
+                        result.resToBnd.put(resource, bundle);
+                        // There's no matching resource
+                        // If the bundle is managed, we need to delete it
+                    } else if (managed.contains(bundle.getBundleId())) {
+                        deployment.toDelete.add(bundle);
+                    }
+                }
+            }
+
+            // Second pass on remaining resources
+            for (Resource resource : toDeploy) {
+                TreeMap<Version, Bundle> matching = new TreeMap<>();
+                VersionRange range = new VersionRange(Macro.transform(request.bundleUpdateRange, getVersion(resource).toString()));
+                for (Bundle bundle : deployment.toDelete) {
+                    if (bundle.getSymbolicName().equals(getSymbolicName(resource)) && range.contains(bundle.getVersion())) {
+                        matching.put(bundle.getVersion(), bundle);
+                    }
+                }
+                if (!matching.isEmpty()) {
+                    Bundle bundle = matching.lastEntry().getValue();
+                    deployment.toUpdate.put(bundle, resource);
+                    deployment.toDelete.remove(bundle);
+                    result.resToBnd.put(resource, bundle);
+                } else {
+                    deployment.toInstall.add(resource);
+                }
+            }
+            Collections.sort(deployment.toInstall, new ResourceComparator());
+
+            // Add this region if there is something to do
+            if (!deployment.toDelete.isEmpty()
+                    || !deployment.toUpdate.isEmpty()
+                    || !deployment.toInstall.isEmpty()) {
+                result.regions.put(region, deployment);
+            }
+        }
+
+        return result;
+    }
+
+    protected <T> MapUtils.Function<Bundle, T> adapt(final Class<T> clazz) {
+        return new MapUtils.Function<Bundle, T>() {
+            @Override
+            public T apply(Bundle bundle) {
+                return bundle.adapt(clazz);
+            }
+        };
+    }
+
+    protected MapUtils.Function<Bundle, Long> bundleId() {
+        return new MapUtils.Function<Bundle, Long>() {
+            @Override
+            public Long apply(Bundle bundle) {
+                return bundle.getBundleId();
+            }
+        };
+    }
+
+    protected MapUtils.Function<Resource, String> featureId() {
+        return new MapUtils.Function<Resource, String>() {
+            @Override
+            public String apply(Resource resource) {
+                return getFeatureId(resource);
+            }
+        };
+    }
+
+    protected boolean isUpdateable(Resource resource) {
+        String uri = getUri(resource);
+        return uri.matches(UPDATEABLE_URIS);
+    }
+
+    protected List<Bundle> getBundlesToStart(Collection<Bundle> bundles, Bundle serviceBundle) {
+        // Restart the features service last, regardless of any other consideration
+        // so that we don't end up with the service trying to do stuff before we're done
+        boolean restart = false;
+
+        SortedMap<Integer, Set<Bundle>> bundlesPerStartLevel = new TreeMap<>();
+        for (Bundle bundle : bundles) {
+            if (bundle == serviceBundle) {
+                restart = true;
+            } else {
+                int sl = bundle.adapt(BundleStartLevel.class).getStartLevel();
+                addToMapSet(bundlesPerStartLevel, sl, bundle);
+            }
+        }
+        if (bundlesPerStartLevel.isEmpty()) {
+            bundles = Collections.emptyList();
+        } else {
+            bundles = bundlesPerStartLevel.remove(bundlesPerStartLevel.firstKey());
+        }
+
+        // We hit FELIX-2949 if we don't use the correct order as Felix resolver isn't greedy.
+        // In order to minimize that, we make sure we resolve the bundles in the order they
+        // are given back by the resolution, meaning that all root bundles (i.e. those that were
+        // not flagged as dependencies in features) are started before the others.   This should
+        // make sure those important bundles are started first and minimize the problem.
+        List<BundleRevision> revs = new ArrayList<>();
+        for (Bundle bundle : bundles) {
+            revs.add(bundle.adapt(BundleRevision.class));
+        }
+        List<Bundle> sorted = new ArrayList<>();
+        for (BundleRevision rev : RequirementSort.sort(revs)) {
+            sorted.add(rev.getBundle());
+        }
+        if (sorted.isEmpty() && restart) {
+            sorted.add(serviceBundle);
+        }
+        return sorted;
+    }
+
+    protected List<Bundle> getBundlesToStop(Collection<Bundle> bundles) {
+        SortedMap<Integer, Set<Bundle>> bundlesPerStartLevel = new TreeMap<>();
+        for (Bundle bundle : bundles) {
+            int sl = bundle.adapt(BundleStartLevel.class).getStartLevel();
+            addToMapSet(bundlesPerStartLevel, sl, bundle);
+        }
+        bundles = bundlesPerStartLevel.get(bundlesPerStartLevel.lastKey());
+
+        List<Bundle> bundlesToDestroy = new ArrayList<>();
+        for (Bundle bundle : bundles) {
+            ServiceReference[] references = bundle.getRegisteredServices();
+            int usage = 0;
+            if (references != null) {
+                for (ServiceReference reference : references) {
+                    usage += getServiceUsage(reference, bundles);
+                }
+            }
+            LOGGER.debug("Usage for bundle {} is {}", bundle, usage);
+            if (usage == 0) {
+                bundlesToDestroy.add(bundle);
+            }
+        }
+        if (!bundlesToDestroy.isEmpty()) {
+            Collections.sort(bundlesToDestroy, new Comparator<Bundle>() {
+                public int compare(Bundle b1, Bundle b2) {
+                    return (int) (b2.getLastModified() - b1.getLastModified());
+                }
+            });
+            LOGGER.debug("Selected bundles {} for destroy (no services in use)", bundlesToDestroy);
+        } else {
+            ServiceReference ref = null;
+            for (Bundle bundle : bundles) {
+                ServiceReference[] references = bundle.getRegisteredServices();
+                for (ServiceReference reference : references) {
+                    if (getServiceUsage(reference, bundles) == 0) {
+                        continue;
+                    }
+                    if (ref == null || reference.compareTo(ref) < 0) {
+                        LOGGER.debug("Currently selecting bundle {} for destroy (with reference {})", bundle, reference);
+                        ref = reference;
+                    }
+                }
+            }
+            if (ref != null) {
+                bundlesToDestroy.add(ref.getBundle());
+            }
+            LOGGER.debug("Selected bundle {} for destroy (lowest ranking service)", bundlesToDestroy);
+        }
+        return bundlesToDestroy;
+    }
+
+    private static int getServiceUsage(ServiceReference ref, Collection<Bundle> bundles) {
+        Bundle[] usingBundles = ref.getUsingBundles();
+        int nb = 0;
+        if (usingBundles != null) {
+            for (Bundle bundle : usingBundles) {
+                if (bundles.contains(bundle)) {
+                    nb++;
+                }
+            }
+        }
+        return nb;
+    }
+
+    protected InputStream getBundleInputStream(Resource resource, Map<String, StreamProvider> providers) throws IOException {
+        String uri = getUri(resource);
+        if (uri == null) {
+            throw new IllegalStateException("Resource has no uri");
+        }
+        StreamProvider provider = providers.get(uri);
+        if (provider == null) {
+            throw new IllegalStateException("Resource " + uri + " has no StreamProvider");
+        }
+        return provider.open();
+    }
+
+}


[7/7] git commit: [KARAF-2888] Fix unit tests caused by the change in Feature#getId()

Posted by gn...@apache.org.
[KARAF-2888] Fix unit tests caused by the change in Feature#getId()


Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/bd916772
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/bd916772
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/bd916772

Branch: refs/heads/master
Commit: bd9167729d3c824914f3e6fecaaa34bd48437fb3
Parents: e334fae
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Wed Apr 30 20:04:17 2014 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Wed Apr 30 20:04:28 2014 +0200

----------------------------------------------------------------------
 .../src/it/test-aggregate-features/control.xml                   | 4 ++--
 .../src/it/test-check-dependencies-failure/control.xml           | 4 ++--
 .../src/it/test-check-dependencies/control.xml                   | 4 ++--
 tooling/karaf-maven-plugin/src/it/test-input-file/control.xml    | 2 +-
 4 files changed, 7 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/bd916772/tooling/karaf-maven-plugin/src/it/test-aggregate-features/control.xml
----------------------------------------------------------------------
diff --git a/tooling/karaf-maven-plugin/src/it/test-aggregate-features/control.xml b/tooling/karaf-maven-plugin/src/it/test-aggregate-features/control.xml
index 01a6721..8369f0d 100644
--- a/tooling/karaf-maven-plugin/src/it/test-aggregate-features/control.xml
+++ b/tooling/karaf-maven-plugin/src/it/test-aggregate-features/control.xml
@@ -19,11 +19,11 @@
   -->
 
 <features xmlns="http://karaf.apache.org/xmlns/features/v1.3.0" name="aggregate-features">
-    <feature description="aggregate-recursive-module-c" version="1.0-SNAPSHOT" name="aggregate-recursive-module-c">
+    <feature description="aggregate-recursive-module-c" version="1.0.0.SNAPSHOT" name="aggregate-recursive-module-c">
         <details>Test Description</details>
         <bundle>mvn:test/aggregate-recursive-module-b/1.0-SNAPSHOT</bundle>
     </feature>
-    <feature description="aggregate-recursive-module-d" version="1.0-SNAPSHOT" name="aggregate-recursive-module-d">
+    <feature description="aggregate-recursive-module-d" version="1.0.0.SNAPSHOT" name="aggregate-recursive-module-d">
         <details>Test Description</details>
         <bundle>mvn:test/aggregate-recursive-module-a/1.0-SNAPSHOT</bundle>
         <bundle>mvn:test/aggregate-recursive-module-c/1.0-SNAPSHOT</bundle>

http://git-wip-us.apache.org/repos/asf/karaf/blob/bd916772/tooling/karaf-maven-plugin/src/it/test-check-dependencies-failure/control.xml
----------------------------------------------------------------------
diff --git a/tooling/karaf-maven-plugin/src/it/test-check-dependencies-failure/control.xml b/tooling/karaf-maven-plugin/src/it/test-check-dependencies-failure/control.xml
index 223d090..ef8eba4 100644
--- a/tooling/karaf-maven-plugin/src/it/test-check-dependencies-failure/control.xml
+++ b/tooling/karaf-maven-plugin/src/it/test-check-dependencies-failure/control.xml
@@ -19,11 +19,11 @@
   -->
 
 <features xmlns="http://karaf.apache.org/xmlns/features/v1.3.0" name="check-dependencies-features">
-    <feature description="dependency-module-c" version="1.0-SNAPSHOT" name="dependency-module-c">
+    <feature description="dependency-module-c" version="1.0.0.SNAPSHOT" name="dependency-module-c">
         <bundle>mvn:test/dependency-module-a/1.0-SNAPSHOT</bundle>
         <bundle>mvn:test/dependency-module-b/1.0-SNAPSHOT</bundle>
     </feature>
-    <feature description="dependency-module-d" version="1.0-SNAPSHOT" name="dependency-module-d">
+    <feature description="dependency-module-d" version="1.0.0.SNAPSHOT" name="dependency-module-d">
         <bundle>mvn:test/dependency-module-c/1.0-SNAPSHOT</bundle>
         <bundle>mvn:test/dependency-module-a/1.0-SNAPSHOT</bundle>
         <bundle>mvn:test/dependency-module-b/1.0-SNAPSHOT</bundle>

http://git-wip-us.apache.org/repos/asf/karaf/blob/bd916772/tooling/karaf-maven-plugin/src/it/test-check-dependencies/control.xml
----------------------------------------------------------------------
diff --git a/tooling/karaf-maven-plugin/src/it/test-check-dependencies/control.xml b/tooling/karaf-maven-plugin/src/it/test-check-dependencies/control.xml
index 223d090..ef8eba4 100644
--- a/tooling/karaf-maven-plugin/src/it/test-check-dependencies/control.xml
+++ b/tooling/karaf-maven-plugin/src/it/test-check-dependencies/control.xml
@@ -19,11 +19,11 @@
   -->
 
 <features xmlns="http://karaf.apache.org/xmlns/features/v1.3.0" name="check-dependencies-features">
-    <feature description="dependency-module-c" version="1.0-SNAPSHOT" name="dependency-module-c">
+    <feature description="dependency-module-c" version="1.0.0.SNAPSHOT" name="dependency-module-c">
         <bundle>mvn:test/dependency-module-a/1.0-SNAPSHOT</bundle>
         <bundle>mvn:test/dependency-module-b/1.0-SNAPSHOT</bundle>
     </feature>
-    <feature description="dependency-module-d" version="1.0-SNAPSHOT" name="dependency-module-d">
+    <feature description="dependency-module-d" version="1.0.0.SNAPSHOT" name="dependency-module-d">
         <bundle>mvn:test/dependency-module-c/1.0-SNAPSHOT</bundle>
         <bundle>mvn:test/dependency-module-a/1.0-SNAPSHOT</bundle>
         <bundle>mvn:test/dependency-module-b/1.0-SNAPSHOT</bundle>

http://git-wip-us.apache.org/repos/asf/karaf/blob/bd916772/tooling/karaf-maven-plugin/src/it/test-input-file/control.xml
----------------------------------------------------------------------
diff --git a/tooling/karaf-maven-plugin/src/it/test-input-file/control.xml b/tooling/karaf-maven-plugin/src/it/test-input-file/control.xml
index 08e8f0e..9310cf7 100644
--- a/tooling/karaf-maven-plugin/src/it/test-input-file/control.xml
+++ b/tooling/karaf-maven-plugin/src/it/test-input-file/control.xml
@@ -19,7 +19,7 @@
   -->
 
 <features xmlns="http://karaf.apache.org/xmlns/features/v1.3.0" name="test-input-file">
-    <feature description="Test Description" version="1.0-SNAPSHOT" name="test-input-file">
+    <feature description="Test Description" version="1.0.0.SNAPSHOT" name="test-input-file">
         <details>Test Description</details>
         <bundle>mvn:test/test-input-file/1.0-SNAPSHOT</bundle>
     </feature>


[3/7] git commit: [KARAF-2805] Upgrade command archetype to the new api

Posted by gn...@apache.org.
[KARAF-2805] Upgrade command archetype to the new api


Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/3d204040
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/3d204040
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/3d204040

Branch: refs/heads/master
Commit: 3d204040f169025fece6eb6a116033407e70d534
Parents: 703c62f
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Tue Apr 29 17:26:17 2014 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Wed Apr 30 20:04:27 2014 +0200

----------------------------------------------------------------------
 .../main/resources/archetype-resources/pom.xml  | 15 ++-------
 .../src/main/java/__command__.java              | 15 +++++----
 .../resources/OSGI-INF/blueprint/shell-log.xml  | 32 --------------------
 3 files changed, 12 insertions(+), 50 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/3d204040/archetypes/command/src/main/resources/archetype-resources/pom.xml
----------------------------------------------------------------------
diff --git a/archetypes/command/src/main/resources/archetype-resources/pom.xml b/archetypes/command/src/main/resources/archetype-resources/pom.xml
index e5b2ab0..208f292 100644
--- a/archetypes/command/src/main/resources/archetype-resources/pom.xml
+++ b/archetypes/command/src/main/resources/archetype-resources/pom.xml
@@ -31,7 +31,7 @@
     <dependencies>
         <dependency>
             <groupId>org.apache.karaf.shell</groupId>
-            <artifactId>org.apache.karaf.shell.console</artifactId>
+            <artifactId>org.apache.karaf.shell.core</artifactId>
             <version>^project.version^</version>
         </dependency>
 
@@ -76,17 +76,8 @@
                 <configuration>
                     <instructions>
                         <Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
-                        <Export-Package>${package}*;version=${project.version}</Export-Package>
-                        <Import-Package>
-                            !${package}*,
-                            org.apache.aries.blueprint,
-                            org.osgi.service.blueprint.container,
-                            org.osgi.service.blueprint.reflect,
-                            org.apache.felix.service.command,
-                            org.apache.karaf.shell.commands,
-                            org.apache.karaf.shell.console,
-                            *
-                        </Import-Package>
+                        <Export-Package>${package}*;version=${project.version};-noimport:=true</Export-Package>
+                        <Karaf-Commands>${package}*</Karaf-Commands>
                     </instructions>
                 </configuration>
             </plugin>

http://git-wip-us.apache.org/repos/asf/karaf/blob/3d204040/archetypes/command/src/main/resources/archetype-resources/src/main/java/__command__.java
----------------------------------------------------------------------
diff --git a/archetypes/command/src/main/resources/archetype-resources/src/main/java/__command__.java b/archetypes/command/src/main/resources/archetype-resources/src/main/java/__command__.java
index 75ccb0a..1c6eb60 100644
--- a/archetypes/command/src/main/resources/archetype-resources/src/main/java/__command__.java
+++ b/archetypes/command/src/main/resources/archetype-resources/src/main/java/__command__.java
@@ -21,13 +21,15 @@
 
 package ${package};
 
-import org.apache.karaf.shell.console.OsgiCommandSupport;
-import org.apache.karaf.shell.commands.Command;
-import org.apache.karaf.shell.commands.Option;
-import org.apache.karaf.shell.commands.Argument;
+import org.apache.karaf.shell.api.action.Action;
+import org.apache.karaf.shell.api.action.Argument;
+import org.apache.karaf.shell.api.action.Command;
+import org.apache.karaf.shell.api.action.Option;
+import org.apache.karaf.shell.api.action.lifecycle.Service;
 
 @Command(scope = "${scope}", name = "${cmd}", description = "${description}")
-public class ${command} extends OsgiCommandSupport {
+@Service
+public class ${command} implements Action {
 
     @Option(name = "-o", aliases = { "--option" }, description = "An option to the command", required = false, multiValued = false)
     private String option;
@@ -35,7 +37,8 @@ public class ${command} extends OsgiCommandSupport {
     @Argument(name = "argument", description = "Argument to the command", required = false, multiValued = false)
     private String argument;
 
-    protected Object doExecute() throws Exception {
+    @Override
+    public Object execute() throws Exception {
          System.out.println("Executing command ${cmd}");
          System.out.println("Option: " + option);
          System.out.println("Argument: " + argument);

http://git-wip-us.apache.org/repos/asf/karaf/blob/3d204040/archetypes/command/src/main/resources/archetype-resources/src/main/resources/OSGI-INF/blueprint/shell-log.xml
----------------------------------------------------------------------
diff --git a/archetypes/command/src/main/resources/archetype-resources/src/main/resources/OSGI-INF/blueprint/shell-log.xml b/archetypes/command/src/main/resources/archetype-resources/src/main/resources/OSGI-INF/blueprint/shell-log.xml
deleted file mode 100644
index 37c1b64..0000000
--- a/archetypes/command/src/main/resources/archetype-resources/src/main/resources/OSGI-INF/blueprint/shell-log.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-##---------------------------------------------------------------------------
-##  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.
-##---------------------------------------------------------------------------
-#set( $symbol_pound = '#' )
-#set( $symbol_dollar = '$' )
-#set( $symbol_escape = '\' )
-#set( $cmd =  $command.toLowerCase())
-<?xml version="1.0" encoding="UTF-8"?>
-
-<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
-           xmlns:cm="http://aries.apache.org/blueprint/xmlns/blueprint-cm/v1.0.0">
-
-    <command-bundle xmlns="http://karaf.apache.org/xmlns/shell/v1.1.0">
-        <command>
-            <action class="${package}.${command}">
-            </action>
-        </command>
-    </command-bundle>
-</blueprint>


[2/7] git commit: [KARAF-2888] A few improvements to the deployer

Posted by gn...@apache.org.
[KARAF-2888] A few improvements to the deployer


Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/c082817f
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/c082817f
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/c082817f

Branch: refs/heads/master
Commit: c082817f21e1754b3e341815e3c00273efa966a3
Parents: 57b609d
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Wed Apr 30 18:02:13 2014 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Wed Apr 30 20:04:27 2014 +0200

----------------------------------------------------------------------
 .../features/internal/service/Deployer.java     | 87 +++++++++++---------
 .../internal/service/FeaturesServiceImpl.java   |  8 +-
 2 files changed, 52 insertions(+), 43 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/c082817f/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
index e26a07a..c636fbe 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/Deployer.java
@@ -38,6 +38,7 @@ import org.apache.karaf.features.BundleInfo;
 import org.apache.karaf.features.Feature;
 import org.apache.karaf.features.FeatureEvent;
 import org.apache.karaf.features.FeaturesService;
+import org.apache.karaf.features.internal.download.DownloadManager;
 import org.apache.karaf.features.internal.download.StreamProvider;
 import org.apache.karaf.features.internal.region.SubsystemResolver;
 import org.apache.karaf.features.internal.util.ChecksumUtils;
@@ -128,7 +129,7 @@ public class Deployer {
     }
 
     static class DeploymentRequest {
-        String overrides;
+        Set<String> overrides;
         String featureResolutionRange;
         String bundleUpdateRange;
         String updateSnaphots;
@@ -153,9 +154,11 @@ public class Deployer {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(Deployer.class);
 
+    private final DownloadManager manager;
     private final DeployCallback callback;
 
-    public Deployer(DeployCallback callback) {
+    public Deployer(DownloadManager manager, DeployCallback callback) {
+        this.manager = manager;
         this.callback = callback;
     }
 
@@ -186,12 +189,12 @@ public class Deployer {
         // TODO: requirements
         // TODO: bundles
 
-        SubsystemResolver resolver = new SubsystemResolver();
+        SubsystemResolver resolver = new SubsystemResolver(manager);
         resolver.resolve(
                 dstate.features.values(),
                 request.requestedFeatures,
                 apply(unmanagedBundles, adapt(BundleRevision.class)),
-                Overrides.loadOverrides(request.overrides),
+                request.overrides,
                 request.featureResolutionRange,
                 request.globalRepository);
 
@@ -302,47 +305,11 @@ public class Deployer {
             }
         }
 
-        //
-        // Log deployment
-        //
-        logDeployment(deployment, verbose);
-
-        if (simulate) {
-            if (!noRefresh && !toRefresh.isEmpty()) {
-                print("  Bundles to refresh:", verbose);
-                for (Bundle bundle : toRefresh) {
-                    print("    " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
-                }
-            }
-            if (!toManage.isEmpty()) {
-                print("  Managing bundle:", verbose);
-                for (Bundle bundle : toManage) {
-                    print("    " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
-                }
-            }
-            return;
-        }
-
         Set<Bundle> toStart = new HashSet<>();
         Set<Bundle> toResolve = new HashSet<>();
         Set<Bundle> toStop = new HashSet<>();
 
         //
-        // Execute deployment
-        //
-        // #1: stop bundles that needs to be updated or uninstalled in order
-        // #2: uninstall needed bundles
-        // #3: update regions
-        // #4: update bundles
-        // #5: install bundles
-        // #6: save state
-        // #7: install configuration
-        // #8: refresh bundles
-        // #9: start bundles in order
-        // #10: send events
-        //
-
-        //
         // Compute bundle states
         //
         Map<Resource, FeaturesService.RequestedState> states = new HashMap<>();
@@ -399,6 +366,42 @@ public class Deployer {
             }
         }
 
+        //
+        // Log deployment
+        //
+        logDeployment(deployment, verbose);
+
+        if (simulate) {
+            if (!noRefresh && !toRefresh.isEmpty()) {
+                print("  Bundles to refresh:", verbose);
+                for (Bundle bundle : toRefresh) {
+                    print("    " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                }
+            }
+            if (!toManage.isEmpty()) {
+                print("  Managing bundle:", verbose);
+                for (Bundle bundle : toManage) {
+                    print("    " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                }
+            }
+            return;
+        }
+
+        //
+        // Execute deployment
+        //
+        // #1: stop bundles that needs to be updated or uninstalled in order
+        // #2: uninstall needed bundles
+        // #3: update regions
+        // #4: update bundles
+        // #5: install bundles
+        // #6: save state
+        // #7: install configuration
+        // #8: refresh bundles
+        // #9: start bundles in order
+        // #10: send events
+        //
+
 
         //
         // Handle updates on the FeaturesService bundle
@@ -605,7 +608,9 @@ public class Deployer {
                         deployment.bundleChecksums.put(bundle.getBundleId(), crc);
                     }
                     int startLevel = startLevels.get(resource);
-                    callback.setBundleStartLevel(bundle, startLevel);
+                    if (startLevel != dstate.initialBundleStartLevel) {
+                        callback.setBundleStartLevel(bundle, startLevel);
+                    }
                     FeaturesService.RequestedState reqState = states.get(resource);
                     switch (reqState) {
                     case Started:

http://git-wip-us.apache.org/repos/asf/karaf/blob/c082817f/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
index 589b719..f0f0677 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
@@ -48,6 +48,7 @@ import org.apache.karaf.features.FeaturesListener;
 import org.apache.karaf.features.FeaturesService;
 import org.apache.karaf.features.Repository;
 import org.apache.karaf.features.RepositoryEvent;
+import org.apache.karaf.features.internal.download.simple.SimpleDownloader;
 import org.apache.karaf.features.internal.util.JsonReader;
 import org.apache.karaf.features.internal.util.JsonWriter;
 import org.apache.karaf.util.collections.CopyOnWriteArrayIdentityList;
@@ -892,7 +893,7 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
         request.bundleUpdateRange = bundleUpdateRange;
         request.featureResolutionRange = featureResolutionRange;
         request.globalRepository = globalRepository;
-        request.overrides = overrides;
+        request.overrides = Overrides.loadOverrides(overrides);
         request.requestedFeatures = requestedFeatures;
         request.stateChanges = stateChanges;
         request.options = options;
@@ -909,7 +910,7 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
 
         Deployer.DeploymentState dstate = getDeploymentState(state);
         Deployer.DeploymentRequest request = getDeploymentRequest(requestedFeatures, stateChanges, options);
-        new Deployer(this).deploy(dstate, request);
+        new Deployer(new SimpleDownloader(), this).deploy(dstate, request);
     }
 
     public void print(String message, boolean verbose) {
@@ -937,6 +938,9 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
     @Override
     public void saveState(State state) {
         synchronized (lock) {
+            state.repositories.clear();
+            state.repositories.addAll(this.state.repositories);
+            state.bootDone.set(this.state.bootDone.get());
             this.state.replace(state);
             saveState();
         }


[6/7] git commit: [KARAF-2888] Make sure Feature#getId() is normalized correctly

Posted by gn...@apache.org.
[KARAF-2888] Make sure Feature#getId() is normalized correctly


Project: http://git-wip-us.apache.org/repos/asf/karaf/repo
Commit: http://git-wip-us.apache.org/repos/asf/karaf/commit/57b609d8
Tree: http://git-wip-us.apache.org/repos/asf/karaf/tree/57b609d8
Diff: http://git-wip-us.apache.org/repos/asf/karaf/diff/57b609d8

Branch: refs/heads/master
Commit: 57b609d8e425eed55ec29082fdb09f788066f682
Parents: 9e2ada3
Author: Guillaume Nodet <gn...@gmail.com>
Authored: Wed Apr 30 16:46:38 2014 +0200
Committer: Guillaume Nodet <gn...@gmail.com>
Committed: Wed Apr 30 20:04:27 2014 +0200

----------------------------------------------------------------------
 .../apache/karaf/features/internal/model/Feature.java    | 11 ++++++++---
 .../features/internal/service/FeaturesServiceImpl.java   |  5 +++--
 .../test/java/org/apache/karaf/features/FeatureTest.java |  4 ++--
 .../internal/service/FeaturesServiceImplTest.java        |  2 +-
 .../internal/service/FeaturesValidationTest.java         |  2 ++
 .../org/apache/karaf/features/internal/service/f07.xml   |  2 +-
 6 files changed, 17 insertions(+), 9 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/karaf/blob/57b609d8/features/core/src/main/java/org/apache/karaf/features/internal/model/Feature.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/model/Feature.java b/features/core/src/main/java/org/apache/karaf/features/internal/model/Feature.java
index 154e89a..6cf3820 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/model/Feature.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/model/Feature.java
@@ -26,8 +26,12 @@ import java.util.regex.Pattern;
 import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
 import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlTransient;
 import javax.xml.bind.annotation.XmlType;
 
+import org.apache.felix.utils.version.VersionCleaner;
+import org.apache.felix.utils.version.VersionTable;
+
 
 /**
  * Definition of the Feature.
@@ -81,7 +85,7 @@ public class Feature extends Content implements org.apache.karaf.features.Featur
     protected String details;
     @XmlAttribute(required = true)
     protected String name;
-    @XmlAttribute
+    @XmlTransient
     protected String version;
     @XmlAttribute
     protected String description;
@@ -105,7 +109,7 @@ public class Feature extends Content implements org.apache.karaf.features.Featur
 
     public Feature(String name, String version) {
         this.name = name;
-        this.version = version;
+        this.version = VersionCleaner.clean(version);
     }
 
 
@@ -167,8 +171,9 @@ public class Feature extends Content implements org.apache.karaf.features.Featur
      * @param value allowed object is
      *              {@link String }
      */
+    @XmlAttribute
     public void setVersion(String value) {
-        this.version = value;
+        this.version = VersionCleaner.clean(value);
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/karaf/blob/57b609d8/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
index 099d175..589b719 100644
--- a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
@@ -39,6 +39,7 @@ import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
+import org.apache.felix.utils.version.VersionCleaner;
 import org.apache.felix.utils.version.VersionRange;
 import org.apache.felix.utils.version.VersionTable;
 import org.apache.karaf.features.Feature;
@@ -799,7 +800,7 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
         int idx = feature.indexOf("/");
         String name = feature.substring(0, idx);
         String version = feature.substring(idx + 1);
-        return name + "/" + VersionTable.getVersion(version).toString();
+        return name + "/" + VersionCleaner.clean(version);
     }
 
     /**
@@ -856,7 +857,7 @@ public class FeaturesServiceImpl implements FeaturesService, Deployer.DeployCall
         dstate.features = new HashMap<>();
         for (Map<String, Feature> m : getFeatures().values()) {
             for (Feature feature : m.values()) {
-                String id = feature.getName() + "/" + VersionTable.getVersion(feature.getVersion());
+                String id = feature.getId();
                 dstate.features.put(id, feature);
             }
         }

http://git-wip-us.apache.org/repos/asf/karaf/blob/57b609d8/features/core/src/test/java/org/apache/karaf/features/FeatureTest.java
----------------------------------------------------------------------
diff --git a/features/core/src/test/java/org/apache/karaf/features/FeatureTest.java b/features/core/src/test/java/org/apache/karaf/features/FeatureTest.java
index b7d4c27..33debdf 100644
--- a/features/core/src/test/java/org/apache/karaf/features/FeatureTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/FeatureTest.java
@@ -21,9 +21,9 @@ import junit.framework.TestCase;
 public class FeatureTest extends TestCase {
 	
 	public void testValueOf() {
-		Feature feature = org.apache.karaf.features.internal.model.Feature.valueOf("name" + org.apache.karaf.features.internal.model.Feature.SPLIT_FOR_NAME_AND_VERSION + "version");
+		Feature feature = org.apache.karaf.features.internal.model.Feature.valueOf("name/1.0.0");
 		assertEquals(feature.getName(), "name");
-		assertEquals(feature.getVersion(), "version");
+		assertEquals(feature.getVersion(), "1.0.0");
 		feature = org.apache.karaf.features.internal.model.Feature.valueOf("name");
 		assertEquals(feature.getName(), "name");
 		assertEquals(feature.getVersion(), org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION);

http://git-wip-us.apache.org/repos/asf/karaf/blob/57b609d8/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java
----------------------------------------------------------------------
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java
index 7f1f358..405a7e9 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java
@@ -92,7 +92,7 @@ public class FeaturesServiceImplTest extends TestBase {
             }
         };
         assertNotNull(impl.getFeature("transaction", org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION));
-        assertSame("2.0.0", impl.getFeature("transaction", org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION).getVersion());
+        assertEquals("2.0.0", impl.getFeature("transaction", org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION).getVersion());
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/karaf/blob/57b609d8/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesValidationTest.java
----------------------------------------------------------------------
diff --git a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesValidationTest.java b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesValidationTest.java
index 9e33ee3..8a0fbca 100644
--- a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesValidationTest.java
+++ b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesValidationTest.java
@@ -24,6 +24,7 @@ import org.apache.karaf.features.internal.model.Features;
 import org.apache.karaf.features.internal.model.JaxbUtil;
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.fail;
 
@@ -97,6 +98,7 @@ public class FeaturesValidationTest {
         URL url = getClass().getResource("f07.xml");
         Features features = JaxbUtil.unmarshal(url.toExternalForm(), true);
         assertNotNull(features);
+        assertEquals("2.5.6.SEC02", features.getFeature().get(0).getVersion());
     }
 
 }

http://git-wip-us.apache.org/repos/asf/karaf/blob/57b609d8/features/core/src/test/resources/org/apache/karaf/features/internal/service/f07.xml
----------------------------------------------------------------------
diff --git a/features/core/src/test/resources/org/apache/karaf/features/internal/service/f07.xml b/features/core/src/test/resources/org/apache/karaf/features/internal/service/f07.xml
index 354ba86..d1a5745 100644
--- a/features/core/src/test/resources/org/apache/karaf/features/internal/service/f07.xml
+++ b/features/core/src/test/resources/org/apache/karaf/features/internal/service/f07.xml
@@ -16,7 +16,7 @@
     limitations under the License.
 -->
 <features name="karaf" xmlns="http://karaf.apache.org/xmlns/features/v1.3.0">
-    <feature name="spring" version="2.5.6.SEC02">
+    <feature name="spring" version="2.5.6-SEC02">
         <bundle dependency='true'>mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.aopalliance/1.0_4</bundle>
         <bundle>mvn:org.springframework/spring-core/2.5.6.SEC02</bundle>
         <bundle>mvn:org.springframework/spring-beans/2.5.6.SEC02</bundle>