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/11 19:20:49 UTC

[27/33] Revert "[KARAF-2852] Merge features/core and features/command"

http://git-wip-us.apache.org/repos/asf/karaf/blob/0c8e8a81/features/core/src/main/java/org/apache/karaf/features/internal/service/BootFeaturesInstaller.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/BootFeaturesInstaller.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/BootFeaturesInstaller.java
new file mode 100644
index 0000000..93827bf
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/BootFeaturesInstaller.java
@@ -0,0 +1,171 @@
+/*
+ * 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.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.karaf.features.BootFinished;
+import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeaturesService;
+import org.osgi.framework.BundleContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BootFeaturesInstaller {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(BootFeaturesInstaller.class);
+
+    public static String VERSION_PREFIX = "version=";
+
+    private final FeaturesServiceImpl featuresService;
+    private final BundleContext bundleContext;
+    private final String repositories;
+    private final String features;
+    private final boolean asynchronous;
+
+    /**
+     *
+     * @param features list of boot features separated by comma. Optionally contains ;version=x.x.x to specify a specific feature version
+     */
+    public BootFeaturesInstaller(BundleContext bundleContext,
+                                 FeaturesServiceImpl featuresService,
+                                 String repositories,
+                                 String features,
+                                 boolean asynchronous) {
+        this.bundleContext = bundleContext;
+        this.featuresService = featuresService;
+        this.repositories = repositories;
+        this.features = features;
+        this.asynchronous = asynchronous;
+    }
+
+    /**
+     * Install boot features
+     */
+    public void start() {
+        if (featuresService.isBootDone()) {
+            publishBootFinished();
+            return;
+        }
+        if (asynchronous) {
+            new Thread("Initial Features Provisioning") {
+                public void run() {
+                    installBootFeatures();
+                }
+            }.start();
+        } else {
+            installBootFeatures();
+        }
+    }
+
+    protected void installBootFeatures() {
+        try {
+            for (String repo : repositories.split(",")) {
+                repo = repo.trim();
+                if (!repo.isEmpty()) {
+                    try {
+                        featuresService.addRepository(URI.create(repo));
+                    } catch (Exception e) {
+                        LOGGER.error("Error installing boot feature repository " + repo, e);
+                    }
+                }
+            }
+
+            List<Set<String>> stagedFeatures = parseBootFeatures(features);
+            for (Set<String> features : stagedFeatures) {
+                featuresService.installFeatures(features, EnumSet.of(FeaturesService.Option.NoFailOnFeatureNotFound));
+            }
+            featuresService.bootDone();
+            publishBootFinished();
+        } catch (Exception e) {
+            // Special handling in case the bundle has been refreshed.
+            // In such a case, simply exits without logging any exception
+            // as the restart should cause the feature service to finish
+            // the work.
+            if (e instanceof IllegalStateException) {
+                try {
+                    bundleContext.getBundle();
+                } catch (IllegalStateException ies) {
+                    return;
+                }
+            }
+            LOGGER.error("Error installing boot features", e);
+        }
+    }
+
+    /**
+     *
+     * @param featureSt either feature name or <featurename>;version=<version>
+     * @return feature matching the feature string
+     * @throws Exception
+     */
+    private Feature getFeature(String featureSt) throws Exception {
+        String[] parts = featureSt.trim().split(";");
+        String featureName = parts[0];
+        String featureVersion = null;
+        for (String part : parts) {
+            // if the part starts with "version=" it contains the version info
+            if (part.startsWith(VERSION_PREFIX)) {
+                featureVersion = part.substring(VERSION_PREFIX.length());
+            }
+        }
+        if (featureVersion == null) {
+            // no version specified - use default version
+            featureVersion = org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION;
+        }
+        return featuresService.getFeature(featureName, featureVersion);
+    }
+
+    protected List<Set<String>> parseBootFeatures(String bootFeatures) {
+        Pattern pattern = Pattern.compile("(\\((.+))\\),|.+");
+        Matcher matcher = pattern.matcher(bootFeatures);
+        List<Set<String>> result = new ArrayList<Set<String>>();
+        while (matcher.find()) {
+            String group = matcher.group(2) != null ? matcher.group(2) : matcher.group();
+            result.add(parseFeatureList(group));
+        }
+        return result;
+    }
+
+    protected Set<String> parseFeatureList(String group) {
+        HashSet<String> features = new HashSet<String>();
+        for (String feature : Arrays.asList(group.trim().split("\\s*,\\s*"))) {
+            if (feature.length() > 0) {
+                features.add(feature);
+            }
+        }
+        return features;
+    }
+
+    private void publishBootFinished() {
+        if (bundleContext != null) {
+            BootFinished bootFinished = new BootFinished() {};
+            bundleContext.registerService(BootFinished.class, bootFinished, new Hashtable<String, String>());
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/0c8e8a81/features/core/src/main/java/org/apache/karaf/features/internal/service/EventAdminListener.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/EventAdminListener.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/EventAdminListener.java
new file mode 100644
index 0000000..b6eaae5
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/EventAdminListener.java
@@ -0,0 +1,91 @@
+/*
+ * 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.util.Dictionary;
+import java.util.Hashtable;
+
+import org.apache.karaf.features.EventConstants;
+import org.apache.karaf.features.FeatureEvent;
+import org.apache.karaf.features.FeaturesListener;
+import org.apache.karaf.features.RepositoryEvent;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.event.Event;
+import org.osgi.service.event.EventAdmin;
+import org.osgi.util.tracker.ServiceTracker;
+
+/**
+ * A listener to publish events to EventAdmin
+ */
+public class EventAdminListener implements FeaturesListener {
+
+    private final ServiceTracker<EventAdmin, EventAdmin> tracker;
+
+    public EventAdminListener(BundleContext context) {
+        tracker = new ServiceTracker<EventAdmin, EventAdmin>(context, EventAdmin.class.getName(), null);
+        tracker.open();
+    }
+
+    public void featureEvent(FeatureEvent event) {
+        EventAdmin eventAdmin = tracker.getService();
+        if (eventAdmin == null) {
+            return;
+        }
+        Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(EventConstants.TYPE, event.getType());
+        props.put(EventConstants.EVENT, event);
+        props.put(EventConstants.TIMESTAMP, System.currentTimeMillis());
+        props.put(EventConstants.FEATURE_NAME, event.getFeature().getName());
+        props.put(EventConstants.FEATURE_VERSION, event.getFeature().getVersion());
+        String topic;
+        switch (event.getType()) {
+            case FeatureInstalled:
+                topic = EventConstants.TOPIC_FEATURES_INSTALLED;
+                break;
+            case FeatureUninstalled:
+                topic = EventConstants.TOPIC_FEATURES_UNINSTALLED;
+                break;
+            default:
+                throw new IllegalStateException("Unknown features event type: " + event.getType());
+        }
+        eventAdmin.postEvent(new Event(topic, props));
+    }
+
+    public void repositoryEvent(RepositoryEvent event) {
+        EventAdmin eventAdmin = tracker.getService();
+        if (eventAdmin == null) {
+            return;
+        }
+        Dictionary<String, Object> props = new Hashtable<String, Object>();
+        props.put(EventConstants.TYPE, event.getType());
+        props.put(EventConstants.EVENT, event);
+        props.put(EventConstants.TIMESTAMP, System.currentTimeMillis());
+        props.put(EventConstants.REPOSITORY_URI, event.getRepository().getURI().toString());
+        String topic;
+        switch (event.getType()) {
+            case RepositoryAdded:
+                topic = EventConstants.TOPIC_REPOSITORY_ADDED;
+                break;
+            case RepositoryRemoved:
+                topic = EventConstants.TOPIC_REPOSITORY_REMOVED;
+                break;
+            default:
+                throw new IllegalStateException("Unknown repository event type: " + event.getType());
+        }
+        eventAdmin.postEvent(new Event(topic, props));
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/0c8e8a81/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureConfigInstaller.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureConfigInstaller.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureConfigInstaller.java
new file mode 100644
index 0000000..0e9038d
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureConfigInstaller.java
@@ -0,0 +1,167 @@
+/*
+ * 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.BufferedInputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.apache.karaf.features.ConfigFileInfo;
+import org.apache.karaf.features.Feature;
+import org.osgi.framework.Constants;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.cm.Configuration;
+import org.osgi.service.cm.ConfigurationAdmin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class FeatureConfigInstaller {
+	private static final Logger LOGGER = LoggerFactory.getLogger(FeaturesServiceImpl.class);
+    private static final String CONFIG_KEY = "org.apache.karaf.features.configKey";
+
+    private final ConfigurationAdmin configAdmin;
+    
+    public FeatureConfigInstaller(ConfigurationAdmin configAdmin) {
+		this.configAdmin = configAdmin;
+	}
+
+    private String[] parsePid(String pid) {
+        int n = pid.indexOf('-');
+        if (n > 0) {
+            String factoryPid = pid.substring(n + 1);
+            pid = pid.substring(0, n);
+            return new String[]{pid, factoryPid};
+        } else {
+            return new String[]{pid, null};
+        }
+    }
+
+    private Configuration createConfiguration(ConfigurationAdmin configurationAdmin,
+                                                String pid, String factoryPid) throws IOException, InvalidSyntaxException {
+        if (factoryPid != null) {
+            return configurationAdmin.createFactoryConfiguration(factoryPid, null);
+        } else {
+            return configurationAdmin.getConfiguration(pid, null);
+        }
+    }
+
+    private Configuration findExistingConfiguration(ConfigurationAdmin configurationAdmin,
+                                                      String pid, String factoryPid) throws IOException, InvalidSyntaxException {
+        String filter;
+        if (factoryPid == null) {
+            filter = "(" + Constants.SERVICE_PID + "=" + pid + ")";
+        } else {
+            String key = createConfigurationKey(pid, factoryPid);
+            filter = "(" + CONFIG_KEY + "=" + key + ")";
+        }
+        Configuration[] configurations = configurationAdmin.listConfigurations(filter);
+        if (configurations != null && configurations.length > 0) {
+            return configurations[0];
+        }
+        return null;
+    }
+
+    void installFeatureConfigs(Feature feature) throws IOException, InvalidSyntaxException {
+        for (String config : feature.getConfigurations().keySet()) {
+            Dictionary<String,String> props = new Hashtable<String, String>(feature.getConfigurations().get(config));
+            String[] pid = parsePid(config);
+            Configuration cfg = findExistingConfiguration(configAdmin, pid[0], pid[1]);
+            if (cfg == null) {
+                cfg = createConfiguration(configAdmin, pid[0], pid[1]);
+                String key = createConfigurationKey(pid[0], pid[1]);
+                props.put(CONFIG_KEY, key);
+                if (cfg.getBundleLocation() != null) {
+                    cfg.setBundleLocation(null);
+                }
+                cfg.update(props);
+            }
+        }
+        for (ConfigFileInfo configFile : feature.getConfigurationFiles()) {
+            installConfigurationFile(configFile.getLocation(), configFile.getFinalname(), configFile.isOverride());
+        }
+    }
+
+    private String createConfigurationKey(String pid, String factoryPid) {
+        return factoryPid == null ? pid : pid + "-" + factoryPid;
+    }
+
+    private void installConfigurationFile(String fileLocation, String finalname, boolean override) throws IOException {
+    	String basePath = System.getProperty("karaf.base");
+    	
+    	if (finalname.contains("${")) {
+    		//remove any placeholder or variable part, this is not valid.
+    		int marker = finalname.indexOf("}");
+    		finalname = finalname.substring(marker+1);
+    	}
+    	
+    	finalname = basePath + File.separator + finalname;
+    	
+    	File file = new File(finalname); 
+    	if (file.exists()) {
+            if (!override) {
+                LOGGER.debug("Configuration file {} already exist, don't override it", finalname);
+                return;
+            } else {
+                LOGGER.info("Configuration file {} already exist, overriding it", finalname);
+            }
+    	} else {
+            LOGGER.info("Creating configuration file {}", finalname);
+        }
+
+        InputStream is = null;
+        FileOutputStream fop = null;
+        try {
+            is = new BufferedInputStream(new URL(fileLocation).openStream());
+
+            if (!file.exists()) {
+                File parentFile = file.getParentFile();
+                if (parentFile != null)
+                    parentFile.mkdirs();
+                file.createNewFile();
+            }
+
+            fop = new FileOutputStream(file);
+        
+            int bytesRead;
+            byte[] buffer = new byte[1024];
+            
+            while ((bytesRead = is.read(buffer)) != -1) {
+                fop.write(buffer, 0, bytesRead);
+            }
+        } catch (RuntimeException e) {
+            LOGGER.error(e.getMessage());
+            throw e;
+        } catch (MalformedURLException e) {
+        	LOGGER.error(e.getMessage());
+            throw e;
+		} finally {
+			if (is != null)
+				is.close();
+            if (fop != null) {
+			    fop.flush();
+			    fop.close();
+            }
+		}
+            
+    }
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/0c8e8a81/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureFinder.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureFinder.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureFinder.java
new file mode 100644
index 0000000..d6defe0
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureFinder.java
@@ -0,0 +1,68 @@
+/*
+ * 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.net.URI;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.osgi.service.cm.ConfigurationException;
+import org.osgi.service.cm.ManagedService;
+
+public class FeatureFinder implements ManagedService {
+
+    final Map<String, String> nameToArtifactMap = new HashMap<String, String>();
+
+    public String[] getNames() {
+        synchronized (nameToArtifactMap) {
+            Set<String> strings = nameToArtifactMap.keySet();
+            return strings.toArray(new String[strings.size()]);
+        }
+    }
+
+    public URI getUriFor(String name, String version) {
+        String coords;
+        synchronized (nameToArtifactMap) {
+            coords = nameToArtifactMap.get(name);
+        }
+        if (coords == null) {
+            return null;
+        }
+        Artifact artifact = new Artifact(coords);
+        return artifact.getMavenUrl(version);
+    }
+
+    @SuppressWarnings("rawtypes")
+    public void updated(Dictionary properties) throws ConfigurationException {
+        synchronized (nameToArtifactMap) {
+            if (properties != null) {
+                nameToArtifactMap.clear();
+                Enumeration keys = properties.keys();
+                while (keys.hasMoreElements()) {
+                    String key = (String) keys.nextElement();
+                    if (!"felix.fileinstall.filename".equals(key) && !"service.pid".equals(key)) {
+                        nameToArtifactMap.put(key, (String) properties.get(key));
+                    }
+                }
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/0c8e8a81/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureValidationUtil.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureValidationUtil.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureValidationUtil.java
new file mode 100644
index 0000000..6903dc0
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeatureValidationUtil.java
@@ -0,0 +1,37 @@
+/*
+ * 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.net.URI;
+
+import org.apache.karaf.features.internal.model.JaxbUtil;
+
+/**
+ * Utility class which fires XML Schema validation.
+ */
+public class FeatureValidationUtil {
+
+    /**
+     * Runs schema validation.
+     * 
+     * @param uri Uri to validate.
+     * @throws Exception When validation fails.
+     */
+    public static void validate(URI uri) throws Exception {
+        JaxbUtil.unmarshal(uri.toASCIIString(), true);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/0c8e8a81/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
new file mode 100644
index 0000000..0243dc0
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
@@ -0,0 +1,1416 @@
+/*
+ * 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.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+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.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+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.deployment.DeploymentBuilder;
+import org.apache.karaf.features.internal.deployment.StreamProvider;
+import org.apache.karaf.features.internal.resolver.FeatureNamespace;
+import org.apache.karaf.features.internal.resolver.UriNamespace;
+import org.apache.karaf.features.internal.util.ChecksumUtils;
+import org.apache.karaf.features.internal.util.Macro;
+import org.apache.karaf.features.internal.util.MultiException;
+import org.apache.karaf.util.collections.CopyOnWriteArrayIdentityList;
+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.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.framework.wiring.FrameworkWiring;
+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;
+
+/**
+ *
+ */
+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;[==,=+)}";
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(FeaturesServiceImpl.class);
+    private static final String SNAPSHOT = "SNAPSHOT";
+    private static final String MAVEN = "mvn:";
+
+    /**
+     * Our bundle.
+     * We use it to check bundle operations affecting our own bundle.
+     */
+    private final Bundle bundle;
+
+    /**
+     * The system bundle context.
+     * For all bundles related operations, we use the system bundle context
+     * to allow this bundle to be stopped and still allow the deployment to
+     * take place.
+     */
+    private final BundleContext systemBundleContext;
+    /**
+     * Used to load and save the {@link State} of this service.
+     */
+    private final StateStorage storage;
+    private final FeatureFinder featureFinder;
+    private final EventAdminListener eventAdminListener;
+    private final FeatureConfigInstaller configInstaller;
+    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}
+     */
+    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}
+     */
+    private final String bundleUpdateRange;
+    /**
+     * Use CRC to check snapshot bundles and update them if changed.
+     * Either:
+     *   - none : never update snapshots
+     *   - always : always update snapshots
+     *   - crc : use CRC to detect changes
+     */
+    private final String updateSnaphots;
+
+    private final List<FeaturesListener> listeners = new CopyOnWriteArrayIdentityList<FeaturesListener>();
+
+    // Synchronized on lock
+    private final Object lock = new Object();
+    private final State state = new State();
+    private final Map<String, Repository> repositoryCache = new HashMap<String, Repository>();
+    private Map<String, Map<String, Feature>> featureCache;
+
+
+    public FeaturesServiceImpl(Bundle bundle,
+                               BundleContext systemBundleContext,
+                               StateStorage storage,
+                               FeatureFinder featureFinder,
+                               EventAdminListener eventAdminListener,
+                               FeatureConfigInstaller configInstaller,
+                               String overrides,
+                               String featureResolutionRange,
+                               String bundleUpdateRange,
+                               String updateSnaphots) {
+        this.bundle = bundle;
+        this.systemBundleContext = systemBundleContext;
+        this.storage = storage;
+        this.featureFinder = featureFinder;
+        this.eventAdminListener = eventAdminListener;
+        this.configInstaller = configInstaller;
+        this.overrides = overrides;
+        this.featureResolutionRange = featureResolutionRange;
+        this.bundleUpdateRange = bundleUpdateRange;
+        this.updateSnaphots = updateSnaphots;
+        loadState();
+    }
+
+    //
+    // State support
+    //
+
+    protected void loadState() {
+        try {
+            synchronized (lock) {
+                storage.load(state);
+            }
+        } catch (IOException e) {
+            LOGGER.warn("Error loading FeaturesService state", e);
+        }
+    }
+
+    protected void saveState() {
+        try {
+            synchronized (lock) {
+                // Make sure we don't store bundle checksums if
+                // it has been disabled through configadmin
+                // so that we don't keep out-of-date checksums.
+                if (!UPDATE_SNAPSHOTS_CRC.equalsIgnoreCase(updateSnaphots)) {
+                    state.bundleChecksums.clear();
+                }
+                storage.save(state);
+            }
+        } catch (IOException e) {
+            LOGGER.warn("Error saving FeaturesService state", e);
+        }
+    }
+
+    boolean isBootDone() {
+        synchronized (lock) {
+            return state.bootDone.get();
+        }
+    }
+
+    void bootDone() {
+        synchronized (lock) {
+            state.bootDone.set(true);
+            saveState();
+        }
+    }
+
+    //
+    // Listeners support
+    //
+
+    public void registerListener(FeaturesListener listener) {
+        listeners.add(listener);
+        try {
+            Set<String> repositories = new TreeSet<String>();
+            Set<String> installedFeatures = new TreeSet<String>();
+            synchronized (lock) {
+                repositories.addAll(state.repositories);
+                installedFeatures.addAll(state.installedFeatures);
+            }
+            for (String uri : repositories) {
+                Repository repository = new RepositoryImpl(URI.create(uri));
+                listener.repositoryEvent(new RepositoryEvent(repository, RepositoryEvent.EventType.RepositoryAdded, true));
+            }
+            for (String id : installedFeatures) {
+                Feature feature = org.apache.karaf.features.internal.model.Feature.valueOf(id);
+                listener.featureEvent(new FeatureEvent(feature, FeatureEvent.EventType.FeatureInstalled, true));
+            }
+        } catch (Exception e) {
+            LOGGER.error("Error notifying listener about the current state", e);
+        }
+    }
+
+    public void unregisterListener(FeaturesListener listener) {
+        listeners.remove(listener);
+    }
+
+    protected void callListeners(FeatureEvent event) {
+        if (eventAdminListener != null) {
+            eventAdminListener.featureEvent(event);
+        }
+        for (FeaturesListener listener : listeners) {
+            listener.featureEvent(event);
+        }
+    }
+
+    protected void callListeners(RepositoryEvent event) {
+        if (eventAdminListener != null) {
+            eventAdminListener.repositoryEvent(event);
+        }
+        for (FeaturesListener listener : listeners) {
+            listener.repositoryEvent(event);
+        }
+    }
+
+    //
+    // Feature Finder support
+    //
+
+    @Override
+    public URI getRepositoryUriFor(String name, String version) {
+        return featureFinder.getUriFor(name, version);
+    }
+
+    @Override
+    public String[] getRepositoryNames() {
+        return featureFinder.getNames();
+    }
+
+
+    //
+    // Repositories support
+    //
+
+    public Repository loadRepository(URI uri) throws Exception {
+        RepositoryImpl repo = new RepositoryImpl(uri);
+        repo.load(true);
+        return repo;
+    }
+
+    @Override
+    public void validateRepository(URI uri) throws Exception {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void addRepository(URI uri) throws Exception {
+        addRepository(uri, false);
+    }
+
+    @Override
+    public void addRepository(URI uri, boolean install) throws Exception {
+        if (install) {
+            // TODO: implement
+            throw new UnsupportedOperationException();
+        }
+        Repository repository = loadRepository(uri);
+        synchronized (lock) {
+            // Clean cache
+            repositoryCache.put(uri.toString(), repository);
+            featureCache = null;
+            // Add repo
+            if (!state.repositories.add(uri.toString())) {
+                return;
+            }
+            saveState();
+        }
+        callListeners(new RepositoryEvent(repository, RepositoryEvent.EventType.RepositoryAdded, false));
+    }
+
+    @Override
+    public void removeRepository(URI uri) throws Exception {
+        removeRepository(uri, true);
+    }
+
+    @Override
+    public void removeRepository(URI uri, boolean uninstall) throws Exception {
+        // TODO: check we don't have any feature installed from this repository
+        Repository repo;
+        synchronized (lock) {
+            // Remove repo
+            if (!state.repositories.remove(uri.toString())) {
+                return;
+            }
+            // Clean cache
+            featureCache = null;
+            repo = repositoryCache.get(uri.toString());
+            List<String> toRemove = new ArrayList<String>();
+            toRemove.add(uri.toString());
+            while (!toRemove.isEmpty()) {
+                Repository rep = repositoryCache.remove(toRemove.remove(0));
+                if (rep != null) {
+                    for (URI u : rep.getRepositories()) {
+                        toRemove.add(u.toString());
+                    }
+                }
+            }
+            saveState();
+        }
+        if (repo == null) {
+            repo = new RepositoryImpl(uri);
+        }
+        callListeners(new RepositoryEvent(repo, RepositoryEvent.EventType.RepositoryRemoved, false));
+    }
+
+    @Override
+    public void restoreRepository(URI uri) throws Exception {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void refreshRepository(URI uri) throws Exception {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Repository[] listRepositories() throws Exception {
+        // Make sure the cache is loaded
+        getFeatures();
+        synchronized (lock) {
+            return repositoryCache.values().toArray(new Repository[repositoryCache.size()]);
+        }
+    }
+
+    @Override
+    public Repository[] listRequiredRepositories() throws Exception {
+        // Make sure the cache is loaded
+        getFeatures();
+        synchronized (lock) {
+            List<Repository> repos = new ArrayList<Repository>();
+            for (Map.Entry<String, Repository> entry : repositoryCache.entrySet()) {
+                if (state.repositories.contains(entry.getKey())) {
+                    repos.add(entry.getValue());
+                }
+            }
+            return repos.toArray(new Repository[repos.size()]);
+        }
+    }
+
+    @Override
+    public Repository getRepository(String name) throws Exception {
+        // Make sure the cache is loaded
+        getFeatures();
+        synchronized (lock) {
+            for (Repository repo : this.repositoryCache.values()) {
+                if (name.equals(repo.getName())) {
+                    return repo;
+                }
+            }
+            return null;
+        }
+    }
+
+    //
+    // Features support
+    //
+
+    public Feature getFeature(String name) throws Exception {
+        return getFeature(name, null);
+    }
+
+    public Feature getFeature(String name, String version) throws Exception {
+        Map<String, Feature> versions = getFeatures().get(name);
+        return getFeatureMatching(versions, version);
+    }
+
+    protected Feature getFeatureMatching(Map<String, Feature> versions, String version) {
+        if (version != null) {
+            version = version.trim();
+            if (version.equals(org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION)) {
+                version = "";
+            }
+        } else {
+            version = "";
+        }
+        if (versions == null || versions.isEmpty()) {
+            return null;
+        } else {
+            Feature feature = version.isEmpty() ? null : versions.get(version);
+            if (feature == null) {
+                // Compute version range. If an version has been given, assume exact range
+                VersionRange versionRange = version.isEmpty() ?
+                        new VersionRange(Version.emptyVersion) :
+                        new VersionRange(version, true, true);
+                Version latest = Version.emptyVersion;
+                for (String available : versions.keySet()) {
+                    Version availableVersion = VersionTable.getVersion(available);
+                    if (availableVersion.compareTo(latest) >= 0 && versionRange.contains(availableVersion)) {
+                        feature = versions.get(available);
+                        latest = availableVersion;
+                    }
+                }
+            }
+            return feature;
+        }
+    }
+
+    public Feature[] listFeatures() throws Exception {
+        Set<Feature> features = new HashSet<Feature>();
+        for (Map<String, Feature> featureWithDifferentVersion : getFeatures().values()) {
+            for (Feature f : featureWithDifferentVersion.values()) {
+                features.add(f);
+            }
+        }
+        return features.toArray(new Feature[features.size()]);
+    }
+
+    protected Map<String, Map<String, Feature>> getFeatures() throws Exception {
+        List<String> uris;
+        synchronized (lock) {
+            if (featureCache != null) {
+                return featureCache;
+            }
+            uris = new ArrayList<String>(state.repositories);
+        }
+        //the outer map's key is feature name, the inner map's key is feature version
+        Map<String, Map<String, Feature>> map = new HashMap<String, Map<String, Feature>>();
+        // Two phase load:
+        // * first load dependent repositories
+        List<String> toLoad = new ArrayList<String>(uris);
+        while (!toLoad.isEmpty()) {
+            String uri = toLoad.remove(0);
+            Repository repo;
+            synchronized (lock) {
+                repo = repositoryCache.get(uri);
+            }
+            if (repo == null) {
+                RepositoryImpl rep = new RepositoryImpl(URI.create(uri));
+                rep.load();
+                repo = rep;
+                synchronized (lock) {
+                    repositoryCache.put(uri, repo);
+                }
+            }
+            for (URI u : repo.getRepositories()) {
+                toLoad.add(u.toString());
+            }
+        }
+        List<Repository> repos;
+        synchronized (lock) {
+            repos = new ArrayList<Repository>(repositoryCache.values());
+        }
+        // * then load all features
+        for (Repository repo : repos) {
+            for (Feature f : repo.getFeatures()) {
+                if (map.get(f.getName()) == null) {
+                    Map<String, Feature> versionMap = new HashMap<String, Feature>();
+                    versionMap.put(f.getVersion(), f);
+                    map.put(f.getName(), versionMap);
+                } else {
+                    map.get(f.getName()).put(f.getVersion(), f);
+                }
+            }
+        }
+        synchronized (lock) {
+            if (uris.size() == state.repositories.size() &&
+                    state.repositories.containsAll(uris)) {
+                featureCache = map;
+            }
+        }
+        return map;
+    }
+
+    //
+    // Installed features
+    //
+
+    @Override
+    public Feature[] listInstalledFeatures() throws Exception {
+        Set<Feature> features = new HashSet<Feature>();
+        Map<String, Map<String, Feature>> allFeatures = getFeatures();
+        synchronized (lock) {
+            for (Map<String, Feature> featureWithDifferentVersion : allFeatures.values()) {
+                for (Feature f : featureWithDifferentVersion.values()) {
+                    if (isInstalled(f)) {
+                        features.add(f);
+                    }
+                }
+            }
+        }
+        return features.toArray(new Feature[features.size()]);
+    }
+
+    @Override
+    public Feature[] listRequiredFeatures() throws Exception {
+        Set<Feature> features = new HashSet<Feature>();
+        Map<String, Map<String, Feature>> allFeatures = getFeatures();
+        synchronized (lock) {
+            for (Map<String, Feature> featureWithDifferentVersion : allFeatures.values()) {
+                for (Feature f : featureWithDifferentVersion.values()) {
+                    if (isRequired(f)) {
+                        features.add(f);
+                    }
+                }
+            }
+        }
+        return features.toArray(new Feature[features.size()]);
+    }
+
+
+    @Override
+    public boolean isInstalled(Feature f) {
+        String id = normalize(f.getId());
+        synchronized (lock) {
+            return state.installedFeatures.contains(id);
+        }
+    }
+
+    @Override
+    public boolean isRequired(Feature f) {
+        String id = normalize(f.getId());
+        synchronized (lock) {
+            return state.features.contains(id);
+        }
+    }
+
+    //
+    // Installation and uninstallation of features
+    //
+
+    public void installFeature(String name) throws Exception {
+        installFeature(name, EnumSet.noneOf(Option.class));
+    }
+
+    public void installFeature(String name, String version) throws Exception {
+        installFeature(version != null ? name + "/" + version : name, EnumSet.noneOf(Option.class));
+    }
+
+    public void installFeature(String name, EnumSet<Option> options) throws Exception {
+        installFeatures(Collections.singleton(name), options);
+    }
+
+    public void installFeature(String name, String version, EnumSet<Option> options) throws Exception {
+        installFeature(version != null ? name + "/" + version : name, options);
+    }
+
+    public void installFeature(Feature feature, EnumSet<Option> options) throws Exception {
+        installFeature(feature.getId());
+    }
+
+    @Override
+    public void uninstallFeature(String name, String version) throws Exception {
+        uninstallFeature(version != null ? name + "/" + version : name);
+    }
+
+    @Override
+    public void uninstallFeature(String name, String version, EnumSet<Option> options) throws Exception {
+        uninstallFeature(version != null ? name + "/" + version : name, options);
+    }
+
+    @Override
+    public void uninstallFeature(String name) throws Exception {
+        uninstallFeature(name, EnumSet.noneOf(Option.class));
+    }
+
+    @Override
+    public void uninstallFeature(String name, EnumSet<Option> options) throws Exception {
+        uninstallFeatures(Collections.singleton(name), options);
+    }
+
+
+    //
+    //
+    //
+    //   RESOLUTION
+    //
+    //
+    //
+
+
+
+
+
+
+    public void installFeatures(Set<String> features, EnumSet<Option> options) throws Exception {
+        Set<String> required;
+        Set<String> installed;
+        Set<Long> managed;
+        synchronized (lock) {
+            required = new HashSet<String>(state.features);
+            installed = new HashSet<String>(state.installedFeatures);
+            managed = new HashSet<Long>(state.managedBundles);
+        }
+        List<String> featuresToAdd = new ArrayList<String>();
+        Map<String, Map<String, Feature>> featuresMap = getFeatures();
+        for (String feature : features) {
+            feature = normalize(feature);
+            String name = feature.substring(0, feature.indexOf("/"));
+            String version = feature.substring(feature.indexOf("/") + 1);
+            Feature f = getFeatureMatching(featuresMap.get(name), version);
+            if (f == null) {
+                if (!options.contains(Option.NoFailOnFeatureNotFound)) {
+                    throw new IllegalArgumentException("No matching features for " + feature);
+                }
+            } else {
+                featuresToAdd.add(normalize(f.getId()));
+            }
+        }
+        featuresToAdd = new ArrayList<String>(new LinkedHashSet<String>(featuresToAdd));
+        StringBuilder sb = new StringBuilder();
+        sb.append("Adding features: ");
+        for (int i = 0; i < featuresToAdd.size(); i++) {
+            if (i > 0) {
+                sb.append(", ");
+            }
+            sb.append(featuresToAdd.get(i));
+        }
+        print(sb.toString(), options.contains(Option.Verbose));
+        required.addAll(featuresToAdd);
+        doInstallFeaturesInThread(required, installed, managed, options);
+    }
+
+    public void uninstallFeatures(Set<String> features, EnumSet<Option> options) throws Exception {
+        Set<String> required;
+        Set<String> installed;
+        Set<Long> managed;
+        synchronized (lock) {
+            required = new HashSet<String>(state.features);
+            installed = new HashSet<String>(state.installedFeatures);
+            managed = new HashSet<Long>(state.managedBundles);
+        }
+        List<String> featuresToRemove = new ArrayList<String>();
+        for (String feature : new HashSet<String>(features)) {
+            List<String> toRemove = new ArrayList<String>();
+            feature = normalize(feature);
+            if (feature.endsWith("/0.0.0")) {
+                String nameSep = feature.substring(0, feature.indexOf("/") + 1);
+                for (String f : required) {
+                    if (normalize(f).startsWith(nameSep)) {
+                        toRemove.add(f);
+                    }
+                }
+            } else {
+                toRemove.add(feature);
+            }
+            toRemove.retainAll(required);
+            if (toRemove.isEmpty()) {
+                throw new IllegalArgumentException("Feature named '" + feature + "' is not installed");
+            } else if (toRemove.size() > 1) {
+                String name = feature.substring(0, feature.indexOf("/"));
+                StringBuilder sb = new StringBuilder();
+                sb.append("Feature named '").append(name).append("' has multiple versions installed (");
+                for (int i = 0; i < toRemove.size(); i++) {
+                    if (i > 0) {
+                        sb.append(", ");
+                    }
+                    sb.append(toRemove.get(i));
+                }
+                sb.append("). Please specify the version to uninstall.");
+                throw new IllegalArgumentException(sb.toString());
+            }
+            featuresToRemove.addAll(toRemove);
+        }
+        featuresToRemove = new ArrayList<String>(new LinkedHashSet<String>(featuresToRemove));
+        StringBuilder sb = new StringBuilder();
+        sb.append("Removing features: ");
+        for (int i = 0; i < featuresToRemove.size(); i++) {
+            if (i > 0) {
+                sb.append(", ");
+            }
+            sb.append(featuresToRemove.get(i));
+        }
+        print(sb.toString(), options.contains(Option.Verbose));
+        required.removeAll(featuresToRemove);
+        doInstallFeaturesInThread(required, installed, managed, options);
+    }
+
+    protected String normalize(String feature) {
+        if (!feature.contains("/")) {
+            feature += "/0.0.0";
+        }
+        int idx = feature.indexOf("/");
+        String name = feature.substring(0, idx);
+        String version = feature.substring(idx + 1);
+        return name + "/" + VersionTable.getVersion(version).toString();
+    }
+
+    /**
+     * Actual deployment needs to be done in a separate thread.
+     * The reason is that if the console is refreshed, the current thread which is running
+     * the command may be interrupted while waiting for the refresh to be done, leading
+     * to bundles not being started after the refresh.
+     */
+    public void doInstallFeaturesInThread(final Set<String> features,
+                                          final Set<String> installed,
+                                          final Set<Long> managed,
+                                          final EnumSet<Option> options) throws Exception {
+        ExecutorService executor = Executors.newCachedThreadPool();
+        try {
+            executor.submit(new Callable<Object>() {
+                @Override
+                public Object call() throws Exception {
+                    doInstallFeatures(features, installed, managed, options);
+                    return null;
+                }
+            }).get();
+        } catch (ExecutionException e) {
+            Throwable t = e.getCause();
+            if (t instanceof RuntimeException) {
+                throw ((RuntimeException) t);
+            } else if (t instanceof Error) {
+                throw ((Error) t);
+            } else if (t instanceof Exception) {
+                throw (Exception) t;
+            } else {
+                throw e;
+            }
+        } finally {
+            executor.shutdown();
+        }
+    }
+
+    public void doInstallFeatures(Set<String> features,    // all request features
+                                  Set<String> installed,   // installed features
+                                  Set<Long> managed,       // currently managed bundles
+                                  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);
+
+        // Get a list of resolved and unmanaged bundles to use as capabilities during resolution
+        List<Resource> systemBundles = new ArrayList<Resource>();
+        Bundle[] bundles = systemBundleContext.getBundles();
+        for (Bundle bundle : bundles) {
+            if (bundle.getState() >= Bundle.RESOLVED && !managed.contains(bundle.getBundleId())) {
+                Resource res = bundle.adapt(BundleRevision.class);
+                systemBundles.add(res);
+            }
+        }
+        // Resolve
+        // TODO: requirements
+        // TODO: bundles
+        // TODO: regions: on isolated regions, we may need different resolution for each region
+        Set<String>  overrides    = Overrides.loadOverrides(this.overrides);
+        Repository[] repositories = listRepositories();
+        DeploymentBuilder builder = createDeploymentBuilder(repositories);
+        builder.setFeatureRange(featureResolutionRange);
+        builder.download(features,
+                         Collections.<String>emptySet(),
+                         Collections.<String>emptySet(),
+                         overrides,
+                         Collections.<String>emptySet());
+        Map<Resource, List<Wire>> resolution = builder.resolve(systemBundles);
+        Collection<Resource> allResources = resolution.keySet();
+        Map<String, StreamProvider> providers = builder.getProviders();
+
+        // Install conditionals
+        List<String> installedFeatureIds = getFeatureIds(allResources);
+        List<String> newFeatures = new ArrayList<String>(installedFeatureIds);
+        newFeatures.removeAll(installed);
+        List<String> delFeatures = new ArrayList<String>(installed);
+        delFeatures.removeAll(installedFeatureIds);
+
+        //
+        // Compute list of installable resources (those with uris)
+        //
+        List<Resource> resources = getBundles(allResources);
+
+        // Compute information for each bundle
+        Map<String, BundleInfo> bundleInfos = new HashMap<String, BundleInfo>();
+        for (Feature feature : getFeatures(repositories, getFeatureIds(allResources))) {
+            for (BundleInfo bi : feature.getBundles()) {
+                BundleInfo oldBi = bundleInfos.get(bi.getLocation());
+                if (oldBi != null) {
+                    bi = mergeBundleInfo(bi, oldBi);
+                }
+                bundleInfos.put(bi.getLocation(), bi);
+            }
+        }
+
+        // TODO: handle bundleInfo.isStart()
+
+        // Get all resources that will be used to satisfy the old features set
+        Set<Resource> resourceLinkedToOldFeatures = new HashSet<Resource>();
+        if (noStart) {
+            for (Resource resource : resolution.keySet()) {
+                String name = FeatureNamespace.getName(resource);
+                if (name != null) {
+                    Version version = FeatureNamespace.getVersion(resource);
+                    String id = version != null ? name + "/" + version : name;
+                    if (installed.contains(id)) {
+                        addTransitive(resource, resourceLinkedToOldFeatures, resolution);
+                    }
+                }
+            }
+        }
+
+        //
+        // Compute deployment
+        //
+        Map<String, Long> bundleChecksums = new HashMap<String, Long>();
+        synchronized (lock) {
+            bundleChecksums.putAll(state.bundleChecksums);
+        }
+        Deployment deployment = computeDeployment(managed, bundles, providers, resources, bundleChecksums);
+
+        if (deployment.toDelete.isEmpty() &&
+                deployment.toUpdate.isEmpty() &&
+                deployment.toInstall.isEmpty()) {
+            print("No deployment change.", verbose);
+            return;
+        }
+        //
+        // Log deployment
+        //
+        logDeployment(deployment, verbose);
+
+        //
+        // Compute the set of bundles to refresh
+        //
+        Set<Bundle> toRefresh = new HashSet<Bundle>();
+        toRefresh.addAll(deployment.toDelete);
+        toRefresh.addAll(deployment.toUpdate.keySet());
+
+        if (!noRefreshManaged) {
+            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 = null;
+                            if (wire.getProvider() instanceof BundleRevision) {
+                                b = ((BundleRevision) wire.getProvider()).getBundle();
+                            } else {
+                                b = deployment.resToBnd.get(wire.getProvider());
+                            }
+                            if (b == null || toRefresh.contains(b)) {
+                                toRefresh.add(bundle);
+                                break;
+                            }
+                        }
+                    }
+                }
+            } while (toRefresh.size() > size);
+        }
+        if (noRefreshUnmanaged) {
+            Set<Bundle> newSet = new HashSet<Bundle>();
+            for (Bundle bundle : toRefresh) {
+                if (managed.contains(bundle.getBundleId())) {
+                    newSet.add(bundle);
+                }
+            }
+            toRefresh = newSet;
+        }
+
+
+        if (simulate) {
+            if (!toRefresh.isEmpty()) {
+                print("  Bundles to refresh:", verbose);
+                for (Bundle bundle : toRefresh) {
+                    print("    " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                }
+            }
+            return;
+        }
+
+        Set<Bundle> toStart = new HashSet<Bundle>();
+
+        //
+        // Execute deployment
+        //
+
+        // TODO: handle update on the features service itself
+        if (deployment.toUpdate.containsKey(bundle) ||
+                deployment.toDelete.contains(bundle)) {
+
+            LOGGER.warn("Updating or uninstalling of the FeaturesService is not supported");
+            deployment.toUpdate.remove(bundle);
+            deployment.toDelete.remove(bundle);
+
+        }
+
+        //
+        // Perform bundle operations
+        //
+
+        // Stop bundles by chunks
+        Set<Bundle> toStop = new HashSet<Bundle>();
+        toStop.addAll(deployment.toUpdate.keySet());
+        toStop.addAll(deployment.toDelete);
+        removeFragmentsAndBundlesInState(toStop, Bundle.UNINSTALLED | Bundle.RESOLVED | Bundle.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(Bundle.STOP_TRANSIENT);
+                    toStop.remove(bundle);
+                }
+            }
+        }
+        if (!deployment.toDelete.isEmpty()) {
+            print("Uninstalling bundles:", verbose);
+            for (Bundle bundle : deployment.toDelete) {
+                print("  " + bundle.getSymbolicName() + " / " + bundle.getVersion(), verbose);
+                bundle.uninstall();
+                managed.remove(bundle.getBundleId());
+            }
+        }
+        if (!deployment.toUpdate.isEmpty()) {
+            print("Updating bundles:", verbose);
+            for (Map.Entry<Bundle, Resource> entry : deployment.toUpdate.entrySet()) {
+                Bundle bundle = entry.getKey();
+                Resource resource = entry.getValue();
+                String uri = UriNamespace.getUri(resource);
+                print("  " + uri, verbose);
+                InputStream is = getBundleInputStream(resource, providers);
+                bundle.update(is);
+                toStart.add(bundle);
+                BundleInfo bi = bundleInfos.get(uri);
+                if (bi != null && bi.getStartLevel() > 0) {
+                    bundle.adapt(BundleStartLevel.class).setStartLevel(bi.getStartLevel());
+                }
+                // TODO: handle region
+            }
+        }
+        if (!deployment.toInstall.isEmpty()) {
+            print("Installing bundles:", verbose);
+            for (Resource resource : deployment.toInstall) {
+                String uri = UriNamespace.getUri(resource);
+                print("  " + uri, verbose);
+                InputStream is = getBundleInputStream(resource, providers);
+                Bundle bundle = systemBundleContext.installBundle(uri, is);
+                managed.add(bundle.getBundleId());
+                if (!noStart || resourceLinkedToOldFeatures.contains(resource)) {
+                    toStart.add(bundle);
+                }
+                deployment.resToBnd.put(resource, bundle);
+                // save a checksum of installed snapshot bundle
+                if (UPDATE_SNAPSHOTS_CRC.equals(updateSnaphots)
+                        && isUpdateable(resource) && !deployment.newCheckums.containsKey(bundle.getLocation())) {
+                    deployment.newCheckums.put(bundle.getLocation(), ChecksumUtils.checksum(getBundleInputStream(resource, providers)));
+                }
+                BundleInfo bi = bundleInfos.get(uri);
+                if (bi != null && bi.getStartLevel() > 0) {
+                    bundle.adapt(BundleStartLevel.class).setStartLevel(bi.getStartLevel());
+                }
+                // TODO: handle region
+            }
+        }
+
+        //
+        // Update and save state
+        //
+        synchronized (lock) {
+            state.bundleChecksums.putAll(deployment.newCheckums);
+            state.features.clear();
+            state.features.addAll(features);
+            state.installedFeatures.clear();
+            state.installedFeatures.addAll(installedFeatureIds);
+            state.managedBundles.clear();
+            state.managedBundles.addAll(managed);
+            saveState();
+        }
+
+        //
+        // Install configurations
+        //
+        if (configInstaller != null && !newFeatures.isEmpty()) {
+            for (Repository repository : repositories) {
+                for (Feature feature : repository.getFeatures()) {
+                    if (newFeatures.contains(feature.getId())) {
+                        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<Bundle>();
+            toStop.addAll(toRefresh);
+            removeFragmentsAndBundlesInState(toStop, Bundle.UNINSTALLED | Bundle.RESOLVED | Bundle.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(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()) {
+                    refreshPackages(toRefresh);
+                }
+            }
+        }
+
+        // Compute bundles to start
+        removeFragmentsAndBundlesInState(toStart, Bundle.UNINSTALLED | Bundle.ACTIVE | Bundle.STARTING);
+        if (!toStart.isEmpty()) {
+            // Compute correct start order
+            List<Exception> exceptions = new ArrayList<Exception>();
+            print("Starting bundles:", verbose);
+            while (!toStart.isEmpty()) {
+                List<Bundle> bs = getBundlesToStart(toStart);
+                for (Bundle bundle : bs) {
+                    LOGGER.info("  " + bundle.getSymbolicName() + " / " + bundle.getVersion());
+                    try {
+                        bundle.start();
+                    } catch (BundleException e) {
+                        exceptions.add(e);
+                    }
+                    toStart.remove(bundle);
+                }
+            }
+            if (!exceptions.isEmpty()) {
+                throw new MultiException("Error restarting bundles", exceptions);
+            }
+        }
+
+        // Call listeners
+        for (Feature feature : getFeatures(repositories, delFeatures)) {
+            callListeners(new FeatureEvent(feature, FeatureEvent.EventType.FeatureUninstalled, false));
+        }
+        for (Feature feature : getFeatures(repositories, newFeatures)) {
+            callListeners(new FeatureEvent(feature, FeatureEvent.EventType.FeatureInstalled, false));
+        }
+
+        print("Done.", verbose);
+    }
+
+    private void addTransitive(Resource resource, Set<Resource> resources, Map<Resource, List<Wire>> resolution) {
+        if (resources.add(resource)) {
+            for (Wire wire : resolution.get(resource)) {
+                addTransitive(wire.getProvider(), resources, resolution);
+            }
+        }
+    }
+
+    protected BundleInfo mergeBundleInfo(BundleInfo bi, BundleInfo oldBi) {
+        // TODO: we need a proper merge strategy when a bundle
+        // TODO: comes from different features
+        return bi;
+    }
+
+    private void print(String message, boolean verbose) {
+        LOGGER.info(message);
+        if (verbose) {
+            System.out.println(message);
+        }
+    }
+
+    private void removeFragmentsAndBundlesInState(Collection<Bundle> bundles, int state) {
+        for (Bundle bundle : new ArrayList<Bundle>(bundles)) {
+            if ((bundle.getState() & state) != 0
+                     || bundle.getHeaders().get(Constants.FRAGMENT_HOST) != null) {
+                bundles.remove(bundle);
+            }
+        }
+    }
+
+    protected void logDeployment(Deployment deployment, boolean verbose) {
+        print("Changes to perform:", 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 " + UriNamespace.getUri(entry.getValue()), verbose);
+            }
+        }
+        if (!deployment.toInstall.isEmpty()) {
+            print("  Bundles to install:", verbose);
+            for (Resource resource : deployment.toInstall) {
+                print("    " + UriNamespace.getUri(resource), verbose);
+            }
+        }
+    }
+
+    protected Deployment computeDeployment(
+                                Set<Long> managed,
+                                Bundle[] bundles,
+                                Map<String, StreamProvider> providers,
+                                List<Resource> resources,
+                                Map<String, Long> bundleChecksums) throws IOException {
+        Deployment deployment = new Deployment();
+
+        // TODO: regions
+        List<Resource> toDeploy = new ArrayList<Resource>(resources);
+
+        // First pass: go through all installed bundles and mark them
+        // as either to ignore or delete
+        for (Bundle bundle : bundles) {
+            if (bundle.getSymbolicName() != null && bundle.getBundleId() != 0) {
+                Resource resource = null;
+                for (Resource res : toDeploy) {
+                    if (bundle.getSymbolicName().equals(getSymbolicName(res))) {
+                        if (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 (managed.contains(bundle.getBundleId()) && 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
+                            InputStream is = null;
+                            try {
+                                is = getBundleInputStream(resource, providers);
+                                long newCrc = ChecksumUtils.checksum(is);
+                                long oldCrc = bundleChecksums.containsKey(bundle.getLocation()) ? bundleChecksums.get(bundle.getLocation()) : 0l;
+                                if (newCrc != oldCrc) {
+                                    LOGGER.debug("New snapshot available for " + bundle.getLocation());
+                                    deployment.toUpdate.put(bundle, resource);
+                                    deployment.newCheckums.put(bundle.getLocation(), newCrc);
+                                }
+                            } finally {
+                                if (is != null) {
+                                    is.close();
+                                }
+                            }
+                        }
+                    }
+                    // We're done for this resource
+                    toDeploy.remove(resource);
+                    deployment.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<Version, Bundle>();
+            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);
+                deployment.resToBnd.put(resource, bundle);
+            } else {
+                deployment.toInstall.add(resource);
+            }
+        }
+        return deployment;
+    }
+
+    protected List<Resource> getBundles(Collection<Resource> allResources) {
+        Map<String, Resource> deploy = new TreeMap<String, Resource>();
+        for (Resource res : allResources) {
+            String uri = UriNamespace.getUri(res);
+            if (uri != null) {
+                deploy.put(uri, res);
+            }
+        }
+        return new ArrayList<Resource>(deploy.values());
+    }
+
+    protected List<Feature> getFeatures(Repository[] repositories, List<String> featureIds) throws Exception {
+        List<Feature> installedFeatures = new ArrayList<Feature>();
+        for (Repository repository : repositories) {
+            for (Feature feature : repository.getFeatures()) {
+                String id = feature.getName() + "/" + VersionTable.getVersion(feature.getVersion());
+                if (featureIds.contains(id)) {
+                    installedFeatures.add(feature);
+                }
+            }
+        }
+        return installedFeatures;
+    }
+
+    protected List<String> getFeatureIds(Collection<Resource> allResources) {
+        List<String> installedFeatureIds = new ArrayList<String>();
+        for (Resource resource : allResources) {
+            String name = FeatureNamespace.getName(resource);
+            if (name != null) {
+                Version version = FeatureNamespace.getVersion(resource);
+                String id = version != null ? name + "/" + version : name;
+                installedFeatureIds.add(id);
+            }
+        }
+        return installedFeatureIds;
+    }
+
+    protected DeploymentBuilder createDeploymentBuilder(Repository[] repositories) {
+        return new DeploymentBuilder(new SimpleDownloader(), Arrays.asList(repositories));
+    }
+
+
+    protected boolean isUpdateable(Resource resource) {
+        return (getVersion(resource).getQualifier().endsWith(SNAPSHOT) ||
+                UriNamespace.getUri(resource).contains(SNAPSHOT) ||
+                !UriNamespace.getUri(resource).contains(MAVEN));
+    }
+
+    protected List<Bundle> getBundlesToStart(Collection<Bundle> bundles) {
+        // TODO: make this pluggable ?
+        // TODO: honor respectStartLvlDuringFeatureStartup
+
+        // 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.
+
+        // 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 = bundles.remove(bundle);
+
+        List<BundleRevision> revs = new ArrayList<BundleRevision>();
+        for (Bundle bundle : bundles) {
+            revs.add(bundle.adapt(BundleRevision.class));
+        }
+        List<Bundle> sorted = new ArrayList<Bundle>();
+        for (BundleRevision rev : RequirementSort.sort(revs)) {
+            sorted.add(rev.getBundle());
+        }
+        if (restart) {
+            sorted.add(bundle);
+        }
+        return sorted;
+    }
+
+    protected List<Bundle> getBundlesToStop(Collection<Bundle> bundles) {
+        // TODO: make this pluggable ?
+        // TODO: honor respectStartLvlDuringFeatureUninstall
+
+        List<Bundle> bundlesToDestroy = new ArrayList<Bundle>();
+        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 = UriNamespace.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<String, Long> newCheckums = new HashMap<String, Long>();
+        Map<Resource, Bundle> resToBnd = new HashMap<Resource, Bundle>();
+        List<Resource> toInstall = new ArrayList<Resource>();
+        List<Bundle> toDelete = new ArrayList<Bundle>();
+        Map<Bundle, Resource> toUpdate = new HashMap<Bundle, Resource>();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/karaf/blob/0c8e8a81/features/core/src/main/java/org/apache/karaf/features/internal/service/Overrides.java
----------------------------------------------------------------------
diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/service/Overrides.java b/features/core/src/main/java/org/apache/karaf/features/internal/service/Overrides.java
new file mode 100644
index 0000000..233a8a2
--- /dev/null
+++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/Overrides.java
@@ -0,0 +1,132 @@
+/*
+ * 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.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.felix.utils.manifest.Clause;
+import org.apache.felix.utils.manifest.Parser;
+import org.apache.felix.utils.version.VersionRange;
+import org.osgi.framework.Version;
+import org.osgi.resource.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.apache.felix.resolver.Util.getSymbolicName;
+import static org.apache.felix.resolver.Util.getVersion;
+
+/**
+ * Helper class to deal with overriden bundles at feature installation time.
+ */
+public class Overrides {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(Overrides.class);
+
+    protected static final String OVERRIDE_RANGE = "range";
+
+    /**
+     * Compute a list of bundles to install, taking into account overrides.
+     *
+     * The file containing the overrides will be loaded from the given url.
+     * Blank lines and lines starting with a '#' will be ignored, all other lines
+     * are considered as urls to override bundles.
+     *
+     * The list of resources to resolve will be scanned and for each bundle,
+     * if a bundle override matches that resource, it will be used instead.
+     *
+     * Matching is done on bundle symbolic name (they have to be the same)
+     * and version (the bundle override version needs to be greater than the
+     * resource to be resolved, and less than the next minor version.  A range
+     * directive can be added to the override url in which case, the matching
+     * will succeed if the resource to be resolved is within the given range.
+     *
+     * @param resources the list of resources to resolve
+     * @param overrides list of bundle overrides
+     */
+    public static void override(Map<String, Resource> resources, Collection<String> overrides) {
+        // Do override replacement
+        for (Clause override : Parser.parseClauses(overrides.toArray(new String[overrides.size()]))) {
+            String url = override.getName();
+            String vr  = override.getAttribute(OVERRIDE_RANGE);
+            Resource over = resources.get(url);
+            if (over == null) {
+                // Ignore invalid overrides
+                continue;
+            }
+            for (String uri : new ArrayList<String>(resources.keySet())) {
+                Resource res = resources.get(uri);
+                if (getSymbolicName(res).equals(getSymbolicName(over))) {
+                    VersionRange range;
+                    if (vr == null) {
+                        // default to micro version compatibility
+                        Version v1 = getVersion(res);
+                        Version v2 = new Version(v1.getMajor(), v1.getMinor() + 1, 0);
+                        range = new VersionRange(false, v1, v2, true);
+                    } else {
+                        range = VersionRange.parseVersionRange(vr);
+                    }
+                    // The resource matches, so replace it with the overridden resource
+                    // if the override is actually a newer version than what we currently have
+                    if (range.contains(getVersion(over)) && getVersion(res).compareTo(getVersion(over)) < 0) {
+                        resources.put(uri, over);
+                    }
+                }
+            }
+        }
+    }
+
+    public static Set<String> loadOverrides(String overridesUrl) {
+        Set<String> overrides = new HashSet<String>();
+        try {
+            if (overridesUrl != null) {
+                InputStream is = new URL(overridesUrl).openStream();
+                try {
+                    BufferedReader reader = new BufferedReader(new InputStreamReader(is));
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        line = line.trim();
+                        if (!line.isEmpty() && !line.startsWith("#")) {
+                            overrides.add(line);
+                        }
+                    }
+                } finally {
+                    is.close();
+                }
+            }
+        } catch (Exception e) {
+            LOGGER.debug("Unable to load overrides bundles list", e);
+        }
+        return overrides;
+    }
+
+    public static String extractUrl(String override) {
+        Clause[] cs = Parser.parseClauses(new String[] { override });
+        if (cs.length != 1) {
+            throw new IllegalStateException("Override contains more than one clause: " + override);
+        }
+        return cs[0].getName();
+    }
+
+}