This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/karaf.git
The following commit(s) were added to refs/heads/main by this push:
new 30fab73eac feat(features): add simple feature resolver bypassing OSGi
capabilities resolution (#2308)
30fab73eac is described below
commit 30fab73eac362814ec90d2088b7467d896d55d40
Author: JB Onofré <[email protected]>
AuthorDate: Tue Sep 8 15:01:42 2026 +0200
feat(features): add simple feature resolver bypassing OSGi capabilities
resolution (#2308)
* feat(features): add simple feature resolver bypassing OSGi capabilities
resolution
Add SimpleFeaturesServiceImpl that installs features and bundles in
declaration order without using the Felix OSGi resolver. This is useful
when OSGi capabilities/requirements resolution overhead is not needed
and deterministic installation order is desired.
- Add SimpleDeployer for sequential feature/bundle deployment
- Add SimpleFeaturesServiceImpl implementing FeaturesService
- Add BootManaged interface to decouple BootFeaturesInstaller from impl
- Update Activator to support resolverSimple=true configuration toggle
- Extract shared tests into AbstractFeaturesServiceTest to validate
both FeaturesServiceImpl and SimpleFeaturesServiceImpl
* fix(features): use jakarta.xml.bind.JAXBException in
SimpleFeaturesServiceImpl
The new simple feature resolver imported the pre-Jakarta
javax.xml.bind.JAXBException, which is no longer on the compile
classpath, breaking the build of org.apache.karaf.features.core.
* feat(features): replace resolverSimple flag with resolver property
Use resolver=default|simple instead of the resolverSimple boolean, in
line with the other string valued properties of the features service
(such as serviceRequirements), and document it.
- Add DEFAULT_RESOLVER and SIMPLE_RESOLVER constants in FeaturesService
- Read the resolver property in the Activator, warning and falling back
to the default resolver on an unknown value
- Document resolver in etc/org.apache.karaf.features.cfg and in the
provisioning section of the user guide
* fix(features): honor conditional feature ownership in simple resolver
Fold a <conditional> block into the install set only when its owning
feature is itself being installed (or already installed), not merely
when its condition token matches a feature name. Otherwise a
conditional bundle could be installed without the owner's bundles that
export the packages it needs, e.g. webconsole.features, scr.management/
scr.state, and bundle.blueprintstate failing to resolve at boot.
---
.../resources/etc/org.apache.karaf.features.cfg | 15 +
.../org/apache/karaf/features/FeaturesService.java | 10 +
.../karaf/features/internal/osgi/Activator.java | 97 ++-
.../internal/service/BootFeaturesInstaller.java | 28 +-
.../features/internal/service/BootManaged.java | 28 +
.../internal/service/FeaturesServiceImpl.java | 8 +-
.../features/internal/service/SimpleDeployer.java | 582 +++++++++++++
...iceImpl.java => SimpleFeaturesServiceImpl.java} | 896 +++++++--------------
.../service/AbstractFeaturesServiceTest.java | 205 +++++
.../internal/service/FeaturesServiceImplTest.java | 66 +-
.../service/SimpleFeaturesServiceImplTest.java | 69 ++
.../src/main/asciidoc/user-guide/provisioning.adoc | 16 +
12 files changed, 1342 insertions(+), 678 deletions(-)
diff --git
a/assemblies/features/base/src/main/filtered-resources/resources/etc/org.apache.karaf.features.cfg
b/assemblies/features/base/src/main/filtered-resources/resources/etc/org.apache.karaf.features.cfg
index 148e2ba617..4b34290ee1 100644
---
a/assemblies/features/base/src/main/filtered-resources/resources/etc/org.apache.karaf.features.cfg
+++
b/assemblies/features/base/src/main/filtered-resources/resources/etc/org.apache.karaf.features.cfg
@@ -45,6 +45,21 @@ featuresBoot=
#
featuresBootAsynchronous=false
+#
+# Resolver used by the features service to install features
+#
+# The following flag can have those values:
+# - default: the features and bundles are installed using the OSGi resolver,
computing
+# the requirements/capabilities. It supports feature and service
requirements,
+# conditional features, and resource repositories.
+# - simple: the features and bundles are installed in the declaration order
(dependency
+# features first), without using the OSGi resolver. It's faster and
+# deterministic, but the requirements/capabilities are not
resolved: feature
+# and service requirements, conditional features, and resource
repositories
+# are ignored.
+#
+#resolver=default
+
#
# Service requirements enforcement
#
diff --git
a/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
b/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
index b4ef2cf808..65d843429c 100644
--- a/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
+++ b/features/core/src/main/java/org/apache/karaf/features/FeaturesService.java
@@ -47,6 +47,16 @@ public interface FeaturesService {
boolean DEFAULT_CONFIG_CFG_STORE = true;
boolean DEFAULT_DIGRAPH_MBEAN = true;
+ /**
+ * Resolver using the OSGi resolver to compute requirements/capabilities
(default).
+ */
+ String DEFAULT_RESOLVER = "default";
+
+ /**
+ * Resolver installing features and bundles in declaration order, without
OSGi resolution.
+ */
+ String SIMPLE_RESOLVER = "simple";
+
enum Option {
NoFailOnFeatureNotFound,
NoAutoRefreshManagedBundles,
diff --git
a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
index a2d3651cc3..6640b6d699 100644
---
a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
+++
b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java
@@ -45,11 +45,13 @@ import
org.apache.karaf.features.internal.repository.JsonRepository;
import org.apache.karaf.features.internal.repository.XmlRepository;
import org.apache.karaf.features.internal.resolver.Slf4jResolverLog;
import org.apache.karaf.features.internal.service.BootFeaturesInstaller;
+import org.apache.karaf.features.internal.service.BootManaged;
import org.apache.karaf.features.internal.service.EventAdminListener;
import org.apache.karaf.features.internal.service.FeatureConfigInstaller;
import org.apache.karaf.features.internal.service.FeatureRepoFinder;
import org.apache.karaf.features.internal.service.FeaturesServiceConfig;
import org.apache.karaf.features.internal.service.FeaturesServiceImpl;
+import org.apache.karaf.features.internal.service.SimpleFeaturesServiceImpl;
import org.apache.karaf.features.internal.service.BundleInstallSupport;
import org.apache.karaf.features.internal.service.BundleInstallSupportImpl;
import org.apache.karaf.features.internal.service.StateStorage;
@@ -101,6 +103,7 @@ public class Activator extends BaseActivator {
private ServiceTracker<FeaturesListener, FeaturesListener>
featuresListenerTracker;
private FeaturesServiceImpl featuresService;
+ private SimpleFeaturesServiceImpl simpleFeaturesService;
private StandardManageableRegionDigraph digraphMBean;
private BundleInstallSupport installSupport;
private ExecutorService executorService;
@@ -182,36 +185,68 @@ public class Activator extends BaseActivator {
Repository globalRepository = getGlobalRepository();
FeaturesServiceConfig cfg = getConfig();
StateStorage stateStorage = createStateStorage();
- featuresService = new FeaturesServiceImpl(
- stateStorage,
- featureFinder,
- configurationAdmin,
- resolver,
- installSupport,
- globalRepository,
- cfg);
- try {
- EventAdminListener eventAdminListener = new
EventAdminListener(bundleContext);
- featuresService.registerListener(eventAdminListener);
- } catch (Throwable t) {
- // No EventAdmin support in this case
+
+ String resolverKind = getString("resolver",
FeaturesService.DEFAULT_RESOLVER);
+ boolean useSimpleResolver =
FeaturesService.SIMPLE_RESOLVER.equalsIgnoreCase(resolverKind);
+ if (!useSimpleResolver &&
!FeaturesService.DEFAULT_RESOLVER.equalsIgnoreCase(resolverKind)) {
+ logger.warn("Unrecognized resolver '{}', using the '{}' resolver",
resolverKind, FeaturesService.DEFAULT_RESOLVER);
+ }
+ FeaturesService registeredService;
+
+ if (useSimpleResolver) {
+ simpleFeaturesService = new SimpleFeaturesServiceImpl(
+ stateStorage,
+ featureFinder,
+ configurationAdmin,
+ installSupport,
+ cfg);
+ try {
+ EventAdminListener eventAdminListener = new
EventAdminListener(bundleContext);
+ simpleFeaturesService.registerListener(eventAdminListener);
+ } catch (Throwable t) {
+ // No EventAdmin support in this case
+ }
+ registeredService = simpleFeaturesService;
+ } else {
+ featuresService = new FeaturesServiceImpl(
+ stateStorage,
+ featureFinder,
+ configurationAdmin,
+ resolver,
+ installSupport,
+ globalRepository,
+ cfg);
+ try {
+ EventAdminListener eventAdminListener = new
EventAdminListener(bundleContext);
+ featuresService.registerListener(eventAdminListener);
+ } catch (Throwable t) {
+ // No EventAdmin support in this case
+ }
+ registeredService = featuresService;
}
- register(FeaturesService.class, featuresService);
+ register(FeaturesService.class, registeredService);
- featuresListenerTracker = createFeatureListenerTracker();
+ featuresListenerTracker =
createFeatureListenerTracker(registeredService);
featuresListenerTracker.open();
FeaturesServiceMBeanImpl featuresServiceMBean = new
FeaturesServiceMBeanImpl();
featuresServiceMBean.setBundleContext(bundleContext);
- featuresServiceMBean.setFeaturesService(featuresService);
+ featuresServiceMBean.setFeaturesService(registeredService);
registerMBean(featuresServiceMBean, "type=feature");
String[] featuresRepositories = getStringArray("featuresRepositories",
"");
String featuresBoot = getString("featuresBoot", "");
boolean featuresBootAsynchronous =
getBoolean("featuresBootAsynchronous", false);
- BootFeaturesInstaller bootFeaturesInstaller = new
BootFeaturesInstaller(
- bundleContext, featuresService, new SystemExitManager(),
- featuresRepositories, featuresBoot, featuresBootAsynchronous);
+ BootFeaturesInstaller bootFeaturesInstaller;
+ if (useSimpleResolver) {
+ bootFeaturesInstaller = new BootFeaturesInstaller(
+ bundleContext, simpleFeaturesService, (BootManaged)
simpleFeaturesService,
+ new SystemExitManager(), featuresRepositories,
featuresBoot, featuresBootAsynchronous);
+ } else {
+ bootFeaturesInstaller = new BootFeaturesInstaller(
+ bundleContext, featuresService, new SystemExitManager(),
+ featuresRepositories, featuresBoot,
featuresBootAsynchronous);
+ }
bootFeaturesInstaller.start();
}
@@ -305,26 +340,26 @@ public class Activator extends BaseActivator {
DigraphHelper.verifyUnmanagedBundles(bundleContext, dg);
}
- private ServiceTracker<FeaturesListener, FeaturesListener>
createFeatureListenerTracker() {
+ private ServiceTracker<FeaturesListener, FeaturesListener>
createFeatureListenerTracker(FeaturesService service) {
return new ServiceTracker<>(
bundleContext,
FeaturesListener.class,
new ServiceTrackerCustomizer<FeaturesListener,
FeaturesListener>() {
@Override
public FeaturesListener
addingService(ServiceReference<FeaturesListener> reference) {
- FeaturesListener service =
bundleContext.getService(reference);
- featuresService.registerListener(service);
- return service;
+ FeaturesListener listener =
bundleContext.getService(reference);
+ service.registerListener(listener);
+ return listener;
}
-
+
@Override
- public void
modifiedService(ServiceReference<FeaturesListener> reference, FeaturesListener
service) {
+ public void
modifiedService(ServiceReference<FeaturesListener> reference, FeaturesListener
listener) {
}
-
+
@Override
- public void
removedService(ServiceReference<FeaturesListener> reference, FeaturesListener
service) {
- if (featuresService != null && service != null) {
- featuresService.unregisterListener(service);
+ public void
removedService(ServiceReference<FeaturesListener> reference, FeaturesListener
listener) {
+ if (service != null && listener != null) {
+ service.unregisterListener(listener);
}
if (bundleContext != null && reference != null) {
bundleContext.ungetService(reference);
@@ -348,6 +383,10 @@ public class Activator extends BaseActivator {
featuresService.stop();
featuresService = null;
}
+ if (simpleFeaturesService != null) {
+ simpleFeaturesService.stop();
+ simpleFeaturesService = null;
+ }
if (installSupport != null) {
installSupport.unregister();
installSupport.saveDigraph();
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
index 9036360d4d..1a9b9d29bb 100644
---
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
@@ -32,13 +32,14 @@ public class BootFeaturesInstaller {
private static final Logger LOGGER =
LoggerFactory.getLogger(BootFeaturesInstaller.class);
private static final String REQUIRE_SUCCESSFUL_BOOT =
"karaf.require.successful.features.boot";
- private final FeaturesServiceImpl featuresService;
+ private final FeaturesService featuresService;
+ private final BootManaged bootManaged;
private final BundleContext bundleContext;
private final ExitManager exitManager;
private final String[] repositories;
private final String features;
private final boolean asynchronous;
-
+
/**
* The Unix separator character.
*/
@@ -53,7 +54,7 @@ public class BootFeaturesInstaller {
* The system separator character.
*/
private static final char SYSTEM_SEPARATOR = File.separatorChar;
-
+
public BootFeaturesInstaller(BundleContext bundleContext,
FeaturesServiceImpl featuresService,
ExitManager exitManager,
@@ -62,6 +63,23 @@ public class BootFeaturesInstaller {
boolean asynchronous) {
this.bundleContext = bundleContext;
this.featuresService = featuresService;
+ this.bootManaged = featuresService;
+ this.exitManager = exitManager;
+ this.repositories = repositories;
+ this.features = features;
+ this.asynchronous = asynchronous;
+ }
+
+ public BootFeaturesInstaller(BundleContext bundleContext,
+ FeaturesService featuresService,
+ BootManaged bootManaged,
+ ExitManager exitManager,
+ String[] repositories,
+ String features,
+ boolean asynchronous) {
+ this.bundleContext = bundleContext;
+ this.featuresService = featuresService;
+ this.bootManaged = bootManaged;
this.exitManager = exitManager;
this.repositories = repositories;
this.features = features;
@@ -72,7 +90,7 @@ public class BootFeaturesInstaller {
* Install boot features
*/
public void start() {
- if (featuresService.isBootDone()) {
+ if (bootManaged.isBootDone()) {
publishBootFinished();
return;
}
@@ -104,7 +122,7 @@ public class BootFeaturesInstaller {
}
featuresService.installFeatures(features, options);
}
- featuresService.bootDone();
+ bootManaged.bootDone();
publishBootFinished();
} catch (Throwable e) {
// Special handling in case the bundle has been refreshed.
diff --git
a/features/core/src/main/java/org/apache/karaf/features/internal/service/BootManaged.java
b/features/core/src/main/java/org/apache/karaf/features/internal/service/BootManaged.java
new file mode 100644
index 0000000000..c6a82ca1dc
--- /dev/null
+++
b/features/core/src/main/java/org/apache/karaf/features/internal/service/BootManaged.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+/**
+ * Interface for features service implementations that support boot lifecycle
management.
+ */
+public interface BootManaged {
+
+ boolean isBootDone();
+
+ void bootDone();
+
+}
diff --git
a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
index 39e9533a36..80c08631ab 100644
---
a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
+++
b/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
@@ -100,7 +100,7 @@ import static
org.apache.karaf.features.internal.util.MapUtils.*;
/**
*
*/
-public class FeaturesServiceImpl implements FeaturesService,
Deployer.DeployCallback {
+public class FeaturesServiceImpl implements FeaturesService, BootManaged,
Deployer.DeployCallback {
private static final String RESOLVE_FILE = "resolve";
private static final Logger LOGGER =
LoggerFactory.getLogger(FeaturesServiceImpl.class);
@@ -243,13 +243,15 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
}
}
- boolean isBootDone() {
+ @Override
+ public boolean isBootDone() {
synchronized (lock) {
return state.bootDone.get();
}
}
- void bootDone() {
+ @Override
+ public void bootDone() {
synchronized (lock) {
state.bootDone.set(true);
saveState();
diff --git
a/features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleDeployer.java
b/features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleDeployer.java
new file mode 100644
index 0000000000..3b806de050
--- /dev/null
+++
b/features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleDeployer.java
@@ -0,0 +1,582 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.karaf.features.internal.service;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.karaf.features.BundleInfo;
+import org.apache.karaf.features.Conditional;
+import org.apache.karaf.features.Dependency;
+import org.apache.karaf.features.DeploymentEvent;
+import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeatureEvent;
+import org.apache.karaf.features.FeatureState;
+import org.apache.karaf.features.FeaturesService;
+import org.apache.karaf.features.FeaturesService.Option;
+import org.apache.karaf.features.internal.download.DownloadManager;
+import org.apache.karaf.features.internal.download.Downloader;
+import org.apache.karaf.features.internal.download.StreamProvider;
+import org.apache.karaf.features.internal.util.MultiException;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.startlevel.BundleStartLevel;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.osgi.framework.Bundle.ACTIVE;
+import static org.osgi.framework.Bundle.UNINSTALLED;
+
+/**
+ * A simple deployer that installs features without using OSGi
capabilities/requirements resolution.
+ * Features are installed in the order they are defined: first feature
dependencies (recursively),
+ * then bundles in declaration order.
+ */
+public class SimpleDeployer {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SimpleDeployer.class);
+
+ /**
+ * Callback interface for the simple deployer to interact with the
features service
+ * and OSGi framework.
+ */
+ public interface SimpleDeployCallback {
+ void print(String message, boolean verbose);
+ void saveState(State state);
+ void callListeners(DeploymentEvent deployEvent);
+ void callListeners(FeatureEvent featureEvent);
+ Bundle installBundle(String region, String uri, InputStream is) throws
BundleException;
+ void updateBundle(Bundle bundle, String uri, InputStream is) throws
BundleException;
+ void uninstall(Bundle bundle) throws BundleException;
+ void startBundle(Bundle bundle) throws BundleException;
+ void stopBundle(Bundle bundle, int options) throws BundleException;
+ void setBundleStartLevel(Bundle bundle, int startLevel);
+ void refreshPackages(Collection<Bundle> bundles) throws
InterruptedException;
+ void installConfigs(Feature feature) throws IOException;
+ void installLibraries(Feature feature) throws IOException;
+ void deleteConfigs(Feature feature) throws IOException;
+ BundleInstallSupport.FrameworkInfo getInfo();
+ }
+
+ private final DownloadManager downloadManager;
+ private final SimpleDeployCallback callback;
+
+ public SimpleDeployer(DownloadManager downloadManager,
SimpleDeployCallback callback) {
+ this.downloadManager = downloadManager;
+ this.callback = callback;
+ }
+
+ /**
+ * Deploy the given requirements.
+ *
+ * @param featuresById all available features indexed by id
+ * @param featuresByName all available features indexed by name, then
version
+ * @param requirements the required features per region
+ * @param state current state
+ * @param options deployment options
+ */
+ public void deploy(Map<String, Feature> featuresById,
+ Map<String, Map<String, Feature>> featuresByName,
+ Map<String, Set<String>> requirements,
+ State state,
+ EnumSet<Option> options) throws Exception {
+ boolean noStart = options.contains(Option.NoAutoStartBundles);
+ boolean verbose = options.contains(Option.Verbose);
+ boolean simulate = options.contains(Option.Simulate);
+ boolean deleteConfigurations =
options.contains(Option.DeleteConfigurations);
+
+ BundleInstallSupport.FrameworkInfo info = callback.getInfo();
+ int initialBundleStartLevel = info.initialBundleStartLevel;
+
+ // Resolve features from requirements
+ Set<String> requiredFeatureIds = new LinkedHashSet<>();
+ for (Map.Entry<String, Set<String>> entry : requirements.entrySet()) {
+ for (String req : entry.getValue()) {
+ FeatureReq featureReq = FeatureReq.parseRequirement(req);
+ if (featureReq != null) {
+ Feature feature = findFeature(featureReq, featuresByName);
+ if (feature != null) {
+ requiredFeatureIds.add(feature.getId());
+ }
+ }
+ }
+ }
+
+ // Flatten features in install order (dependencies first)
+ List<Feature> orderedFeatures = new ArrayList<>();
+ Set<String> visited = new HashSet<>();
+ for (String featureId : requiredFeatureIds) {
+ Feature feature = featuresById.get(featureId);
+ if (feature != null) {
+ collectFeaturesInOrder(feature, featuresById, featuresByName,
orderedFeatures, visited);
+ }
+ }
+
+ // Determine currently installed features
+ Set<String> previouslyInstalledIds = new TreeSet<>();
+ for (Set<String> ids : state.installedFeatures.values()) {
+ previouslyInstalledIds.addAll(ids);
+ }
+
+ // Fold in conditional features (<conditional>) whose condition(s) are
satisfied
+ // by the set of features being installed, e.g. a feature's
conditional on "shell".
+ resolveConditionalFeatures(orderedFeatures, featuresById,
featuresByName, visited, previouslyInstalledIds);
+
+ // Determine new full set of installed features
+ Set<String> newInstalledIds = new LinkedHashSet<>();
+ for (Feature f : orderedFeatures) {
+ newInstalledIds.add(f.getId());
+ }
+
+ // Compute changes
+ Set<String> toInstallIds = new LinkedHashSet<>(newInstalledIds);
+ toInstallIds.removeAll(previouslyInstalledIds);
+
+ Set<String> toUninstallIds = new
LinkedHashSet<>(previouslyInstalledIds);
+ toUninstallIds.removeAll(newInstalledIds);
+
+ if (toInstallIds.isEmpty() && toUninstallIds.isEmpty()) {
+ print("No deployment change.", verbose);
+ return;
+ }
+
+ if (simulate) {
+ if (!toInstallIds.isEmpty()) {
+ print("Simulation - features to install:", verbose);
+ for (String id : toInstallIds) {
+ print(" " + id, verbose);
+ }
+ }
+ if (!toUninstallIds.isEmpty()) {
+ print("Simulation - features to uninstall:", verbose);
+ for (String id : toUninstallIds) {
+ print(" " + id, verbose);
+ }
+ }
+ return;
+ }
+
+ callback.callListeners(DeploymentEvent.DEPLOYMENT_STARTED);
+
+ Map<String, Set<Long>> managedBundles = new HashMap<>();
+ for (Map.Entry<String, Set<Long>> entry :
state.managedBundles.entrySet()) {
+ managedBundles.put(entry.getKey(), new
TreeSet<>(entry.getValue()));
+ }
+ Map<Long, Long> bundleChecksums = new HashMap<>(state.bundleChecksums);
+
+ // Uninstall features being removed
+ if (!toUninstallIds.isEmpty()) {
+ uninstallFeatures(toUninstallIds, featuresById, info,
managedBundles, verbose);
+ }
+
+ // Install new features in order
+ if (!toInstallIds.isEmpty()) {
+ installFeatures(toInstallIds, featuresById, info, managedBundles,
bundleChecksums,
+ noStart, initialBundleStartLevel, verbose);
+ }
+
+ // Delete configurations for removed features
+ if (deleteConfigurations && !toUninstallIds.isEmpty()) {
+ for (String id : toUninstallIds) {
+ Feature feature = featuresById.get(id);
+ if (feature != null) {
+ try {
+ callback.deleteConfigs(feature);
+ } catch (Exception e) {
+ LOGGER.warn("Error deleting configurations for feature
{}", id, e);
+ }
+ }
+ }
+ }
+
+ // Update and save state
+ Map<String, Set<String>> installedFeatures = new HashMap<>();
+ installedFeatures.put(FeaturesService.ROOT_REGION, new
TreeSet<>(newInstalledIds));
+ Map<String, Map<String, String>> stateFeatures = new HashMap<>();
+ Map<String, String> featureStates = new HashMap<>();
+ for (String id : newInstalledIds) {
+ featureStates.put(id, noStart ? FeatureState.Installed.name() :
FeatureState.Started.name());
+ }
+ stateFeatures.put(FeaturesService.ROOT_REGION, featureStates);
+
+ State newState = new State();
+ newState.bundleChecksums.putAll(bundleChecksums);
+ newState.requirements.putAll(requirements);
+ newState.installedFeatures.putAll(installedFeatures);
+ newState.stateFeatures.putAll(stateFeatures);
+ newState.managedBundles.putAll(managedBundles);
+ callback.saveState(newState);
+
+ // Fire events
+ for (String id : toUninstallIds) {
+ Feature feature = featuresById.get(id);
+ if (feature != null) {
+ callback.callListeners(new
FeatureEvent(FeatureEvent.EventType.FeatureUninstalled,
+ feature, FeaturesService.ROOT_REGION, false));
+ }
+ }
+ for (String id : toInstallIds) {
+ Feature feature = featuresById.get(id);
+ if (feature != null) {
+ callback.callListeners(new
FeatureEvent(FeatureEvent.EventType.FeatureInstalled,
+ feature, FeaturesService.ROOT_REGION, false));
+ }
+ }
+
+ callback.callListeners(DeploymentEvent.DEPLOYMENT_FINISHED);
+ print("Done.", verbose);
+ }
+
+ private void installFeatures(Set<String> featureIds,
+ Map<String, Feature> featuresById,
+ BundleInstallSupport.FrameworkInfo info,
+ Map<String, Set<Long>> managedBundles,
+ Map<Long, Long> bundleChecksums,
+ boolean noStart,
+ int initialBundleStartLevel,
+ boolean verbose) throws Exception {
+ Downloader downloader = downloadManager.createDownloader();
+ for (String featureId : featureIds) {
+ Feature feature = featuresById.get(featureId);
+ if (feature == null) {
+ continue;
+ }
+ for (BundleInfo bundleInfo : feature.getBundles()) {
+ if (!bundleInfo.isBlacklisted()) {
+ downloader.download(bundleInfo.getLocation(), null);
+ }
+ }
+ }
+ downloader.await();
+
+ Map<String, StreamProvider> providers = downloadManager.getProviders();
+ List<Bundle> bundlesToStart = new ArrayList<>();
+ List<Exception> exceptions = new ArrayList<>();
+
+ for (String featureId : featureIds) {
+ Feature feature = featuresById.get(featureId);
+ if (feature == null) {
+ continue;
+ }
+
+ print("Installing feature " + feature.getName() + "/" +
feature.getVersion(), verbose);
+
+ // Install bundles in declaration order
+ for (BundleInfo bundleInfo : feature.getBundles()) {
+ if (bundleInfo.isBlacklisted()) {
+ print(" Skipping blacklisted bundle: " +
bundleInfo.getLocation(), verbose);
+ continue;
+ }
+
+ String location = bundleInfo.getLocation();
+ print(" Installing bundle: " + location, verbose);
+
+ // Check if the bundle is already installed
+ Bundle existing = findBundleByLocation(location,
info.bundles.values());
+ if (existing != null) {
+ print(" Bundle already installed: " +
existing.getSymbolicName() + "/"
+ + existing.getVersion(), verbose);
+ if (!noStart && bundleInfo.isStart() &&
existing.getState() != ACTIVE) {
+ bundlesToStart.add(existing);
+ }
+ Set<Long> regionBundles = managedBundles.computeIfAbsent(
+ FeaturesService.ROOT_REGION, k -> new TreeSet<>());
+ regionBundles.add(existing.getBundleId());
+ continue;
+ }
+
+ try {
+ Bundle bundle;
+ StreamProvider provider = providers.get(location);
+ if (provider != null) {
+ try (InputStream is = provider.open()) {
+ bundle =
callback.installBundle(FeaturesService.ROOT_REGION, location, is);
+ }
+ } else {
+ try (InputStream is = new URL(location).openStream()) {
+ bundle =
callback.installBundle(FeaturesService.ROOT_REGION, location, is);
+ }
+ }
+
+ Set<Long> regionBundles = managedBundles.computeIfAbsent(
+ FeaturesService.ROOT_REGION, k -> new TreeSet<>());
+ regionBundles.add(bundle.getBundleId());
+
+ // Set start level
+ int startLevel = bundleInfo.getStartLevel() > 0
+ ? bundleInfo.getStartLevel()
+ : initialBundleStartLevel;
+ if (startLevel != initialBundleStartLevel) {
+
bundle.adapt(BundleStartLevel.class).setStartLevel(startLevel);
+ }
+
+ if (!noStart && bundleInfo.isStart()) {
+ bundlesToStart.add(bundle);
+ }
+
+ print(" Installed: " + bundle.getSymbolicName() + "/" +
bundle.getVersion(), verbose);
+ } catch (Exception e) {
+ LOGGER.warn("Error installing bundle {}", location, e);
+ exceptions.add(e);
+ }
+ }
+
+ // Install configs and libraries
+ try {
+ callback.installConfigs(feature);
+ } catch (Exception e) {
+ LOGGER.warn("Error installing configs for feature {}",
featureId, e);
+ }
+ try {
+ callback.installLibraries(feature);
+ } catch (Exception e) {
+ LOGGER.warn("Error installing libraries for feature {}",
featureId, e);
+ }
+ }
+
+ // Start bundles after all are installed
+ callback.callListeners(DeploymentEvent.BUNDLES_INSTALLED);
+ callback.callListeners(DeploymentEvent.BUNDLES_RESOLVED);
+
+ if (!bundlesToStart.isEmpty()) {
+ print("Starting bundles:", verbose);
+ for (Bundle bundle : bundlesToStart) {
+ if (bundle.getState() != ACTIVE && bundle.getState() !=
UNINSTALLED
+ && bundle.getHeaders().get("Fragment-Host") == null) {
+ print(" " + bundle.getSymbolicName() + "/" +
bundle.getVersion(), verbose);
+ try {
+ callback.startBundle(bundle);
+ } catch (BundleException e) {
+ exceptions.add(e);
+ }
+ }
+ }
+ }
+
+ if (!exceptions.isEmpty()) {
+ throw new MultiException("Error deploying features", exceptions);
+ }
+ }
+
+ private void uninstallFeatures(Set<String> featureIds,
+ Map<String, Feature> featuresById,
+ BundleInstallSupport.FrameworkInfo info,
+ Map<String, Set<Long>> managedBundles,
+ boolean verbose) throws Exception {
+ // Collect bundles that belong to features being removed but not to
features being kept
+ Set<String> bundleLocationsToKeep = new HashSet<>();
+ for (Map.Entry<String, Feature> entry : featuresById.entrySet()) {
+ if (!featureIds.contains(entry.getKey())) {
+ for (BundleInfo bi : entry.getValue().getBundles()) {
+ bundleLocationsToKeep.add(bi.getLocation());
+ }
+ }
+ }
+
+ List<Bundle> bundlesToUninstall = new ArrayList<>();
+ for (String featureId : featureIds) {
+ Feature feature = featuresById.get(featureId);
+ if (feature == null) {
+ continue;
+ }
+ for (BundleInfo bundleInfo : feature.getBundles()) {
+ if (bundleLocationsToKeep.contains(bundleInfo.getLocation())) {
+ continue;
+ }
+ Bundle bundle = findBundleByLocation(bundleInfo.getLocation(),
info.bundles.values());
+ if (bundle != null && !bundlesToUninstall.contains(bundle)) {
+ bundlesToUninstall.add(bundle);
+ }
+ }
+ }
+
+ if (!bundlesToUninstall.isEmpty()) {
+ print("Uninstalling bundles:", verbose);
+ for (Bundle bundle : bundlesToUninstall) {
+ print(" " + bundle.getSymbolicName() + "/" +
bundle.getVersion(), verbose);
+ try {
+ callback.uninstall(bundle);
+ // Remove from managed bundles
+ Set<Long> regionBundles =
managedBundles.get(FeaturesService.ROOT_REGION);
+ if (regionBundles != null) {
+ regionBundles.remove(bundle.getBundleId());
+ }
+ } catch (BundleException e) {
+ LOGGER.warn("Error uninstalling bundle {}",
bundle.getSymbolicName(), e);
+ }
+ }
+ }
+ }
+
+ private Bundle findBundleByLocation(String location, Collection<Bundle>
bundles) {
+ for (Bundle bundle : bundles) {
+ if (location.equals(bundle.getLocation())) {
+ return bundle;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Recursively collect features in install order: dependencies first.
+ */
+ private void collectFeaturesInOrder(Feature feature,
+ Map<String, Feature> featuresById,
+ Map<String, Map<String, Feature>>
featuresByName,
+ List<Feature> result,
+ Set<String> visited) {
+ if (visited.contains(feature.getId())) {
+ return;
+ }
+ visited.add(feature.getId());
+
+ // First, process dependencies
+ for (Dependency dep : feature.getDependencies()) {
+ Feature depFeature = findDependencyFeature(dep, featuresById,
featuresByName);
+ if (depFeature != null) {
+ collectFeaturesInOrder(depFeature, featuresById,
featuresByName, result, visited);
+ } else {
+ LOGGER.warn("Could not find dependency feature: {} version
{}", dep.getName(), dep.getVersion());
+ }
+ }
+
+ // Then add this feature
+ result.add(feature);
+ }
+
+ /**
+ * Resolve {@code <conditional>} blocks: register every conditional
feature (from any known
+ * feature) so it can be looked up by id later (e.g. on uninstall), and
fold into
+ * {@code orderedFeatures} those whose condition(s) are satisfied by the
final set of features
+ * being installed. Since the simple deployer does not perform OSGi
capability resolution, this
+ * feature-name based check replaces the wiring the full resolver would
otherwise compute.
+ * <p>
+ * A {@code <conditional>} block only applies when its <em>owning</em>
feature is itself part
+ * of the install (currently being installed, or already installed) --
matching the condition
+ * token by name is not enough on its own, otherwise a conditional could
be folded in for a
+ * feature that was never selected, pulling in bundles whose exporting
bundles (the owner's own
+ * non-conditional bundles) were never installed.
+ */
+ private void resolveConditionalFeatures(List<Feature> orderedFeatures,
+ Map<String, Feature> featuresById,
+ Map<String, Map<String, Feature>>
featuresByName,
+ Set<String> visited,
+ Set<String>
previouslyInstalledIds) {
+ Map<Conditional, Feature> conditionalOwners = new LinkedHashMap<>();
+ Map<Conditional, Feature> conditionalFeatures = new LinkedHashMap<>();
+ for (Feature f : new ArrayList<>(featuresById.values())) {
+ for (Conditional cond : f.getConditional()) {
+ if (!cond.isBlacklisted()) {
+ Feature condFeature = cond.asFeature();
+ featuresById.putIfAbsent(condFeature.getId(), condFeature);
+ conditionalOwners.put(cond, f);
+ conditionalFeatures.put(cond, condFeature);
+ }
+ }
+ }
+
+ Set<String> satisfiedNames = new HashSet<>();
+ Set<String> satisfiedIds = new HashSet<>();
+ for (Feature f : orderedFeatures) {
+ satisfiedNames.add(f.getName());
+ satisfiedIds.add(f.getId());
+ }
+ for (String id : previouslyInstalledIds) {
+ satisfiedIds.add(id);
+ int sep = id.indexOf('/');
+ satisfiedNames.add(sep > 0 ? id.substring(0, sep) : id);
+ }
+
+ boolean added = true;
+ while (added) {
+ added = false;
+ for (Map.Entry<Conditional, Feature> entry :
conditionalFeatures.entrySet()) {
+ Conditional cond = entry.getKey();
+ Feature condFeature = entry.getValue();
+ if (visited.contains(condFeature.getId())) {
+ continue;
+ }
+ Feature owner = conditionalOwners.get(cond);
+ boolean ownerSatisfied = satisfiedIds.contains(owner.getId())
+ || satisfiedNames.contains(owner.getName());
+ if (ownerSatisfied && isConditionSatisfied(cond,
satisfiedNames, satisfiedIds)) {
+ collectFeaturesInOrder(condFeature, featuresById,
featuresByName, orderedFeatures, visited);
+ satisfiedNames.add(condFeature.getName());
+ satisfiedIds.add(condFeature.getId());
+ added = true;
+ }
+ }
+ }
+ }
+
+ private boolean isConditionSatisfied(Conditional cond, Set<String>
satisfiedNames, Set<String> satisfiedIds) {
+ for (String condition : cond.getCondition()) {
+ if (condition.startsWith("req:")) {
+ LOGGER.warn("Conditional requirement '{}' is not supported by
the simple resolver; "
+ + "the corresponding conditional feature will not be
installed", condition);
+ return false;
+ }
+ String[] parts = condition.split("/");
+ boolean satisfied = parts.length > 1
+ ? satisfiedIds.contains(parts[0] + "/" + parts[1])
+ : satisfiedNames.contains(parts[0]);
+ if (!satisfied) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private Feature findFeature(FeatureReq featureReq, Map<String, Map<String,
Feature>> featuresByName) {
+ return
featureReq.getMatchingFeatures(featuresByName).findFirst().orElse(null);
+ }
+
+ private Feature findDependencyFeature(Dependency dep,
+ Map<String, Feature> featuresById,
+ Map<String, Map<String, Feature>>
featuresByName) {
+ // Try exact match first
+ if (dep.hasVersion()) {
+ String id = dep.getName() + "/" + dep.getVersion();
+ Feature feature = featuresById.get(id);
+ if (feature != null) {
+ return feature;
+ }
+ }
+ // Fall back to matching by name/version range
+ FeatureReq req = dep.hasVersion()
+ ? new FeatureReq(dep.getName(), dep.getVersion())
+ : new FeatureReq(dep.getName());
+ return findFeature(req, featuresByName);
+ }
+
+ private void print(String message, boolean verbose) {
+ callback.print(message, verbose);
+ }
+
+}
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/SimpleFeaturesServiceImpl.java
similarity index 57%
copy from
features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
copy to
features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImpl.java
index 39e9533a36..1e33492b47 100644
---
a/features/core/src/main/java/org/apache/karaf/features/internal/service/FeaturesServiceImpl.java
+++
b/features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImpl.java
@@ -16,11 +16,7 @@
*/
package org.apache.karaf.features.internal.service;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.io.StringWriter;
import java.net.URI;
import java.util.ArrayDeque;
@@ -47,12 +43,10 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
-import org.apache.felix.utils.version.VersionCleaner;
import org.apache.karaf.features.BundleInfo;
import org.apache.karaf.features.DeploymentEvent;
import org.apache.karaf.features.DeploymentListener;
@@ -63,28 +57,21 @@ 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 java.io.InputStream;
import org.apache.karaf.features.internal.download.DownloadManager;
import org.apache.karaf.features.internal.download.DownloadManagers;
import org.apache.karaf.features.internal.model.Features;
import org.apache.karaf.features.internal.model.JacksonUtil;
import org.apache.karaf.features.internal.model.JaxbUtil;
-import org.apache.karaf.features.internal.region.DigraphHelper;
-import
org.apache.karaf.features.internal.service.BundleInstallSupport.FrameworkInfo;
import org.apache.karaf.util.ThreadUtils;
-import org.apache.karaf.util.json.JsonReader;
-import org.apache.karaf.util.json.JsonWriter;
import org.apache.karaf.util.collections.CopyOnWriteArrayIdentityList;
-import org.eclipse.equinox.region.RegionDigraph;
import org.ops4j.pax.url.mvn.MavenResolver;
import org.ops4j.pax.url.mvn.MavenResolvers;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleException;
import org.osgi.framework.InvalidSyntaxException;
-import org.osgi.resource.Resource;
-import org.osgi.resource.Wire;
import org.osgi.service.cm.Configuration;
import org.osgi.service.cm.ConfigurationAdmin;
-import org.osgi.service.resolver.Resolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -98,120 +85,62 @@ import static
org.apache.karaf.features.internal.service.StateStorage.toStringSt
import static org.apache.karaf.features.internal.util.MapUtils.*;
/**
- *
+ * A simplified features service implementation that uses a simple feature
resolver.
+ * <p>
+ * Unlike the standard {@link FeaturesServiceImpl}, this implementation does
not use
+ * OSGi capabilities/requirements resolution (Felix resolver). Instead, it
installs
+ * features and their bundles in the order they are defined in the feature
descriptor,
+ * resolving feature dependencies recursively.
+ * <p>
+ * This is useful for scenarios where:
+ * <ul>
+ * <li>OSGi resolver overhead is not needed</li>
+ * <li>Deterministic installation order is desired</li>
+ * <li>Features are self-contained and don't rely on package wiring for
resolution</li>
+ * </ul>
*/
-public class FeaturesServiceImpl implements FeaturesService,
Deployer.DeployCallback {
+public class SimpleFeaturesServiceImpl implements FeaturesService,
BootManaged, SimpleDeployer.SimpleDeployCallback {
- private static final String RESOLVE_FILE = "resolve";
- private static final Logger LOGGER =
LoggerFactory.getLogger(FeaturesServiceImpl.class);
+ private static final Logger LOGGER =
LoggerFactory.getLogger(SimpleFeaturesServiceImpl.class);
- /**
- * Used to load and save the {@link State} of this service.
- */
private final StateStorage storage;
private final FeatureRepoFinder featureFinder;
private final ConfigurationAdmin configurationAdmin;
- private final Resolver resolver;
private final BundleInstallSupport installSupport;
private final FeaturesServiceConfig cfg;
private RepositoryCache repositories;
private FeaturesProcessor featuresProcessor;
- private final ThreadLocal<String> outputFile = new ThreadLocal<>();
-
- /**
- * Optional global repository
- */
- private final org.osgi.service.repository.Repository globalRepository;
-
private final List<FeaturesListener> listeners = new
CopyOnWriteArrayIdentityList<>();
private final List<DeploymentListener> deploymentListeners = new
CopyOnWriteArrayIdentityList<>();
private DeploymentEvent lastDeploymentEvent =
DeploymentEvent.DEPLOYMENT_FINISHED;
- // Synchronized on lock
private final Object lock = new Object();
- /**
- * {@link State} persisted to data directory of features.core bundle.
- */
private final State state = new State();
-
private final ExecutorService executor;
- //the outer map's key is feature name, the inner map's key is feature
version
private Map<String, Map<String, Feature>> featureCache;
-
- public FeaturesServiceImpl(StateStorage storage,
- FeatureRepoFinder featureFinder,
- ConfigurationAdmin configurationAdmin,
- Resolver resolver,
- BundleInstallSupport installSupport,
- org.osgi.service.repository.Repository
globalRepository,
- FeaturesServiceConfig cfg) {
+ public SimpleFeaturesServiceImpl(StateStorage storage,
+ FeatureRepoFinder featureFinder,
+ ConfigurationAdmin configurationAdmin,
+ BundleInstallSupport installSupport,
+ FeaturesServiceConfig cfg) {
this.storage = storage;
this.featureFinder = featureFinder;
this.configurationAdmin = configurationAdmin;
- this.resolver = resolver;
this.installSupport = installSupport;
- this.globalRepository = globalRepository;
this.featuresProcessor = new FeaturesProcessorImpl(cfg);
this.repositories = new RepositoryCacheImpl(featuresProcessor);
this.cfg = cfg;
- this.executor =
Executors.newSingleThreadExecutor(ThreadUtils.namedThreadFactory("features"));
+ this.executor =
Executors.newSingleThreadExecutor(ThreadUtils.namedThreadFactory("simple-features"));
loadState();
- checkResolve();
}
public void stop() {
this.executor.shutdown();
}
- @SuppressWarnings({"unchecked", "rawtypes"})
- private void checkResolve() {
- File resolveFile = installSupport.getDataFile(RESOLVE_FILE);
- if (resolveFile == null || !resolveFile.exists()) {
- return;
- }
- Map<String, Object> request;
- try (
- FileInputStream fis = new FileInputStream(resolveFile)
- ) {
- request = (Map<String, Object>) JsonReader.read(fis);
- } catch (IOException e) {
- LOGGER.warn("Error reading resolution request", e);
- return;
- }
- Map<String, Set<String>> requestedFeatures =
toStringStringSetMap((Map) request.get("features"));
- Collection<String> opts = (Collection<String>) request.get("options");
- EnumSet<Option> options = EnumSet.noneOf(Option.class);
- for (String opt : opts) {
- options.add(Option.valueOf(opt));
- }
- // Resolve
- try {
- Map<String, Map<String, FeatureState>> stateChanges =
Collections.emptyMap();
- doProvisionInThread(requestedFeatures, stateChanges, copyState(),
getFeaturesById(), options);
- } catch (Exception e) {
- LOGGER.warn("Error updating state", e);
- }
- }
-
- private void writeResolve(Map<String, Set<String>> requestedFeatures,
EnumSet<Option> options) throws IOException {
- File resolveFile = installSupport.getDataFile(RESOLVE_FILE);
- Map<String, Object> request = new HashMap<>();
- List<String> opts = new ArrayList<>();
- for (Option opt : options) {
- opts.add(opt.toString());
- }
- request.put("features", requestedFeatures);
- request.put("options", opts);
- try (
- FileOutputStream fos = new FileOutputStream(resolveFile)
- ) {
- JsonWriter.write(fos, request);
- }
- }
-
//
// State support
//
@@ -222,34 +151,30 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
storage.load(state);
}
} catch (IOException e) {
- LOGGER.warn("Error loading FeaturesService state", e);
+ LOGGER.warn("Error loading SimpleFeaturesService 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
(!SnapshotUpdateBehavior.Crc.getValue().equalsIgnoreCase(cfg.updateSnapshots)) {
- state.bundleChecksums.clear();
- }
storage.save(state);
installSupport.saveDigraph();
}
} catch (IOException e) {
- LOGGER.warn("Error saving FeaturesService state", e);
+ LOGGER.warn("Error saving SimpleFeaturesService state", e);
}
}
- boolean isBootDone() {
+ @Override
+ public boolean isBootDone() {
synchronized (lock) {
return state.bootDone.get();
}
}
- void bootDone() {
+ @Override
+ public void bootDone() {
synchronized (lock) {
state.bootDone.set(true);
saveState();
@@ -301,9 +226,6 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
deploymentListeners.remove(listener);
}
- /*
- * Should never be called while holding a lock as we're calling outside
our bundle.
- */
@Override
public void callListeners(FeatureEvent event) {
for (FeaturesListener listener : listeners) {
@@ -311,9 +233,6 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
}
}
- /*
- * Should never be called while holding a lock as we're calling outside
our bundle.
- */
@Override
public void callListeners(DeploymentEvent event) {
lastDeploymentEvent = event;
@@ -326,9 +245,6 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
}
}
- /*
- * Should never be called while holding a lock as we're calling outside
our bundle.
- */
protected void callListeners(RepositoryEvent event) {
for (FeaturesListener listener : listeners) {
listener.repositoryEvent(event);
@@ -349,7 +265,6 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
return featureFinder.getNames();
}
-
//
// Repositories support
//
@@ -386,321 +301,177 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
Repository repository = repositories.create(uri, true);
synchronized (lock) {
repositories.addRepository(repository);
+ state.repositories.add(uri.toString());
featureCache = null;
- // Add repo
- if (!state.repositories.add(uri.toString())) {
- return;
- }
saveState();
}
callListeners(new RepositoryEvent(repository,
RepositoryEvent.EventType.RepositoryAdded, false));
- // install the features in the repo
if (install) {
- HashSet<String> features = new HashSet<>();
- for (Feature feature : repository.getFeatures()) {
- features.add(feature.getId());
+ Feature[] features = repository.getFeatures();
+ Set<String> featureNames = new HashSet<>();
+ for (Feature f : features) {
+ featureNames.add(f.getName());
}
- installFeatures(features,
EnumSet.noneOf(FeaturesService.Option.class));
+ installFeatures(featureNames, EnumSet.noneOf(Option.class));
}
}
@Override
public void removeRepository(URI uri) throws Exception {
- removeRepository(uri, true);
+ removeRepository(uri, false);
}
@Override
public void removeRepository(URI uri, boolean uninstall) throws Exception {
- // This will also ensure the cache is loaded
- Repository repo = getRepository(uri);
- if (repo == null) {
- return;
- }
-
- Map<String, Set<String>> reqsToRemove;
- Set<String> features;
+ Repository repository;
synchronized (lock) {
- getFeatureCache();
- features = new HashSet<>();
- for (Set<String> reqs : state.requirements.values()) {
- features.addAll(reqs);
- }
- Set<Repository> repos = new HashSet<>();
- for (String r : state.repositories) {
- if (!uri.toString().equals(r)) {
- Repository rep = repositories.getRepository(r);
- if (rep != null) {
- repos.addAll(repositories.getRepositoryClosure(rep));
- } else {
- throw new IllegalArgumentException("Repository URI " +
uri + " seems to have changed, can't remove repository");
- }
- }
- }
- for (Repository rep : repos) {
- for (Feature f : rep.getFeatures()) {
- features.remove(new FeatureReq(f).toRequirement());
- }
- }
- reqsToRemove = new HashMap<>();
- for (Map.Entry<String, Set<String>> entry :
state.requirements.entrySet()) {
- Set<String> reqs = new HashSet<>(entry.getValue());
- reqs.retainAll(features);
- if (!reqs.isEmpty()) {
- reqsToRemove.put(entry.getKey(), reqs);
- }
+ repository = repositories.getRepository(uri.toString());
+ if (repository == null) {
+ return;
}
- }
-
- if (!features.isEmpty()) {
if (uninstall) {
- removeRequirements(reqsToRemove, EnumSet.noneOf(Option.class));
- } else {
- throw new IllegalStateException("The following features are
required from the repository: " + String.join(", ", features));
- }
- }
-
- synchronized (lock) {
- // Remove repo
- if (!state.repositories.remove(uri.toString())) {
- return;
+ Feature[] features = repository.getFeatures();
+ Set<String> featureNames = new HashSet<>();
+ for (Feature f : features) {
+ featureNames.add(f.getName());
+ }
+ uninstallFeatures(featureNames, EnumSet.noneOf(Option.class));
}
- // Clean cache
- featureCache = null;
+ state.repositories.remove(uri.toString());
repositories.removeRepository(uri);
+ featureCache = null;
saveState();
}
- callListeners(new RepositoryEvent(repo,
RepositoryEvent.EventType.RepositoryRemoved, false));
- }
-
- private Set<String> getRequiredFeatureIds(Repository repo) throws
Exception {
- synchronized (lock) {
- return Stream.of(repo.getFeatures())
- .filter(this::isRequired)
- .map(Feature::getId)
- .collect(toSet());
- }
+ callListeners(new RepositoryEvent(repository,
RepositoryEvent.EventType.RepositoryRemoved, false));
}
@Override
public void restoreRepository(URI uri) throws Exception {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void refreshRepository(URI uri) throws Exception {
- refreshRepositories(Collections.singleton(uri));
+ addRepository(uri);
}
@Override
- public void refreshRepositories(Set<URI> uris) throws Exception {
+ public Repository[] listRequiredRepositories() throws Exception {
synchronized (lock) {
- for (URI uri : uris) {
- repositories.removeRepository(uri);
- }
- featureCache = null;
+ return repositories.listMatchingRepositories(state.repositories);
}
}
@Override
public Repository[] listRepositories() throws Exception {
- ensureCacheLoaded();
- synchronized (lock) {
- return repositories.listRepositories();
- }
- }
-
- @Override
- public Repository[] listRequiredRepositories() throws Exception {
- ensureCacheLoaded();
- synchronized (lock) {
- return repositories.listMatchingRepositories(state.repositories);
- }
+ return repositories.listRepositories();
}
@Override
- public Repository getRepository(String name) throws Exception {
- ensureCacheLoaded();
- synchronized (lock) {
- return repositories.getRepositoryByName(name);
- }
+ public Repository getRepository(String repoName) throws Exception {
+ return repositories.getRepositoryByName(repoName);
}
@Override
public Repository getRepository(URI uri) throws Exception {
- ensureCacheLoaded();
- synchronized (lock) {
- return repositories.getRepository(uri.toString());
- }
+ return repositories.getRepository(uri.toString());
}
@Override
public String getRepositoryName(URI uri) throws Exception {
- Repository repo = getRepository(uri);
- return (repo != null) ? repo.getName() : null;
- }
-
- //
- // Features support
- //
-
- @Override
- public Feature getFeature(String name) throws Exception {
- Feature[] features = getFeatures(name);
- if (features.length < 1) {
- return null;
- } else {
- return features[0];
- }
+ Repository repo = repositories.getRepository(uri.toString());
+ return repo != null ? repo.getName() : null;
}
@Override
- public Feature getFeature(String name, String version) throws Exception {
- Feature[] features = getFeatures(name, version);
- if (features.length < 1) {
- return null;
- } else {
- return features[0];
- }
- }
-
- @Override
- public Feature[] getFeatures(String nameOrId) throws Exception {
- return getFeatures(FeatureReq.parseNameAndRange(nameOrId));
+ public void setResolutionOutputFile(String outputFile) {
+ // Not used in simple mode
}
@Override
- public Feature[] getFeatures(String name, String version) throws Exception
{
- return getFeatures(new FeatureReq(name, version));
- }
-
- private Feature[] getFeatures(FeatureReq featureReq) throws Exception {
- Map<String, Map<String, Feature>> allFeatures = getFeatureCache();
- return
featureReq.getMatchingFeatures(allFeatures).toArray(Feature[]::new);
- }
-
- private void ensureCacheLoaded() throws Exception {
- getFeatureCache();
- }
-
- /**
- * Should not be called while holding a lock.
- * @return map from feature name to map from feature version to Feature
- */
- protected Map<String, Map<String, Feature>> getFeatureCache() throws
Exception {
- Set<String> uris;
- synchronized (lock) {
- if (featureCache != null) {
- return featureCache;
- }
- uris = new TreeSet<>(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<>();
- // Two phase load:
- // * first load dependent repositories
- Set<String> loaded = new HashSet<>();
- Queue<String> toLoad = new ArrayDeque<>(uris);
- while (!toLoad.isEmpty()) {
- String uri = toLoad.remove();
- Repository repo;
- synchronized (lock) {
- repo = repositories.getRepository(uri);
- }
- try {
- if (repo == null) {
- repo = repositories.create(URI.create(uri), false);
- synchronized (lock) {
- repositories.addRepository(repo);
- }
- }
- if (loaded.add(uri)) {
- for (URI u : repo.getRepositories()) {
- toLoad.add(u.toString());
- }
- }
- } catch (Exception e) {
- LOGGER.warn("Can't load features repository {}", uri, e);
- }
- }
- List<Repository> repos;
- synchronized (lock) {
- repos = Arrays.asList(repositories.listRepositories());
- }
- // * then load all features
- for (Repository repo : repos) {
- for (Feature f : repo.getFeatures()) {
- Map<String, Feature> versionMap =
map.computeIfAbsent(f.getName(), key -> new HashMap<>());
- versionMap.put(f.getVersion(), f);
- }
- }
+ public void refreshRepositories(Set<URI> uris) throws Exception {
synchronized (lock) {
- if (uris.equals(state.repositories)) {
- featureCache = map;
+ for (URI uri : uris) {
+ repositories.removeRepository(uri);
+ repositories.addRepository(repositories.create(uri, true));
}
+ featureCache = null;
}
- return map;
}
- protected Map<String, Feature> getFeaturesById() throws Exception {
- return getFeatureCache().values().stream().flatMap(m ->
m.values().stream())
- .collect(Collectors.toMap(Feature::getId,
Function.identity()));
+ @Override
+ public void refreshRepository(URI uri) throws Exception {
+ refreshRepositories(Collections.singleton(uri));
}
- //
- // Installed features
+ //
+ // Features support
//
@Override
public Feature[] listFeatures() throws Exception {
- Map<String, Map<String, Feature>> allFeatures = getFeatureCache();
- return flattenFeatures(allFeatures, f -> true);
+ List<Feature> features = new ArrayList<>();
+ for (Map<String, Feature> versions : getFeatureCache().values()) {
+ features.addAll(versions.values());
+ }
+ return features.toArray(new Feature[0]);
}
@Override
- public Feature[] listInstalledFeatures() throws Exception {
- Map<String, Map<String, Feature>> allFeatures = getFeatureCache();
+ public Feature[] listRequiredFeatures() throws Exception {
+ Set<String> installed;
synchronized (lock) {
- return flattenFeatures(allFeatures, this::isInstalled);
+ installed = new HashSet<>();
+ for (Set<String> reqs : state.requirements.values()) {
+ installed.addAll(reqs);
+ }
+ }
+ List<Feature> result = new ArrayList<>();
+ Map<String, Map<String, Feature>> allFeatures = getFeatureCache();
+ for (String req : installed) {
+ FeatureReq featureReq = FeatureReq.parseRequirement(req);
+ if (featureReq != null) {
+
featureReq.getMatchingFeatures(allFeatures).forEach(result::add);
+ }
}
+ return result.toArray(new Feature[0]);
}
@Override
- public Feature[] listRequiredFeatures() throws Exception {
- Map<String, Map<String, Feature>> allFeatures = getFeatureCache();
+ public Feature[] listInstalledFeatures() throws Exception {
+ Set<String> installed;
synchronized (lock) {
- return flattenFeatures(allFeatures, this::isRequired);
+ installed = new HashSet<>();
+ for (Set<String> ids : state.installedFeatures.values()) {
+ installed.addAll(ids);
+ }
}
- }
-
- private Feature[] flattenFeatures(Map<String, Map<String, Feature>>
features, Predicate<Feature> pred) {
- return features.values().stream()
- .map(Map::values)
- .flatMap(Collection::stream)
- .filter(pred)
- .toArray(Feature[]::new);
+ Map<String, Feature> featuresById = getFeaturesById();
+ List<Feature> result = new ArrayList<>();
+ for (String id : installed) {
+ Feature f = featuresById.get(id);
+ if (f != null) {
+ result.add(f);
+ }
+ }
+ return result.toArray(new Feature[0]);
}
@Override
public boolean isInstalled(Feature f) {
- String id = normalize(f.getId());
synchronized (lock) {
Set<String> installed = state.installedFeatures.get(ROOT_REGION);
- return installed != null && installed.contains(id);
+ return installed != null && installed.contains(f.getId());
}
}
@Override
public FeatureState getState(String featureId) {
- String id = normalize(featureId);
synchronized (lock) {
- Set<String> installed = state.installedFeatures.get(ROOT_REGION);
- if (!installed.contains(id)) {
- return FeatureState.Uninstalled;
- } else {
- String stateSt = state.stateFeatures.get(ROOT_REGION).get(id);
- return FeatureState.valueOf(stateSt);
+ Map<String, String> regionStates =
state.stateFeatures.get(ROOT_REGION);
+ if (regionStates != null) {
+ String st = regionStates.get(featureId);
+ if (st != null) {
+ return FeatureState.valueOf(st);
+ }
}
}
+ return null;
}
@Override
@@ -712,6 +483,28 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
}
}
+ @Override
+ public Feature getFeature(String name) throws Exception {
+ return getFeature(name, null);
+ }
+
+ @Override
+ public Feature getFeature(String name, String version) throws Exception {
+ Feature[] features = getFeatures(name, version);
+ return features.length > 0 ? features[0] : null;
+ }
+
+ @Override
+ public Feature[] getFeatures(String name) throws Exception {
+ return getFeatures(name, null);
+ }
+
+ @Override
+ public Feature[] getFeatures(String name, String version) throws Exception
{
+ Map<String, Map<String, Feature>> allFeatures = getFeatureCache();
+ return new FeatureReq(name,
version).getMatchingFeatures(allFeatures).toArray(Feature[]::new);
+ }
+
//
// Installation and uninstallation of features
//
@@ -746,50 +539,6 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
installFeatures(features, ROOT_REGION, options);
}
- @Override
- public void uninstallFeature(String name, String version) throws Exception
{
- uninstallFeature(getId(name, version));
- }
-
- @Override
- public void uninstallFeature(String name, String version, EnumSet<Option>
options) throws Exception {
- uninstallFeature(getId(name, version), 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);
- }
-
- @Override
- public void uninstallFeatures(Set<String> features, EnumSet<Option>
options) throws Exception {
- uninstallFeatures(features, ROOT_REGION, options);
- }
-
- private String getId(String name, String version) {
- return version != null ? name + VERSION_SEPARATOR + version : name;
- }
-
-
- //
- //
- //
- // RESOLUTION
- //
- //
- //
-
-
- @Override
- public void setResolutionOutputFile(String outputFile) {
- this.outputFile.set(outputFile);
- }
-
@Override
public void installFeatures(Set<String> featuresIn, String region,
EnumSet<Option> options) throws Exception {
Set<FeatureReq> toInstall = map(featuresIn,
FeatureReq::parseNameAndRange);
@@ -811,7 +560,7 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
});
List<FeatureReq> notBlacklisted = toAdd.stream()
.filter(fr ->
!fr.isBlacklisted()).collect(Collectors.toList());
- if (notBlacklisted.size() > 0) {
+ if (!notBlacklisted.isEmpty()) {
print("Adding features: " + join(notBlacklisted),
options.contains(Option.Verbose));
}
@@ -823,40 +572,32 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
}
}
- doProvisionInThread(requires, emptyMap(), state, getFeaturesById(),
options);
+ doProvisionInThread(requires, state, options);
}
-
- private Set<FeatureReq> computeFeaturesToAdd(EnumSet<Option> options,
- Set<FeatureReq> toInstall)
throws Exception {
- Map<String, Map<String, Feature>> allFeatures = getFeatureCache();
- Feature[] installedFeatures = listInstalledFeatures();
- Set<FeatureReq> toAdd = new HashSet<>();
- for (FeatureReq featureReq : toInstall) {
- Collection<Feature> matching =
featureReq.getMatchingFeatures(allFeatures).collect(toSet());
- for (Feature f : matching) {
- toAdd.add(new FeatureReq(f));
- Arrays.stream(installedFeatures).filter(fi -> isSameFeature(f,
fi)).forEach(this::logInstalledOrUpdated);
- }
- if (matching.isEmpty() &&
!options.contains(Option.NoFailOnFeatureNotFound)) {
- throw new IllegalArgumentException("No matching features for "
+ featureReq);
- }
- }
- return toAdd;
+
+ @Override
+ public void uninstallFeature(String name) throws Exception {
+ uninstallFeature(name, EnumSet.noneOf(Option.class));
}
- private void logInstalledOrUpdated(Feature f) {
- String msg = f.getVersion().endsWith("SNAPSHOT") ? "has been
upgraded": "is already installed";
- LOGGER.info("The specified feature: '{}' version '{}' {}",
f.getName(), f.getVersion(), msg);
+ @Override
+ public void uninstallFeature(String name, EnumSet<Option> options) throws
Exception {
+ uninstallFeatures(Collections.singleton(name), options);
}
- private boolean isSameFeature(Feature a, Feature b) {
- return b.getName().equals(a.getName()) &&
b.getVersion().equals(a.getVersion());
+ @Override
+ public void uninstallFeature(String name, String version) throws Exception
{
+ uninstallFeature(getId(name, version), EnumSet.noneOf(Option.class));
}
- private Set<FeatureReq> computeFeaturesToRemoveOnUpdate(Set<FeatureReq>
featuresToAdd,
- Set<FeatureReq>
existingFeatures) throws Exception {
- Set<String> namedToAdd = map(featuresToAdd, FeatureReq::getName);
- return filter(existingFeatures, f -> namedToAdd.contains(f.getName())
&& !featuresToAdd.contains(f));
+ @Override
+ public void uninstallFeature(String name, String version, EnumSet<Option>
options) throws Exception {
+ uninstallFeature(getId(name, version), options);
+ }
+
+ @Override
+ public void uninstallFeatures(Set<String> features, EnumSet<Option>
options) throws Exception {
+ uninstallFeatures(features, ROOT_REGION, options);
}
@Override
@@ -882,13 +623,23 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
if (requirements.isEmpty()) {
required.remove(region);
}
- doProvisionInThread(required, emptyMap(), state, getFeaturesById(),
options);
+ doProvisionInThread(required, state, options);
}
@Override
public void updateFeaturesState(Map<String, Map<String, FeatureState>>
stateChanges, EnumSet<Option> options) throws Exception {
- State state = copyState();
- doProvisionInThread(copy(state.requirements), stateChanges, state,
getFeaturesById(), options);
+ // In simple mode, state changes are not supported via resolver
+ // Just update the state directly
+ synchronized (lock) {
+ for (Map.Entry<String, Map<String, FeatureState>> regionEntry :
stateChanges.entrySet()) {
+ Map<String, String> regionStates =
state.stateFeatures.computeIfAbsent(
+ regionEntry.getKey(), k -> new HashMap<>());
+ for (Map.Entry<String, FeatureState> featureEntry :
regionEntry.getValue().entrySet()) {
+ regionStates.put(featureEntry.getKey(),
featureEntry.getValue().name());
+ }
+ }
+ saveState();
+ }
}
@Override
@@ -896,7 +647,7 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
State state = copyState();
Map<String, Set<String>> required = copy(state.requirements);
add(required, requirements);
- doProvisionInThread(required, emptyMap(), state, getFeaturesById(),
options);
+ doProvisionInThread(required, state, options);
}
@Override
@@ -904,14 +655,13 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
State state = copyState();
Map<String, Set<String>> required = copy(state.requirements);
remove(required, requirements);
- doProvisionInThread(required, emptyMap(), state, getFeaturesById(),
options);
+ doProvisionInThread(required, state, options);
}
@Override
public void updateReposAndRequirements(Set<URI> repos, Map<String,
Set<String>> requirements, EnumSet<Option> options) throws Exception {
State stateCopy;
synchronized (lock) {
- // Remove repo
Set<String> reps = map(repos, URI::toString);
Set<String> toRemove = diff(state.repositories, reps);
Set<String> toAdd = diff(reps, state.repositories);
@@ -927,7 +677,7 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
saveState();
stateCopy = state.copy();
}
- doProvisionInThread(requirements, emptyMap(), stateCopy,
getFeaturesById(), options, false);
+ doProvisionInThread(requirements, stateCopy, options);
}
@Override
@@ -942,71 +692,131 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
}
}
+ @Override
+ public String getFeatureXml(Feature feature) {
+ try {
+ StringWriter sw = new StringWriter();
+ Features r = new Features();
+
r.getFeature().add((org.apache.karaf.features.internal.model.Feature) feature);
+ JaxbUtil.marshal(r, sw);
+ String[] strs = sw.toString().split("\n");
+ StringJoiner joiner = new StringJoiner("\n");
+ for (int i = 2; i < strs.length - 1; i++) {
+ joiner.add(strs[i]);
+ }
+ return joiner.toString();
+ } catch (JAXBException e) {
+ return null;
+ }
+ }
+
+ @Override
+ public void refreshFeatures(EnumSet<Option> options) throws Exception {
+ Set<URI> uris = new LinkedHashSet<>();
+ for (Repository r : this.repositories.listRepositories()) {
+ uris.add(r.getURI());
+ }
+ this.refreshRepositories(uris);
+ this.featuresProcessor = new FeaturesProcessorImpl(cfg);
+ this.repositories = new RepositoryCacheImpl(featuresProcessor);
+
+ State state = copyState();
+ doProvisionInThread(state.requirements, state, options);
+ }
+
+ //
+ // Internal helpers
+ //
+
+ private String getId(String name, String version) {
+ return version != null ? name + VERSION_SEPARATOR + version : name;
+ }
+
private State copyState() {
synchronized (lock) {
return this.state.copy();
}
}
- private String normalize(String feature) {
- int idx = feature.indexOf(VERSION_SEPARATOR);
- if (idx < 0) {
- return feature + VERSION_SEPARATOR + DEFAULT_VERSION;
+ protected Map<String, Map<String, Feature>> getFeatureCache() throws
Exception {
+ if (featureCache == null) {
+ Map<String, Map<String, Feature>> map = new HashMap<>();
+ Set<String> uris;
+ synchronized (lock) {
+ uris = new TreeSet<>(state.repositories);
+ }
+ // Load features from all repositories (and their transitive
references)
+ Queue<String> toProcess = new ArrayDeque<>(uris);
+ Set<String> processed = new HashSet<>();
+ while (!toProcess.isEmpty()) {
+ String uri = toProcess.poll();
+ if (processed.contains(uri)) {
+ continue;
+ }
+ processed.add(uri);
+ try {
+ Repository repo = repositories.getRepository(uri);
+ if (repo == null) {
+ repo = repositories.create(URI.create(uri), false);
+ repositories.addRepository(repo);
+ }
+ for (Feature f : repo.getFeatures()) {
+ map.computeIfAbsent(f.getName(), k -> new HashMap<>())
+ .put(f.getVersion(), f);
+ }
+ for (URI repoUri : repo.getRepositories()) {
+ toProcess.add(repoUri.toString());
+ }
+ } catch (Exception e) {
+ LOGGER.warn("Error loading repository {}", uri, e);
+ }
+ }
+ featureCache = map;
}
- String name = feature.substring(0, idx);
- String version = feature.substring(idx + 1);
- return name + VERSION_SEPARATOR + VersionCleaner.clean(version);
- }
-
- /**
- * 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.
- *
- * @param requirements the provided requirements to match.
- * @param stateChanges the current features state.
- * @param state the current provisioning state.
- * @param options the provisioning options.
- * @throws Exception in case of provisioning failure.
- */
- private void doProvisionInThread(final Map<String, Set<String>>
requirements,
- final Map<String, Map<String,
FeatureState>> stateChanges,
- final State state,
- final Map<String, Feature> featureById,
- final EnumSet<Option> options) throws
Exception {
- doProvisionInThread(requirements, stateChanges, state, featureById,
options, true);
- }
-
- /**
- * 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.
- *
- * @param requirements the provided requirements to match.
- * @param stateChanges the current features state.
- * @param state the current provisioning state.
- * @param options the provisioning options.
- * @param wait wait for provisioning to complete
- * @throws Exception in case of provisioning failure.
- */
+ return featureCache;
+ }
+
+ private Map<String, Feature> getFeaturesById() throws Exception {
+ Map<String, Feature> result = new HashMap<>();
+ for (Map<String, Feature> versions : getFeatureCache().values()) {
+ for (Feature f : versions.values()) {
+ result.put(f.getId(), f);
+ }
+ }
+ return result;
+ }
+
+ private Set<FeatureReq> computeFeaturesToAdd(EnumSet<Option> options,
+ Set<FeatureReq> toInstall)
throws Exception {
+ Map<String, Map<String, Feature>> allFeatures = getFeatureCache();
+ Set<FeatureReq> toAdd = new HashSet<>();
+ for (FeatureReq featureReq : toInstall) {
+ Collection<Feature> matching =
featureReq.getMatchingFeatures(allFeatures).collect(toSet());
+ for (Feature f : matching) {
+ toAdd.add(new FeatureReq(f));
+ }
+ if (matching.isEmpty() &&
!options.contains(Option.NoFailOnFeatureNotFound)) {
+ throw new IllegalArgumentException("No matching features for "
+ featureReq);
+ }
+ }
+ return toAdd;
+ }
+
+ private Set<FeatureReq> computeFeaturesToRemoveOnUpdate(Set<FeatureReq>
featuresToAdd,
+ Set<FeatureReq>
existingFeatures) {
+ Set<String> namedToAdd = map(featuresToAdd, FeatureReq::getName);
+ return filter(existingFeatures, f -> namedToAdd.contains(f.getName())
&& !featuresToAdd.contains(f));
+ }
+
private void doProvisionInThread(final Map<String, Set<String>>
requirements,
- final Map<String, Map<String,
FeatureState>> stateChanges,
final State state,
- final Map<String, Feature> featureById,
- final EnumSet<Option> options,
- boolean wait) throws Exception {
+ final EnumSet<Option> options) throws
Exception {
try {
- final String outputFile = this.outputFile.get();
- this.outputFile.set(null);
Future<Object> future = executor.submit(() -> {
- doProvision(requirements, stateChanges, state, featureById,
options, outputFile);
+ doProvision(requirements, state, options);
return null;
});
- if (wait) {
- future.get();
- }
+ future.get();
} catch (ExecutionException e) {
Throwable t = e.getCause();
if (t instanceof RuntimeException) {
@@ -1021,66 +831,14 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
}
}
- private Deployer.DeploymentState getDeploymentState(State state,
Map<String, Feature> featuresById) throws Exception {
- Deployer.DeploymentState dstate = new Deployer.DeploymentState();
- dstate.state = state;
- FrameworkInfo info = installSupport.getInfo();
- dstate.serviceBundle = info.ourBundle;
- dstate.configadminBundle = info.cmBundle;
- dstate.initialBundleStartLevel = info.initialBundleStartLevel;
- dstate.currentStartLevel = info.currentStartLevel;
- dstate.bundles = info.bundles;
- // Features
- dstate.partitionFeatures(featuresById.values());
- RegionDigraph regionDigraph = installSupport.getDiGraphCopy();
- dstate.bundlesPerRegion =
DigraphHelper.getBundlesPerRegion(regionDigraph);
- dstate.filtersPerRegion = DigraphHelper.getPolicies(regionDigraph);
- return dstate;
- }
-
- private Deployer.DeploymentRequest getDeploymentRequest(Map<String,
Set<String>> requirements, Map<String, Map<String, FeatureState>> stateChanges,
EnumSet<Option> options, String outputFile) {
- Deployer.DeploymentRequest request =
Deployer.DeploymentRequest.defaultDeploymentRequest();
- request.bundleUpdateRange = cfg.bundleUpdateRange;
- request.featureResolutionRange = cfg.featureResolutionRange;
- request.autoRefresh = cfg.autoRefresh;
- request.serviceRequirements =
ServiceRequirementsBehavior.fromString(cfg.serviceRequirements);
- request.updateSnaphots =
SnapshotUpdateBehavior.fromString(cfg.updateSnapshots);
- request.globalRepository = globalRepository;
- request.requirements = requirements;
- request.stateChanges = stateChanges;
- request.options = options;
- request.outputFile = outputFile;
- return request;
- }
-
- private void doProvision(Map<String, Set<String>> requirements,
// all requirements
- Map<String, Map<String, FeatureState>>
stateChanges, // features state changes
- State state,
// current state
- Map<String, Feature> featuresById,
// features by id
- EnumSet<Option> options,
// installation options
- String outputFile
// file to store the resolution or null
- ) throws Exception {
+ private void doProvision(Map<String, Set<String>> requirements,
+ State state,
+ EnumSet<Option> options) throws Exception {
try (DownloadManager manager = createDownloadManager()) {
- Set<String> prereqs = new HashSet<>();
- while (true) {
- try {
- Deployer.DeploymentState dstate =
getDeploymentState(state, featuresById);
- Deployer.DeploymentRequest request =
getDeploymentRequest(requirements, stateChanges, options, outputFile);
- new Deployer(manager, this.resolver, this).deploy(dstate,
request);
- break;
- } catch (Deployer.PartialDeploymentException e) {
- if (!prereqs.containsAll(e.getMissing())) {
- prereqs.addAll(e.getMissing());
- state = copyState();
- } else {
- throw new Exception("Deployment aborted due to loop in
missing prerequisites: " + e.getMissing());
- }
- } catch (Throwable t) {
- // Print stack trace to stdout, there may be no log anymore
- t.printStackTrace();
- throw t;
- }
- }
+ Map<String, Feature> featuresById = getFeaturesById();
+ Map<String, Map<String, Feature>> featuresByName =
getFeatureCache();
+ SimpleDeployer deployer = new SimpleDeployer(manager, this);
+ deployer.deploy(featuresById, featuresByName, requirements, state,
options);
}
}
@@ -1112,6 +870,10 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
return props;
}
+ //
+ // SimpleDeployCallback implementation
+ //
+
@Override
public void print(String message, boolean verbose) {
LOGGER.info(message);
@@ -1120,6 +882,7 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
}
}
+ @Override
public void saveState(State state) {
synchronized (lock) {
state.repositories.clear();
@@ -1131,13 +894,8 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
}
@Override
- public void persistResolveRequest(Deployer.DeploymentRequest request)
throws IOException {
- writeResolve(request.requirements, request.options);
- }
-
- @Override
- public void refreshPackages(Collection<Bundle> bundles) throws
InterruptedException {
- installSupport.refreshPackages(bundles);
+ public BundleInstallSupport.FrameworkInfo getInfo() {
+ return installSupport.getInfo();
}
@Override
@@ -1171,23 +929,26 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
}
@Override
- public void resolveBundles(Set<Bundle> bundles, Map<Resource, List<Wire>>
wiring, Map<Resource, Bundle> resToBnd) {
- installSupport.resolveBundles(bundles, wiring, resToBnd);
- }
-
- @Override
- public void replaceDigraph(Map<String, Map<String, Map<String,
Set<String>>>> policies, Map<String, Set<Long>> bundles) throws
BundleException, InvalidSyntaxException {
- installSupport.replaceDigraph(policies, bundles);
+ public void refreshPackages(Collection<Bundle> bundles) throws
InterruptedException {
+ installSupport.refreshPackages(bundles);
}
@Override
- public void installConfigs(Feature feature) throws IOException,
InvalidSyntaxException {
- installSupport.installConfigs(feature);
+ public void installConfigs(Feature feature) throws IOException {
+ try {
+ installSupport.installConfigs(feature);
+ } catch (InvalidSyntaxException e) {
+ throw new IOException("Error installing configs for " +
feature.getId(), e);
+ }
}
@Override
- public void deleteConfigs(Feature feature) throws IOException,
InvalidSyntaxException {
- installSupport.deleteConfigs(feature);
+ public void deleteConfigs(Feature feature) throws IOException {
+ try {
+ installSupport.deleteConfigs(feature);
+ } catch (InvalidSyntaxException e) {
+ throw new IOException("Error deleting configs for " +
feature.getId(), e);
+ }
}
@Override
@@ -1195,45 +956,8 @@ public class FeaturesServiceImpl implements
FeaturesService, Deployer.DeployCall
installSupport.installLibraries(feature);
}
- @Override
- public void bundleBlacklisted(BundleInfo bundleInfo) {
-
- }
-
private String join(Collection<FeatureReq> reqs) {
return
reqs.stream().map(FeatureReq::toString).collect(Collectors.joining(","));
}
- @Override
- public String getFeatureXml(Feature feature) {
- try {
- StringWriter sw = new StringWriter();
- Features r = new Features();
-
r.getFeature().add((org.apache.karaf.features.internal.model.Feature) feature);
- JaxbUtil.marshal(r, sw);
- String[] strs = sw.toString().split("\n");
- StringJoiner joiner = new StringJoiner("\n");
- for (int i = 2; i < strs.length - 1; i++) {
- joiner.add(strs[i]);
- }
- return joiner.toString();
- } catch (JAXBException e) {
- return null;
- }
- }
-
- @Override
- public void refreshFeatures(EnumSet<Option> options) throws Exception {
- Set<URI> uris = new LinkedHashSet<>();
- for (Repository r : this.repositories.listRepositories()) {
- uris.add(r.getURI());
- }
- this.refreshRepositories(uris);
- this.featuresProcessor = new FeaturesProcessorImpl(cfg);
- this.repositories = new RepositoryCacheImpl(featuresProcessor);
-
- State state = copyState();
- doProvisionInThread(state.requirements, emptyMap(), state,
getFeaturesById(), options);
- }
-
}
diff --git
a/features/core/src/test/java/org/apache/karaf/features/internal/service/AbstractFeaturesServiceTest.java
b/features/core/src/test/java/org/apache/karaf/features/internal/service/AbstractFeaturesServiceTest.java
new file mode 100644
index 0000000000..f82479f00d
--- /dev/null
+++
b/features/core/src/test/java/org/apache/karaf/features/internal/service/AbstractFeaturesServiceTest.java
@@ -0,0 +1,205 @@
+/*
+ * 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.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.net.URI;
+import java.util.Map;
+
+import org.apache.karaf.features.BundleInfo;
+import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeaturesService;
+import org.apache.karaf.features.TestBase;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+
+/**
+ * Abstract test class with tests that work against the {@link
FeaturesService} API,
+ * shared between {@link FeaturesServiceImpl} and {@link
SimpleFeaturesServiceImpl}.
+ */
+public abstract class AbstractFeaturesServiceTest extends TestBase {
+
+ /**
+ * Create a FeaturesService pre-populated with the given features in its
cache.
+ */
+ protected abstract FeaturesService createServiceWithFeatures(Feature...
features) throws Exception;
+
+ /**
+ * Create a FeaturesService that can load repositories from URIs.
+ */
+ protected abstract FeaturesService createServiceForRepoTests() throws
Exception;
+
+ protected URI createTempRepo(String repoContent, Object... variables)
throws IOException {
+ File tmp = File.createTempFile("karaf", ".feature");
+ PrintWriter pw = new PrintWriter(new FileWriter(tmp));
+ pw.printf(repoContent, variables);
+ pw.close();
+ return tmp.toURI();
+ }
+
+ //
+ // Feature query tests
+ //
+
+ @Test
+ public void testListFeatureWithoutVersion() throws Exception {
+ Feature transactionFeature = feature("transaction", "1.0.0");
+ FeaturesService svc = createServiceWithFeatures(transactionFeature);
+ assertNotNull(svc.getFeatures("transaction", null));
+ assertSame(transactionFeature, svc.getFeatures("transaction",
+
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION)[0]);
+ }
+
+ @Test
+ public void testGetFeature() throws Exception {
+ Feature transactionFeature = feature("transaction", "1.0.0");
+ FeaturesService svc = createServiceWithFeatures(transactionFeature);
+ assertNotNull(svc.getFeatures("transaction",
+
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION));
+ assertSame(transactionFeature, svc.getFeatures("transaction",
+
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION)[0]);
+ }
+
+ @Test
+ public void testGetFeatureStripVersion() throws Exception {
+ Feature transactionFeature = feature("transaction", "1.0.0");
+ FeaturesService svc = createServiceWithFeatures(transactionFeature);
+ Feature[] features = svc.getFeatures("transaction", " 1.0.0 ");
+ assertEquals(1, features.length);
+ Feature feature = features[0];
+ assertNotNull(feature);
+ assertSame("transaction", feature.getName());
+ }
+
+ @Test
+ public void testGetFeatureNotAvailable() throws Exception {
+ Feature transactionFeature = feature("transaction", "1.0.0");
+ FeaturesService svc = createServiceWithFeatures(transactionFeature);
+ assertEquals(0, svc.getFeatures("activemq",
+
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION).length);
+ }
+
+ @Test
+ public void testGetFeatureHighestAvailable() throws Exception {
+ FeaturesService svc = createServiceWithFeatures(
+ feature("transaction", "1.0.0"),
+ feature("transaction", "2.0.0"));
+ assertNotNull(svc.getFeatures("transaction",
+
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION));
+ assertEquals("2.0.0", svc.getFeatures("transaction",
+
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION)[0].getVersion());
+ }
+
+ //
+ // Repository tests
+ //
+
+ @Test
+ public void testGetFeaturesShouldHandleDifferentVersionPatterns() throws
Exception {
+ URI uri = createTempRepo("<features name='test'
xmlns='http://karaf.apache.org/xmlns/features/v1.0.0'>"
+ + " <feature name='f1' version='0.1'><feature
version='[0.1,0.3)'>f2</feature></feature>"
+ + " <feature name='f2'
version='0.1'><bundle>bundle1</bundle></feature>"
+ + " <feature name='f2'
version='0.2'><bundle>bundle2</bundle></feature>"
+ + "</features>");
+
+ FeaturesService svc = createServiceForRepoTests();
+ svc.addRepository(uri);
+
+ assertEquals(feature("f2", "0.2"), svc.getFeatures("f2",
"[0.1,0.3)")[0]);
+ assertEquals(feature("f2", "0.2"), svc.getFeatures("f2", "0.0.0")[0]);
+ assertEquals(feature("f2", "0.2"), svc.getFeatures("f2", "0.2")[0]);
+ assertEquals(0, svc.getFeatures("f2", "0.3").length);
+ }
+
+ @Test
+ public void testSchemaValidation() throws Exception {
+ URI uri = createTempRepo("<features name='test'
xmlns='http://karaf.apache.org/xmlns/features/v1.0.0'>"
+ + " <featur><bundle>somebundle</bundle></featur></features>");
+
+ FeaturesService svc = createServiceForRepoTests();
+ try {
+ svc.addRepository(uri);
+ org.junit.Assert.fail("exception expected");
+ } catch (Exception e) {
+ org.junit.Assert.assertTrue(e.getMessage().contains("Unable to
validate"));
+ }
+ }
+
+ @Test
+ public void testLoadOldFeatureFile() throws Exception {
+ URI uri = createTempRepo("<features name='test'
xmlns='http://karaf.apache.org/xmlns/features/v1.0.0'>"
+ + " <feature
name='f1'><bundle>file:bundle1</bundle><bundle>file:bundle2</bundle></feature>"
+ + "</features>");
+
+ FeaturesService svc = createServiceForRepoTests();
+ svc.addRepository(uri);
+ Feature[] features = svc.getFeatures("f1");
+ assertEquals(1, features.length);
+ Feature feat = features[0];
+ assertNotNull("No feature named f1 found", feat);
+ java.util.List<BundleInfo> bundles = feat.getBundles();
+ assertEquals(2, bundles.size());
+ }
+
+ @Test
+ public void testJsonFeatureFile() throws Exception {
+ URI uri = createTempRepo("{" +
+ "\"name\": \"test\"," +
+ "\"feature\": [" +
+ "{ \"name\": \"f1\", " +
+ "\"bundle\": [" +
+ "{ \"location\": \"file:bundle1\" }," +
+ "{ \"location\": \"file:bundle2\" }" +
+ "]" +
+ "}" +
+ "]" +
+ "}");
+
+ FeaturesService svc = createServiceForRepoTests();
+ svc.addRepository(uri);
+ Feature[] features = svc.getFeatures("f1");
+ assertEquals(1, features.length);
+ Feature feat = features[0];
+ assertNotNull("No feature named f1 found", feat);
+ java.util.List<BundleInfo> bundles = feat.getBundles();
+ assertEquals(2, bundles.size());
+ }
+
+ //
+ // Shared helpers
+ //
+
+ static class Storage extends StateStorage {
+ @Override
+ protected InputStream getInputStream() throws IOException {
+ return null;
+ }
+ @Override
+ protected OutputStream getOutputStream() throws IOException {
+ return null;
+ }
+ }
+
+}
diff --git
a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java
b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java
index c5bca449cc..53944e470e 100644
---
a/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java
+++
b/features/core/src/test/java/org/apache/karaf/features/internal/service/FeaturesServiceImplTest.java
@@ -18,8 +18,6 @@ package org.apache.karaf.features.internal.service;
import java.io.File;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.*;
import java.util.Collections;
@@ -32,7 +30,6 @@ import org.apache.felix.resolver.ResolverImpl;
import org.apache.karaf.features.Feature;
import org.apache.karaf.features.FeatureState;
import org.apache.karaf.features.FeaturesService;
-import org.apache.karaf.features.TestBase;
import org.apache.karaf.features.FeaturesService.Option;
import org.apache.karaf.features.internal.download.DownloadManager;
import org.apache.karaf.features.internal.resolver.Slf4jResolverLog;
@@ -61,9 +58,10 @@ import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
/**
- * Test cases for {@link
org.apache.karaf.features.internal.service.FeaturesServiceImpl}
+ * Test cases for {@link
org.apache.karaf.features.internal.service.FeaturesServiceImpl},
+ * including shared tests inherited from {@link AbstractFeaturesServiceTest}.
*/
-public class FeaturesServiceImplTest extends TestBase {
+public class FeaturesServiceImplTest extends AbstractFeaturesServiceTest {
Logger logger = LoggerFactory.getLogger(FeaturesServiceImplTest.class);
Resolver resolver = new ResolverImpl(new Slf4jResolverLog(logger));
@@ -79,54 +77,22 @@ public class FeaturesServiceImplTest extends TestBase {
}
} : null);
}
-
+
@After
public void after() throws Exception {
Field field = URL.class.getDeclaredField("factory");
field.setAccessible(true);
field.set(null, null);
}
-
- @Test
- public void testListFeatureWithoutVersion() throws Exception {
- Feature transactionFeature = feature("transaction", "1.0.0");
- FeaturesServiceImpl impl =
featuresServiceWithFeatures(transactionFeature);
- assertNotNull(impl.getFeatures("transaction", null));
- assertSame(transactionFeature, impl.getFeatures("transaction",
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION)[0]);
- }
- @Test
- public void testGetFeature() throws Exception {
- Feature transactionFeature = feature("transaction", "1.0.0");
- FeaturesServiceImpl impl =
featuresServiceWithFeatures(transactionFeature);
- assertNotNull(impl.getFeatures("transaction",
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION));
- assertSame(transactionFeature, impl.getFeatures("transaction",
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION)[0]);
- }
-
- @Test
- public void testGetFeatureStripVersion() throws Exception {
- Feature transactionFeature = feature("transaction", "1.0.0");
- FeaturesServiceImpl impl =
featuresServiceWithFeatures(transactionFeature);
- Feature[] features = impl.getFeatures("transaction", " 1.0.0 ");
- assertEquals(1, features.length);
- Feature feature = features[0];
- assertNotNull(feature);
- assertSame("transaction", feature.getName());
+ @Override
+ protected FeaturesService createServiceWithFeatures(Feature...
staticFeatures) throws Exception {
+ return featuresServiceWithFeatures(staticFeatures);
}
-
- @Test
- public void testGetFeatureNotAvailable() throws Exception {
- Feature transactionFeature = feature("transaction", "1.0.0");
- FeaturesServiceImpl impl =
featuresServiceWithFeatures(transactionFeature);
- assertEquals(0, impl.getFeatures("activemq",
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION).length);
- }
-
- @Test
- public void testGetFeatureHighestAvailable() throws Exception {
- FeaturesServiceImpl impl =
featuresServiceWithFeatures(feature("transaction", "1.0.0"),
-
feature("transaction", "2.0.0"));
- assertNotNull(impl.getFeatures("transaction",
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION));
- assertEquals("2.0.0", impl.getFeatures("transaction",
org.apache.karaf.features.internal.model.Feature.DEFAULT_VERSION)[0].getVersion());
+
+ @Override
+ protected FeaturesService createServiceForRepoTests() throws Exception {
+ return createTestFeatureService();
}
@Test
@@ -363,14 +329,4 @@ public class FeaturesServiceImplTest extends TestBase {
}
*/
- static class Storage extends StateStorage {
- @Override
- protected InputStream getInputStream() throws IOException {
- return null;
- }
- @Override
- protected OutputStream getOutputStream() throws IOException {
- return null;
- }
- }
}
diff --git
a/features/core/src/test/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImplTest.java
b/features/core/src/test/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImplTest.java
new file mode 100644
index 0000000000..5023d41036
--- /dev/null
+++
b/features/core/src/test/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImplTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.io.OutputStream;
+import java.util.Map;
+
+import org.apache.karaf.features.Feature;
+import org.apache.karaf.features.FeaturesService;
+import org.easymock.EasyMock;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Test cases for {@link SimpleFeaturesServiceImpl}, including shared tests
+ * inherited from {@link AbstractFeaturesServiceTest}.
+ */
+public class SimpleFeaturesServiceImplTest extends AbstractFeaturesServiceTest
{
+
+ @Override
+ protected FeaturesService createServiceWithFeatures(Feature...
staticFeatures) throws Exception {
+ final Map<String, Map<String, Feature>> features =
features(staticFeatures);
+ FeaturesServiceConfig cfg = new FeaturesServiceConfig();
+ BundleInstallSupport installSupport =
EasyMock.niceMock(BundleInstallSupport.class);
+ EasyMock.replay(installSupport);
+ return new SimpleFeaturesServiceImpl(new Storage(), null, null,
installSupport, cfg) {
+ @Override
+ protected Map<String, Map<String, Feature>> getFeatureCache()
throws Exception {
+ return features;
+ }
+ };
+ }
+
+ @Override
+ protected FeaturesService createServiceForRepoTests() throws Exception {
+ FeaturesServiceConfig cfg = new FeaturesServiceConfig();
+ BundleInstallSupport installSupport =
EasyMock.niceMock(BundleInstallSupport.class);
+ EasyMock.replay(installSupport);
+ return new SimpleFeaturesServiceImpl(new Storage(), null, null,
installSupport, cfg);
+ }
+
+ @Test
+ public void testBootManagedInterface() throws Exception {
+ FeaturesServiceConfig cfg = new FeaturesServiceConfig();
+ BundleInstallSupport installSupport =
EasyMock.niceMock(BundleInstallSupport.class);
+ EasyMock.replay(installSupport);
+ SimpleFeaturesServiceImpl svc = new SimpleFeaturesServiceImpl(new
Storage(), null, null, installSupport, cfg);
+ assertNotNull(svc);
+ assertTrue(svc instanceof BootManaged);
+ }
+}
diff --git a/manual/src/main/asciidoc/user-guide/provisioning.adoc
b/manual/src/main/asciidoc/user-guide/provisioning.adoc
index 334d3ebc9a..30a681e562 100644
--- a/manual/src/main/asciidoc/user-guide/provisioning.adoc
+++ b/manual/src/main/asciidoc/user-guide/provisioning.adoc
@@ -56,6 +56,22 @@ A feature describes an application as:
When you install a feature, Apache Karaf installs all resources described in
the feature. It means that it will
automatically resolve and install all bundles, configuration, and dependency
features described in the feature.
+Apache Karaf provides two resolvers, that you can select with the `resolver`
property in the `etc/org.apache.karaf.features.cfg` file:
+
+----
+resolver=default
+----
+
+The possible values are:
+
+* default: the features and bundles are installed using the OSGi resolver,
computing the requirements/capabilities. It supports
+feature and service requirements, conditional features, and resource
repositories.
+* simple: the features and bundles are installed in the declaration order
(dependency features first), without using the OSGi
+resolver. It's faster and deterministic, but the requirements/capabilities are
not resolved: feature and service requirements,
+conditional features, and resource repositories are ignored.
+
+The rest of this section describes the `default` resolver behavior.
+
The feature resolver checks the service requirements, and installs the bundles
providing the services matching the requirements.
The default mode enables this behavior only for "new style" features
repositories (basically, the features repositories XML with
schema equal or greater to 1.3.0). It doesn't apply for "old style" features
repositories (coming from Karaf 2 or 3).