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();
+ }
+
+}