From 453b5b3aa134773913a77bf4b04f7390984a55ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?JB=20Onofr=C3=A9?= Date: Fri, 13 Mar 2026 14:45:10 +0100 Subject: [PATCH] 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 --- .../features/internal/osgi/Activator.java | 93 +- .../service/BootFeaturesInstaller.java | 28 +- .../internal/service/BootManaged.java | 28 + .../internal/service/FeaturesServiceImpl.java | 8 +- .../internal/service/SimpleDeployer.java | 478 +++++++++ .../service/SimpleFeaturesServiceImpl.java | 963 ++++++++++++++++++ .../service/AbstractFeaturesServiceTest.java | 205 ++++ .../service/FeaturesServiceImplTest.java | 66 +- .../SimpleFeaturesServiceImplTest.java | 69 ++ 9 files changed, 1846 insertions(+), 92 deletions(-) create mode 100644 features/core/src/main/java/org/apache/karaf/features/internal/service/BootManaged.java create mode 100644 features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleDeployer.java create mode 100644 features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImpl.java create mode 100644 features/core/src/test/java/org/apache/karaf/features/internal/service/AbstractFeaturesServiceTest.java create mode 100644 features/core/src/test/java/org/apache/karaf/features/internal/service/SimpleFeaturesServiceImplTest.java diff --git a/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java b/features/core/src/main/java/org/apache/karaf/features/internal/osgi/Activator.java index a2d3651cc35..488036350ca 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.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 featuresListenerTracker; private FeaturesServiceImpl featuresService; + private SimpleFeaturesServiceImpl simpleFeaturesService; private StandardManageableRegionDigraph digraphMBean; private BundleInstallSupport installSupport; private ExecutorService executorService; @@ -182,36 +185,64 @@ protected void doStart() throws Exception { 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 + + boolean useSimpleResolver = getBoolean("resolverSimple", false); + 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 +336,26 @@ private void registerRegionDiGraph(StandardRegionDigraph dg) throws BundleExcept DigraphHelper.verifyUnmanagedBundles(bundleContext, dg); } - private ServiceTracker createFeatureListenerTracker() { + private ServiceTracker createFeatureListenerTracker(FeaturesService service) { return new ServiceTracker<>( bundleContext, FeaturesListener.class, new ServiceTrackerCustomizer() { @Override public FeaturesListener addingService(ServiceReference 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 reference, FeaturesListener service) { + public void modifiedService(ServiceReference reference, FeaturesListener listener) { } - + @Override - public void removedService(ServiceReference reference, FeaturesListener service) { - if (featuresService != null && service != null) { - featuresService.unregisterListener(service); + public void removedService(ServiceReference reference, FeaturesListener listener) { + if (service != null && listener != null) { + service.unregisterListener(listener); } if (bundleContext != null && reference != null) { bundleContext.ungetService(reference); @@ -348,6 +379,10 @@ protected void doStop() { 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 9036360d4d2..1a9b9d29bb6 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 BootFeaturesInstaller(BundleContext bundleContext, 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 BootFeaturesInstaller(BundleContext bundleContext, * Install boot features */ public void start() { - if (featuresService.isBootDone()) { + if (bootManaged.isBootDone()) { publishBootFinished(); return; } @@ -104,7 +122,7 @@ protected void installBootFeatures(boolean quitIfUnsuccessful) { } 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 00000000000..c6a82ca1dc2 --- /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 39e9533a365..80c08631ab8 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 @@ /** * */ -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 @@ protected void saveState() { } } - 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 00000000000..14a0bee28b6 --- /dev/null +++ b/features/core/src/main/java/org/apache/karaf/features/internal/service/SimpleDeployer.java @@ -0,0 +1,478 @@ +/* + * 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.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.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.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 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 featuresById, + Map> featuresByName, + Map> requirements, + State state, + EnumSet