From 79f51891e261a10689b33b13518a2f38d2c2995d Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 20:37:33 +0200 Subject: [PATCH 01/26] Parse ServiceLoader provider files per specification --- .../ProviderBundleTrackerCustomizer.java | 169 ++++++++++-------- .../spifly/ServiceLoaderProviderFileTest.java | 56 ++++++ .../META-INF/services/org.example.Service | 21 +++ .../META-INF/services/org.example.Service | 19 ++ 4 files changed, 190 insertions(+), 75 deletions(-) create mode 100644 spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ServiceLoaderProviderFileTest.java create mode 100644 spi-fly/spi-fly-core/src/test/resources/provider-config/first/META-INF/services/org.example.Service create mode 100644 spi-fly/spi-fly-core/src/test/resources/provider-config/second/META-INF/services/org.example.Service diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index 1dc7e6befe..6d2801d29c 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -24,18 +24,22 @@ import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; -import java.net.URL; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.logging.Level; @@ -181,69 +185,84 @@ public List addingBundle(final Bundle bundle, BundleEvent e return registrations; } - private List collectServiceDetails(Bundle bundle, List serviceFileURLs, DiscoveryMode discoveryMode) { - List serviceDetails = new ArrayList<>(); - - for (URL serviceFileURL : serviceFileURLs) { - log(Level.FINE, "Found SPI resource: " + serviceFileURL); - - try { - BufferedReader reader = new BufferedReader( - new InputStreamReader(serviceFileURL.openStream())); - String className = null; - while((className = reader.readLine()) != null) { - try { - className = className.trim(); - - if (className.length() == 0) - continue; // empty line - - if (className.startsWith("#")) - continue; // a comment - - String serviceFile = serviceFileURL.toExternalForm(); - int idx = serviceFile.lastIndexOf('/'); - String registrationClassName = className; - if (serviceFile.length() > idx) { - registrationClassName = serviceFile.substring(idx + 1); - } - - final Hashtable properties; - if (discoveryMode == DiscoveryMode.SPI_PROVIDER_HEADER) { - properties = new Hashtable(); - } - else if (discoveryMode == DiscoveryMode.AUTO_PROVIDERS_PROPERTY) { - properties = activator.getAutoProviderInstructions().map( - Parameters::stream - ).orElseGet(MapStream::empty).filterKey( - i -> Glob.toPattern(i).asPredicate().test(bundle.getSymbolicName()) - ).values().findFirst().map( - Hashtable::new - ).orElseGet(() -> new Hashtable()); - } - else { - properties = findServiceRegistrationProperties(bundle, registrationClassName, className); - } - - if (properties != null) { - properties.put(SpiFlyConstants.SERVICELOADER_MEDIATOR_PROPERTY, spiBundle.getBundleId()); - properties.put(SpiFlyConstants.PROVIDER_IMPLCLASS_PROPERTY, className); - properties.put(SpiFlyConstants.PROVIDER_DISCOVERY_MODE, discoveryMode.toString()); - } - - serviceDetails.add(new ServiceDetails(registrationClassName, className, properties)); - } catch (Exception e) { - log(Level.FINE, - "Could not load SPI implementation referred from " + serviceFileURL, e); - } - } - } catch (IOException e) { - log(Level.FINE, "Could not read SPI metadata from " + serviceFileURL, e); - } - } - - return serviceDetails; - } + private List collectServiceDetails(Bundle bundle, List serviceFileURLs, DiscoveryMode discoveryMode) { + List serviceDetails = new ArrayList<>(); + + for (Entry> providerFile : readServiceProviderFiles(serviceFileURLs).entrySet()) { + String registrationClassName = providerFile.getKey(); + for (String className : providerFile.getValue()) { + try { + final Hashtable properties; + if (discoveryMode == DiscoveryMode.SPI_PROVIDER_HEADER) { + properties = new Hashtable(); + } + else if (discoveryMode == DiscoveryMode.AUTO_PROVIDERS_PROPERTY) { + properties = activator.getAutoProviderInstructions().map( + Parameters::stream + ).orElseGet(MapStream::empty).filterKey( + i -> Glob.toPattern(i).asPredicate().test(bundle.getSymbolicName()) + ).values().findFirst().map( + Hashtable::new + ).orElseGet(() -> new Hashtable()); + } + else { + properties = findServiceRegistrationProperties(bundle, registrationClassName, className); + } + + if (properties != null) { + properties.put(SpiFlyConstants.SERVICELOADER_MEDIATOR_PROPERTY, spiBundle.getBundleId()); + properties.put(SpiFlyConstants.PROVIDER_IMPLCLASS_PROPERTY, className); + properties.put(SpiFlyConstants.PROVIDER_DISCOVERY_MODE, discoveryMode.toString()); + } + + serviceDetails.add(new ServiceDetails(registrationClassName, className, properties)); + } catch (Exception e) { + log(Level.FINE, + "Could not process SPI implementation " + className + " for " + registrationClassName, e); + } + } + } + + return serviceDetails; + } + + Map> readServiceProviderFiles(List serviceFileURLs) { + Map> providers = new LinkedHashMap>(); + + for (URL serviceFileURL : serviceFileURLs) { + log(Level.FINE, "Found SPI resource: " + serviceFileURL); + + String serviceFile = serviceFileURL.toExternalForm(); + int idx = serviceFile.lastIndexOf('/'); + String serviceType = serviceFile.substring(idx + 1); + Set serviceProviders = providers.computeIfAbsent( + serviceType, key -> new LinkedHashSet()); + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(serviceFileURL.openStream(), StandardCharsets.UTF_8))) { + String className; + while ((className = reader.readLine()) != null) { + int comment = className.indexOf('#'); + if (comment >= 0) { + className = className.substring(0, comment); + } + + className = className.trim(); + if (!className.isEmpty()) { + serviceProviders.add(className); + } + } + } catch (IOException e) { + log(Level.FINE, "Could not read SPI metadata from " + serviceFileURL, e); + } + } + + Map> result = new LinkedHashMap>(); + for (Entry> entry : providers.entrySet()) { + result.put(entry.getKey(), new ArrayList(entry.getValue())); + } + return result; + } private Entry, List> getFromAutoProviderProperty(Bundle bundle, Map customAttributes) { return activator.getAutoProviderInstructions().map( Parameters::stream diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ServiceLoaderProviderFileTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ServiceLoaderProviderFileTest.java new file mode 100644 index 0000000000..40fecfba69 --- /dev/null +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ServiceLoaderProviderFileTest.java @@ -0,0 +1,56 @@ +/** + * 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.aries.spifly; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.net.URL; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.osgi.framework.BundleContext; + +public class ServiceLoaderProviderFileTest { + @Test + public void parsesUtf8CommentsAndDuplicatesUsingServiceLoaderRules() { + URL first = getClass().getResource( + "/provider-config/first/META-INF/services/org.example.Service"); + URL second = getClass().getResource( + "/provider-config/second/META-INF/services/org.example.Service"); + assertNotNull(first); + assertNotNull(second); + + ProviderBundleTrackerCustomizer customizer = new ProviderBundleTrackerCustomizer( + new BaseActivator() { + @Override + public void start(BundleContext context) throws Exception {} + }, null); + Map> providers = customizer.readServiceProviderFiles( + Arrays.asList(first, second)); + + assertEquals(1, providers.size()); + assertEquals(Arrays.asList( + "org.example.First", + "org.example.Žluťoučký", + "org.example.Second"), providers.get("org.example.Service")); + } +} diff --git a/spi-fly/spi-fly-core/src/test/resources/provider-config/first/META-INF/services/org.example.Service b/spi-fly/spi-fly-core/src/test/resources/provider-config/first/META-INF/services/org.example.Service new file mode 100644 index 0000000000..d6cc4560b6 --- /dev/null +++ b/spi-fly/spi-fly-core/src/test/resources/provider-config/first/META-INF/services/org.example.Service @@ -0,0 +1,21 @@ +# 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. + +# full-line comment +org.example.First # trailing comment +org.example.Žluťoučký +org.example.First diff --git a/spi-fly/spi-fly-core/src/test/resources/provider-config/second/META-INF/services/org.example.Service b/spi-fly/spi-fly-core/src/test/resources/provider-config/second/META-INF/services/org.example.Service new file mode 100644 index 0000000000..6f14900d8d --- /dev/null +++ b/spi-fly/spi-fly-core/src/test/resources/provider-config/second/META-INF/services/org.example.Service @@ -0,0 +1,19 @@ +# 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. + +org.example.First +org.example.Second # another trailing comment From 358bc16250e8f532657a741eceb0e8beb0b45294 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 20:50:44 +0200 Subject: [PATCH 02/26] Use resolved wiring for ServiceLoader mediation --- .../apache/aries/spifly/BaseActivator.java | 146 ++++++++- .../aries/spifly/ConsumerHeaderProcessor.java | 63 ++-- .../ProviderBundleTrackerCustomizer.java | 97 +++--- .../apache/aries/spifly/SpiFlyConstants.java | 4 +- .../java/org/apache/aries/spifly/Util.java | 23 +- .../org/apache/aries/spifly/WiringUtils.java | 57 ++++ ...rackerCustomizerGenericCapabilityTest.java | 185 +++++++++-- .../ProviderBundleTrackerCustomizerTest.java | 76 ++++- .../aries/spifly/ResolvedWiringTest.java | 292 ++++++++++++++++++ .../spi-fly-example-provider2-bundle/pom.xml | 1 - .../spi-fly-example-provider5-bundle/pom.xml | 1 - spi-fly/spi-fly-static-bundle/pom.xml | 3 +- .../apache/aries/spifly/statictool/Main.java | 19 +- .../spifly/statictool/RequirementTest.java | 8 +- 14 files changed, 808 insertions(+), 167 deletions(-) create mode 100644 spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/WiringUtils.java create mode 100644 spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index ad34f13cda..342b79c283 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -42,6 +42,7 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.framework.wiring.BundleRequirement; import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; @@ -52,6 +53,8 @@ import aQute.libg.glob.Glob; public abstract class BaseActivator implements BundleActivator { + private static final String PROCESSED_REQUIRE_CAPABILITY_HEADER = + "X-SpiFly-Processed-Require-Capability"; private static final Set NON_WOVEN_BUNDLE = Collections.emptySet(); private static final Logger logger = Logger.getLogger(BaseActivator.class.getName()); @@ -77,6 +80,9 @@ public abstract class BaseActivator implements BundleActivator { private final ConcurrentMap>> consumerRestrictions = new ConcurrentHashMap>>(); + private final ConcurrentMap standardConsumerWirings = + new ConcurrentHashMap(); + @SuppressWarnings({ "unchecked", "rawtypes" }) public synchronized void start(BundleContext context, final String consumerHeaderName) throws Exception { bundleContext = context; @@ -115,17 +121,49 @@ public void addConsumerWeavingData(Bundle bundle, String consumerHeaderName) thr return; } - Map> allHeaders = new HashMap>(); - Set addedHeaders = new HashSet(); - List added = allHeaders.put(consumerHeaderName, getAllHeaders(consumerHeaderName, bundle)); - if (added != null) { - added.stream().forEach(addedHeaders::add); + Bundle mediatorBundle = bundleContext == null ? null : bundleContext.getBundle(); + List proprietaryHeaders = getAllHeaders(consumerHeaderName, bundle); + boolean directRequirementCompatibility = + SpiFlyConstants.REQUIRE_CAPABILITY.equals(consumerHeaderName); + boolean standardCandidate = proprietaryHeaders.isEmpty() + && !directRequirementCompatibility; + BundleWiring wiring = mediatorBundle == null || !standardCandidate + ? null : WiringUtils.getWiring(bundle); + boolean staticMediator = SpiFlyConstants.PROCESSED_SPI_CONSUMER_HEADER.equals(consumerHeaderName); + boolean processedStandardBundle = !staticMediator + || !getAllHeaders(PROCESSED_REQUIRE_CAPABILITY_HEADER, bundle).isEmpty(); + + if (mediatorBundle != null && processedStandardBundle + && WiringUtils.isWiredToExtender( + wiring, mediatorBundle, SpiFlyConstants.PROCESSOR_EXTENDER_NAME)) { + registerStandardConsumer(bundle, wiring); + return; } - added = allHeaders.put(SpiFlyConstants.REQUIRE_CAPABILITY, getAllHeaders(SpiFlyConstants.REQUIRE_CAPABILITY, bundle)); - if (added != null) { - added.stream().forEach(addedHeaders::add); + + // Older statically woven bundles had their processor requirement removed. Keep them + // working as an explicitly separate compatibility path, but use their remaining resolved + // ServiceLoader wires instead of reapplying the saved requirement filters. + boolean legacyStaticBundle = staticMediator + && !getAllHeaders(PROCESSED_REQUIRE_CAPABILITY_HEADER, bundle).isEmpty() + && getAllHeaders(SpiFlyConstants.REQUIRE_CAPABILITY, bundle).stream() + .noneMatch(header -> header.contains(SpiFlyConstants.PROCESSOR_EXTENDER_NAME)); + if (legacyStaticBundle) { + registerStandardConsumer(bundle, wiring); + return; } - if (addedHeaders.isEmpty()) { + + // A statically woven standard consumer will call Util even when its processor + // requirement resolves to another mediator. Record an explicit deny state so + // those calls cannot fall through to the unrestricted compatibility path. + if (staticMediator && processedStandardBundle && standardCandidate) { + standardConsumerWirings.put(bundle, StandardConsumerWiring.denied()); + } + + Map> allHeaders = new HashMap>(); + if (!proprietaryHeaders.isEmpty()) { + allHeaders.put(consumerHeaderName, proprietaryHeaders); + } + else { getAutoConsumerInstructions().map(Parameters::stream).orElseGet(MapStream::empty).filterKey( i -> Glob.toPattern(i).asPredicate().test(bundle.getSymbolicName()) ).findFirst().ifPresent( @@ -155,6 +193,12 @@ public void addConsumerWeavingData(Bundle bundle, String consumerHeaderName) thr } } + private void registerStandardConsumer(Bundle bundle, BundleWiring wiring) { + Set weavingData = ConsumerHeaderProcessor.createServiceLoaderWeavingData(); + standardConsumerWirings.put(bundle, StandardConsumerWiring.from(wiring)); + bundleWeavingData.put(bundle, Collections.unmodifiableSet(weavingData)); + } + private List getAllHeaders(String headerName, Bundle bundle) { List bundlesFragments = new ArrayList(); bundlesFragments.add(bundle); @@ -183,6 +227,7 @@ private List getAllHeaders(String headerName, Bundle bundle) { public void removeWeavingData(Bundle bundle) { bundleWeavingData.remove(bundle); consumerRestrictions.remove(bundle); + standardConsumerWirings.remove(bundle); } @Override @@ -298,6 +343,27 @@ public Collection findProviderBundles(String name) { return bundles; } + Collection filterCompatibleProviderBundles( + Bundle consumer, Class serviceType, Collection providers) { + if (!standardConsumerWirings.containsKey(consumer)) { + return providers; + } + + List compatible = new ArrayList(); + for (Bundle provider : providers) { + try { + if (provider.loadClass(serviceType.getName()) == serviceType) { + compatible.add(provider); + } + } + catch (ClassNotFoundException | LinkageError e) { + log(Level.FINE, "Provider " + provider + + " is not type-space compatible with " + serviceType.getName(), e); + } + } + return compatible; + } + public Map getCustomBundleAttributes(String name, Bundle b) { SortedMap>> map = registeredProviders.get(name); if (map == null) @@ -321,6 +387,14 @@ public void registerConsumerBundle(Bundle consumerBundle, public Collection findConsumerRestrictions(Bundle consumer, String className, String methodName, Map, String> args) { + StandardConsumerWiring standardWiring = standardConsumerWirings.get(consumer); + if (standardWiring != null && ServiceLoader.class.getName().equals(className) + && "load".equals(methodName)) { + String serviceType = args == null ? null + : args.get(new Pair(0, Class.class.getName())); + return standardWiring.getProviders(serviceType); + } + Map> restrictions = consumerRestrictions.get(consumer); if (restrictions == null) { // Null means: no restrictions @@ -393,4 +467,58 @@ private Collection getBundles(List descriptors, String return bundles; } + private static final class StandardConsumerWiring { + private final boolean restricted; + private final Map> providersByServiceType; + + private StandardConsumerWiring(boolean restricted, Map> providersByServiceType) { + this.restricted = restricted; + this.providersByServiceType = providersByServiceType; + } + + static StandardConsumerWiring from(BundleWiring wiring) { + if (wiring == null) { + return new StandardConsumerWiring(true, Collections.>emptyMap()); + } + + List requirements = wiring.getRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); + if (requirements.isEmpty()) { + return new StandardConsumerWiring(false, Collections.>emptyMap()); + } + + Map> providers = new HashMap>(); + for (BundleWire wire : wiring.getRequiredWires( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) { + Object serviceType = wire.getCapability().getAttributes().get( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); + BundleWiring providerWiring = wire.getProviderWiring(); + if (serviceType instanceof String && providerWiring != null) { + providers.computeIfAbsent((String) serviceType, key -> new HashSet()) + .add(providerWiring.getBundle()); + } + } + + Map> immutableProviders = new HashMap>(); + for (Map.Entry> entry : providers.entrySet()) { + immutableProviders.put(entry.getKey(), Collections.unmodifiableSet(entry.getValue())); + } + return new StandardConsumerWiring( + true, Collections.unmodifiableMap(immutableProviders)); + } + + static StandardConsumerWiring denied() { + return new StandardConsumerWiring( + true, Collections.>emptyMap()); + } + + Collection getProviders(String serviceType) { + if (!restricted) { + return null; + } + Set providers = providersByServiceType.get(serviceType); + return providers == null ? Collections.emptySet() : providers; + } + } + } diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerHeaderProcessor.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerHeaderProcessor.java index b3cb8a96b4..dc5040317f 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerHeaderProcessor.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerHeaderProcessor.java @@ -18,9 +18,10 @@ */ package org.apache.aries.spifly; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Dictionary; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; import java.util.HashSet; import java.util.Hashtable; import java.util.List; @@ -197,8 +198,8 @@ private static Set processRequireCapabilityHeader(String consumerHe Entry> extenderRequirement = findRequirement(requirements, SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, SpiFlyConstants.PROCESSOR_EXTENDER_NAME); Collection>> serviceLoaderRequirements = findAllMetadata(requirements, SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); - if (extenderRequirement != null) { - List allowedBundles = new ArrayList(); + if (extenderRequirement != null) { + List allowedBundles = new ArrayList(); for (Entry> req : serviceLoaderRequirements) { String slFilterString = req.getValue().get(SpiFlyConstants.FILTER_DIRECTIVE); if (slFilterString != null) { @@ -207,26 +208,38 @@ private static Set processRequireCapabilityHeader(String consumerHe } } - // ServiceLoader.load(Class) - { - ArgRestrictions ar = new ArgRestrictions(); - ar.addRestriction(0, Class.class.getName()); - MethodRestriction mr = new MethodRestriction("load", ar); - weavingData.add(createWeavingData(ServiceLoader.class.getName(), "load", mr, allowedBundles)); - } - - // ServiceLoader.load(Class, ClassLoader) - { - ArgRestrictions ar = new ArgRestrictions(); - ar.addRestriction(0, Class.class.getName()); - ar.addRestriction(1, ClassLoader.class.getName()); - MethodRestriction mr = new MethodRestriction("load", ar); - weavingData.add(createWeavingData(ServiceLoader.class.getName(), "load", mr, allowedBundles)); - } - } - - return weavingData; - } + weavingData.addAll(createServiceLoaderWeavingData(allowedBundles)); + } + + return weavingData; + } + + static Set createServiceLoaderWeavingData() { + return createServiceLoaderWeavingData(Collections.emptyList()); + } + + private static Set createServiceLoaderWeavingData(List allowedBundles) { + Set weavingData = new HashSet(); + + // ServiceLoader.load(Class) + { + ArgRestrictions ar = new ArgRestrictions(); + ar.addRestriction(0, Class.class.getName()); + MethodRestriction mr = new MethodRestriction("load", ar); + weavingData.add(createWeavingData(ServiceLoader.class.getName(), "load", mr, allowedBundles)); + } + + // ServiceLoader.load(Class, ClassLoader) + { + ArgRestrictions ar = new ArgRestrictions(); + ar.addRestriction(0, Class.class.getName()); + ar.addRestriction(1, ClassLoader.class.getName()); + MethodRestriction mr = new MethodRestriction("load", ar); + weavingData.add(createWeavingData(ServiceLoader.class.getName(), "load", mr, allowedBundles)); + } + + return weavingData; + } private static WeavingData createWeavingData(String className, String methodName, MethodRestriction methodRestriction, List allowedBundles) { diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index 6d2801d29c..56ba0817c0 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -48,10 +48,10 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleEvent; import org.osgi.framework.Constants; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.framework.ServicePermission; -import org.osgi.framework.ServiceRegistration; -import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.ServicePermission; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; import org.osgi.util.tracker.BundleTrackerCustomizer; @@ -89,16 +89,27 @@ public List addingBundle(final Bundle bundle, BundleEvent e log(Level.FINE, "Bundle Considered for SPI providers: " + bundle.getSymbolicName()); - DiscoveryMode discoveryMode = DiscoveryMode.SERVICELOADER_CAPABILITIES; - List providedServices = null; - Map customAttributes = new HashMap(); - if (bundle.getHeaders().get(SpiFlyConstants.REQUIRE_CAPABILITY) != null) { - try { - providedServices = readServiceLoaderMediatorCapabilityMetadata(bundle, customAttributes); - } catch (InvalidSyntaxException e) { - log(Level.FINE, "Unable to read capabilities from bundle " + bundle, e); - } - } + DiscoveryMode discoveryMode = DiscoveryMode.SERVICELOADER_CAPABILITIES; + List providedServices = null; + boolean registerServiceLoaderServices = false; + Map customAttributes = new HashMap(); + BundleWiring wiring = WiringUtils.getWiring(bundle); + if (wiring != null) { + List serviceLoaderCapabilities = wiring.getCapabilities( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); + if (!serviceLoaderCapabilities.isEmpty()) { + providedServices = new ArrayList(); + for (BundleCapability capability : serviceLoaderCapabilities) { + Object serviceType = capability.getAttributes().get( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); + if (serviceType instanceof String) { + providedServices.add(((String) serviceType).trim()); + } + } + registerServiceLoaderServices = WiringUtils.isWiredToExtender( + wiring, spiBundle, SpiFlyConstants.REGISTRAR_EXTENDER_NAME); + } + } String spiProviderHeader = getHeaderFromBundleOrFragment(bundle, SpiFlyConstants.SPI_PROVIDER_HEADER); if (providedServices == null && spiProviderHeader != null) { @@ -139,9 +150,12 @@ public List addingBundle(final Bundle bundle, BundleEvent e } final List registrations = new ArrayList(); - for (ServiceDetails details : collectServiceDetails(bundle, serviceFileURLs, discoveryMode)) { - if (providedServices.size() > 0 && !providedServices.contains(details.serviceType)) - continue; + for (ServiceDetails details : collectServiceDetails( + bundle, serviceFileURLs, discoveryMode, registerServiceLoaderServices)) { + if ((discoveryMode == DiscoveryMode.SERVICELOADER_CAPABILITIES + || providedServices.size() > 0) + && !providedServices.contains(details.serviceType)) + continue; try { final Class cls = bundle.loadClass(details.instanceType); @@ -185,7 +199,8 @@ public List addingBundle(final Bundle bundle, BundleEvent e return registrations; } - private List collectServiceDetails(Bundle bundle, List serviceFileURLs, DiscoveryMode discoveryMode) { + private List collectServiceDetails(Bundle bundle, List serviceFileURLs, + DiscoveryMode discoveryMode, boolean registerServiceLoaderServices) { List serviceDetails = new ArrayList<>(); for (Entry> providerFile : readServiceProviderFiles(serviceFileURLs).entrySet()) { @@ -205,9 +220,12 @@ else if (discoveryMode == DiscoveryMode.AUTO_PROVIDERS_PROPERTY) { Hashtable::new ).orElseGet(() -> new Hashtable()); } - else { + else if (registerServiceLoaderServices) { properties = findServiceRegistrationProperties(bundle, registrationClassName, className); } + else { + properties = null; + } if (properties != null) { properties.put(SpiFlyConstants.SERVICELOADER_MEDIATOR_PROPERTY, spiBundle.getBundleId()); @@ -273,7 +291,8 @@ private Entry, List> getFromAutoProviderProperty(Bundle bundle un -> { List serviceFileURLs = getServiceFileUrls(bundle); - List collectServiceDetails = collectServiceDetails(bundle, serviceFileURLs, DiscoveryMode.AUTO_PROVIDERS_PROPERTY); + List collectServiceDetails = collectServiceDetails( + bundle, serviceFileURLs, DiscoveryMode.AUTO_PROVIDERS_PROPERTY, false); collectServiceDetails.stream().map(ServiceDetails::getProperties).filter(Objects::nonNull).forEach( hashtable -> hashtable.forEach(customAttributes::put) @@ -361,44 +380,6 @@ private boolean matches(String val, String matchString) { return idx >= 0; } - // An empty list returned means 'all SPIs' - // A return value of null means no SPIs - // A populated list means: only these SPIs - private List readServiceLoaderMediatorCapabilityMetadata(Bundle bundle, Map customAttributes) throws InvalidSyntaxException { - String requirementHeader = getHeaderFromBundleOrFragment(bundle, SpiFlyConstants.REQUIRE_CAPABILITY, SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); - if (requirementHeader == null) - return null; - - Parameters requirements = OSGiHeader.parseHeader(requirementHeader); - Entry> extenderRequirement = ConsumerHeaderProcessor.findRequirement(requirements, SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, SpiFlyConstants.REGISTRAR_EXTENDER_NAME); - if (extenderRequirement == null) - return null; - - Parameters capabilities; - String capabilityHeader = getHeaderFromBundleOrFragment(bundle, SpiFlyConstants.PROVIDE_CAPABILITY, SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); - if (capabilityHeader == null) { - capabilities = new Parameters(); - } else { - capabilities = OSGiHeader.parseHeader(capabilityHeader); - } - - List serviceNames = new ArrayList(); - for (Entry> serviceLoaderCapability : ConsumerHeaderProcessor.findAllMetadata(capabilities, SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) { - for (Entry entry : serviceLoaderCapability.getValue().entrySet()) { - if (SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE.equals(entry.getKey())) { - serviceNames.add(entry.getValue().trim()); - continue; - } - if (SpiFlyConstants.REGISTER_DIRECTIVE.equals(entry.getKey()) && entry.getValue().equals("")) { - continue; - } - - customAttributes.put(entry.getKey(), entry.getValue()); - } - } - return serviceNames; - } - // null means don't register, // otherwise the return value should be taken as the service registration properties private Hashtable findServiceRegistrationProperties(Bundle bundle, String spiName, String implName) { diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/SpiFlyConstants.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/SpiFlyConstants.java index afdf77bc95..7a8b6451a2 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/SpiFlyConstants.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/SpiFlyConstants.java @@ -53,5 +53,5 @@ public interface SpiFlyConstants { String PROVIDER_REQUIREMENT = EXTENDER_CAPABILITY_NAMESPACE + "; " + FILTER_DIRECTIVE + "=\"(" + EXTENDER_CAPABILITY_NAMESPACE + "=" + REGISTRAR_EXTENDER_NAME + ")\""; - String PROCESSED_SPI_CONSUMER_HEADER = "X-SpiFly-Processed-SPI-Consumer"; -} + String PROCESSED_SPI_CONSUMER_HEADER = "X-SpiFly-Processed-SPI-Consumer"; +} diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java index 1bed94702d..3d112cc620 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java @@ -208,15 +208,20 @@ private static ClassLoader findContextClassloader(Bundle consumerBundle, String Collection allowedBundles = activator.findConsumerRestrictions(consumerBundle, className, methodName, args); - if (allowedBundles != null) { - for (Iterator it = bundles.iterator(); it.hasNext(); ) { - if (!allowedBundles.contains(it.next())) { - it.remove(); - } - } - } - - switch (bundles.size()) { + if (allowedBundles != null) { + for (Iterator it = bundles.iterator(); it.hasNext(); ) { + if (!allowedBundles.contains(it.next())) { + it.remove(); + } + } + } + + if (ServiceLoader.class.getName().equals(className) && "load".equals(methodName)) { + bundles = activator.filterCompatibleProviderBundles( + consumerBundle, clsArg, bundles); + } + + switch (bundles.size()) { case 0: return null; case 1: diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/WiringUtils.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/WiringUtils.java new file mode 100644 index 0000000000..52c8776e6b --- /dev/null +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/WiringUtils.java @@ -0,0 +1,57 @@ +/** + * 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.aries.spifly; + +import org.osgi.framework.Bundle; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +final class WiringUtils { + private WiringUtils() { + } + + static BundleWiring getWiring(Bundle bundle) { + BundleWiring wiring = bundle.adapt(BundleWiring.class); + if (wiring != null) { + return wiring; + } + + BundleRevision revision = bundle.adapt(BundleRevision.class); + return revision == null ? null : revision.getWiring(); + } + + static boolean isWiredToExtender(BundleWiring wiring, Bundle mediatorBundle, String extenderName) { + if (wiring == null) { + return false; + } + + for (BundleWire wire : wiring.getRequiredWires(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE)) { + Object name = wire.getCapability().getAttributes().get( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE); + BundleWiring providerWiring = wire.getProviderWiring(); + if (extenderName.equals(name) && providerWiring != null + && providerWiring.getBundle() != null + && providerWiring.getBundle().getBundleId() == mediatorBundle.getBundleId()) { + return true; + } + } + return false; + } +} diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java index 4cf5ddbff3..2d62cc408f 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java @@ -31,8 +31,9 @@ import java.util.Collection; import java.util.Collections; import java.util.Dictionary; -import java.util.Enumeration; -import java.util.HashSet; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; import java.util.Hashtable; import java.util.List; import java.util.Map; @@ -52,13 +53,14 @@ import org.osgi.framework.Constants; import org.osgi.framework.ServiceFactory; import org.osgi.framework.ServiceReference; -import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleRequirement; import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; -import aQute.bnd.header.Parameters; +import aQute.bnd.header.Parameters; public class ProviderBundleTrackerCustomizerGenericCapabilityTest { @Test @@ -154,10 +156,9 @@ public void start(BundleContext context) throws Exception {} ProviderBundleTrackerCustomizer customizer = new ProviderBundleTrackerCustomizer(activator, mediatorBundle); BundleContext implBC = mockSPIBundleContext4(); - Dictionary headers = new Hashtable(); - headers.put(SpiFlyConstants.REQUIRE_CAPABILITY, "osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.6))\""); - - Dictionary fheaders1 = new Hashtable(); + Dictionary headers = new Hashtable(); + + Dictionary fheaders1 = new Hashtable(); fheaders1.put(SpiFlyConstants.REQUIRE_CAPABILITY, SpiFlyConstants.PROVIDER_REQUIREMENT); fheaders1.put(SpiFlyConstants.PROVIDE_CAPABILITY, SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "; " + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "=org.apache.aries.mytest.MySPI"); @@ -341,9 +342,8 @@ public void start(BundleContext context) throws Exception {} assertEquals(1, bundles.size()); assertSame(implBundle, bundles.iterator().next()); - Map attrs = activator.getCustomBundleAttributes("org.apache.aries.mytest.MySPI", implBundle); - assertEquals(attrs.toString(), 1, attrs.size()); - assertEquals("yeah", attrs.get("approval")); + Map attrs = activator.getCustomBundleAttributes("org.apache.aries.mytest.MySPI", implBundle); + assertTrue(attrs.isEmpty()); } @Test @@ -498,7 +498,7 @@ public void start(BundleContext context) throws Exception {} } @Test - public void testNoServiceRegistration() throws Exception { + public void testPublishedProviderWithoutRegistrarWireIsNotRegisteredAsService() throws Exception { Bundle mediatorBundle = EasyMock.createMock(Bundle.class); EasyMock.expect(mediatorBundle.getBundleId()).andReturn(42l).anyTimes(); EasyMock.replay(mediatorBundle); @@ -514,15 +514,49 @@ public void start(BundleContext context) throws Exception {} EasyMock.replay(sreg); BundleContext implBC = mockSPIBundleContext(sreg); - Bundle implBundle = mockSPIBundle(implBC, SpiFlyConstants.PROVIDER_REQUIREMENT); + Dictionary headers = new Hashtable(); + headers.put(SpiFlyConstants.PROVIDE_CAPABILITY, + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "; " + + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "=org.apache.aries.mytest.MySPI"); + Bundle implBundle = mockSPIBundle(implBC, headers); @SuppressWarnings("rawtypes") List registrations = customizer.addingBundle(implBundle, null); assertEquals(0, registrations.size()); Collection bundles = activator.findProviderBundles("org.apache.aries.mytest.MySPI"); assertEquals(1, bundles.size()); - assertSame(implBundle, bundles.iterator().next()); - } + assertSame(implBundle, bundles.iterator().next()); + } + + @Test + public void testRegistrarWireToAnotherMediatorDoesNotRegisterServices() throws Exception { + Bundle mediatorBundle = EasyMock.createMock(Bundle.class); + EasyMock.expect(mediatorBundle.getBundleId()).andReturn(42L).anyTimes(); + EasyMock.replay(mediatorBundle); + BaseActivator activator = new BaseActivator() { + @Override + public void start(BundleContext context) throws Exception {} + }; + ProviderBundleTrackerCustomizer customizer = new ProviderBundleTrackerCustomizer( + activator, mediatorBundle); + + @SuppressWarnings("rawtypes") + ServiceRegistration registration = EasyMock.createNiceMock(ServiceRegistration.class); + BundleContext implBC = mockSPIBundleContext(registration); + Dictionary headers = new Hashtable(); + headers.put(SpiFlyConstants.REQUIRE_CAPABILITY, SpiFlyConstants.PROVIDER_REQUIREMENT); + headers.put(SpiFlyConstants.PROVIDE_CAPABILITY, + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "; " + + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "=org.apache.aries.mytest.MySPI"); + Bundle implBundle = mockSPIBundle(implBC, headers, null, + mockProviderWiring(headers, null, 99L)); + + @SuppressWarnings("rawtypes") + List registrations = customizer.addingBundle(implBundle, null); + + assertTrue(registrations.isEmpty()); + assertProviderBundle(activator, "org.apache.aries.mytest.MySPI", implBundle); + } @SuppressWarnings({ "resource", "unchecked" }) @Test @@ -550,11 +584,13 @@ public void start(BundleContext context) throws Exception {} Dictionary headers = new Hashtable(); headers.put(SpiFlyConstants.REQUIRE_CAPABILITY, SpiFlyConstants.PROVIDER_REQUIREMENT); - headers.put(SpiFlyConstants.PROVIDE_CAPABILITY, - SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "; " + - SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "=org.apache.aries.mytest.MySPI"); - headers.put(Constants.BUNDLE_CLASSPATH, ".,non-jar.jar,embedded.jar,embedded2.jar"); - EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); + headers.put(SpiFlyConstants.PROVIDE_CAPABILITY, + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "; " + + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "=org.apache.aries.mytest.MySPI"); + headers.put(Constants.BUNDLE_CLASSPATH, ".,non-jar.jar,embedded.jar,embedded2.jar"); + EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); + EasyMock.expect(implBundle.adapt(BundleWiring.class)).andReturn( + mockProviderWiring(headers, null)).anyTimes(); URL embeddedJar = getClass().getResource("/embedded.jar"); assertNotNull("precondition", embeddedJar); @@ -610,9 +646,14 @@ private Bundle mockSPIBundle(BundleContext implBC, Dictionary he return mockSPIBundle(implBC, headers, null); } - private Bundle mockSPIBundle(BundleContext implBC, Dictionary headers, BundleRevision rev) throws ClassNotFoundException { - if (headers == null) - headers = new Hashtable(); + private Bundle mockSPIBundle(BundleContext implBC, Dictionary headers, BundleRevision rev) throws ClassNotFoundException { + return mockSPIBundle(implBC, headers, rev, mockProviderWiring(headers, rev)); + } + + private Bundle mockSPIBundle(BundleContext implBC, Dictionary headers, + BundleRevision rev, BundleWiring providerWiring) throws ClassNotFoundException { + if (headers == null) + headers = new Hashtable(); Bundle implBundle = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); @@ -630,8 +671,9 @@ private Bundle mockSPIBundle(BundleContext implBC, Dictionary he Class cls = getClass().getClassLoader().loadClass("org.apache.aries.spifly.impl1.MySPIImpl1"); EasyMock.expect(implBundle.loadClass("org.apache.aries.spifly.impl1.MySPIImpl1")).andReturn(cls).anyTimes(); - if (rev != null) - EasyMock.expect(implBundle.adapt(BundleRevision.class)).andReturn(rev).anyTimes(); + if (rev != null) + EasyMock.expect(implBundle.adapt(BundleRevision.class)).andReturn(rev).anyTimes(); + EasyMock.expect(implBundle.adapt(BundleWiring.class)).andReturn(providerWiring).anyTimes(); EasyMock.replay(implBundle); return implBundle; @@ -682,8 +724,10 @@ private Bundle mockSPIBundle4(BundleContext implBC, Dictionary h Bundle implBundle = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); - if (rev != null) - EasyMock.expect(implBundle.adapt(BundleRevision.class)).andReturn(rev).anyTimes(); + if (rev != null) + EasyMock.expect(implBundle.adapt(BundleRevision.class)).andReturn(rev).anyTimes(); + EasyMock.expect(implBundle.adapt(BundleWiring.class)).andReturn( + mockProviderWiring(headers, rev)).anyTimes(); // List the resources found at META-INF/services in the test bundle URL dir = getClass().getResource("impl4/META-INF/services"); @@ -716,7 +760,7 @@ private Bundle mockFragment(Dictionary headers) { return fragment; } - private BundleRevision mockHostRevision(Bundle... fragments) { + private BundleRevision mockHostRevision(Bundle... fragments) { List wires = new ArrayList(); for (Bundle fragment : fragments) { BundleRevision fragmentRevision = EasyMock.createMock(BundleRevision.class); @@ -741,8 +785,89 @@ private BundleRevision mockHostRevision(Bundle... fragments) { EasyMock.expect(revision.getWiring()).andReturn(wiring).anyTimes(); EasyMock.expect(revision.getTypes()).andReturn(0).anyTimes(); EasyMock.replay(revision); - return revision; - } + return revision; + } + + private BundleWiring mockProviderWiring(Dictionary hostHeaders, BundleRevision hostRevision) { + return mockProviderWiring(hostHeaders, hostRevision, 42L); + } + + private BundleWiring mockProviderWiring(Dictionary hostHeaders, + BundleRevision hostRevision, long mediatorBundleId) { + if (hostHeaders == null) { + hostHeaders = new Hashtable(); + } + List> headerSources = new ArrayList>(); + headerSources.add(hostHeaders); + if (hostRevision != null) { + for (BundleWire hostWire : hostRevision.getWiring().getProvidedWires("osgi.wiring.host")) { + @SuppressWarnings("unchecked") + Dictionary fragmentHeaders = + hostWire.getRequirement().getRevision().getBundle().getHeaders(); + headerSources.add(fragmentHeaders); + } + } + + List capabilities = new ArrayList(); + boolean registrarRequired = false; + for (Dictionary headers : headerSources) { + String requireCapability = headers.get(SpiFlyConstants.REQUIRE_CAPABILITY); + registrarRequired |= requireCapability != null + && requireCapability.contains(SpiFlyConstants.REGISTRAR_EXTENDER_NAME); + + String provideCapability = headers.get(SpiFlyConstants.PROVIDE_CAPABILITY); + if (provideCapability == null) { + continue; + } + Parameters parameters = new Parameters(provideCapability); + for (Map.Entry entry : parameters.entrySet()) { + if (!SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE.equals( + ConsumerHeaderProcessor.removeDuplicateMarker(entry.getKey()))) { + continue; + } + Map attributes = new HashMap(); + attributes.put(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, + entry.getValue().get(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)); + BundleCapability capability = EasyMock.createNiceMock(BundleCapability.class); + EasyMock.expect(capability.getAttributes()).andReturn(attributes).anyTimes(); + EasyMock.replay(capability); + capabilities.add(capability); + } + } + + List extenderWires = registrarRequired + ? Collections.singletonList(mockExtenderWire( + SpiFlyConstants.REGISTRAR_EXTENDER_NAME, mediatorBundleId)) + : Collections.emptyList(); + BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(wiring.getCapabilities(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(capabilities).anyTimes(); + EasyMock.expect(wiring.getRequiredWires(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE)) + .andReturn(extenderWires).anyTimes(); + EasyMock.replay(wiring); + return wiring; + } + + private BundleWire mockExtenderWire(String extenderName, long mediatorBundleId) { + Map attributes = new HashMap(); + attributes.put(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, extenderName); + BundleCapability capability = EasyMock.createNiceMock(BundleCapability.class); + EasyMock.expect(capability.getAttributes()).andReturn(attributes).anyTimes(); + EasyMock.replay(capability); + + Bundle provider = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(provider.getBundleId()).andReturn(mediatorBundleId).anyTimes(); + EasyMock.replay(provider); + BundleWiring providerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(providerWiring.getBundle()).andReturn(provider).anyTimes(); + EasyMock.replay(providerWiring); + + BundleWire wire = EasyMock.createNiceMock(BundleWire.class); + EasyMock.expect(wire.getCapability()).andReturn(capability).anyTimes(); + EasyMock.expect(wire.getProviderWiring()).andReturn(providerWiring).anyTimes(); + EasyMock.replay(wire); + return wire; + } private void assertProviderBundle(BaseActivator activator, String serviceName, Bundle implBundle) { Collection bundles = activator.findProviderBundles(serviceName); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index ae7a6b5f9c..e4cf73fcc4 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -28,9 +28,11 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.List; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; import org.easymock.EasyMock; import org.junit.Test; @@ -38,7 +40,10 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.ServiceFactory; -import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; public class ProviderBundleTrackerCustomizerTest { @@ -118,9 +123,9 @@ public void testAddingBundleWithBundleClassPath() throws Exception { EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); Dictionary headers = new Hashtable(); - headers.put(SpiFlyConstants.SPI_PROVIDER_HEADER, "*"); - headers.put(Constants.BUNDLE_CLASSPATH, ".,non-jar.jar,embedded.jar,embedded2.jar"); - EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); + headers.put(SpiFlyConstants.SPI_PROVIDER_HEADER, "*"); + headers.put(Constants.BUNDLE_CLASSPATH, ".,non-jar.jar,embedded.jar,embedded2.jar"); + EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); URL embeddedJar = getClass().getResource("/embedded.jar"); assertNotNull("precondition", embeddedJar); @@ -214,12 +219,14 @@ private Bundle mockMultiSPIBundle(BundleContext implBC) throws ClassNotFoundExce Constants.REQUIRE_CAPABILITY, "osgi.extender;filter:='(osgi.extender=osgi.serviceloader.registrar)'" ); - headers.put( - Constants.PROVIDE_CAPABILITY, - "osgi.serviceloader;osgi.serviceloader='org.apache.aries.mytest.MySPI2';register:='org.apache.aries.spifly.impl4.MySPIImpl4b';foo='bbb'," + - "osgi.serviceloader;osgi.serviceloader='org.apache.aries.mytest.MySPI2';register:='org.apache.aries.spifly.impl4.MySPIImpl4c';foo='ccc'" - ); - EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); + headers.put( + Constants.PROVIDE_CAPABILITY, + "osgi.serviceloader;osgi.serviceloader='org.apache.aries.mytest.MySPI2';register:='org.apache.aries.spifly.impl4.MySPIImpl4b';foo='bbb'," + + "osgi.serviceloader;osgi.serviceloader='org.apache.aries.mytest.MySPI2';register:='org.apache.aries.spifly.impl4.MySPIImpl4c';foo='ccc'" + ); + EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); + EasyMock.expect(implBundle.adapt(BundleWiring.class)).andReturn( + mockStandardProviderWiring("org.apache.aries.mytest.MySPI2", 25L)).anyTimes(); // List the resources found at META-INF/services in the test bundle URL dir = getClass().getResource("impl4/META-INF/services"); @@ -235,7 +242,42 @@ private Bundle mockMultiSPIBundle(BundleContext implBC) throws ClassNotFoundExce EasyMock.expect(implBundle.loadClass("org.apache.aries.spifly.impl4.MySPIImpl4b")).andReturn(cls).anyTimes(); cls = getClass().getClassLoader().loadClass("org.apache.aries.spifly.impl4.MySPIImpl4c"); EasyMock.expect(implBundle.loadClass("org.apache.aries.spifly.impl4.MySPIImpl4c")).andReturn(cls).anyTimes(); - EasyMock.replay(implBundle); - return implBundle; - } -} + EasyMock.replay(implBundle); + return implBundle; + } + + private BundleWiring mockStandardProviderWiring(String serviceType, long mediatorBundleId) { + Map serviceAttributes = new HashMap(); + serviceAttributes.put(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, serviceType); + BundleCapability serviceCapability = EasyMock.createNiceMock(BundleCapability.class); + EasyMock.expect(serviceCapability.getAttributes()).andReturn(serviceAttributes).anyTimes(); + EasyMock.replay(serviceCapability); + + Map extenderAttributes = new HashMap(); + extenderAttributes.put(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.REGISTRAR_EXTENDER_NAME); + BundleCapability extenderCapability = EasyMock.createNiceMock(BundleCapability.class); + EasyMock.expect(extenderCapability.getAttributes()).andReturn(extenderAttributes).anyTimes(); + EasyMock.replay(extenderCapability); + + Bundle mediator = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(mediator.getBundleId()).andReturn(mediatorBundleId).anyTimes(); + EasyMock.replay(mediator); + BundleWiring mediatorWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(mediatorWiring.getBundle()).andReturn(mediator).anyTimes(); + EasyMock.replay(mediatorWiring); + + BundleWire extenderWire = EasyMock.createNiceMock(BundleWire.class); + EasyMock.expect(extenderWire.getCapability()).andReturn(extenderCapability).anyTimes(); + EasyMock.expect(extenderWire.getProviderWiring()).andReturn(mediatorWiring).anyTimes(); + EasyMock.replay(extenderWire); + + BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(wiring.getCapabilities(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(Collections.singletonList(serviceCapability)).anyTimes(); + EasyMock.expect(wiring.getRequiredWires(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE)) + .andReturn(Collections.singletonList(extenderWire)).anyTimes(); + EasyMock.replay(wiring); + return wiring; + } +} diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java new file mode 100644 index 0000000000..d2e071ab7e --- /dev/null +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java @@ -0,0 +1,292 @@ +/** + * 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.aries.spifly; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; + +import org.easymock.EasyMock; +import org.junit.Before; +import org.junit.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +public class ResolvedWiringTest { + private static final String SERVICE_TYPE = "org.example.Service"; + private static final String PROCESSED_REQUIRE_CAPABILITY_HEADER = + "X-SpiFly-Processed-Require-Capability"; + + private final BaseActivator activator = new BaseActivator() { + @Override + public void start(BundleContext context) throws Exception {} + }; + private Bundle mediator; + + @Before + public void setUp() throws Exception { + mediator = mockBundle(42L); + BundleContext context = EasyMock.createNiceMock(BundleContext.class); + EasyMock.expect(context.getBundle()).andReturn(mediator).anyTimes(); + EasyMock.replay(context); + + Field contextField = BaseActivator.class.getDeclaredField("bundleContext"); + contextField.setAccessible(true); + contextField.set(activator, context); + } + + @Test + public void ignoresProcessorRequirementThatIsNotWired() throws Exception { + BundleWiring wiring = mockConsumerWiring( + Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList()); + Bundle consumer = mockConsumer(wiring); + + activator.addConsumerWeavingData(consumer, SpiFlyConstants.SPI_CONSUMER_HEADER); + + assertNull(activator.getWeavingData(consumer)); + } + + @Test + public void ignoresProcessorWireToAnotherMediator() throws Exception { + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mockBundle(99L))), + Collections.emptyList(), + Collections.emptyList()); + Bundle consumer = mockConsumer(wiring); + + activator.addConsumerWeavingData(consumer, SpiFlyConstants.SPI_CONSUMER_HEADER); + + assertNull(activator.getWeavingData(consumer)); + } + + @Test + public void restrictsConsumerToActuallyWiredProvider() throws Exception { + Bundle selectedProvider = mockBundle(7L); + BundleRequirement serviceRequirement = EasyMock.createNiceMock(BundleRequirement.class); + EasyMock.replay(serviceRequirement); + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mediator)), + Collections.singletonList(serviceRequirement), + Collections.singletonList(mockWire( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, + SERVICE_TYPE, selectedProvider))); + Bundle consumer = mockConsumer(wiring); + + activator.addConsumerWeavingData(consumer, SpiFlyConstants.SPI_CONSUMER_HEADER); + + assertNotNull(activator.getWeavingData(consumer)); + Collection providers = activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "load", serviceArguments(SERVICE_TYPE)); + assertEquals(Collections.singleton(selectedProvider), providers); + } + + @Test + public void declaredButUnwiredServiceRequirementAllowsNoProviders() throws Exception { + BundleRequirement serviceRequirement = EasyMock.createNiceMock(BundleRequirement.class); + EasyMock.replay(serviceRequirement); + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mediator)), + Collections.singletonList(serviceRequirement), + Collections.emptyList()); + Bundle consumer = mockConsumer(wiring); + + activator.addConsumerWeavingData(consumer, SpiFlyConstants.SPI_CONSUMER_HEADER); + + assertEquals(Collections.emptySet(), activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "load", serviceArguments(SERVICE_TYPE))); + } + + @Test + public void consumerWithoutServiceRequirementCanSeeAllPublishedProviders() throws Exception { + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mediator)), + Collections.emptyList(), + Collections.emptyList()); + Bundle consumer = mockConsumer(wiring); + + activator.addConsumerWeavingData(consumer, SpiFlyConstants.SPI_CONSUMER_HEADER); + + assertNull(activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "load", serviceArguments(SERVICE_TYPE))); + } + + @Test + public void unrestrictedConsumerOnlySeesTypeSpaceCompatibleProviders() throws Exception { + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mediator)), + Collections.emptyList(), + Collections.emptyList()); + Bundle consumer = mockConsumer(wiring); + Bundle compatible = mockProvider(7L, TestService.class.getName(), TestService.class); + Bundle incompatible = mockProvider(8L, TestService.class.getName(), String.class); + + activator.addConsumerWeavingData(consumer, SpiFlyConstants.SPI_CONSUMER_HEADER); + + assertEquals(Collections.singletonList(compatible), + activator.filterCompatibleProviderBundles(consumer, TestService.class, + Arrays.asList(compatible, incompatible))); + } + + @Test + public void staticallyWovenConsumerWiredToAnotherMediatorIsDenied() throws Exception { + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mockBundle(99L))), + Collections.emptyList(), + Collections.emptyList()); + Bundle consumer = mockConsumer(wiring, true, true); + + activator.addConsumerWeavingData( + consumer, SpiFlyConstants.PROCESSED_SPI_CONSUMER_HEADER); + + assertNull(activator.getWeavingData(consumer)); + assertEquals(Collections.emptySet(), activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "load", serviceArguments(SERVICE_TYPE))); + } + + @Test + public void legacyStaticConsumerWithoutProcessorRequirementRemainsSupported() throws Exception { + Bundle selectedProvider = mockBundle(7L); + BundleRequirement serviceRequirement = EasyMock.createNiceMock(BundleRequirement.class); + EasyMock.replay(serviceRequirement); + BundleWiring wiring = mockConsumerWiring( + Collections.emptyList(), + Collections.singletonList(serviceRequirement), + Collections.singletonList(mockWire( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, + SERVICE_TYPE, selectedProvider))); + Bundle consumer = mockConsumer(wiring, true, false); + + activator.addConsumerWeavingData( + consumer, SpiFlyConstants.PROCESSED_SPI_CONSUMER_HEADER); + + assertNotNull(activator.getWeavingData(consumer)); + assertEquals(Collections.singleton(selectedProvider), activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "load", serviceArguments(SERVICE_TYPE))); + } + + private BundleWiring mockConsumerWiring(List extenderWires, + List serviceRequirements, List serviceWires) { + BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(wiring.getRequiredWires(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE)) + .andReturn(extenderWires).anyTimes(); + EasyMock.expect(wiring.getRequirements(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(serviceRequirements).anyTimes(); + EasyMock.expect(wiring.getRequiredWires(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(serviceWires).anyTimes(); + EasyMock.replay(wiring); + return wiring; + } + + private BundleWire mockWire(String namespace, String value, Bundle provider) { + Map attributes = new HashMap(); + attributes.put(namespace, value); + BundleCapability capability = EasyMock.createNiceMock(BundleCapability.class); + EasyMock.expect(capability.getAttributes()).andReturn(attributes).anyTimes(); + EasyMock.replay(capability); + + BundleWiring providerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(providerWiring.getBundle()).andReturn(provider).anyTimes(); + EasyMock.replay(providerWiring); + + BundleWire wire = EasyMock.createNiceMock(BundleWire.class); + EasyMock.expect(wire.getCapability()).andReturn(capability).anyTimes(); + EasyMock.expect(wire.getProviderWiring()).andReturn(providerWiring).anyTimes(); + EasyMock.replay(wire); + return wire; + } + + private Bundle mockConsumer(BundleWiring wiring) { + return mockConsumer(wiring, false, true); + } + + private Bundle mockConsumer(BundleWiring wiring, boolean processed, boolean processorRequirement) { + Dictionary headers = new Hashtable(); + String serviceRequirement = SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + + ";filter:=\"(" + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + + "=" + SERVICE_TYPE + ")\""; + headers.put(SpiFlyConstants.REQUIRE_CAPABILITY, + processorRequirement + ? SpiFlyConstants.CLIENT_REQUIREMENT + "," + serviceRequirement + : serviceRequirement); + if (processed) { + headers.put(PROCESSED_REQUIRE_CAPABILITY_HEADER, + SpiFlyConstants.CLIENT_REQUIREMENT + "," + serviceRequirement); + } + Bundle consumer = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(consumer.getHeaders()).andReturn(headers).anyTimes(); + EasyMock.expect(consumer.adapt(BundleWiring.class)).andReturn(wiring).anyTimes(); + EasyMock.replay(consumer); + return consumer; + } + + private Bundle mockBundle(long id) { + Bundle bundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(bundle.getBundleId()).andReturn(id).anyTimes(); + EasyMock.replay(bundle); + return bundle; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Bundle mockProvider(long id, String serviceType, Class providerServiceType) + throws ClassNotFoundException { + Bundle bundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(bundle.getBundleId()).andReturn(id).anyTimes(); + EasyMock.expect(bundle.loadClass(serviceType)).andReturn((Class) providerServiceType).anyTimes(); + EasyMock.replay(bundle); + return bundle; + } + + private Map, String> serviceArguments(String serviceType) { + Map, String> arguments = new HashMap, String>(); + arguments.put(new Pair(0, Class.class.getName()), serviceType); + return arguments; + } + + private interface TestService { + } +} diff --git a/spi-fly/spi-fly-examples/spi-fly-example-provider2-bundle/pom.xml b/spi-fly/spi-fly-examples/spi-fly-example-provider2-bundle/pom.xml index 0ee2a0f95c..ba949305bf 100644 --- a/spi-fly/spi-fly-examples/spi-fly-example-provider2-bundle/pom.xml +++ b/spi-fly/spi-fly-examples/spi-fly-example-provider2-bundle/pom.xml @@ -63,7 +63,6 @@ under the License. org.apache.aries.spifly.mysvc.impl2 - osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)" osgi.serviceloader; osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider diff --git a/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml b/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml index 9d7102b1f8..cd50e456d8 100644 --- a/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml +++ b/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml @@ -68,7 +68,6 @@ org.apache.aries.spifly.mysvc.impl5 - osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)" osgi.serviceloader; osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider diff --git a/spi-fly/spi-fly-static-bundle/pom.xml b/spi-fly/spi-fly-static-bundle/pom.xml index 14ecffd4f8..1a2e953795 100644 --- a/spi-fly/spi-fly-static-bundle/pom.xml +++ b/spi-fly/spi-fly-static-bundle/pom.xml @@ -113,7 +113,8 @@ META-INF/LICENSE=LICENSE,\ META-INF/NOTICE=NOTICE Provide-Capability: \ - osgi.extender;osgi.extender=osgi.serviceloader.registrar;version:Version=1.0 + osgi.extender;osgi.extender=osgi.serviceloader.registrar;version:Version=1.0,\ + osgi.extender;osgi.extender=osgi.serviceloader.processor;version:Version=1.0;uses:="org.apache.aries.spifly" -fixupmessages: Export org.apache.aries.spifly, has 1, private references ]]> diff --git a/spi-fly/spi-fly-static-tool/src/main/java/org/apache/aries/spifly/statictool/Main.java b/spi-fly/spi-fly-static-tool/src/main/java/org/apache/aries/spifly/statictool/Main.java index 557f55c626..71a008b9bc 100644 --- a/spi-fly/spi-fly-static-tool/src/main/java/org/apache/aries/spifly/statictool/Main.java +++ b/spi-fly/spi-fly-static-tool/src/main/java/org/apache/aries/spifly/statictool/Main.java @@ -47,6 +47,8 @@ import org.osgi.framework.Version; public class Main { + static final String PROCESSED_REQUIRE_CAPABILITY_HEADER = + "X-SpiFly-Processed-Require-Capability"; private static final String MODIFIED_BUNDLE_SUFFIX = "_spifly.jar"; private static final String IMPORT_PACKAGE = "Import-Package"; @@ -100,18 +102,10 @@ private static void weaveJar(String jarPath) throws Exception { manifest.getMainAttributes().putValue(SpiFlyConstants.PROCESSED_SPI_CONSUMER_HEADER, consumerHeaderVal); } else { // It's SpiFlyConstants.REQUIRE_CAPABILITY - - // Take out the processor requirement, this probably needs to be improved a little bit - String newConsumerHeaderVal = consumerHeaderVal.replaceAll( - "osgi[.]extender;\\s*filter[:][=][\"]?[(]osgi[.]extender[=]osgi[.]serviceloader[.]processor[)][\"]?", ""). - trim(); - if (newConsumerHeaderVal.startsWith(",")) - newConsumerHeaderVal = newConsumerHeaderVal.substring(1); - - if (newConsumerHeaderVal.endsWith(",")) - newConsumerHeaderVal = newConsumerHeaderVal.substring(0, newConsumerHeaderVal.length()-1); - manifest.getMainAttributes().putValue(SpiFlyConstants.REQUIRE_CAPABILITY, newConsumerHeaderVal); - manifest.getMainAttributes().putValue("X-SpiFly-Processed-Require-Capability", consumerHeaderVal); + // Keep the processor requirement so the transformed consumer is resolved to the + // mediator that will enforce its provider wires at runtime. + manifest.getMainAttributes().putValue( + PROCESSED_REQUIRE_CAPABILITY_HEADER, consumerHeaderVal); } // TODO if new packages needed then... @@ -352,4 +346,3 @@ private static void ensureDirectory(File outDir) throws IOException { throw new IOException("Unable to create directory " + outDir.getAbsolutePath()); } } - diff --git a/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java index e1f0151ba5..b915fe08a8 100644 --- a/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java +++ b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java @@ -86,8 +86,14 @@ public void testConsumerBundle() throws Exception { assertEquals("2.0", actualMF.getMainAttributes().getValue("Bundle-ManifestVersion")); assertEquals("testbundle", actualMF.getMainAttributes().getValue("Bundle-SymbolicName")); assertEquals("Bar Bar", actualMF.getMainAttributes().getValue("Foo")); - assertEquals("osgi.serviceloader; filter:=\"(osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider)\";cardinality:=multiple", + String requirement = + "osgi.serviceloader; filter:=\"(osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider)\";cardinality:=multiple, " + + "osgi.extender; filter:=\"(osgi.extender=osgi.serviceloader.processor)\""; + assertEquals(requirement, actualMF.getMainAttributes().getValue(SpiFlyConstants.REQUIRE_CAPABILITY)); + assertEquals(requirement, + actualMF.getMainAttributes().getValue( + Main.PROCESSED_REQUIRE_CAPABILITY_HEADER)); assertNull("Should not generate this header when processing Require-Capability", actualMF.getMainAttributes().getValue(SpiFlyConstants.PROCESSED_SPI_CONSUMER_HEADER)); String importPackage = actualMF.getMainAttributes().getValue("Import-Package"); From f48504405fbc61243e4e3483f080a63d2e15fb61 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 20:56:58 +0200 Subject: [PATCH 03/26] Register each ServiceLoader capability decoration --- .../ProviderBundleTrackerCustomizer.java | 111 ++++++++-------- .../apache/aries/spifly/SpiFlyConstants.java | 2 +- ...rackerCustomizerGenericCapabilityTest.java | 119 +++++++++++++++--- .../ProviderBundleTrackerCustomizerTest.java | 2 + .../spi-fly-example-provider5-bundle/pom.xml | 15 ++- .../aries/spifly/itests/InitialTest.java | 22 +++- 6 files changed, 196 insertions(+), 75 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index 56ba0817c0..e10fc58290 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -57,7 +57,6 @@ import org.osgi.util.tracker.BundleTrackerCustomizer; import aQute.bnd.header.Attrs; -import aQute.bnd.header.OSGiHeader; import aQute.bnd.header.Parameters; import aQute.bnd.stream.MapStream; import aQute.libg.glob.Glob; @@ -67,7 +66,8 @@ */ @SuppressWarnings("rawtypes") public class ProviderBundleTrackerCustomizer implements BundleTrackerCustomizer { - private static final String METAINF_SERVICES = "META-INF/services"; + private static final String METAINF_SERVICES = "META-INF/services"; + private static final String REGISTER_DIRECTIVE_NAME = "register"; private static final List MERGE_HEADERS = Arrays.asList( Constants.IMPORT_PACKAGE, Constants.REQUIRE_BUNDLE, Constants.EXPORT_PACKAGE, Constants.PROVIDE_CAPABILITY, Constants.REQUIRE_CAPABILITY); @@ -91,11 +91,12 @@ public List addingBundle(final Bundle bundle, BundleEvent e DiscoveryMode discoveryMode = DiscoveryMode.SERVICELOADER_CAPABILITIES; List providedServices = null; + List serviceLoaderCapabilities = Collections.emptyList(); boolean registerServiceLoaderServices = false; Map customAttributes = new HashMap(); BundleWiring wiring = WiringUtils.getWiring(bundle); if (wiring != null) { - List serviceLoaderCapabilities = wiring.getCapabilities( + serviceLoaderCapabilities = wiring.getCapabilities( SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); if (!serviceLoaderCapabilities.isEmpty()) { providedServices = new ArrayList(); @@ -150,8 +151,8 @@ public List addingBundle(final Bundle bundle, BundleEvent e } final List registrations = new ArrayList(); - for (ServiceDetails details : collectServiceDetails( - bundle, serviceFileURLs, discoveryMode, registerServiceLoaderServices)) { + for (ServiceDetails details : collectServiceDetails(bundle, serviceFileURLs, discoveryMode, + serviceLoaderCapabilities, registerServiceLoaderServices)) { if ((discoveryMode == DiscoveryMode.SERVICELOADER_CAPABILITIES || providedServices.size() > 0) && !providedServices.contains(details.serviceType)) @@ -188,7 +189,9 @@ public List addingBundle(final Bundle bundle, BundleEvent e } } - activator.registerProviderBundle(details.serviceType, bundle, details.properties); + activator.registerProviderBundle(details.serviceType, bundle, + details.properties == null + ? Collections.emptyMap() : details.properties); log(Level.INFO, "Registered provider " + details.instanceType + " of service " + details.serviceType + " in bundle " + bundle.getSymbolicName()); } catch (Exception | NoClassDefFoundError e) { log(Level.FINE, @@ -200,40 +203,47 @@ public List addingBundle(final Bundle bundle, BundleEvent e } private List collectServiceDetails(Bundle bundle, List serviceFileURLs, - DiscoveryMode discoveryMode, boolean registerServiceLoaderServices) { + DiscoveryMode discoveryMode, List serviceLoaderCapabilities, + boolean registerServiceLoaderServices) { List serviceDetails = new ArrayList<>(); for (Entry> providerFile : readServiceProviderFiles(serviceFileURLs).entrySet()) { String registrationClassName = providerFile.getKey(); for (String className : providerFile.getValue()) { try { - final Hashtable properties; + final List> registrations; if (discoveryMode == DiscoveryMode.SPI_PROVIDER_HEADER) { - properties = new Hashtable(); + registrations = Collections.singletonList(new Hashtable()); } else if (discoveryMode == DiscoveryMode.AUTO_PROVIDERS_PROPERTY) { - properties = activator.getAutoProviderInstructions().map( + Hashtable properties = activator.getAutoProviderInstructions().map( Parameters::stream ).orElseGet(MapStream::empty).filterKey( i -> Glob.toPattern(i).asPredicate().test(bundle.getSymbolicName()) ).values().findFirst().map( Hashtable::new ).orElseGet(() -> new Hashtable()); + registrations = Collections.singletonList(properties); } else if (registerServiceLoaderServices) { - properties = findServiceRegistrationProperties(bundle, registrationClassName, className); + registrations = findServiceRegistrationProperties( + serviceLoaderCapabilities, registrationClassName, className); } else { - properties = null; + registrations = Collections.emptyList(); } - if (properties != null) { + if (registrations.isEmpty()) { + serviceDetails.add(new ServiceDetails( + registrationClassName, className, null)); + } + for (Hashtable properties : registrations) { properties.put(SpiFlyConstants.SERVICELOADER_MEDIATOR_PROPERTY, spiBundle.getBundleId()); properties.put(SpiFlyConstants.PROVIDER_IMPLCLASS_PROPERTY, className); properties.put(SpiFlyConstants.PROVIDER_DISCOVERY_MODE, discoveryMode.toString()); + serviceDetails.add(new ServiceDetails( + registrationClassName, className, properties)); } - - serviceDetails.add(new ServiceDetails(registrationClassName, className, properties)); } catch (Exception e) { log(Level.FINE, "Could not process SPI implementation " + className + " for " + registrationClassName, e); @@ -291,8 +301,9 @@ private Entry, List> getFromAutoProviderProperty(Bundle bundle un -> { List serviceFileURLs = getServiceFileUrls(bundle); - List collectServiceDetails = collectServiceDetails( - bundle, serviceFileURLs, DiscoveryMode.AUTO_PROVIDERS_PROPERTY, false); + List collectServiceDetails = collectServiceDetails(bundle, + serviceFileURLs, DiscoveryMode.AUTO_PROVIDERS_PROPERTY, + Collections.emptyList(), false); collectServiceDetails.stream().map(ServiceDetails::getProperties).filter(Objects::nonNull).forEach( hashtable -> hashtable.forEach(customAttributes::put) @@ -380,43 +391,35 @@ private boolean matches(String val, String matchString) { return idx >= 0; } - // null means don't register, - // otherwise the return value should be taken as the service registration properties - private Hashtable findServiceRegistrationProperties(Bundle bundle, String spiName, String implName) { - Object capabilityHeader = getHeaderFromBundleOrFragment(bundle, SpiFlyConstants.PROVIDE_CAPABILITY); - if (capabilityHeader == null) - return null; - - Parameters capabilities = OSGiHeader.parseHeader(capabilityHeader.toString()); - - for (Map.Entry entry : capabilities.entrySet()) { - String key = ConsumerHeaderProcessor.removeDuplicateMarker(entry.getKey()); - Attrs attrs = entry.getValue(); - - if (!SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE.equals(key)) - continue; - - if (!attrs.containsKey(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE) || - !attrs.get(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE).equals(spiName)) - continue; - - if (attrs.containsKey(SpiFlyConstants.REGISTER_DIRECTIVE) && - !attrs.get(SpiFlyConstants.REGISTER_DIRECTIVE).equals(implName)) - continue; - - Hashtable properties = new Hashtable(); - for (Map.Entry prop : attrs.entrySet()) { - if (SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE.equals(prop.getKey()) || - SpiFlyConstants.REGISTER_DIRECTIVE.equals(prop.getKey()) || - key.startsWith(".")) - continue; - - properties.put(prop.getKey(), prop.getValue()); - } - return properties; - } - - return null; + private List> findServiceRegistrationProperties( + List capabilities, String spiName, String implName) { + List> registrations = + new ArrayList>(); + for (BundleCapability capability : capabilities) { + Map attributes = capability.getAttributes(); + if (!spiName.equals(attributes.get( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE))) { + continue; + } + + String register = capability.getDirectives().get(REGISTER_DIRECTIVE_NAME); + if (register != null && !register.equals(implName)) { + continue; + } + + Hashtable properties = new Hashtable(); + for (Map.Entry attribute : attributes.entrySet()) { + String name = attribute.getKey(); + if (SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE.equals(name) + || name.startsWith(".")) { + continue; + } + + properties.put(name, attribute.getValue()); + } + registrations.add(properties); + } + return registrations; } private List getMetaInfServiceURLsFromJar(URL url) { diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/SpiFlyConstants.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/SpiFlyConstants.java index 7a8b6451a2..80a6408cbc 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/SpiFlyConstants.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/SpiFlyConstants.java @@ -36,7 +36,7 @@ public interface SpiFlyConstants { // ServiceLoader capability and related directive String SERVICELOADER_CAPABILITY_NAMESPACE = "osgi.serviceloader"; - String REGISTER_DIRECTIVE = "register:"; + String REGISTER_DIRECTIVE = "register:"; // Service registration property String SERVICELOADER_MEDIATOR_PROPERTY = "serviceloader.mediator"; diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java index 2d62cc408f..785cbb94af 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java @@ -52,8 +52,9 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.ServiceFactory; -import org.osgi.framework.ServiceReference; +import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.Version; import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleRequirement; import org.osgi.framework.wiring.BundleRevision; @@ -346,9 +347,76 @@ public void start(BundleContext context) throws Exception {} assertTrue(attrs.isEmpty()); } - @Test - public void testRegisterAltAttributeDatatype() throws Exception { - // TODO + @Test + public void testRegisterAltAttributeDatatype() throws Exception { + Bundle mediatorBundle = EasyMock.createMock(Bundle.class); + EasyMock.expect(mediatorBundle.getBundleId()).andReturn(42L).anyTimes(); + EasyMock.replay(mediatorBundle); + BaseActivator activator = new BaseActivator() { + @Override + public void start(BundleContext context) throws Exception {} + }; + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, mediatorBundle); + BundleContext implBC = mockSPIBundleContext4(); + + Map firstAttributes = new HashMap(); + firstAttributes.put(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, + "org.apache.aries.mytest.MySPI"); + firstAttributes.put("decorator", "first"); + firstAttributes.put("longValue", Long.valueOf(7)); + firstAttributes.put("doubleValue", Double.valueOf(2.5)); + firstAttributes.put("versionValue", new Version("1.2.3")); + firstAttributes.put("listValue", Arrays.asList(Long.valueOf(1), Long.valueOf(2))); + firstAttributes.put(".private", "hidden"); + firstAttributes.put(SpiFlyConstants.SERVICELOADER_MEDIATOR_PROPERTY, Long.valueOf(99)); + Map firstDirectives = new HashMap(); + firstDirectives.put("register", + "org.apache.aries.spifly.impl1.MySPIImpl1"); + firstDirectives.put("effective", "active"); + + Map secondAttributes = new HashMap(); + secondAttributes.put(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, + "org.apache.aries.mytest.MySPI"); + secondAttributes.put("decorator", "second"); + Map secondDirectives = Collections.singletonMap( + "register", + "org.apache.aries.spifly.impl1.MySPIImpl1"); + + BundleWiring wiring = mockProviderWiring(Arrays.asList( + mockCapability(firstAttributes, firstDirectives), + mockCapability(secondAttributes, secondDirectives)), true, 42L); + Dictionary headers = new Hashtable(); + headers.put(SpiFlyConstants.REQUIRE_CAPABILITY, SpiFlyConstants.PROVIDER_REQUIREMENT); + headers.put(SpiFlyConstants.PROVIDE_CAPABILITY, + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + ";" + + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + + "=org.apache.aries.mytest.MySPI"); + Bundle implBundle = mockSPIBundle(implBC, headers, null, wiring); + + @SuppressWarnings("rawtypes") + List registrations = customizer.addingBundle(implBundle, null); + + assertEquals(2, registrations.size()); + Set decorators = new HashSet(); + for (@SuppressWarnings("rawtypes") ServiceRegistration registration : registrations) { + ServiceReference reference = registration.getReference(); + decorators.add(reference.getProperty("decorator")); + assertEquals(Long.valueOf(42), reference.getProperty( + SpiFlyConstants.SERVICELOADER_MEDIATOR_PROPERTY)); + assertNull(reference.getProperty(".private")); + assertNull(reference.getProperty("effective")); + + if ("first".equals(reference.getProperty("decorator"))) { + assertEquals(Long.valueOf(7), reference.getProperty("longValue")); + assertEquals(Double.valueOf(2.5), reference.getProperty("doubleValue")); + assertEquals(new Version("1.2.3"), reference.getProperty("versionValue")); + assertEquals(Arrays.asList(Long.valueOf(1), Long.valueOf(2)), + reference.getProperty("listValue")); + } + } + assertEquals(new HashSet(Arrays.asList("first", "second")), decorators); } @Test @@ -720,14 +788,18 @@ private Bundle mockSPIBundle4(BundleContext implBC, Dictionary h return mockSPIBundle4(implBC, headers, null); } - private Bundle mockSPIBundle4(BundleContext implBC, Dictionary headers, BundleRevision rev) throws ClassNotFoundException { - Bundle implBundle = EasyMock.createNiceMock(Bundle.class); + private Bundle mockSPIBundle4(BundleContext implBC, Dictionary headers, BundleRevision rev) throws ClassNotFoundException { + return mockSPIBundle4(implBC, headers, rev, mockProviderWiring(headers, rev)); + } + + private Bundle mockSPIBundle4(BundleContext implBC, Dictionary headers, + BundleRevision rev, BundleWiring providerWiring) throws ClassNotFoundException { + Bundle implBundle = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); if (rev != null) EasyMock.expect(implBundle.adapt(BundleRevision.class)).andReturn(rev).anyTimes(); - EasyMock.expect(implBundle.adapt(BundleWiring.class)).andReturn( - mockProviderWiring(headers, rev)).anyTimes(); + EasyMock.expect(implBundle.adapt(BundleWiring.class)).andReturn(providerWiring).anyTimes(); // List the resources found at META-INF/services in the test bundle URL dir = getClass().getResource("impl4/META-INF/services"); @@ -826,15 +898,25 @@ private BundleWiring mockProviderWiring(Dictionary hostHeaders, continue; } Map attributes = new HashMap(); - attributes.put(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, - entry.getValue().get(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)); - BundleCapability capability = EasyMock.createNiceMock(BundleCapability.class); - EasyMock.expect(capability.getAttributes()).andReturn(attributes).anyTimes(); - EasyMock.replay(capability); - capabilities.add(capability); + Map directives = new HashMap(); + for (String name : entry.getValue().keySet()) { + if (aQute.bnd.header.Attrs.isDirective(name)) { + directives.put(name.substring(0, name.length() - 1), + entry.getValue().get(name)); + } + else { + attributes.put(name, entry.getValue().getTyped(name)); + } + } + capabilities.add(mockCapability(attributes, directives)); } } + return mockProviderWiring(capabilities, registrarRequired, mediatorBundleId); + } + + private BundleWiring mockProviderWiring(List capabilities, + boolean registrarRequired, long mediatorBundleId) { List extenderWires = registrarRequired ? Collections.singletonList(mockExtenderWire( SpiFlyConstants.REGISTRAR_EXTENDER_NAME, mediatorBundleId)) @@ -848,6 +930,15 @@ private BundleWiring mockProviderWiring(Dictionary hostHeaders, return wiring; } + private BundleCapability mockCapability(Map attributes, + Map directives) { + BundleCapability capability = EasyMock.createNiceMock(BundleCapability.class); + EasyMock.expect(capability.getAttributes()).andReturn(attributes).anyTimes(); + EasyMock.expect(capability.getDirectives()).andReturn(directives).anyTimes(); + EasyMock.replay(capability); + return capability; + } + private BundleWire mockExtenderWire(String extenderName, long mediatorBundleId) { Map attributes = new HashMap(); attributes.put(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, extenderName); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index e4cf73fcc4..65b4d89aba 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -251,6 +251,8 @@ private BundleWiring mockStandardProviderWiring(String serviceType, long mediato serviceAttributes.put(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, serviceType); BundleCapability serviceCapability = EasyMock.createNiceMock(BundleCapability.class); EasyMock.expect(serviceCapability.getAttributes()).andReturn(serviceAttributes).anyTimes(); + EasyMock.expect(serviceCapability.getDirectives()).andReturn( + Collections.emptyMap()).anyTimes(); EasyMock.replay(serviceCapability); Map extenderAttributes = new HashMap(); diff --git a/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml b/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml index cd50e456d8..f216b8f60b 100644 --- a/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml +++ b/spi-fly/spi-fly-examples/spi-fly-example-provider5-bundle/pom.xml @@ -68,7 +68,20 @@ org.apache.aries.spifly.mysvc.impl5 - osgi.serviceloader; osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider + + osgi.serviceloader; + osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider; + register:="org.apache.aries.spifly.mysvc.impl5.SPIProviderImpl5"; + decorator=first; + ranking:Long=5; + .private=hidden; + x-test-directive:=hidden, + osgi.serviceloader; + osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider; + register:="org.apache.aries.spifly.mysvc.impl5.SPIProviderImpl5"; + decorator=second; + ranking:Long=6 + diff --git a/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java b/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java index 386fd6eb9d..e1708a7f88 100644 --- a/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java +++ b/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java @@ -28,6 +28,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; import org.apache.aries.spifly.itests.util.TeeOutputStream; import org.junit.jupiter.api.AfterEach; @@ -37,6 +39,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; @@ -149,11 +152,20 @@ public void example5() throws Exception { BundleAssert.assertThat(provider5fragment).isFragment().isInState(Bundle.RESOLVED); assertFragmentAttached(provider5Bundle, provider5fragment); - assertThat( - bundleContext.getServiceReferences("org.apache.aries.spifly.mysvc.SPIProvider", null) - ).as( - "the host bundle's own native osgi.serviceloader capability should always be registered" - ).isNotEmpty(); + Collection> providerRegistrations = Arrays.asList( + bundleContext.getServiceReferences( + "org.apache.aries.spifly.mysvc.SPIProvider", null)); + assertThat(providerRegistrations).as( + "each host decorating capability should create a separate registration" + ).hasSize(2).extracting(reference -> reference.getProperty("decorator")) + .containsExactlyInAnyOrder("first", "second"); + + ServiceReference firstDecorator = providerRegistrations.stream() + .filter(reference -> "first".equals(reference.getProperty("decorator"))) + .findFirst().get(); + assertThat(firstDecorator.getProperty("ranking")).isEqualTo(Long.valueOf(5)); + assertThat(firstDecorator.getProperty(".private")).isNull(); + assertThat(firstDecorator.getProperty("x-test-directive")).isNull(); assertThat( bundleContext.getServiceReferences("org.apache.aries.spifly.mysvc.SPIProvider2", null) From bbf1d85ba6c435acc14d02c308b40fb96272538a Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 21:46:09 +0200 Subject: [PATCH 04/26] Use bundle-scoped ServiceLoader provider factories --- .../aries/spifly/ProviderBundleTrackerCustomizer.java | 6 +----- ...oviderBundleTrackerCustomizerGenericCapabilityTest.java | 7 ++++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index e10fc58290..a92571c871 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -164,11 +164,7 @@ public List addingBundle(final Bundle bundle, BundleEvent e if (details.properties != null) { ServiceRegistration reg = null; - Object instance = - (details.properties.containsKey("service.scope") && - "prototype".equalsIgnoreCase(String.valueOf(details.properties.get("service.scope")))) ? - new ProviderPrototypeServiceFactory(cls) : - new ProviderServiceFactory(cls); + Object instance = new ProviderServiceFactory(cls); SecurityManager sm = System.getSecurityManager(); if (sm != null) { diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java index 785cbb94af..a282612ae4 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java @@ -18,7 +18,8 @@ */ package org.apache.aries.spifly; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -369,6 +370,7 @@ public void start(BundleContext context) throws Exception {} firstAttributes.put("doubleValue", Double.valueOf(2.5)); firstAttributes.put("versionValue", new Version("1.2.3")); firstAttributes.put("listValue", Arrays.asList(Long.valueOf(1), Long.valueOf(2))); + firstAttributes.put(Constants.SERVICE_SCOPE, Constants.SCOPE_PROTOTYPE); firstAttributes.put(".private", "hidden"); firstAttributes.put(SpiFlyConstants.SERVICELOADER_MEDIATOR_PROPERTY, Long.valueOf(99)); Map firstDirectives = new HashMap(); @@ -401,6 +403,9 @@ public void start(BundleContext context) throws Exception {} assertEquals(2, registrations.size()); Set decorators = new HashSet(); for (@SuppressWarnings("rawtypes") ServiceRegistration registration : registrations) { + Object serviceObject = ((ServiceRegistrationImpl) registration).getServiceObject(); + assertTrue(serviceObject instanceof ProviderServiceFactory); + assertFalse(serviceObject instanceof ProviderPrototypeServiceFactory); ServiceReference reference = registration.getReference(); decorators.add(reference.getProperty("decorator")); assertEquals(Long.valueOf(42), reference.getProperty( From 2a2b3ef9a99d6a9bac9d3f3a5d3502a6ea5c6ad1 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 21:49:06 +0200 Subject: [PATCH 05/26] Enforce provider REGISTER permission --- .../ProviderBundleTrackerCustomizer.java | 50 +++++++++------- ...rackerCustomizerGenericCapabilityTest.java | 59 ++++++++++++++++--- .../ProviderBundleTrackerCustomizerTest.java | 21 ++++--- 3 files changed, 94 insertions(+), 36 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index a92571c871..ada367e853 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -141,10 +141,12 @@ public List addingBundle(final Bundle bundle, BundleEvent e + bundle.getSymbolicName()); } - for (String serviceType : providedServices) { - // Eagerly register any services that are explicitly listed, as they may not be found in META-INF/services - activator.registerProviderBundle(serviceType, bundle, customAttributes); - } + for (String serviceType : providedServices) { + // Eagerly register any services that are explicitly listed, as they may not be found in META-INF/services + if (hasRegisterPermission(bundle, serviceType)) { + activator.registerProviderBundle(serviceType, bundle, customAttributes); + } + } if (serviceFileURLs == null) { serviceFileURLs = getServiceFileUrls(bundle); @@ -157,8 +159,12 @@ public List addingBundle(final Bundle bundle, BundleEvent e || providedServices.size() > 0) && !providedServices.contains(details.serviceType)) continue; - - try { + + if (!hasRegisterPermission(bundle, details.serviceType)) { + continue; + } + + try { final Class cls = bundle.loadClass(details.instanceType); log(Level.FINE, "Loaded SPI provider: " + cls); @@ -166,20 +172,10 @@ public List addingBundle(final Bundle bundle, BundleEvent e ServiceRegistration reg = null; Object instance = new ProviderServiceFactory(cls); - SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - if (bundle.hasPermission(new ServicePermission(details.serviceType, ServicePermission.REGISTER))) { - reg = bundle.getBundleContext().registerService( - details.serviceType, instance, details.properties); - } else { - log(Level.FINE, "Bundle " + bundle + " does not have the permission to register services of type: " + details.serviceType); - } - } else { - reg = bundle.getBundleContext().registerService( - details.serviceType, instance, details.properties); - } - - if (reg != null) { + reg = bundle.getBundleContext().registerService( + details.serviceType, instance, details.properties); + + if (reg != null) { registrations.add(reg); log(Level.FINE, "Registered service: " + reg); } @@ -195,8 +191,18 @@ public List addingBundle(final Bundle bundle, BundleEvent e } } - return registrations; - } + return registrations; + } + + private boolean hasRegisterPermission(Bundle bundle, String serviceType) { + boolean permitted = bundle.hasPermission( + new ServicePermission(serviceType, ServicePermission.REGISTER)); + if (!permitted) { + log(Level.FINE, "Bundle " + bundle + + " does not have permission to provide services of type: " + serviceType); + } + return permitted; + } private List collectServiceDetails(Bundle bundle, List serviceFileURLs, DiscoveryMode discoveryMode, List serviceLoaderCapabilities, diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java index a282612ae4..668bbe2fa4 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java @@ -52,7 +52,8 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; -import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServicePermission; import org.osgi.framework.ServiceReference; import org.osgi.framework.ServiceRegistration; import org.osgi.framework.Version; @@ -630,6 +631,38 @@ public void start(BundleContext context) throws Exception {} assertTrue(registrations.isEmpty()); assertProviderBundle(activator, "org.apache.aries.mytest.MySPI", implBundle); } + + @Test + public void testProviderWithoutRegisterPermissionIsNotExposed() throws Exception { + Bundle mediatorBundle = EasyMock.createMock(Bundle.class); + EasyMock.expect(mediatorBundle.getBundleId()).andReturn(42L).anyTimes(); + EasyMock.replay(mediatorBundle); + BaseActivator activator = new BaseActivator() { + @Override + public void start(BundleContext context) throws Exception {} + }; + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, mediatorBundle); + + BundleContext implBC = EasyMock.createNiceMock(BundleContext.class); + EasyMock.replay(implBC); + Dictionary headers = new Hashtable(); + headers.put(SpiFlyConstants.REQUIRE_CAPABILITY, SpiFlyConstants.PROVIDER_REQUIREMENT); + headers.put(SpiFlyConstants.PROVIDE_CAPABILITY, + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "; " + + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + + "=org.apache.aries.mytest.MySPI"); + Bundle implBundle = mockSPIBundle(implBC, headers, null, + mockProviderWiring(headers, null), false); + + @SuppressWarnings("rawtypes") + List registrations = customizer.addingBundle(implBundle, null); + + assertTrue(registrations.isEmpty()); + assertTrue(activator.findProviderBundles( + "org.apache.aries.mytest.MySPI").isEmpty()); + EasyMock.verify(implBC); + } @SuppressWarnings({ "resource", "unchecked" }) @Test @@ -652,8 +685,10 @@ public void start(BundleContext context) throws Exception {} EasyMock.replay(implBC); - Bundle implBundle = EasyMock.createNiceMock(Bundle.class); - EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + Bundle implBundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + EasyMock.expect(implBundle.hasPermission(EasyMock.isA(ServicePermission.class))) + .andReturn(true).anyTimes(); Dictionary headers = new Hashtable(); headers.put(SpiFlyConstants.REQUIRE_CAPABILITY, SpiFlyConstants.PROVIDER_REQUIREMENT); @@ -725,11 +760,19 @@ private Bundle mockSPIBundle(BundleContext implBC, Dictionary he private Bundle mockSPIBundle(BundleContext implBC, Dictionary headers, BundleRevision rev, BundleWiring providerWiring) throws ClassNotFoundException { + return mockSPIBundle(implBC, headers, rev, providerWiring, true); + } + + private Bundle mockSPIBundle(BundleContext implBC, Dictionary headers, + BundleRevision rev, BundleWiring providerWiring, boolean registerPermission) + throws ClassNotFoundException { if (headers == null) headers = new Hashtable(); - - Bundle implBundle = EasyMock.createNiceMock(Bundle.class); - EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + + Bundle implBundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + EasyMock.expect(implBundle.hasPermission(EasyMock.isA(ServicePermission.class))) + .andReturn(registerPermission).anyTimes(); EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); EasyMock.expect(implBundle.getSymbolicName()).andReturn("bsn").anyTimes(); @@ -800,7 +843,9 @@ private Bundle mockSPIBundle4(BundleContext implBC, Dictionary h private Bundle mockSPIBundle4(BundleContext implBC, Dictionary headers, BundleRevision rev, BundleWiring providerWiring) throws ClassNotFoundException { Bundle implBundle = EasyMock.createNiceMock(Bundle.class); - EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + EasyMock.expect(implBundle.hasPermission(EasyMock.isA(ServicePermission.class))) + .andReturn(true).anyTimes(); EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); if (rev != null) EasyMock.expect(implBundle.adapt(BundleRevision.class)).andReturn(rev).anyTimes(); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index 65b4d89aba..cc1b4efb7f 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -39,7 +39,8 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; -import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServicePermission; import org.osgi.framework.ServiceRegistration; import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleWire; @@ -119,8 +120,10 @@ public void testAddingBundleWithBundleClassPath() throws Exception { EasyMock.replay(implBC); - Bundle implBundle = EasyMock.createNiceMock(Bundle.class); - EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + Bundle implBundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + EasyMock.expect(implBundle.hasPermission(EasyMock.isA(ServicePermission.class))) + .andReturn(true).anyTimes(); Dictionary headers = new Hashtable(); headers.put(SpiFlyConstants.SPI_PROVIDER_HEADER, "*"); @@ -188,8 +191,10 @@ private Bundle mockSPIBundle(BundleContext implBC) throws ClassNotFoundException } private Bundle mockSPIBundle(BundleContext implBC, String spiProviderHeader) throws ClassNotFoundException { - Bundle implBundle = EasyMock.createNiceMock(Bundle.class); - EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + Bundle implBundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + EasyMock.expect(implBundle.hasPermission(EasyMock.isA(ServicePermission.class))) + .andReturn(true).anyTimes(); Dictionary headers = new Hashtable(); if (spiProviderHeader != null) @@ -211,8 +216,10 @@ private Bundle mockSPIBundle(BundleContext implBC, String spiProviderHeader) thr } private Bundle mockMultiSPIBundle(BundleContext implBC) throws ClassNotFoundException { - Bundle implBundle = EasyMock.createNiceMock(Bundle.class); - EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + Bundle implBundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + EasyMock.expect(implBundle.hasPermission(EasyMock.isA(ServicePermission.class))) + .andReturn(true).anyTimes(); Dictionary headers = new Hashtable(); headers.put( From 756d2f89d839051f9009f4e650062c7165dd9bba Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 21:57:13 +0200 Subject: [PATCH 06/26] Reprocess dynamically attached ServiceLoader fragments --- .../apache/aries/spifly/BaseActivator.java | 34 +++++++- .../ConsumerBundleTrackerCustomizer.java | 17 ++-- .../ProviderBundleTrackerCustomizer.java | 87 +++++++++++++++---- .../ProviderBundleTrackerCustomizerTest.java | 40 ++++++++- .../aries/spifly/ResolvedWiringTest.java | 29 +++++++ .../pom.xml | 15 +++- .../aries/spifly/itests/InitialTest.java | 15 +++- 7 files changed, 204 insertions(+), 33 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 342b79c283..414f9a68c5 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -42,6 +42,7 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.wiring.BundleRequirement; import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWire; @@ -68,6 +69,7 @@ public abstract class BaseActivator implements BundleActivator { private BundleTracker consumerBundleTracker; @SuppressWarnings("rawtypes") private BundleTracker providerBundleTracker; + private ProviderBundleTrackerCustomizer providerBundleTrackerCustomizer; private Optional autoConsumerInstructions; private Optional autoProviderInstructions; @@ -100,8 +102,10 @@ public synchronized void start(BundleContext context, final String consumerHeade log(Level.FINE, t.getMessage(), t); } + providerBundleTrackerCustomizer = + new ProviderBundleTrackerCustomizer(this, context.getBundle()); providerBundleTracker = new BundleTracker(context, - Bundle.ACTIVE | Bundle.STARTING, new ProviderBundleTrackerCustomizer(this, context.getBundle())); + Bundle.ACTIVE | Bundle.STARTING, providerBundleTrackerCustomizer); providerBundleTracker.open(); consumerBundleTracker = new BundleTracker(context, @@ -230,6 +234,34 @@ public void removeWeavingData(Bundle bundle) { standardConsumerWirings.remove(bundle); } + void fragmentAttached(Bundle fragment, String consumerHeaderName) throws Exception { + BundleWiring fragmentWiring = WiringUtils.getWiring(fragment); + if (fragmentWiring == null) { + return; + } + + for (BundleWire hostWire : fragmentWiring.getRequiredWires( + HostNamespace.HOST_NAMESPACE)) { + BundleWiring hostWiring = hostWire.getProviderWiring(); + Bundle host = hostWiring == null ? null : hostWiring.getBundle(); + if (host == null) { + continue; + } + + if (providerBundleTracker != null && providerBundleTrackerCustomizer != null) { + Object registrations = providerBundleTracker.getObject(host); + if (registrations != null) { + providerBundleTrackerCustomizer.reprocessBundle(host, registrations); + } + } + + if (consumerBundleTracker != null && consumerBundleTracker.getObject(host) != null) { + removeWeavingData(host); + addConsumerWeavingData(host, consumerHeaderName); + } + } + } + @Override public synchronized void stop(BundleContext context) throws Exception { activator = null; diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerBundleTrackerCustomizer.java index c5e98d8205..5fd783b05e 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerBundleTrackerCustomizer.java @@ -19,7 +19,8 @@ package org.apache.aries.spifly; import org.osgi.framework.Bundle; -import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.wiring.BundleRevision; import org.osgi.util.tracker.BundleTrackerCustomizer; public class ConsumerBundleTrackerCustomizer implements BundleTrackerCustomizer { @@ -31,10 +32,16 @@ public ConsumerBundleTrackerCustomizer(BaseActivator baseActivator, String consu headerName = consumerHeaderName; } - @Override - public Object addingBundle(Bundle bundle, BundleEvent event) { - try { - activator.addConsumerWeavingData(bundle, headerName); + @Override + public Object addingBundle(Bundle bundle, BundleEvent event) { + try { + BundleRevision revision = bundle.adapt(BundleRevision.class); + if (revision != null + && (revision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0) { + activator.fragmentAttached(bundle, headerName); + return bundle; + } + activator.addConsumerWeavingData(bundle, headerName); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index ada367e853..972c9ac83d 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -132,10 +132,12 @@ public List addingBundle(final Bundle bundle, BundleEvent e discoveryMode = DiscoveryMode.AUTO_PROVIDERS_PROPERTY; } - if (providedServices == null) { - log(Level.FINE, "No provided SPI services. Skipping bundle: " - + bundle.getSymbolicName()); - return null; + if (providedServices == null) { + log(Level.FINE, "No provided SPI services. Skipping bundle: " + + bundle.getSymbolicName()); + // Keep active hosts tracked so a fragment attached later can add provider + // capabilities and configuration resources to them. + return new ArrayList(); } else { log(Level.FINE, "Examining bundle for SPI provider: " + bundle.getSymbolicName()); @@ -148,8 +150,10 @@ public List addingBundle(final Bundle bundle, BundleEvent e } } - if (serviceFileURLs == null) { - serviceFileURLs = getServiceFileUrls(bundle); + if (serviceFileURLs == null) { + serviceFileURLs = getServiceFileUrls(bundle, + discoveryMode == DiscoveryMode.SERVICELOADER_CAPABILITIES + ? providedServices : null); } final List registrations = new ArrayList(); @@ -318,8 +322,31 @@ private Entry, List> getFromAutoProviderProperty(Bundle bundle ).orElseGet(() -> new AbstractMap.SimpleImmutableEntry<>(null, null)); } - private List getServiceFileUrls(Bundle bundle) { - List serviceFileURLs = new ArrayList(); + private List getServiceFileUrls(Bundle bundle) { + return getServiceFileUrls(bundle, null); + } + + List getServiceFileUrls(Bundle bundle, List serviceTypes) { + if (serviceTypes != null) { + BundleWiring wiring = WiringUtils.getWiring(bundle); + ClassLoader classLoader = wiring == null ? null : wiring.getClassLoader(); + if (classLoader != null) { + Set serviceFileURLs = new LinkedHashSet(); + for (String serviceType : new LinkedHashSet(serviceTypes)) { + try { + Enumeration resources = classLoader.getResources( + METAINF_SERVICES + "/" + serviceType); + serviceFileURLs.addAll(Collections.list(resources)); + } + catch (IOException e) { + log(Level.FINE, "Could not find SPI metadata for " + serviceType, e); + } + } + return new ArrayList(serviceFileURLs); + } + } + + List serviceFileURLs = new ArrayList(); Enumeration entries = bundle.findEntries(METAINF_SERVICES, "*", false); if (entries != null) { @@ -449,20 +476,42 @@ private List getMetaInfServiceURLsFromJar(URL url) { return urls; } - @Override - public void modifiedBundle(Bundle bundle, BundleEvent event, Object registrations) { - // implementation is unnecessary for this use case - } + @Override + public void modifiedBundle(Bundle bundle, BundleEvent event, Object registrations) { + // implementation is unnecessary for this use case + } + + @SuppressWarnings("unchecked") + void reprocessBundle(Bundle bundle, Object registrations) { + List current = (List) registrations; + synchronized (current) { + activator.unregisterProviderBundle(bundle); + unregister(current); + current.clear(); + + List replacements = addingBundle(bundle, null); + if (replacements != null) { + current.addAll(replacements); + } + } + } @Override @SuppressWarnings("unchecked") - public void removedBundle(Bundle bundle, BundleEvent event, Object registrations) { - activator.unregisterProviderBundle(bundle); - - if (registrations == null) - return; - - for (ServiceRegistration reg : (List) registrations) { + public void removedBundle(Bundle bundle, BundleEvent event, Object registrations) { + activator.unregisterProviderBundle(bundle); + + if (registrations == null) + return; + + List current = (List) registrations; + synchronized (current) { + unregister(current); + } + } + + private void unregister(List registrations) { + for (ServiceRegistration reg : registrations) { try { reg.unregister(); log(Level.FINE, "Unregistered: " + reg); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index cc1b4efb7f..60e8b16913 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -23,12 +23,14 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import java.net.URL; -import java.net.URLClassLoader; +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; +import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.List; @@ -100,8 +102,38 @@ public void testAddingNonOptInBundle() throws Exception { Bundle implBundle = mockSPIBundle(implBC, null); ProviderBundleTrackerCustomizer customizer = new ProviderBundleTrackerCustomizer(activator, null); - assertNull("Bundle doesn't opt-in so should be ignored", customizer.addingBundle(implBundle, null)); - } + assertEquals("Bundle without providers should remain tracked for late fragments", + Collections.emptyList(), customizer.addingBundle(implBundle, null)); + } + + @Test + public void testStandardDiscoveryUsesWiringClassLoader() throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + final URL serviceFile = getClass().getResource( + "impl1/META-INF/services/" + serviceType); + assertNotNull("precondition", serviceFile); + + ClassLoader classLoader = new ClassLoader() { + @Override + public Enumeration getResources(String name) throws IOException { + assertEquals("META-INF/services/" + serviceType, name); + return Collections.enumeration(Collections.singleton(serviceFile)); + } + }; + BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(wiring.getClassLoader()).andReturn(classLoader).anyTimes(); + EasyMock.replay(wiring); + Bundle bundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(bundle.adapt(BundleWiring.class)).andReturn(wiring).anyTimes(); + EasyMock.replay(bundle); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + + assertEquals(Collections.singletonList(serviceFile), + customizer.getServiceFileUrls(bundle, + Arrays.asList(serviceType, serviceType))); + } @Test @SuppressWarnings("unchecked") diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java index d2e071ab7e..5596ac0fc8 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import java.lang.reflect.Field; import java.util.Arrays; @@ -40,6 +41,7 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; @@ -209,6 +211,33 @@ public void legacyStaticConsumerWithoutProcessorRequirementRemainsSupported() th consumer, ServiceLoader.class.getName(), "load", serviceArguments(SERVICE_TYPE))); } + @Test + public void resolvedFragmentTriggersHostRefreshPath() { + final boolean[] attached = new boolean[1]; + BaseActivator fragmentActivator = new BaseActivator() { + @Override + public void start(BundleContext context) throws Exception {} + + @Override + void fragmentAttached(Bundle fragment, String consumerHeaderName) { + attached[0] = true; + } + }; + BundleRevision revision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.expect(revision.getTypes()).andReturn( + BundleRevision.TYPE_FRAGMENT).anyTimes(); + EasyMock.replay(revision); + Bundle fragment = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(fragment.adapt(BundleRevision.class)) + .andReturn(revision).anyTimes(); + EasyMock.replay(fragment); + + new ConsumerBundleTrackerCustomizer(fragmentActivator, + SpiFlyConstants.SPI_CONSUMER_HEADER).addingBundle(fragment, null); + + assertTrue(attached[0]); + } + private BundleWiring mockConsumerWiring(List extenderWires, List serviceRequirements, List serviceWires) { BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); diff --git a/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/pom.xml b/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/pom.xml index 6f55f72d90..51f3c8ef8c 100644 --- a/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/pom.xml +++ b/spi-fly/spi-fly-examples/spi-fly-example-provider5-fragment/pom.xml @@ -46,6 +46,11 @@ ${project.version} provided + + org.apache.aries.spifly.examples + org.apache.aries.spifly.examples.provider1.jar + ${project.version} + @@ -69,8 +74,16 @@ org.apache.aries.spifly.mysvc.impl5frag + *;scope=compile;inline=false osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)" - osgi.serviceloader; osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider2 + + osgi.serviceloader; + osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider2, + osgi.serviceloader; + osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider; + register:="org.apache.aries.spifly.mysvc.impl.SPIProviderImpl"; + decorator=fragment-embedded + diff --git a/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java b/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java index e1708a7f88..748ed2de48 100644 --- a/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java +++ b/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java @@ -30,6 +30,7 @@ import java.nio.file.Paths; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import org.apache.aries.spifly.itests.util.TeeOutputStream; import org.junit.jupiter.api.AfterEach; @@ -43,6 +44,7 @@ import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; +import org.osgi.framework.wiring.FrameworkWiring; import org.osgi.test.assertj.bundle.BundleAssert; import org.osgi.test.common.annotation.InjectBundleContext; import org.osgi.test.junit5.context.BundleContextExtension; @@ -142,13 +144,20 @@ public void example3() throws Exception { *

The first assertion verifies the host's own capability. The second is the regression * assertion: the merge must keep same-named capability clauses from different sources * distinct so that the fragment's capability is registered alongside the host's. + * The host is started before the fragment is installed, and the fragment contributes a + * provider configuration from an embedded Bundle-ClassPath entry, covering both late + * attachment reprocessing and effective class-path discovery. * * @throws Exception if the example bundles cannot be installed or inspected */ @Test public void example5() throws Exception { - Bundle provider5fragment = assertBundleInstallation(getExampleJar("spi-fly-example-provider5-fragment"), true); Bundle provider5Bundle = assertBundleInstallation(getExampleJar("spi-fly-example-provider5-bundle")); + Bundle provider5fragment = assertBundleInstallation( + getExampleJar("spi-fly-example-provider5-fragment"), true); + FrameworkWiring frameworkWiring = bundleContext.getBundle(0).adapt(FrameworkWiring.class); + assertThat(frameworkWiring.resolveBundles( + Collections.singleton(provider5fragment))).isTrue(); BundleAssert.assertThat(provider5fragment).isFragment().isInState(Bundle.RESOLVED); assertFragmentAttached(provider5Bundle, provider5fragment); @@ -157,8 +166,8 @@ public void example5() throws Exception { "org.apache.aries.spifly.mysvc.SPIProvider", null)); assertThat(providerRegistrations).as( "each host decorating capability should create a separate registration" - ).hasSize(2).extracting(reference -> reference.getProperty("decorator")) - .containsExactlyInAnyOrder("first", "second"); + ).hasSize(3).extracting(reference -> reference.getProperty("decorator")) + .containsExactlyInAnyOrder("first", "second", "fragment-embedded"); ServiceReference firstDecorator = providerRegistrations.stream() .filter(reference -> "first".equals(reference.getProperty("decorator"))) From 106488f2f37cd815c2f7868abf05d2f51463b784 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 22:29:39 +0200 Subject: [PATCH 07/26] Close mediated ServiceLoader provider views --- .../apache/aries/spifly/BaseActivator.java | 6 +- .../java/org/apache/aries/spifly/Util.java | 137 +++++++++---- .../org/apache/aries/spifly/UtilTest.java | 185 ++++++++++++++++-- 3 files changed, 266 insertions(+), 62 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 414f9a68c5..5c58d314a5 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -197,12 +197,16 @@ && getAllHeaders(SpiFlyConstants.REQUIRE_CAPABILITY, bundle).stream() } } - private void registerStandardConsumer(Bundle bundle, BundleWiring wiring) { + void registerStandardConsumer(Bundle bundle, BundleWiring wiring) { Set weavingData = ConsumerHeaderProcessor.createServiceLoaderWeavingData(); standardConsumerWirings.put(bundle, StandardConsumerWiring.from(wiring)); bundleWeavingData.put(bundle, Collections.unmodifiableSet(weavingData)); } + boolean isStandardConsumer(Bundle bundle) { + return standardConsumerWirings.containsKey(bundle); + } + private List getAllHeaders(String headerName, Bundle bundle) { List bundlesFragments = new ArrayList(); bundlesFragments.add(bundle); diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java index 3d112cc620..3ec2e3d0d5 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java @@ -71,7 +71,7 @@ public Void run() { }); } - public static ServiceLoader serviceLoaderLoad(Class service, Class caller) { + public static ServiceLoader serviceLoaderLoad(Class service, Class caller) { if (BaseActivator.activator == null) { // The system is not yet initialized. We can't do anything. return null; @@ -93,32 +93,26 @@ public ClassLoader run() { BundleReference bundleReference = (BundleReference)bundleLoader; - final ClassLoader bundleClassloader = findContextClassloader( - bundleReference.getBundle(), ServiceLoader.class.getName(), "load", service); - - if (bundleClassloader == null) { - return ServiceLoader.load(service); - } - - Thread thread = Thread.currentThread(); - - return AccessController.doPrivileged( - new PrivilegedAction>() { - @Override - public ServiceLoader run() { - ClassLoader contextClassLoader = thread.getContextClassLoader(); - - try { - thread.setContextClassLoader(bundleClassloader); - - return ServiceLoader.load(service); - } - finally { - thread.setContextClassLoader(contextClassLoader); - } - } - } - ); + Bundle consumerBundle = bundleReference.getBundle(); + final ClassLoader bundleClassloader = findContextClassloader( + consumerBundle, ServiceLoader.class.getName(), "load", service); + + if (bundleClassloader == null + && !BaseActivator.activator.isStandardConsumer(consumerBundle)) { + return ServiceLoader.load(service); + } + + return AccessController.doPrivileged( + new PrivilegedAction>() { + @Override + public ServiceLoader run() { + ClassLoader contextClassLoader = + Thread.currentThread().getContextClassLoader(); + return ServiceLoader.load(service, new ProviderViewClassLoader( + contextClassLoader, bundleClassloader, service.getName())); + } + } + ); } @@ -146,14 +140,23 @@ public ClassLoader run() { BundleReference bundleReference = (BundleReference)bundleLoader; - final ClassLoader bundleClassloader = findContextClassloader( - bundleReference.getBundle(), ServiceLoader.class.getName(), "load", service); - - if (bundleClassloader == null) { - return ServiceLoader.load(service, specifiedClassLoader); - } - - return ServiceLoader.load(service, new WrapperCL(specifiedClassLoader, bundleClassloader)); + Bundle consumerBundle = bundleReference.getBundle(); + final ClassLoader bundleClassloader = findContextClassloader( + consumerBundle, ServiceLoader.class.getName(), "load", service); + + if (bundleClassloader == null) { + if (BaseActivator.activator.isStandardConsumer(consumerBundle)) { + return ServiceLoader.load(service, new ProviderViewClassLoader( + specifiedClassLoader, null, service.getName())); + } + return ServiceLoader.load(service, specifiedClassLoader); + } + + if (BaseActivator.activator.isStandardConsumer(consumerBundle)) { + return ServiceLoader.load(service, new ProviderViewClassLoader( + specifiedClassLoader, bundleClassloader, service.getName())); + } + return ServiceLoader.load(service, new WrapperCL(specifiedClassLoader, bundleClassloader)); } public static void fixContextClassloader(String cls, String method, Class clsArg, ClassLoader bundleLoader) { @@ -373,7 +376,7 @@ private static ClassLoader getClassLoaderFromClassResource(Bundle b, String path return null; } - private static class WrapperCL extends ClassLoader { + private static class WrapperCL extends ClassLoader { private final ClassLoader bundleClassloader; public WrapperCL(ClassLoader specifiedClassLoader, ClassLoader bundleClassloader) { super(specifiedClassLoader); @@ -391,8 +394,60 @@ protected URL findResource(String name) { } @Override - protected Enumeration findResources(String name) throws IOException { - return bundleClassloader.getResources(name); - } - } -} + protected Enumeration findResources(String name) throws IOException { + return bundleClassloader.getResources(name); + } + } + + private static class ProviderViewClassLoader extends ClassLoader { + private final ClassLoader providerClassLoader; + private final String providerConfiguration; + + ProviderViewClassLoader(ClassLoader parent, ClassLoader providerClassLoader, + String serviceType) { + super(parent); + this.providerClassLoader = providerClassLoader; + providerConfiguration = "META-INF/services/" + serviceType; + } + + @Override + public URL getResource(String name) { + if (providerConfiguration.equals(name)) { + return providerClassLoader == null + ? null : providerClassLoader.getResource(name); + } + return super.getResource(name); + } + + @Override + public Enumeration getResources(String name) throws IOException { + if (providerConfiguration.equals(name)) { + return providerClassLoader == null + ? java.util.Collections.emptyEnumeration() + : providerClassLoader.getResources(name); + } + return super.getResources(name); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + if (providerClassLoader == null) { + throw new ClassNotFoundException(name); + } + return providerClassLoader.loadClass(name); + } + + @Override + protected URL findResource(String name) { + return providerClassLoader == null + ? null : providerClassLoader.getResource(name); + } + + @Override + protected Enumeration findResources(String name) throws IOException { + return providerClassLoader == null + ? java.util.Collections.emptyEnumeration() + : providerClassLoader.getResources(name); + } + } +} diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java index f2ef9cfde1..19f550a087 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java @@ -18,15 +18,21 @@ */ package org.apache.aries.spifly; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; - -import java.net.URL; -import java.net.URLClassLoader; -import java.util.Dictionary; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.ServiceLoader; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.ServiceLoader; import org.apache.aries.mytest.MySPI; import org.easymock.EasyMock; @@ -36,8 +42,11 @@ import org.junit.Test; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleReference; -import org.osgi.framework.Constants; +import org.osgi.framework.BundleReference; +import org.osgi.framework.Constants; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWiring; public class UtilTest { private ClassLoader storedTCCL; @@ -48,9 +57,10 @@ public void setup() { } @After - public void tearDown() { - Thread.currentThread().setContextClassLoader(storedTCCL); - storedTCCL = null; + public void tearDown() { + Thread.currentThread().setContextClassLoader(storedTCCL); + storedTCCL = null; + BaseActivator.activator = null; } @Test @@ -105,7 +115,7 @@ public Class answer() throws Throwable { } @Test - public void testNotInitialized() throws Exception { + public void testNotInitialized() throws Exception { BaseActivator.activator = null; URL url = getClass().getResource("/embedded3.jar"); @@ -134,11 +144,146 @@ public Class answer() throws Throwable { Thread.currentThread().setContextClassLoader(null); Util.fixContextClassloader(ServiceLoader.class.getName(), "load", MySPI.class, clientCL); - assertSame("The system is not yet initialized, so the TCCL should not be set", - null, Thread.currentThread().getContextClassLoader()); - } - - private static class TestBundleClassLoader extends URLClassLoader implements BundleReference { + assertSame("The system is not yet initialized, so the TCCL should not be set", + null, Thread.currentThread().getContextClassLoader()); + } + + @Test + public void standardConsumerWithNoProvidersHasClosedView() throws Exception { + BaseActivator activator = newActivator(); + Bundle consumer = EasyMock.createNiceMock(Bundle.class); + EasyMock.replay(consumer); + activator.registerStandardConsumer(consumer, null); + + URL forbidden = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", forbidden); + Thread.currentThread().setContextClassLoader( + new URLClassLoader(new URL[] {forbidden}, getClass().getClassLoader())); + + ServiceLoader loader = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)); + + assertFalse(loader.iterator().hasNext()); + } + + @Test + public void explicitLoaderCannotAddProviderConfigurations() throws Exception { + BaseActivator activator = newActivator(); + Bundle consumer = EasyMock.createNiceMock(Bundle.class); + EasyMock.replay(consumer); + BundleWiring consumerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(consumerWiring.getRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.replay(consumerWiring); + activator.registerStandardConsumer(consumer, consumerWiring); + + URL selected = getClass().getResource("/embedded.jar"); + URL forbidden = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", selected); + assertNotNull("precondition", forbidden); + Bundle provider = mockProviderBundle(42L, selected); + activator.registerProviderBundle( + MySPI.class.getName(), provider, new HashMap()); + + ClassLoader specified = new URLClassLoader( + new URL[] {forbidden}, getClass().getClassLoader()); + ServiceLoader loader = Util.serviceLoaderLoad( + MySPI.class, specified, callerClass(consumer)); + + List providerTypes = new ArrayList(); + for (MySPI providerInstance : loader) { + providerTypes.add(providerInstance.getClass().getName()); + } + assertEquals(2, providerTypes.size()); + assertTrue(providerTypes.contains( + "org.apache.aries.spifly.impl2.MySPIImpl2a")); + assertTrue(providerTypes.contains( + "org.apache.aries.spifly.impl2.MySPIImpl2b")); + assertFalse(providerTypes.contains( + "org.apache.aries.spifly.impl3.MySPIImpl3")); + } + + @Test + public void unprocessedCallerRetainsOriginalLoaderFallback() throws Exception { + newActivator(); + Bundle consumer = EasyMock.createNiceMock(Bundle.class); + EasyMock.replay(consumer); + URL providerConfig = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", providerConfig); + Thread.currentThread().setContextClassLoader(new URLClassLoader( + new URL[] {providerConfig}, getClass().getClassLoader())); + + ServiceLoader loader = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)); + + assertEquals("org.apache.aries.spifly.impl3.MySPIImpl3", + loader.iterator().next().getClass().getName()); + } + + private BaseActivator newActivator() { + BaseActivator activator = new BaseActivator() { + @Override + public void start(BundleContext context) throws Exception { + } + }; + BaseActivator.activator = activator; + return activator; + } + + @SuppressWarnings("unchecked") + private Class callerClass(Bundle consumer) throws Exception { + URL callerJar = getClass().getResource("/embedded3.jar"); + assertNotNull("precondition", callerJar); + return (Class) new TestBundleClassLoader( + new URL[] {callerJar}, getClass().getClassLoader(), consumer) + .loadClass("org.apache.aries.spifly.testpkg.TestClass"); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Bundle mockProviderBundle(long bundleId, URL providerJar) throws Exception { + Bundle providerBundle = EasyMock.createMock(Bundle.class); + final ClassLoader providerCL = new TestBundleClassLoader( + new URL[] {providerJar}, getClass().getClassLoader(), providerBundle); + BundleWiring providerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(providerWiring.getClassLoader()).andReturn(providerCL).anyTimes(); + EasyMock.replay(providerWiring); + BundleRevision providerRevision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.expect(providerRevision.getWiring()).andReturn(providerWiring).anyTimes(); + EasyMock.replay(providerRevision); + Bundle systemBundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(systemBundle.loadClass(BundleRevision.class.getName())) + .andReturn((Class) BundleRevision.class).anyTimes(); + EasyMock.expect(systemBundle.loadClass(BundleWiring.class.getName())) + .andReturn((Class) BundleWiring.class).anyTimes(); + EasyMock.replay(systemBundle); + BundleContext providerContext = EasyMock.createNiceMock(BundleContext.class); + EasyMock.expect(providerContext.getBundle(0)).andReturn(systemBundle).anyTimes(); + EasyMock.replay(providerContext); + EasyMock.expect(providerBundle.getBundleContext()) + .andReturn(providerContext).anyTimes(); + EasyMock.expect(providerBundle.adapt(BundleRevision.class)) + .andReturn(providerRevision).anyTimes(); + EasyMock.expect(providerBundle.getBundleId()).andReturn(bundleId).anyTimes(); + EasyMock.expect(providerBundle.getEntryPaths((String) EasyMock.anyObject())) + .andReturn(null).anyTimes(); + Dictionary providerHeaders = new Hashtable(); + providerHeaders.put(Constants.BUNDLE_CLASSPATH, ".,provider.jar"); + EasyMock.expect(providerBundle.getHeaders()).andReturn(providerHeaders).anyTimes(); + EasyMock.expect(providerBundle.getResource("provider.jar")) + .andReturn(providerJar).anyTimes(); + providerBundle.loadClass((String) EasyMock.anyObject()); + EasyMock.expectLastCall().andAnswer(new IAnswer>() { + @Override + public Class answer() throws Throwable { + return providerCL.loadClass((String) EasyMock.getCurrentArguments()[0]); + } + }).anyTimes(); + EasyMock.replay(providerBundle); + return providerBundle; + } + + private static class TestBundleClassLoader extends URLClassLoader implements BundleReference { private final Bundle bundle; public TestBundleClassLoader(URL[] urls, ClassLoader parent, Bundle bundle) { From 8720de563bdf0e109c9f9025c8f5711ab8b14ce9 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 22:38:29 +0200 Subject: [PATCH 08/26] Enforce ServiceLoader permissions at provisioning time --- .../ProviderBundleTrackerCustomizer.java | 24 ++- .../aries/spifly/ProviderServiceFactory.java | 13 ++ .../java/org/apache/aries/spifly/Util.java | 162 +++++++++++++----- ...rackerCustomizerGenericCapabilityTest.java | 6 +- .../org/apache/aries/spifly/UtilTest.java | 129 +++++++++++++- ...lientWeavingHookGenericCapabilityTest.java | 23 +-- .../dynamic/ClientWeavingHookOSGi43Test.java | 3 + .../spifly/dynamic/ClientWeavingHookTest.java | 23 +-- 8 files changed, 296 insertions(+), 87 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index 972c9ac83d..3c7959fbb0 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -145,9 +145,9 @@ public List addingBundle(final Bundle bundle, BundleEvent e for (String serviceType : providedServices) { // Eagerly register any services that are explicitly listed, as they may not be found in META-INF/services - if (hasRegisterPermission(bundle, serviceType)) { - activator.registerProviderBundle(serviceType, bundle, customAttributes); - } + // Keep every eligible bundle indexed so Conditional Permission Admin grants and + // revocations can be observed by the lazy ServiceLoader view without reprocessing. + activator.registerProviderBundle(serviceType, bundle, customAttributes); } if (serviceFileURLs == null) { @@ -164,17 +164,15 @@ public List addingBundle(final Bundle bundle, BundleEvent e && !providedServices.contains(details.serviceType)) continue; - if (!hasRegisterPermission(bundle, details.serviceType)) { - continue; - } - try { - final Class cls = bundle.loadClass(details.instanceType); - log(Level.FINE, "Loaded SPI provider: " + cls); - - if (details.properties != null) { - ServiceRegistration reg = null; - Object instance = new ProviderServiceFactory(cls); + final Class cls = bundle.loadClass(details.instanceType); + log(Level.FINE, "Loaded SPI provider: " + cls); + + if (details.properties != null + && hasRegisterPermission(bundle, details.serviceType)) { + ServiceRegistration reg = null; + Object instance = new ProviderServiceFactory( + cls, bundle, details.serviceType); reg = bundle.getBundleContext().registerService( details.serviceType, instance, details.properties); diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderServiceFactory.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderServiceFactory.java index 8fa4af9d66..34b3d3e529 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderServiceFactory.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderServiceFactory.java @@ -20,17 +20,30 @@ import org.osgi.framework.Bundle; import org.osgi.framework.ServiceFactory; +import org.osgi.framework.ServicePermission; import org.osgi.framework.ServiceRegistration; public class ProviderServiceFactory implements ServiceFactory { private final Class providerClass; + private final Bundle providerBundle; + private final String serviceType; public ProviderServiceFactory(Class cls) { + this(cls, null, null); + } + + ProviderServiceFactory(Class cls, Bundle providerBundle, String serviceType) { providerClass = cls; + this.providerBundle = providerBundle; + this.serviceType = serviceType; } @Override public Object getService(Bundle bundle, ServiceRegistration registration) { + if (providerBundle != null && !providerBundle.hasPermission( + new ServicePermission(serviceType, ServicePermission.REGISTER))) { + return null; + } try { return providerClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java index 3ec2e3d0d5..d1f124dac6 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java @@ -21,8 +21,7 @@ import java.io.IOException; import java.lang.reflect.Method; import java.net.URL; -import java.security.AccessControlException; -import java.security.AccessController; +import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collection; @@ -95,7 +94,7 @@ public ClassLoader run() { Bundle consumerBundle = bundleReference.getBundle(); final ClassLoader bundleClassloader = findContextClassloader( - consumerBundle, ServiceLoader.class.getName(), "load", service); + consumerBundle, ServiceLoader.class.getName(), "load", service, true); if (bundleClassloader == null && !BaseActivator.activator.isStandardConsumer(consumerBundle)) { @@ -109,7 +108,8 @@ public ServiceLoader run() { ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, new ProviderViewClassLoader( - contextClassLoader, bundleClassloader, service.getName())); + contextClassLoader, bundleClassloader, consumerBundle, + service.getName())); } } ); @@ -142,19 +142,20 @@ public ClassLoader run() { Bundle consumerBundle = bundleReference.getBundle(); final ClassLoader bundleClassloader = findContextClassloader( - consumerBundle, ServiceLoader.class.getName(), "load", service); + consumerBundle, ServiceLoader.class.getName(), "load", service, true); if (bundleClassloader == null) { if (BaseActivator.activator.isStandardConsumer(consumerBundle)) { return ServiceLoader.load(service, new ProviderViewClassLoader( - specifiedClassLoader, null, service.getName())); + specifiedClassLoader, null, consumerBundle, service.getName())); } return ServiceLoader.load(service, specifiedClassLoader); } if (BaseActivator.activator.isStandardConsumer(consumerBundle)) { return ServiceLoader.load(service, new ProviderViewClassLoader( - specifiedClassLoader, bundleClassloader, service.getName())); + specifiedClassLoader, bundleClassloader, consumerBundle, + service.getName())); } return ServiceLoader.load(service, new WrapperCL(specifiedClassLoader, bundleClassloader)); } @@ -166,7 +167,8 @@ public static void fixContextClassloader(String cls, String method, Class cls return; } - final ClassLoader cl = findContextClassloader(br.getBundle(), cls, method, clsArg); + final ClassLoader cl = findContextClassloader( + br.getBundle(), cls, method, clsArg, false); if (cl != null) { BaseActivator.activator.log(Level.FINE, "Temporarily setting Thread Context Classloader to: " + cl); AccessController.doPrivileged(new PrivilegedAction() { @@ -181,27 +183,20 @@ public Void run() { } } - private static ClassLoader findContextClassloader(Bundle consumerBundle, String className, String methodName, Class clsArg) { - BaseActivator activator = BaseActivator.activator; - - String requestedClass; - Map, String> args; - if (ServiceLoader.class.getName().equals(className) && "load".equals(methodName)) { + private static ClassLoader findContextClassloader(Bundle consumerBundle, String className, + String methodName, Class clsArg, boolean permissionAware) { + BaseActivator activator = BaseActivator.activator; + + String requestedClass; + Map, String> args; + boolean serviceLoaderCall = ServiceLoader.class.getName().equals(className) + && "load".equals(methodName); + if (serviceLoaderCall) { requestedClass = clsArg.getName(); args = new HashMap, String>(); args.put(new Pair(0, Class.class.getName()), requestedClass); - SecurityManager sm = System.getSecurityManager(); - if (sm != null) { - try { - sm.checkPermission(new ServicePermission(requestedClass, ServicePermission.GET)); - } catch (AccessControlException ace) { - // access denied - activator.log(Level.FINE, "No permission to obtain service of type: " + requestedClass); - return null; - } - } - } else { + } else { requestedClass = className; args = null; // only supported on ServiceLoader.load() at the moment } @@ -225,28 +220,38 @@ private static ClassLoader findContextClassloader(Bundle consumerBundle, String } switch (bundles.size()) { - case 0: - return null; - case 1: - Bundle bundle = bundles.iterator().next(); - return getBundleClassLoader(bundle); - default: - List loaders = new ArrayList(); - for (Bundle b : bundles) { - loaders.add(getBundleClassLoader(b)); - } + case 0: + return null; + case 1: + Bundle bundle = bundles.iterator().next(); + return serviceLoaderCall && permissionAware + ? getProviderClassLoader(bundle, requestedClass) + : getBundleClassLoader(bundle); + default: + List loaders = new ArrayList(); + for (Bundle b : bundles) { + loaders.add(serviceLoaderCall && permissionAware + ? getProviderClassLoader(b, requestedClass) + : getBundleClassLoader(b)); + } return new MultiDelegationClassloader(loaders.toArray(new ClassLoader[loaders.size()])); } } - private static ClassLoader getBundleClassLoader(final Bundle b) { + private static ClassLoader getBundleClassLoader(final Bundle b) { return AccessController.doPrivileged(new PrivilegedAction() { @Override public ClassLoader run() { return getBundleClassLoaderPrivileged(b); } - }); - } + }); + } + + private static ClassLoader getProviderClassLoader(Bundle providerBundle, + String serviceType) { + return new ProviderBundleClassLoader(providerBundle, + getBundleClassLoader(providerBundle), serviceType); + } private static ClassLoader getBundleClassLoaderPrivileged(Bundle b) { // In 4.3 this can be done much easier by using the BundleWiring, but we want this code to @@ -401,19 +406,23 @@ protected Enumeration findResources(String name) throws IOException { private static class ProviderViewClassLoader extends ClassLoader { private final ClassLoader providerClassLoader; + private final Bundle consumerBundle; + private final String serviceType; private final String providerConfiguration; ProviderViewClassLoader(ClassLoader parent, ClassLoader providerClassLoader, - String serviceType) { + Bundle consumerBundle, String serviceType) { super(parent); this.providerClassLoader = providerClassLoader; + this.consumerBundle = consumerBundle; + this.serviceType = serviceType; providerConfiguration = "META-INF/services/" + serviceType; } @Override public URL getResource(String name) { if (providerConfiguration.equals(name)) { - return providerClassLoader == null + return !hasGetPermission() || providerClassLoader == null ? null : providerClassLoader.getResource(name); } return super.getResource(name); @@ -422,7 +431,7 @@ public URL getResource(String name) { @Override public Enumeration getResources(String name) throws IOException { if (providerConfiguration.equals(name)) { - return providerClassLoader == null + return !hasGetPermission() || providerClassLoader == null ? java.util.Collections.emptyEnumeration() : providerClassLoader.getResources(name); } @@ -430,24 +439,85 @@ public Enumeration getResources(String name) throws IOException { } @Override - protected Class findClass(String name) throws ClassNotFoundException { - if (providerClassLoader == null) { + protected synchronized Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (!hasGetPermission() || providerClassLoader == null) { throw new ClassNotFoundException(name); } - return providerClassLoader.loadClass(name); + Class cls = providerClassLoader.loadClass(name); + if (resolve) { + resolveClass(cls); + } + return cls; } @Override protected URL findResource(String name) { - return providerClassLoader == null + return !hasGetPermission() || providerClassLoader == null ? null : providerClassLoader.getResource(name); } @Override protected Enumeration findResources(String name) throws IOException { - return providerClassLoader == null + return !hasGetPermission() || providerClassLoader == null ? java.util.Collections.emptyEnumeration() : providerClassLoader.getResources(name); } + + private boolean hasGetPermission() { + boolean permitted = consumerBundle.hasPermission( + new ServicePermission(serviceType, ServicePermission.GET)); + if (!permitted) { + BaseActivator.activator.log(Level.FINE, "Bundle " + consumerBundle + + " does not have permission to obtain services of type: " + + serviceType); + } + return permitted; + } + } + + private static class ProviderBundleClassLoader extends ClassLoader { + private final Bundle providerBundle; + private final ClassLoader delegate; + private final String serviceType; + + ProviderBundleClassLoader(Bundle providerBundle, ClassLoader delegate, + String serviceType) { + super(null); + this.providerBundle = providerBundle; + this.delegate = delegate; + this.serviceType = serviceType; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + if (!hasRegisterPermission()) { + throw new ClassNotFoundException(name); + } + return delegate.loadClass(name); + } + + @Override + public URL getResource(String name) { + return hasRegisterPermission() ? delegate.getResource(name) : null; + } + + @Override + public Enumeration getResources(String name) throws IOException { + return hasRegisterPermission() + ? delegate.getResources(name) + : java.util.Collections.emptyEnumeration(); + } + + private boolean hasRegisterPermission() { + boolean permitted = providerBundle.hasPermission( + new ServicePermission(serviceType, ServicePermission.REGISTER)); + if (!permitted) { + BaseActivator.activator.log(Level.FINE, "Bundle " + providerBundle + + " does not have permission to provide services of type: " + + serviceType); + } + return permitted; + } } } diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java index 668bbe2fa4..ff8d95e78a 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java @@ -633,7 +633,7 @@ public void start(BundleContext context) throws Exception {} } @Test - public void testProviderWithoutRegisterPermissionIsNotExposed() throws Exception { + public void testProviderWithoutRegisterPermissionRemainsCandidate() throws Exception { Bundle mediatorBundle = EasyMock.createMock(Bundle.class); EasyMock.expect(mediatorBundle.getBundleId()).andReturn(42L).anyTimes(); EasyMock.replay(mediatorBundle); @@ -659,8 +659,8 @@ public void start(BundleContext context) throws Exception {} List registrations = customizer.addingBundle(implBundle, null); assertTrue(registrations.isEmpty()); - assertTrue(activator.findProviderBundles( - "org.apache.aries.mytest.MySPI").isEmpty()); + assertProviderBundle(activator, + "org.apache.aries.mytest.MySPI", implBundle); EasyMock.verify(implBC); } diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java index 19f550a087..1844223c8e 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import java.net.URL; @@ -31,8 +32,11 @@ import java.util.Dictionary; import java.util.HashMap; import java.util.Hashtable; +import java.util.Iterator; import java.util.List; +import java.util.ServiceConfigurationError; import java.util.ServiceLoader; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.aries.mytest.MySPI; import org.easymock.EasyMock; @@ -44,6 +48,7 @@ import org.osgi.framework.BundleContext; import org.osgi.framework.BundleReference; import org.osgi.framework.Constants; +import org.osgi.framework.ServicePermission; import org.osgi.framework.wiring.BundleRequirement; import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWiring; @@ -151,8 +156,7 @@ public Class answer() throws Throwable { @Test public void standardConsumerWithNoProvidersHasClosedView() throws Exception { BaseActivator activator = newActivator(); - Bundle consumer = EasyMock.createNiceMock(Bundle.class); - EasyMock.replay(consumer); + Bundle consumer = mockPermissionBundle(new AtomicBoolean(true)); activator.registerStandardConsumer(consumer, null); URL forbidden = getClass().getResource("/embedded2.jar"); @@ -169,8 +173,7 @@ public void standardConsumerWithNoProvidersHasClosedView() throws Exception { @Test public void explicitLoaderCannotAddProviderConfigurations() throws Exception { BaseActivator activator = newActivator(); - Bundle consumer = EasyMock.createNiceMock(Bundle.class); - EasyMock.replay(consumer); + Bundle consumer = mockPermissionBundle(new AtomicBoolean(true)); BundleWiring consumerWiring = EasyMock.createNiceMock(BundleWiring.class); EasyMock.expect(consumerWiring.getRequirements( SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) @@ -187,7 +190,17 @@ public void explicitLoaderCannotAddProviderConfigurations() throws Exception { MySPI.class.getName(), provider, new HashMap()); ClassLoader specified = new URLClassLoader( - new URL[] {forbidden}, getClass().getClassLoader()); + new URL[] {forbidden}, getClass().getClassLoader()) { + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (name.startsWith("org.apache.aries.spifly.impl2.")) { + throw new AssertionError( + "selected provider classes must not come from the specified loader"); + } + return super.loadClass(name, resolve); + } + }; ServiceLoader loader = Util.serviceLoaderLoad( MySPI.class, specified, callerClass(consumer)); @@ -221,6 +234,86 @@ public void unprocessedCallerRetainsOriginalLoaderFallback() throws Exception { loader.iterator().next().getClass().getName()); } + @Test + public void consumerPermissionIsCheckedAtLazyIteration() throws Exception { + BaseActivator activator = newActivator(); + AtomicBoolean consumerPermission = new AtomicBoolean(true); + Bundle consumer = mockPermissionBundle(consumerPermission); + BundleWiring consumerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(consumerWiring.getRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.replay(consumerWiring); + activator.registerStandardConsumer(consumer, consumerWiring); + + URL selected = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", selected); + Bundle provider = mockProviderBundle(42L, selected); + activator.registerProviderBundle( + MySPI.class.getName(), provider, new HashMap()); + + ServiceLoader loader = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)); + consumerPermission.set(false); + assertFalse(loader.iterator().hasNext()); + + consumerPermission.set(true); + Iterator iterator = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)).iterator(); + assertTrue(iterator.hasNext()); + consumerPermission.set(false); + assertThrows(ServiceConfigurationError.class, iterator::next); + + consumerPermission.set(true); + assertTrue(Util.serviceLoaderLoad(MySPI.class, callerClass(consumer)) + .iterator().hasNext()); + } + + @Test + public void providerPermissionChangesDoNotRequireReindexing() throws Exception { + BaseActivator activator = newActivator(); + Bundle consumer = mockPermissionBundle(new AtomicBoolean(true)); + BundleWiring consumerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(consumerWiring.getRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.replay(consumerWiring); + activator.registerStandardConsumer(consumer, consumerWiring); + + AtomicBoolean providerPermission = new AtomicBoolean(false); + URL selected = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", selected); + Bundle provider = mockProviderBundle(42L, selected, providerPermission); + activator.registerProviderBundle( + MySPI.class.getName(), provider, new HashMap()); + + assertFalse(Util.serviceLoaderLoad(MySPI.class, callerClass(consumer)) + .iterator().hasNext()); + providerPermission.set(true); + Iterator iterator = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)).iterator(); + assertTrue(iterator.hasNext()); + providerPermission.set(false); + assertThrows(ServiceConfigurationError.class, iterator::next); + + providerPermission.set(true); + assertTrue(Util.serviceLoaderLoad(MySPI.class, callerClass(consumer)) + .iterator().hasNext()); + } + + @Test + public void providerFactoryRechecksRegisterPermission() { + AtomicBoolean providerPermission = new AtomicBoolean(true); + Bundle provider = mockPermissionBundle(providerPermission); + ProviderServiceFactory factory = new ProviderServiceFactory( + org.apache.aries.spifly.impl3.MySPIImpl3.class, + provider, MySPI.class.getName()); + + assertNotNull(factory.getService(null, null)); + providerPermission.set(false); + assertSame(null, factory.getService(null, null)); + } + private BaseActivator newActivator() { BaseActivator activator = new BaseActivator() { @Override @@ -242,6 +335,12 @@ private Class callerClass(Bundle consumer) throws Exception { @SuppressWarnings({ "unchecked", "rawtypes" }) private Bundle mockProviderBundle(long bundleId, URL providerJar) throws Exception { + return mockProviderBundle(bundleId, providerJar, new AtomicBoolean(true)); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Bundle mockProviderBundle(long bundleId, URL providerJar, + final AtomicBoolean permission) throws Exception { Bundle providerBundle = EasyMock.createMock(Bundle.class); final ClassLoader providerCL = new TestBundleClassLoader( new URL[] {providerJar}, getClass().getClassLoader(), providerBundle); @@ -265,6 +364,13 @@ private Bundle mockProviderBundle(long bundleId, URL providerJar) throws Excepti EasyMock.expect(providerBundle.adapt(BundleRevision.class)) .andReturn(providerRevision).anyTimes(); EasyMock.expect(providerBundle.getBundleId()).andReturn(bundleId).anyTimes(); + EasyMock.expect(providerBundle.hasPermission( + EasyMock.isA(ServicePermission.class))).andAnswer(new IAnswer() { + @Override + public Boolean answer() throws Throwable { + return permission.get(); + } + }).anyTimes(); EasyMock.expect(providerBundle.getEntryPaths((String) EasyMock.anyObject())) .andReturn(null).anyTimes(); Dictionary providerHeaders = new Hashtable(); @@ -283,6 +389,19 @@ public Class answer() throws Throwable { return providerBundle; } + private Bundle mockPermissionBundle(final AtomicBoolean permission) { + Bundle bundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(bundle.hasPermission(EasyMock.isA(ServicePermission.class))) + .andAnswer(new IAnswer() { + @Override + public Boolean answer() throws Throwable { + return permission.get(); + } + }).anyTimes(); + EasyMock.replay(bundle); + return bundle; + } + private static class TestBundleClassLoader extends URLClassLoader implements BundleReference { private final Bundle bundle; diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookGenericCapabilityTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookGenericCapabilityTest.java index bcd3036c58..decf4199d7 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookGenericCapabilityTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookGenericCapabilityTest.java @@ -58,9 +58,10 @@ import org.junit.Before; import org.junit.Test; import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleReference; -import org.osgi.framework.Version; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleReference; +import org.osgi.framework.ServicePermission; +import org.osgi.framework.Version; import org.osgi.framework.hooks.weaving.WeavingHook; import org.osgi.framework.hooks.weaving.WovenClass; import org.osgi.framework.wiring.BundleRequirement; @@ -785,9 +786,10 @@ private Bundle mockProviderBundle(String subdir, long id, Version version) throw } EasyMock.expect(providerBundle.getSymbolicName()).andReturn(bsn).anyTimes(); EasyMock.expect(providerBundle.getBundleId()).andReturn(id).anyTimes(); - EasyMock.expect(providerBundle.getBundleContext()).andReturn(bc).anyTimes(); - EasyMock.expect(providerBundle.getVersion()).andReturn(version).anyTimes(); - EasyMock.expect(providerBundle.getEntryPaths("/")).andAnswer(new IAnswer>() { + EasyMock.expect(providerBundle.getBundleContext()).andReturn(bc).anyTimes(); + EasyMock.expect(providerBundle.getVersion()).andReturn(version).anyTimes(); + EasyMock.expect(providerBundle.hasPermission(EasyMock.isA(ServicePermission.class))).andReturn(true).anyTimes(); + EasyMock.expect(providerBundle.getEntryPaths("/")).andAnswer(new IAnswer>() { @Override public Enumeration answer() throws Throwable { return Collections.enumeration(classResources); @@ -840,10 +842,11 @@ private Bundle mockConsumerBundle(Dictionary headers, BundleRevi EasyMock.expect(consumerBundle.getVersion()).andReturn(new Version(1, 2, 3)).anyTimes(); EasyMock.expect(consumerBundle.getHeaders()).andReturn(headers).anyTimes(); EasyMock.expect(consumerBundle.getBundleContext()).andReturn(bc).anyTimes(); - EasyMock.expect(consumerBundle.getBundleId()).andReturn(Long.MAX_VALUE).anyTimes(); - EasyMock.expect(consumerBundle.adapt(BundleRevision.class)).andReturn(rev).anyTimes(); - - EasyMock.replay(consumerBundle); + EasyMock.expect(consumerBundle.getBundleId()).andReturn(Long.MAX_VALUE).anyTimes(); + EasyMock.expect(consumerBundle.adapt(BundleRevision.class)).andReturn(rev).anyTimes(); + EasyMock.expect(consumerBundle.hasPermission(EasyMock.isA(ServicePermission.class))).andReturn(true).anyTimes(); + + EasyMock.replay(consumerBundle); List allBundles = new ArrayList(Arrays.asList(otherBundles)); allBundles.add(consumerBundle); diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookOSGi43Test.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookOSGi43Test.java index 2400eb133a..4344e3ad59 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookOSGi43Test.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookOSGi43Test.java @@ -53,6 +53,7 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleReference; +import org.osgi.framework.ServicePermission; import org.osgi.framework.Version; import org.osgi.framework.hooks.weaving.WeavingHook; import org.osgi.framework.hooks.weaving.WovenClass; @@ -205,6 +206,7 @@ public Class answer() throws Throwable { EasyMock.expect(providerBundle.getBundleContext()).andReturn(bc).anyTimes(); EasyMock.expect(providerBundle.getVersion()).andReturn(version).anyTimes(); EasyMock.expect(providerBundle.adapt(BundleRevision.class)).andReturn(bundleRevision).anyTimes(); + EasyMock.expect(providerBundle.hasPermission(EasyMock.isA(ServicePermission.class))).andReturn(true).anyTimes(); EasyMock.>expect(providerBundle.loadClass(EasyMock.anyObject(String.class))).andAnswer(new IAnswer>() { @Override public Class answer() throws Throwable { @@ -245,6 +247,7 @@ private Bundle mockConsumerBundle(Dictionary headers, Bundle ... EasyMock.expect(consumerBundle.getBundleContext()).andReturn(bc).anyTimes(); EasyMock.expect(consumerBundle.getBundleId()).andReturn(Long.MAX_VALUE).anyTimes(); EasyMock.expect(consumerBundle.adapt(BundleRevision.class)).andReturn(null).anyTimes(); + EasyMock.expect(consumerBundle.hasPermission(EasyMock.isA(ServicePermission.class))).andReturn(true).anyTimes(); EasyMock.replay(consumerBundle); List allBundles = new ArrayList(Arrays.asList(otherBundles)); diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java index 57ef54ef18..121643161d 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java @@ -57,9 +57,10 @@ import org.junit.Before; import org.junit.Test; import org.osgi.framework.Bundle; -import org.osgi.framework.BundleContext; -import org.osgi.framework.BundleReference; -import org.osgi.framework.Version; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleReference; +import org.osgi.framework.ServicePermission; +import org.osgi.framework.Version; import org.osgi.framework.hooks.weaving.WeavingHook; import org.osgi.framework.hooks.weaving.WovenClass; import org.osgi.framework.wiring.BundleRevision; @@ -769,9 +770,10 @@ private Bundle mockProviderBundle(String subdir, long id, Version version) throw } EasyMock.expect(providerBundle.getSymbolicName()).andReturn(bsn).anyTimes(); EasyMock.expect(providerBundle.getBundleId()).andReturn(id).anyTimes(); - EasyMock.expect(providerBundle.getBundleContext()).andReturn(bc).anyTimes(); - EasyMock.expect(providerBundle.getVersion()).andReturn(version).anyTimes(); - EasyMock.expect(providerBundle.getEntryPaths("/")).andAnswer(new IAnswer>() { + EasyMock.expect(providerBundle.getBundleContext()).andReturn(bc).anyTimes(); + EasyMock.expect(providerBundle.getVersion()).andReturn(version).anyTimes(); + EasyMock.expect(providerBundle.hasPermission(EasyMock.isA(ServicePermission.class))).andReturn(true).anyTimes(); + EasyMock.expect(providerBundle.getEntryPaths("/")).andAnswer(new IAnswer>() { @Override public Enumeration answer() throws Throwable { return Collections.enumeration(classResources); @@ -817,10 +819,11 @@ private Bundle mockConsumerBundle(Dictionary headers, Bundle ... Bundle consumerBundle = EasyMock.createMock(Bundle.class); EasyMock.expect(consumerBundle.getSymbolicName()).andReturn("testConsumer").anyTimes(); EasyMock.expect(consumerBundle.getHeaders()).andReturn(headers).anyTimes(); - EasyMock.expect(consumerBundle.getBundleContext()).andReturn(bc).anyTimes(); - EasyMock.expect(consumerBundle.getBundleId()).andReturn(Long.MAX_VALUE).anyTimes(); - EasyMock.expect(consumerBundle.adapt(BundleRevision.class)).andReturn(null).anyTimes(); - EasyMock.replay(consumerBundle); + EasyMock.expect(consumerBundle.getBundleContext()).andReturn(bc).anyTimes(); + EasyMock.expect(consumerBundle.getBundleId()).andReturn(Long.MAX_VALUE).anyTimes(); + EasyMock.expect(consumerBundle.adapt(BundleRevision.class)).andReturn(null).anyTimes(); + EasyMock.expect(consumerBundle.hasPermission(EasyMock.isA(ServicePermission.class))).andReturn(true).anyTimes(); + EasyMock.replay(consumerBundle); List allBundles = new ArrayList(Arrays.asList(otherBundles)); allBundles.add(consumerBundle); From ca9f955e3a5986107a17dd3a4577cd1fd9e80283 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 23:04:02 +0200 Subject: [PATCH 09/26] Refresh consumers after late processor fragments --- .../apache/aries/spifly/BaseActivator.java | 55 +++++++++++- .../aries/spifly/ResolvedWiringTest.java | 85 ++++++++++++++++++- .../aries/spifly/itests/InitialTest.java | 37 +++++++- 3 files changed, 169 insertions(+), 8 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 5c58d314a5..40704d8ef1 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -42,11 +42,13 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkEvent; import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.wiring.BundleRequirement; import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; +import org.osgi.framework.wiring.FrameworkWiring; import org.osgi.util.tracker.BundleTracker; import aQute.bnd.header.Parameters; @@ -85,6 +87,10 @@ public abstract class BaseActivator implements BundleActivator { private final ConcurrentMap standardConsumerWirings = new ConcurrentHashMap(); + private final Set> refreshedConsumerHosts = + Collections.newSetFromMap( + new ConcurrentHashMap, Boolean>()); + @SuppressWarnings({ "unchecked", "rawtypes" }) public synchronized void start(BundleContext context, final String consumerHeaderName) throws Exception { bundleContext = context; @@ -260,18 +266,63 @@ void fragmentAttached(Bundle fragment, String consumerHeaderName) throws Excepti } if (consumerBundleTracker != null && consumerBundleTracker.getObject(host) != null) { - removeWeavingData(host); - addConsumerWeavingData(host, consumerHeaderName); + reprocessConsumerHost(host, fragmentWiring.getRevision(), consumerHeaderName); } } } + void reprocessConsumerHost(Bundle host, BundleRevision fragmentRevision, + String consumerHeaderName) throws Exception { + boolean wasStandardConsumer = isStandardConsumer(host); + removeWeavingData(host); + addConsumerWeavingData(host, consumerHeaderName); + + if (SpiFlyConstants.SPI_CONSUMER_HEADER.equals(consumerHeaderName) + && !wasStandardConsumer && isStandardConsumer(host)) { + refreshConsumerHost(host, fragmentRevision); + } + } + + private void refreshConsumerHost(Bundle host, BundleRevision fragmentRevision) { + if (fragmentRevision == null) { + return; + } + Bundle systemBundle = bundleContext == null ? null : bundleContext.getBundle(0); + FrameworkWiring frameworkWiring = systemBundle == null + ? null : systemBundle.adapt(FrameworkWiring.class); + if (frameworkWiring == null) { + log(Level.WARNING, "Cannot refresh consumer host " + host + + " after processor fragment attachment: FrameworkWiring is unavailable"); + return; + } + Pair refreshKey = + new Pair(host, fragmentRevision); + if (!refreshedConsumerHosts.add(refreshKey)) { + return; + } + + try { + frameworkWiring.refreshBundles(Collections.singleton(host), event -> { + if (event.getType() == FrameworkEvent.ERROR) { + log(Level.WARNING, "Could not refresh consumer host " + host + + " after processor fragment attachment", event.getThrowable()); + } + }); + } + catch (RuntimeException e) { + refreshedConsumerHosts.remove(refreshKey); + log(Level.WARNING, "Could not request refresh of consumer host " + host + + " after processor fragment attachment", e); + } + } + @Override public synchronized void stop(BundleContext context) throws Exception { activator = null; consumerBundleTracker.close(); providerBundleTracker.close(); + refreshedConsumerHosts.clear(); } public boolean isLogEnabled(Level level) { diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java index 5596ac0fc8..647209f304 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java @@ -39,11 +39,13 @@ import org.junit.Test; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkListener; import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleRequirement; import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; +import org.osgi.framework.wiring.FrameworkWiring; public class ResolvedWiringTest { private static final String SERVICE_TYPE = "org.example.Service"; @@ -63,9 +65,7 @@ public void setUp() throws Exception { EasyMock.expect(context.getBundle()).andReturn(mediator).anyTimes(); EasyMock.replay(context); - Field contextField = BaseActivator.class.getDeclaredField("bundleContext"); - contextField.setAccessible(true); - contextField.set(activator, context); + setBundleContext(context); } @Test @@ -238,6 +238,79 @@ void fragmentAttached(Bundle fragment, String consumerHeaderName) { assertTrue(attached[0]); } + @Test + public void lateProcessorFragmentRefreshesConsumerHostOnce() throws Exception { + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mediator)), + Collections.emptyList(), + Collections.emptyList()); + Bundle consumer = mockConsumer(wiring); + BundleRevision fragmentRevision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.replay(fragmentRevision); + + FrameworkWiring frameworkWiring = EasyMock.createMock(FrameworkWiring.class); + frameworkWiring.refreshBundles( + EasyMock.eq(Collections.singleton(consumer)), + EasyMock.anyObject()); + EasyMock.expectLastCall().once(); + EasyMock.replay(frameworkWiring); + + Bundle systemBundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(systemBundle.adapt(FrameworkWiring.class)) + .andReturn(frameworkWiring).anyTimes(); + EasyMock.replay(systemBundle); + BundleContext context = EasyMock.createNiceMock(BundleContext.class); + EasyMock.expect(context.getBundle()).andReturn(mediator).anyTimes(); + EasyMock.expect(context.getBundle(0)).andReturn(systemBundle).anyTimes(); + EasyMock.replay(context); + setBundleContext(context); + + activator.reprocessConsumerHost( + consumer, fragmentRevision, SpiFlyConstants.SPI_CONSUMER_HEADER); + assertTrue(activator.isStandardConsumer(consumer)); + + // A refresh can make the tracker rebuild the host metadata and observe the same + // attached fragment revision again. It must not start a refresh loop. + activator.removeWeavingData(consumer); + activator.reprocessConsumerHost( + consumer, fragmentRevision, SpiFlyConstants.SPI_CONSUMER_HEADER); + + EasyMock.verify(frameworkWiring); + } + + @Test + public void lateProcessorFragmentDoesNotRefreshStaticConsumer() throws Exception { + FrameworkWiring frameworkWiring = EasyMock.createMock(FrameworkWiring.class); + EasyMock.replay(frameworkWiring); + Bundle systemBundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(systemBundle.adapt(FrameworkWiring.class)) + .andReturn(frameworkWiring).anyTimes(); + EasyMock.replay(systemBundle); + BundleContext context = EasyMock.createNiceMock(BundleContext.class); + EasyMock.expect(context.getBundle()).andReturn(mediator).anyTimes(); + EasyMock.expect(context.getBundle(0)).andReturn(systemBundle).anyTimes(); + EasyMock.replay(context); + setBundleContext(context); + + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mediator)), + Collections.emptyList(), + Collections.emptyList()); + Bundle consumer = mockConsumer(wiring, true, true); + BundleRevision fragmentRevision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.replay(fragmentRevision); + + activator.reprocessConsumerHost(consumer, fragmentRevision, + SpiFlyConstants.PROCESSED_SPI_CONSUMER_HEADER); + + assertTrue(activator.isStandardConsumer(consumer)); + EasyMock.verify(frameworkWiring); + } + private BundleWiring mockConsumerWiring(List extenderWires, List serviceRequirements, List serviceWires) { BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); @@ -293,6 +366,12 @@ private Bundle mockConsumer(BundleWiring wiring, boolean processed, boolean proc return consumer; } + private void setBundleContext(BundleContext context) throws Exception { + Field contextField = BaseActivator.class.getDeclaredField("bundleContext"); + contextField.setAccessible(true); + contextField.set(activator, context); + } + private Bundle mockBundle(long id) { Bundle bundle = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(bundle.getBundleId()).andReturn(id).anyTimes(); diff --git a/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java b/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java index 748ed2de48..5b01a64a82 100644 --- a/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java +++ b/spi-fly/spi-fly-itests/src/main/java/org/apache/aries/spifly/itests/InitialTest.java @@ -31,6 +31,8 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import org.apache.aries.spifly.itests.util.TeeOutputStream; import org.junit.jupiter.api.AfterEach; @@ -40,6 +42,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleListener; import org.osgi.framework.ServiceReference; import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.wiring.BundleWire; @@ -114,10 +118,37 @@ public void example3() throws Exception { BundleAssert.assertThat(provider3fragment).isFragment().isInState(Bundle.RESOLVED); assertFragmentAttached(provider3Bundle, provider3fragment); - Bundle client3fragment = assertBundleInstallation(getExampleJar("spi-fly-example-client3-fragment"), true); Bundle client3Bundle = assertBundleInstallation(getExampleJar("spi-fly-example-client3-bundle")); - BundleAssert.assertThat(client3fragment).isFragment().isInState(Bundle.RESOLVED); - assertFragmentAttached(client3Bundle, client3fragment); + assertThat(outContent.toString()).contains( + "*** Result from invoking the SPI from untreated bundle:") + .doesNotContain("Doing it as well!"); + + CountDownLatch clientRestarted = new CountDownLatch(1); + BundleListener restartListener = event -> { + if (event.getType() == BundleEvent.STARTED + && client3Bundle.equals(event.getBundle())) { + clientRestarted.countDown(); + } + }; + bundleContext.addBundleListener(restartListener); + try { + Bundle client3fragment = assertBundleInstallation( + getExampleJar("spi-fly-example-client3-fragment"), true); + FrameworkWiring frameworkWiring = + bundleContext.getBundle(0).adapt(FrameworkWiring.class); + assertThat(frameworkWiring.resolveBundles( + Collections.singleton(client3fragment))).isTrue(); + assertThat(clientRestarted.await(30, TimeUnit.SECONDS)) + .as("the late processor fragment should refresh and restart its host") + .isTrue(); + + BundleAssert.assertThat(client3fragment).isFragment().isInState(Bundle.RESOLVED); + BundleAssert.assertThat(client3Bundle).isInState(Bundle.ACTIVE); + assertFragmentAttached(client3Bundle, client3fragment); + } + finally { + bundleContext.removeBundleListener(restartListener); + } assertThat( outContent.toString() From 75bbdd8ff05d7da737f443a0d16860784c75eaf0 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 23:49:11 +0200 Subject: [PATCH 10/26] Make static mediation offline-only --- spi-fly/spi-fly-static-bundle/pom.xml | 3 +- .../apache/aries/spifly/statictool/Main.java | 55 ++++++++++++++++++- .../spifly/statictool/RequirementTest.java | 17 +++++- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/spi-fly/spi-fly-static-bundle/pom.xml b/spi-fly/spi-fly-static-bundle/pom.xml index 1a2e953795..14ecffd4f8 100644 --- a/spi-fly/spi-fly-static-bundle/pom.xml +++ b/spi-fly/spi-fly-static-bundle/pom.xml @@ -113,8 +113,7 @@ META-INF/LICENSE=LICENSE,\ META-INF/NOTICE=NOTICE Provide-Capability: \ - osgi.extender;osgi.extender=osgi.serviceloader.registrar;version:Version=1.0,\ - osgi.extender;osgi.extender=osgi.serviceloader.processor;version:Version=1.0;uses:="org.apache.aries.spifly" + osgi.extender;osgi.extender=osgi.serviceloader.registrar;version:Version=1.0 -fixupmessages: Export org.apache.aries.spifly, has 1, private references ]]> diff --git a/spi-fly/spi-fly-static-tool/src/main/java/org/apache/aries/spifly/statictool/Main.java b/spi-fly/spi-fly-static-tool/src/main/java/org/apache/aries/spifly/statictool/Main.java index 71a008b9bc..15569d2592 100644 --- a/spi-fly/spi-fly-static-tool/src/main/java/org/apache/aries/spifly/statictool/Main.java +++ b/spi-fly/spi-fly-static-tool/src/main/java/org/apache/aries/spifly/statictool/Main.java @@ -27,6 +27,10 @@ import java.io.OutputStream; import java.net.URL; import java.net.URLClassLoader; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.jar.Attributes; @@ -44,8 +48,13 @@ import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.osgi.framework.Constants; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; import org.osgi.framework.Version; +import aQute.bnd.header.Attrs; +import aQute.bnd.header.Parameters; + public class Main { static final String PROCESSED_REQUIRE_CAPABILITY_HEADER = "X-SpiFly-Processed-Require-Capability"; @@ -102,8 +111,15 @@ private static void weaveJar(String jarPath) throws Exception { manifest.getMainAttributes().putValue(SpiFlyConstants.PROCESSED_SPI_CONSUMER_HEADER, consumerHeaderVal); } else { // It's SpiFlyConstants.REQUIRE_CAPABILITY - // Keep the processor requirement so the transformed consumer is resolved to the - // mediator that will enforce its provider wires at runtime. + String remainingRequirements = removeProcessorRequirement(consumerHeaderVal); + if (remainingRequirements.isEmpty()) { + manifest.getMainAttributes().remove( + new Attributes.Name(SpiFlyConstants.REQUIRE_CAPABILITY)); + } + else { + manifest.getMainAttributes().putValue( + SpiFlyConstants.REQUIRE_CAPABILITY, remainingRequirements); + } manifest.getMainAttributes().putValue( PROCESSED_REQUIRE_CAPABILITY_HEADER, consumerHeaderVal); } @@ -119,6 +135,41 @@ private static void weaveJar(String jarPath) throws Exception { delTree(tempDir); } + static String removeProcessorRequirement(String header) throws Exception { + Parameters requirements = new Parameters(header); + Dictionary processorCapability = new Hashtable(); + processorCapability.put(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME); + processorCapability.put("version", SpiFlyConstants.SPECIFICATION_VERSION); + + for (Iterator> iterator = + requirements.entrySet().iterator(); iterator.hasNext();) { + Map.Entry requirement = iterator.next(); + if (!SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE.equals( + removeDuplicateMarker(requirement.getKey()))) { + continue; + } + + String filterString = requirement.getValue().get(SpiFlyConstants.FILTER_DIRECTIVE); + if (filterString == null) { + continue; + } + Filter filter = FrameworkUtil.createFilter(filterString); + if (filter.match(processorCapability)) { + iterator.remove(); + } + } + return requirements.toString(); + } + + private static String removeDuplicateMarker(String key) { + int end = key.length(); + while (end > 0 && key.charAt(end - 1) == '~') { + end--; + } + return key.substring(0, end); + } + private static void extendImportPackage(Manifest manifest) throws IOException { String utilPkgVersion = getPackageVersion(Util.class); diff --git a/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java index b915fe08a8..16e81a02b6 100644 --- a/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java +++ b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java @@ -41,6 +41,8 @@ import org.apache.aries.spifly.statictool.bundle.TestClass; import org.junit.Test; +import aQute.bnd.header.Parameters; + public class RequirementTest { @Test public void testConsumerBundle() throws Exception { @@ -64,7 +66,8 @@ public void testConsumerBundle() throws Exception { mainAttributes.putValue("Import-Package", "org.foo.bar"); mainAttributes.putValue(SpiFlyConstants.REQUIRE_CAPABILITY, "osgi.serviceloader; filter:=\"(osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider)\";cardinality:=multiple, " + - "osgi.extender; filter:=\"(osgi.extender=osgi.serviceloader.processor)\""); + "osgi.extender; filter:=\"(&(osgi.extender=osgi.serviceloader.processor)(version>=1.0))\", " + + "osgi.extender; filter:=\"(osgi.extender=example.other)\";resolution:=optional"); JarOutputStream jos = new JarOutputStream(new FileOutputStream(jarFile), mf); jos.putNextEntry(new ZipEntry(testClassFileName)); @@ -88,9 +91,17 @@ public void testConsumerBundle() throws Exception { assertEquals("Bar Bar", actualMF.getMainAttributes().getValue("Foo")); String requirement = "osgi.serviceloader; filter:=\"(osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider)\";cardinality:=multiple, " + - "osgi.extender; filter:=\"(osgi.extender=osgi.serviceloader.processor)\""; - assertEquals(requirement, + "osgi.extender; filter:=\"(&(osgi.extender=osgi.serviceloader.processor)(version>=1.0))\", " + + "osgi.extender; filter:=\"(osgi.extender=example.other)\";resolution:=optional"; + Parameters remainingRequirements = new Parameters( actualMF.getMainAttributes().getValue(SpiFlyConstants.REQUIRE_CAPABILITY)); + assertEquals(2, remainingRequirements.size()); + assertTrue(remainingRequirements.toString(), remainingRequirements.toString().contains( + "osgi.serviceloader=org.apache.aries.spifly.mysvc.SPIProvider")); + assertTrue(remainingRequirements.toString(), remainingRequirements.toString().contains( + "osgi.extender=example.other")); + assertFalse(remainingRequirements.toString(), remainingRequirements.toString().contains( + SpiFlyConstants.PROCESSOR_EXTENDER_NAME)); assertEquals(requirement, actualMF.getMainAttributes().getValue( Main.PROCESSED_REQUIRE_CAPABILITY_HEADER)); From c01d020e7b8868e9d1b7814ef8a6bcaf5e544b73 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 23:56:13 +0200 Subject: [PATCH 11/26] Mediate all ServiceLoader entry points --- .../apache/aries/spifly/BaseActivator.java | 10 +- .../aries/spifly/ConsumerHeaderProcessor.java | 27 +++- .../java/org/apache/aries/spifly/Util.java | 118 ++++++++++++------ .../aries/spifly/ResolvedWiringTest.java | 6 + .../org/apache/aries/spifly/UtilTest.java | 2 + .../spifly/dynamic/ClientWeavingHookTest.java | 40 +++++- .../aries/spifly/dynamic/TestClient.java | 17 ++- .../spifly/statictool/RequirementTest.java | 9 ++ .../spifly/statictool/bundle/Test4Class.java | 27 ++++ .../spifly/weaver/TCCLSetterVisitor.java | 17 ++- 10 files changed, 213 insertions(+), 60 deletions(-) create mode 100644 spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/bundle/Test4Class.java diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 40704d8ef1..6a196bf50a 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -476,7 +476,7 @@ public Collection findConsumerRestrictions(Bundle consumer, String class Map, String> args) { StandardConsumerWiring standardWiring = standardConsumerWirings.get(consumer); if (standardWiring != null && ServiceLoader.class.getName().equals(className) - && "load".equals(methodName)) { + && isServiceLoaderMethod(methodName)) { String serviceType = args == null ? null : args.get(new Pair(0, Class.class.getName())); return standardWiring.getProviders(serviceType); @@ -532,8 +532,8 @@ private Collection getBundles(List descriptors, String } else if (desc.getFilter() != null) { Hashtable d = new Hashtable(); - if (ServiceLoader.class.getName().equals(className) && - "load".equals(methodName)) { + if (ServiceLoader.class.getName().equals(className) + && isServiceLoaderMethod(methodName)) { String type = args.get(new Pair(0, Class.class.getName())); if (type != null) { d.put(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, type); @@ -554,6 +554,10 @@ private Collection getBundles(List descriptors, String return bundles; } + private static boolean isServiceLoaderMethod(String methodName) { + return "load".equals(methodName) || "loadInstalled".equals(methodName); + } + private static final class StandardConsumerWiring { private final boolean restricted; private final Map> providersByServiceType; diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerHeaderProcessor.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerHeaderProcessor.java index dc5040317f..2d85dd0845 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerHeaderProcessor.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerHeaderProcessor.java @@ -178,15 +178,21 @@ public static Set processHeader(String consumerHeaderName, String c weavingData.add(createWeavingData(className, methodName, methodRestriction, allowedBundles)); - if (serviceLoader) { - className = ServiceLoader.class.getName(); - methodName = "load"; + if (serviceLoader) { + className = ServiceLoader.class.getName(); + methodName = "load"; ArgRestrictions argRestrictions = new ArgRestrictions(); argRestrictions.addRestriction(0, Class.class.getName()); argRestrictions.addRestriction(1, ClassLoader.class.getName()); - methodRestriction = new MethodRestriction(methodName, argRestrictions); - weavingData.add(createWeavingData(className, methodName, methodRestriction, allowedBundles)); - } + methodRestriction = new MethodRestriction(methodName, argRestrictions); + weavingData.add(createWeavingData(className, methodName, methodRestriction, allowedBundles)); + + methodName = "loadInstalled"; + argRestrictions = new ArgRestrictions(); + argRestrictions.addRestriction(0, Class.class.getName()); + methodRestriction = new MethodRestriction(methodName, argRestrictions); + weavingData.add(createWeavingData(className, methodName, methodRestriction, allowedBundles)); + } } return weavingData; } @@ -238,6 +244,15 @@ private static Set createServiceLoaderWeavingData(List ServiceLoader serviceLoaderLoad(Class service, Class() { - @Override - public ClassLoader run() { - return caller.getClassLoader(); - } - } - ); - - if (!(bundleLoader instanceof BundleReference)) { - BaseActivator.activator.log(Level.FINE, "Classloader of consuming bundle doesn't implement BundleReference: " + bundleLoader); - return ServiceLoader.load(service); - } - - BundleReference bundleReference = (BundleReference)bundleLoader; - - Bundle consumerBundle = bundleReference.getBundle(); + Bundle consumerBundle = getConsumerBundle(caller); + if (consumerBundle == null) { + return ServiceLoader.load(service); + } final ClassLoader bundleClassloader = findContextClassloader( consumerBundle, ServiceLoader.class.getName(), "load", service, true); @@ -124,23 +114,10 @@ public static ServiceLoader serviceLoaderLoad( return null; } - ClassLoader bundleLoader = AccessController.doPrivileged( - new PrivilegedAction() { - @Override - public ClassLoader run() { - return caller.getClassLoader(); - } - } - ); - - if (!(bundleLoader instanceof BundleReference)) { - BaseActivator.activator.log(Level.FINE, "Classloader of consuming bundle doesn't implement BundleReference: " + bundleLoader); - return ServiceLoader.load(service, specifiedClassLoader); - } - - BundleReference bundleReference = (BundleReference)bundleLoader; - - Bundle consumerBundle = bundleReference.getBundle(); + Bundle consumerBundle = getConsumerBundle(caller); + if (consumerBundle == null) { + return ServiceLoader.load(service, specifiedClassLoader); + } final ClassLoader bundleClassloader = findContextClassloader( consumerBundle, ServiceLoader.class.getName(), "load", service, true); @@ -158,7 +135,68 @@ public ClassLoader run() { service.getName())); } return ServiceLoader.load(service, new WrapperCL(specifiedClassLoader, bundleClassloader)); - } + } + + @BaselineIgnore("1.4.0") + public static ServiceLoader serviceLoaderLoadInstalled( + Class service, Class caller) { + if (BaseActivator.activator == null) { + return null; + } + + Bundle consumerBundle = getConsumerBundle(caller); + if (consumerBundle == null) { + return ServiceLoader.loadInstalled(service); + } + + ClassLoader bundleClassloader = findContextClassloader( + consumerBundle, ServiceLoader.class.getName(), + "loadInstalled", service, true); + if (bundleClassloader == null + && !BaseActivator.activator.isStandardConsumer(consumerBundle)) { + return ServiceLoader.loadInstalled(service); + } + + ClassLoader installedClassLoader = getInstalledClassLoader(); + return ServiceLoader.load(service, new ProviderViewClassLoader( + installedClassLoader, bundleClassloader, consumerBundle, + service.getName())); + } + + private static ClassLoader getInstalledClassLoader() { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public ClassLoader run() { + ClassLoader loader = ClassLoader.getSystemClassLoader(); + while (loader != null && loader.getParent() != null) { + loader = loader.getParent(); + } + return loader; + } + }); + } + + private static Bundle getConsumerBundle(final Class caller) { + Bundle bundle = FrameworkUtil.getBundle(caller); + if (bundle != null) { + return bundle; + } + + ClassLoader bundleLoader = AccessController.doPrivileged( + new PrivilegedAction() { + @Override + public ClassLoader run() { + return caller.getClassLoader(); + } + }); + if (bundleLoader instanceof BundleReference) { + return ((BundleReference) bundleLoader).getBundle(); + } + + BaseActivator.activator.log(Level.FINE, + "Could not identify consuming bundle for class " + caller.getName()); + return null; + } public static void fixContextClassloader(String cls, String method, Class clsArg, ClassLoader bundleLoader) { BundleReference br = getBundleReference(bundleLoader); @@ -190,7 +228,7 @@ private static ClassLoader findContextClassloader(Bundle consumerBundle, String String requestedClass; Map, String> args; boolean serviceLoaderCall = ServiceLoader.class.getName().equals(className) - && "load".equals(methodName); + && ("load".equals(methodName) || "loadInstalled".equals(methodName)); if (serviceLoaderCall) { requestedClass = clsArg.getName(); args = new HashMap, String>(); @@ -214,7 +252,7 @@ private static ClassLoader findContextClassloader(Bundle consumerBundle, String } } - if (ServiceLoader.class.getName().equals(className) && "load".equals(methodName)) { + if (serviceLoaderCall) { bundles = activator.filterCompatibleProviderBundles( consumerBundle, clsArg, bundles); } diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java index 647209f304..40a90ff64c 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java @@ -135,6 +135,9 @@ public void declaredButUnwiredServiceRequirementAllowsNoProviders() throws Excep assertEquals(Collections.emptySet(), activator.findConsumerRestrictions( consumer, ServiceLoader.class.getName(), "load", serviceArguments(SERVICE_TYPE))); + assertEquals(Collections.emptySet(), activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "loadInstalled", + serviceArguments(SERVICE_TYPE))); } @Test @@ -151,6 +154,9 @@ public void consumerWithoutServiceRequirementCanSeeAllPublishedProviders() throw assertNull(activator.findConsumerRestrictions( consumer, ServiceLoader.class.getName(), "load", serviceArguments(SERVICE_TYPE))); + assertNull(activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "loadInstalled", + serviceArguments(SERVICE_TYPE))); } @Test diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java index 1844223c8e..5dacedaff2 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java @@ -168,6 +168,8 @@ public void standardConsumerWithNoProvidersHasClosedView() throws Exception { MySPI.class, callerClass(consumer)); assertFalse(loader.iterator().hasNext()); + assertFalse(Util.serviceLoaderLoadInstalled( + MySPI.class, callerClass(consumer)).iterator().hasNext()); } @Test diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java index 121643161d..569c9d38b1 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java @@ -160,7 +160,7 @@ public void test_ARIES_1755_ServiceLoaderUsage() throws Exception { } @Test - public void testBasicServiceLoaderUsage2() throws Exception { + public void testBasicServiceLoaderUsage2() throws Exception { Dictionary consumerHeaders = new Hashtable(); consumerHeaders.put(SpiFlyConstants.SPI_CONSUMER_HEADER, "*"); @@ -192,8 +192,42 @@ public void testBasicServiceLoaderUsage2() throws Exception { Class cls = wc.getDefinedClass(); Method method = cls.getMethod("testService2", new Class [] {String.class}); Object result = method.invoke(cls.getDeclaredConstructor().newInstance(), "hello"); - assertEquals(Collections.singleton("olleh"), result); - } + assertEquals(Collections.singleton("olleh"), result); + } + + @Test + public void testServiceLoaderLoadInstalled() throws Exception { + Dictionary consumerHeaders = new Hashtable(); + consumerHeaders.put(SpiFlyConstants.SPI_CONSUMER_HEADER, "*"); + + Bundle providerBundle = mockProviderBundle("impl1", 1); + activator.registerProviderBundle("org.apache.aries.mytest.MySPI", + providerBundle, new HashMap()); + + Bundle consumerBundle = mockConsumerBundle(consumerHeaders, providerBundle); + activator.addConsumerWeavingData( + consumerBundle, SpiFlyConstants.SPI_CONSUMER_HEADER); + + Bundle spiFlyBundle = mockSpiFlyBundle( + "spifly", Version.parseVersion("1.9.4"), + consumerBundle, providerBundle); + WeavingHook wh = new ClientWeavingHook( + spiFlyBundle.getBundleContext(), activator); + + URL clsUrl = getClass().getResource("TestClient.class"); + assertNotNull("Precondition", clsUrl); + WovenClass wc = new MyWovenClass(clsUrl, + "org.apache.aries.spifly.dynamic.TestClient", consumerBundle); + wh.weave(wc); + + Class cls = wc.getDefinedClass(); + assertTrue(activator.getWeavingData(consumerBundle).toString(), + activator.getWeavingData(consumerBundle).stream() + .anyMatch(data -> "loadInstalled".equals(data.getMethodName()))); + Method method = cls.getMethod("testInstalled", String.class); + Object result = method.invoke(cls.getDeclaredConstructor().newInstance(), "hello"); + assertEquals(Collections.singleton("olleh"), result); + } @Test public void testBasicServiceLoaderUsage3() throws Exception { diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/TestClient.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/TestClient.java index 9c85299ca9..5e9931c897 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/TestClient.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/TestClient.java @@ -58,7 +58,7 @@ public Set testService(String input, Class service) { /** * Slightly different example where the class is loaded using Class.forName() */ - public Set testService2(String input) throws Exception { + public Set testService2(String input) throws Exception { Set results = new HashSet(); Class cls = Class.forName("org.apache.aries.mytest.MySPI"); @@ -66,6 +66,15 @@ public Set testService2(String input) throws Exception { for (Object obj : loader) { results.add(((MySPI) obj).someMethod(input)); } - return results; - } -} + return results; + } + + public Set testInstalled(String input) { + Set results = new HashSet(); + ServiceLoader loader = ServiceLoader.loadInstalled(MySPI.class); + for (MySPI mySPI : loader) { + results.add(mySPI.someMethod(input)); + } + return results; + } +} diff --git a/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java index 16e81a02b6..785a1eeb53 100644 --- a/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java +++ b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java @@ -38,6 +38,7 @@ import org.apache.aries.spifly.Streams; import org.apache.aries.spifly.statictool.bundle.Test2Class; import org.apache.aries.spifly.statictool.bundle.Test3Class; +import org.apache.aries.spifly.statictool.bundle.Test4Class; import org.apache.aries.spifly.statictool.bundle.TestClass; import org.junit.Test; @@ -52,6 +53,8 @@ public void testConsumerBundle() throws Exception { URL test2ClassURL = getClass().getResource("/" + test2ClassFileName); String test3ClassFileName = Test3Class.class.getName().replace('.', '/') + ".class"; URL test3ClassURL = getClass().getResource("/" + test3ClassFileName); + String test4ClassFileName = Test4Class.class.getName().replace('.', '/') + ".class"; + URL test4ClassURL = getClass().getResource("/" + test4ClassFileName); File jarFile = new File(System.getProperty("java.io.tmpdir") + "/testjar_" + System.currentTimeMillis() + ".jar"); File expectedFile = null; @@ -76,6 +79,8 @@ public void testConsumerBundle() throws Exception { Streams.pump(test2ClassURL.openStream(), jos); jos.putNextEntry(new ZipEntry(test3ClassFileName)); Streams.pump(test3ClassURL.openStream(), jos); + jos.putNextEntry(new ZipEntry(test4ClassFileName)); + Streams.pump(test4ClassURL.openStream(), jos); jos.close(); Main.main(jarFile.getCanonicalPath()); @@ -126,6 +131,10 @@ public void testConsumerBundle() throws Exception { byte[] transBytes3 = Streams.suck(transformedJarFile.getInputStream(new ZipEntry(test3ClassFileName))); assertFalse("The transformed class should be different", Arrays.equals(orgBytes3, transBytes3)); + byte[] orgBytes4 = Streams.suck(initialJarFile.getInputStream(new ZipEntry(test4ClassFileName))); + byte[] transBytes4 = Streams.suck(transformedJarFile.getInputStream(new ZipEntry(test4ClassFileName))); + assertFalse("The loadInstalled class should be transformed", Arrays.equals(orgBytes4, transBytes4)); + initialJarFile.close(); transformedJarFile.close(); } finally { diff --git a/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/bundle/Test4Class.java b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/bundle/Test4Class.java new file mode 100644 index 0000000000..a85edb2e8b --- /dev/null +++ b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/bundle/Test4Class.java @@ -0,0 +1,27 @@ +/** + * 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.aries.spifly.statictool.bundle; + +import java.util.ServiceLoader; + +public class Test4Class { + public ServiceLoader load() { + return ServiceLoader.loadInstalled(String.class); + } +} diff --git a/spi-fly/spi-fly-weaver/src/main/java/org/apache/aries/spifly/weaver/TCCLSetterVisitor.java b/spi-fly/spi-fly-weaver/src/main/java/org/apache/aries/spifly/weaver/TCCLSetterVisitor.java index 9a05deec5a..05468ad49b 100644 --- a/spi-fly/spi-fly-weaver/src/main/java/org/apache/aries/spifly/weaver/TCCLSetterVisitor.java +++ b/spi-fly/spi-fly-weaver/src/main/java/org/apache/aries/spifly/weaver/TCCLSetterVisitor.java @@ -213,10 +213,19 @@ public void visitMethodInsn(int opcode, String owner, String name, String desc, return; } - additionalImportRequired = true; - woven = true; - - // ServiceLoader.load(Class, ClassLoader) + additionalImportRequired = true; + woven = true; + + // ServiceLoader.loadInstalled(Class) + if (ServiceLoader.class.getName().equals(wd.getClassName()) + && "loadInstalled".equals(wd.getMethodName())) { + visitLdcInsn(targetClass); + invokeStatic(UTIL_CLASS, new Method("serviceLoaderLoadInstalled", + SERVICELOADER_TYPE, new Type[] {CLASS_TYPE, CLASS_TYPE})); + return; + } + + // ServiceLoader.load(Class, ClassLoader) if (ServiceLoader.class.getName().equals(wd.getClassName()) && "load".equals(wd.getMethodName()) && Arrays.equals(new String [] {Class.class.getName(), ClassLoader.class.getName()}, wd.getArgClasses())) { From b865d87a21796b1286fc36a86e2480956605b3d7 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Thu, 6 Aug 2026 23:58:17 +0200 Subject: [PATCH 12/26] Honor declared ServiceLoader requirements --- .../apache/aries/spifly/BaseActivator.java | 43 ++++++++++- .../aries/spifly/ResolvedWiringTest.java | 77 +++++++++++++++++++ 2 files changed, 117 insertions(+), 3 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 6a196bf50a..7ebe66a2a7 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -572,9 +572,7 @@ static StandardConsumerWiring from(BundleWiring wiring) { return new StandardConsumerWiring(true, Collections.>emptyMap()); } - List requirements = wiring.getRequirements( - SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); - if (requirements.isEmpty()) { + if (!hasDeclaredServiceLoaderRequirement(wiring)) { return new StandardConsumerWiring(false, Collections.>emptyMap()); } @@ -598,6 +596,45 @@ static StandardConsumerWiring from(BundleWiring wiring) { true, Collections.unmodifiableMap(immutableProviders)); } + private static boolean hasDeclaredServiceLoaderRequirement(BundleWiring wiring) { + BundleRevision hostRevision = wiring.getRevision(); + if (hostRevision == null) { + // Compatibility for older wiring implementations and test doubles. A real + // R8 wiring supplies its revision, whose declared view is authoritative. + List requirements = wiring.getRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); + return requirements != null && !requirements.isEmpty(); + } + + if (hasDeclaredServiceLoaderRequirement(hostRevision)) { + return true; + } + List hostWires = wiring.getProvidedWires( + HostNamespace.HOST_NAMESPACE); + if (hostWires == null) { + return false; + } + for (BundleWire hostWire : hostWires) { + BundleRequirement hostRequirement = hostWire.getRequirement(); + BundleRevision fragmentRevision = hostRequirement == null + ? null : hostRequirement.getRevision(); + if (hasDeclaredServiceLoaderRequirement(fragmentRevision)) { + return true; + } + } + return false; + } + + private static boolean hasDeclaredServiceLoaderRequirement( + BundleRevision revision) { + if (revision == null) { + return false; + } + List requirements = revision.getDeclaredRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); + return requirements != null && !requirements.isEmpty(); + } + static StandardConsumerWiring denied() { return new StandardConsumerWiring( true, Collections.>emptyMap()); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java index 40a90ff64c..b77f2887cc 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java @@ -40,6 +40,7 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkListener; +import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleRequirement; import org.osgi.framework.wiring.BundleRevision; @@ -140,6 +141,63 @@ public void declaredButUnwiredServiceRequirementAllowsNoProviders() throws Excep serviceArguments(SERVICE_TYPE))); } + @Test + public void optionalDeclaredButDiscardedRequirementAllowsNoProviders() + throws Exception { + BundleRequirement declaredRequirement = + EasyMock.createNiceMock(BundleRequirement.class); + EasyMock.replay(declaredRequirement); + BundleRevision hostRevision = mockRevision( + Collections.singletonList(declaredRequirement)); + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mediator)), + Collections.emptyList(), + Collections.emptyList(), hostRevision, + Collections.emptyList()); + Bundle consumer = mockConsumer(wiring); + + activator.addConsumerWeavingData(consumer, SpiFlyConstants.SPI_CONSUMER_HEADER); + + assertEquals(Collections.emptySet(), activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "load", + serviceArguments(SERVICE_TYPE))); + } + + @Test + public void fragmentDeclaredButDiscardedRequirementAllowsNoProviders() + throws Exception { + BundleRequirement declaredRequirement = + EasyMock.createNiceMock(BundleRequirement.class); + EasyMock.replay(declaredRequirement); + BundleRevision fragmentRevision = mockRevision( + Collections.singletonList(declaredRequirement)); + BundleRequirement hostRequirement = EasyMock.createNiceMock(BundleRequirement.class); + EasyMock.expect(hostRequirement.getRevision()) + .andReturn(fragmentRevision).anyTimes(); + EasyMock.replay(hostRequirement); + BundleWire hostWire = EasyMock.createNiceMock(BundleWire.class); + EasyMock.expect(hostWire.getRequirement()).andReturn(hostRequirement).anyTimes(); + EasyMock.replay(hostWire); + + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mediator)), + Collections.emptyList(), + Collections.emptyList(), + mockRevision(Collections.emptyList()), + Collections.singletonList(hostWire)); + Bundle consumer = mockConsumer(wiring); + + activator.addConsumerWeavingData(consumer, SpiFlyConstants.SPI_CONSUMER_HEADER); + + assertEquals(Collections.emptySet(), activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "loadInstalled", + serviceArguments(SERVICE_TYPE))); + } + @Test public void consumerWithoutServiceRequirementCanSeeAllPublishedProviders() throws Exception { BundleWiring wiring = mockConsumerWiring( @@ -319,6 +377,13 @@ public void lateProcessorFragmentDoesNotRefreshStaticConsumer() throws Exception private BundleWiring mockConsumerWiring(List extenderWires, List serviceRequirements, List serviceWires) { + return mockConsumerWiring(extenderWires, serviceRequirements, serviceWires, + null, Collections.emptyList()); + } + + private BundleWiring mockConsumerWiring(List extenderWires, + List serviceRequirements, List serviceWires, + BundleRevision revision, List hostWires) { BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); EasyMock.expect(wiring.getRequiredWires(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE)) .andReturn(extenderWires).anyTimes(); @@ -326,10 +391,22 @@ private BundleWiring mockConsumerWiring(List extenderWires, .andReturn(serviceRequirements).anyTimes(); EasyMock.expect(wiring.getRequiredWires(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) .andReturn(serviceWires).anyTimes(); + EasyMock.expect(wiring.getRevision()).andReturn(revision).anyTimes(); + EasyMock.expect(wiring.getProvidedWires(HostNamespace.HOST_NAMESPACE)) + .andReturn(hostWires).anyTimes(); EasyMock.replay(wiring); return wiring; } + private BundleRevision mockRevision(List serviceRequirements) { + BundleRevision revision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.expect(revision.getDeclaredRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(serviceRequirements).anyTimes(); + EasyMock.replay(revision); + return revision; + } + private BundleWire mockWire(String namespace, String value, Bundle provider) { Map attributes = new HashMap(); attributes.put(namespace, value); From 1f6ac3511ee7efec42b917ec4cdeca6e482e6caa Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 7 Aug 2026 00:22:14 +0200 Subject: [PATCH 13/26] Bind providers to local advertisements --- .../apache/aries/spifly/BaseActivator.java | 79 ++++++++ .../ProviderBundleTrackerCustomizer.java | 104 ++++++++--- .../aries/spifly/ProviderServiceFactory.java | 9 +- .../java/org/apache/aries/spifly/Util.java | 173 ++++++++++++++++- ...rackerCustomizerGenericCapabilityTest.java | 7 +- .../ProviderBundleTrackerCustomizerTest.java | 20 +- .../org/apache/aries/spifly/UtilTest.java | 174 +++++++++++++++++- .../spifly/dynamic/ClientWeavingHookTest.java | 3 +- 8 files changed, 513 insertions(+), 56 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 7ebe66a2a7..68e4b9d1ca 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -81,6 +81,9 @@ public abstract class BaseActivator implements BundleActivator { private final ConcurrentMap>>> registeredProviders = new ConcurrentHashMap>>>(); + private final ConcurrentMap> providerAdvertisements = + new ConcurrentHashMap>(); + private final ConcurrentMap>> consumerRestrictions = new ConcurrentHashMap>>(); @@ -406,6 +409,25 @@ public void registerProviderBundle(String registrationClassName, Bundle bundle, }); } + void registerProviderBundle(String serviceType, String implementationName, + Bundle bundle, Map customAttributes) { + registerProviderBundle(serviceType, bundle, customAttributes); + SortedMap advertisements = + providerAdvertisements.computeIfAbsent(serviceType, + key -> Collections.synchronizedSortedMap( + new TreeMap())); + synchronized (advertisements) { + ProviderAdvertisement advertisement = advertisements.get(bundle.getBundleId()); + if (advertisement == null) { + BundleWiring wiring = WiringUtils.getWiring(bundle); + BundleRevision revision = wiring == null ? null : wiring.getRevision(); + advertisement = new ProviderAdvertisement(bundle, revision); + advertisements.put(bundle.getBundleId(), advertisement); + } + advertisement.addImplementation(implementationName); + } + } + public void unregisterProviderBundle(Bundle bundle) { for (Map>> value : registeredProviders.values()) { for(Iterator>>> it = value.entrySet().iterator(); it.hasNext(); ) { @@ -415,6 +437,10 @@ public void unregisterProviderBundle(Bundle bundle) { } } } + for (Map advertisements + : providerAdvertisements.values()) { + advertisements.remove(bundle.getBundleId()); + } } public Collection findProviderBundles(String name) { @@ -451,6 +477,25 @@ Collection filterCompatibleProviderBundles( return compatible; } + List findProviderAdvertisements( + String serviceType, Collection selectedBundles) { + SortedMap advertisements = + providerAdvertisements.get(serviceType); + if (advertisements == null || selectedBundles.isEmpty()) { + return Collections.emptyList(); + } + + List selected = new ArrayList(); + synchronized (advertisements) { + for (ProviderAdvertisement advertisement : advertisements.values()) { + if (selectedBundles.contains(advertisement.getBundle())) { + selected.add(advertisement.snapshot()); + } + } + } + return selected; + } + public Map getCustomBundleAttributes(String name, Bundle b) { SortedMap>> map = registeredProviders.get(name); if (map == null) @@ -649,4 +694,38 @@ Collection getProviders(String serviceType) { } } + static final class ProviderAdvertisement { + private final Bundle bundle; + private final BundleRevision revision; + private final Set implementationNames = + new java.util.LinkedHashSet(); + + private ProviderAdvertisement(Bundle bundle, BundleRevision revision) { + this.bundle = bundle; + this.revision = revision; + } + + private synchronized void addImplementation(String implementationName) { + implementationNames.add(implementationName); + } + + private synchronized ProviderAdvertisement snapshot() { + ProviderAdvertisement snapshot = new ProviderAdvertisement(bundle, revision); + snapshot.implementationNames.addAll(implementationNames); + return snapshot; + } + + Bundle getBundle() { + return bundle; + } + + BundleRevision getRevision() { + return revision; + } + + synchronized List getImplementationNames() { + return new ArrayList(implementationNames); + } + } + } diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index 3c7959fbb0..c36400fd34 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -50,6 +50,7 @@ import org.osgi.framework.Constants; import org.osgi.framework.ServicePermission; import org.osgi.framework.ServiceRegistration; +import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.wiring.BundleCapability; import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWire; @@ -183,7 +184,8 @@ && hasRegisterPermission(bundle, details.serviceType)) { } } - activator.registerProviderBundle(details.serviceType, bundle, + activator.registerProviderBundle(details.serviceType, + details.instanceType, bundle, details.properties == null ? Collections.emptyMap() : details.properties); log(Level.INFO, "Registered provider " + details.instanceType + " of service " + details.serviceType + " in bundle " + bundle.getSymbolicName()); @@ -327,21 +329,52 @@ private List getServiceFileUrls(Bundle bundle) { List getServiceFileUrls(Bundle bundle, List serviceTypes) { if (serviceTypes != null) { BundleWiring wiring = WiringUtils.getWiring(bundle); - ClassLoader classLoader = wiring == null ? null : wiring.getClassLoader(); - if (classLoader != null) { - Set serviceFileURLs = new LinkedHashSet(); - for (String serviceType : new LinkedHashSet(serviceTypes)) { - try { - Enumeration resources = classLoader.getResources( - METAINF_SERVICES + "/" + serviceType); - serviceFileURLs.addAll(Collections.list(resources)); + if (wiring == null) { + return Collections.emptyList(); + } + + Set requestedTypes = new LinkedHashSet(serviceTypes); + Set serviceFileURLs = new LinkedHashSet(); + for (String serviceType : requestedTypes) { + List entries = wiring.findEntries( + METAINF_SERVICES, serviceType, 0); + if (entries != null) { + serviceFileURLs.addAll(entries); + } + } + if (serviceFileURLs.isEmpty()) { + Enumeration entries = bundle.findEntries( + METAINF_SERVICES, "*", false); + if (entries != null) { + for (URL entry : Collections.list(entries)) { + String path = entry.getPath(); + int separator = path.lastIndexOf('/'); + String serviceType = separator < 0 + ? path : path.substring(separator + 1); + if (requestedTypes.contains(serviceType)) { + serviceFileURLs.add(entry); + } } - catch (IOException e) { - log(Level.FINE, "Could not find SPI metadata for " + serviceType, e); + } + } + + addBundleClassPathServiceFiles( + bundle, requestedTypes, serviceFileURLs); + List hostWires = wiring.getProvidedWires( + HostNamespace.HOST_NAMESPACE); + if (hostWires != null) { + for (BundleWire hostWire : hostWires) { + BundleRevision fragmentRevision = hostWire.getRequirement() == null + ? null : hostWire.getRequirement().getRevision(); + Bundle fragment = fragmentRevision == null + ? null : fragmentRevision.getBundle(); + if (fragment != null) { + addBundleClassPathServiceFiles( + fragment, requestedTypes, serviceFileURLs); } } - return new ArrayList(serviceFileURLs); } + return new ArrayList(serviceFileURLs); } List serviceFileURLs = new ArrayList(); @@ -358,15 +391,35 @@ List getServiceFileUrls(Bundle bundle, List serviceTypes) { if (entry.equals(".")) continue; - URL url = bundle.getResource(entry); - if (url != null) { - serviceFileURLs.addAll(getMetaInfServiceURLsFromJar(url)); - } + URL url = bundle.getEntry(entry); + if (url != null) { + serviceFileURLs.addAll(getMetaInfServiceURLsFromJar(url)); + } } } - return serviceFileURLs; - } + return serviceFileURLs; + } + + private void addBundleClassPathServiceFiles(Bundle bundle, + Set serviceTypes, Set serviceFileURLs) { + Object bcp = bundle.getHeaders().get(Constants.BUNDLE_CLASSPATH); + if (!(bcp instanceof String)) { + return; + } + + for (String entry : ((String) bcp).split(",")) { + entry = entry.trim(); + if (entry.equals(".")) { + continue; + } + URL url = bundle.getEntry(entry); + if (url != null) { + serviceFileURLs.addAll( + getMetaInfServiceURLsFromJar(url, serviceTypes)); + } + } + } private String getHeaderFromBundleOrFragment(Bundle bundle, String headerName) { return getHeaderFromBundleOrFragment(bundle, headerName, null); @@ -449,8 +502,13 @@ private List> findServiceRegistrationProperties( return registrations; } - private List getMetaInfServiceURLsFromJar(URL url) { - List urls = new ArrayList(); + private List getMetaInfServiceURLsFromJar(URL url) { + return getMetaInfServiceURLsFromJar(url, null); + } + + private List getMetaInfServiceURLsFromJar( + URL url, Set serviceTypes) { + List urls = new ArrayList(); try { JarInputStream jis = null; try { @@ -458,8 +516,10 @@ private List getMetaInfServiceURLsFromJar(URL url) { JarEntry je = null; while((je = jis.getNextJarEntry()) != null) { - if (je.getName().startsWith(METAINF_SERVICES) && - je.getName().length() > (METAINF_SERVICES.length() + 1)) { + if (je.getName().startsWith(METAINF_SERVICES + "/") + && je.getName().length() > (METAINF_SERVICES.length() + 1) + && (serviceTypes == null || serviceTypes.contains( + je.getName().substring(METAINF_SERVICES.length() + 1)))) { urls.add(new URL("jar:" + url + "!/" + je.getName())); } } diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderServiceFactory.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderServiceFactory.java index 34b3d3e529..d9305cbab7 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderServiceFactory.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderServiceFactory.java @@ -40,9 +40,12 @@ public ProviderServiceFactory(Class cls) { @Override public Object getService(Bundle bundle, ServiceRegistration registration) { - if (providerBundle != null && !providerBundle.hasPermission( - new ServicePermission(serviceType, ServicePermission.REGISTER))) { - return null; + if (providerBundle != null) { + if (providerBundle.getState() != Bundle.ACTIVE + || !providerBundle.hasPermission(new ServicePermission( + serviceType, ServicePermission.REGISTER))) { + return null; + } } try { return providerClass.getDeclaredConstructor().newInstance(); diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java index 13f9787a19..cb47dae2b4 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java @@ -18,19 +18,27 @@ */ package org.apache.aries.spifly; -import java.io.IOException; -import java.lang.reflect.Method; -import java.net.URL; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; +import java.nio.charset.StandardCharsets; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.ServiceLoader; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; import java.util.logging.Level; @@ -40,6 +48,8 @@ import org.osgi.framework.Constants; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.ServicePermission; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWiring; import aQute.bnd.annotation.baseline.BaselineIgnore; @@ -257,6 +267,14 @@ private static ClassLoader findContextClassloader(Bundle consumerBundle, String consumerBundle, clsArg, bundles); } + if (serviceLoaderCall && permissionAware + && activator.isStandardConsumer(consumerBundle) + && !bundles.isEmpty()) { + return new ProviderAdvertisementClassLoader( + activator.findProviderAdvertisements(requestedClass, bundles), + requestedClass); + } + switch (bundles.size()) { case 0: return null; @@ -514,6 +532,114 @@ private boolean hasGetPermission() { } } + private static class ProviderAdvertisementClassLoader extends ClassLoader { + private final String serviceType; + private final String providerConfiguration; + private final Map> + advertisersByImplementation = + new LinkedHashMap>(); + + ProviderAdvertisementClassLoader( + List advertisements, + String serviceType) { + super(null); + this.serviceType = serviceType; + providerConfiguration = "META-INF/services/" + serviceType; + for (BaseActivator.ProviderAdvertisement advertisement : advertisements) { + for (String implementation : advertisement.getImplementationNames()) { + advertisersByImplementation.computeIfAbsent(implementation, + key -> new ArrayList()) + .add(advertisement); + } + } + } + + @Override + public URL getResource(String name) { + if (!providerConfiguration.equals(name)) { + return null; + } + return createProviderConfiguration(); + } + + @Override + public Enumeration getResources(String name) throws IOException { + URL configuration = getResource(name); + return configuration == null + ? java.util.Collections.emptyEnumeration() + : java.util.Collections.enumeration( + java.util.Collections.singleton(configuration)); + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + List advertisements = + advertisersByImplementation.get(name); + if (advertisements == null) { + throw new ClassNotFoundException(name); + } + + ClassNotFoundException last = null; + for (BaseActivator.ProviderAdvertisement advertisement : advertisements) { + if (!isProviderAvailable(advertisement, serviceType)) { + continue; + } + try { + return advertisement.getBundle().loadClass(name); + } + catch (ClassNotFoundException e) { + last = e; + } + } + throw last == null ? new ClassNotFoundException(name) : last; + } + + private URL createProviderConfiguration() { + Set implementations = new LinkedHashSet(); + for (Map.Entry> entry + : advertisersByImplementation.entrySet()) { + for (BaseActivator.ProviderAdvertisement advertisement : entry.getValue()) { + if (isProviderAvailable(advertisement, serviceType)) { + implementations.add(entry.getKey()); + break; + } + } + } + if (implementations.isEmpty()) { + return null; + } + + StringBuilder contents = new StringBuilder(); + for (String implementation : implementations) { + contents.append(implementation).append('\n'); + } + final byte[] bytes = contents.toString().getBytes(StandardCharsets.UTF_8); + try { + return new URL(null, + "spifly:" + serviceType + "/" + + Integer.toHexString(System.identityHashCode(this)), + new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL url) { + return new URLConnection(url) { + @Override + public void connect() { + } + + @Override + public InputStream getInputStream() { + return new ByteArrayInputStream(bytes); + } + }; + } + }); + } + catch (java.net.MalformedURLException e) { + throw new IllegalStateException(e); + } + } + } + private static class ProviderBundleClassLoader extends ClassLoader { private final Bundle providerBundle; private final ClassLoader delegate; @@ -558,4 +684,37 @@ private boolean hasRegisterPermission() { return permitted; } } + + private static boolean isProviderAvailable( + BaseActivator.ProviderAdvertisement advertisement, String serviceType) { + return isProviderAvailable(advertisement.getBundle(), + advertisement.getRevision(), serviceType); + } + + private static boolean isProviderAvailable(Bundle providerBundle, + BundleRevision providerRevision, String serviceType) { + if (providerBundle.getState() != Bundle.ACTIVE) { + BaseActivator.activator.log(Level.FINE, "Bundle " + providerBundle + + " is not active and cannot provide services of type: " + + serviceType); + return false; + } + if (!providerBundle.hasPermission( + new ServicePermission(serviceType, ServicePermission.REGISTER))) { + BaseActivator.activator.log(Level.FINE, "Bundle " + providerBundle + + " does not have permission to provide services of type: " + + serviceType); + return false; + } + if (providerRevision != null) { + BundleWiring wiring = WiringUtils.getWiring(providerBundle); + if (wiring == null || wiring.getRevision() != providerRevision) { + BaseActivator.activator.log(Level.FINE, "Bundle " + providerBundle + + " no longer has the revision that advertised service type: " + + serviceType); + return false; + } + } + return true; + } } diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java index ff8d95e78a..9098952d55 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java @@ -687,6 +687,7 @@ public void start(BundleContext context) throws Exception {} Bundle implBundle = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + EasyMock.expect(implBundle.getState()).andReturn(Bundle.ACTIVE).anyTimes(); EasyMock.expect(implBundle.hasPermission(EasyMock.isA(ServicePermission.class))) .andReturn(true).anyTimes(); @@ -702,10 +703,10 @@ public void start(BundleContext context) throws Exception {} URL embeddedJar = getClass().getResource("/embedded.jar"); assertNotNull("precondition", embeddedJar); - EasyMock.expect(implBundle.getResource("embedded.jar")).andReturn(embeddedJar).anyTimes(); + EasyMock.expect(implBundle.getEntry("embedded.jar")).andReturn(embeddedJar).anyTimes(); URL embedded2Jar = getClass().getResource("/embedded2.jar"); assertNotNull("precondition", embedded2Jar); - EasyMock.expect(implBundle.getResource("embedded2.jar")).andReturn(embedded2Jar).anyTimes(); + EasyMock.expect(implBundle.getEntry("embedded2.jar")).andReturn(embedded2Jar).anyTimes(); URL dir = new URL("jar:" + embeddedJar + "!/META-INF/services"); assertNotNull("precondition", dir); EasyMock.expect(implBundle.getResource("/META-INF/services")).andReturn(dir).anyTimes(); @@ -771,6 +772,7 @@ private Bundle mockSPIBundle(BundleContext implBC, Dictionary he Bundle implBundle = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + EasyMock.expect(implBundle.getState()).andReturn(Bundle.ACTIVE).anyTimes(); EasyMock.expect(implBundle.hasPermission(EasyMock.isA(ServicePermission.class))) .andReturn(registerPermission).anyTimes(); EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); @@ -844,6 +846,7 @@ private Bundle mockSPIBundle4(BundleContext implBC, Dictionary h BundleRevision rev, BundleWiring providerWiring) throws ClassNotFoundException { Bundle implBundle = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + EasyMock.expect(implBundle.getState()).andReturn(Bundle.ACTIVE).anyTimes(); EasyMock.expect(implBundle.hasPermission(EasyMock.isA(ServicePermission.class))) .andReturn(true).anyTimes(); EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index 60e8b16913..08eec99c7a 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -23,14 +23,12 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; -import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; import java.util.List; @@ -107,24 +105,20 @@ public void testAddingNonOptInBundle() throws Exception { } @Test - public void testStandardDiscoveryUsesWiringClassLoader() throws Exception { + public void testStandardDiscoveryUsesBundleLocalEntries() throws Exception { final String serviceType = "org.apache.aries.mytest.MySPI"; final URL serviceFile = getClass().getResource( "impl1/META-INF/services/" + serviceType); assertNotNull("precondition", serviceFile); - ClassLoader classLoader = new ClassLoader() { - @Override - public Enumeration getResources(String name) throws IOException { - assertEquals("META-INF/services/" + serviceType, name); - return Collections.enumeration(Collections.singleton(serviceFile)); - } - }; BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); - EasyMock.expect(wiring.getClassLoader()).andReturn(classLoader).anyTimes(); + EasyMock.expect(wiring.findEntries("META-INF/services", serviceType, 0)) + .andReturn(Collections.singletonList(serviceFile)).anyTimes(); EasyMock.replay(wiring); Bundle bundle = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(bundle.adapt(BundleWiring.class)).andReturn(wiring).anyTimes(); + EasyMock.expect(bundle.getHeaders()) + .andReturn(new Hashtable()).anyTimes(); EasyMock.replay(bundle); ProviderBundleTrackerCustomizer customizer = @@ -164,10 +158,10 @@ public void testAddingBundleWithBundleClassPath() throws Exception { URL embeddedJar = getClass().getResource("/embedded.jar"); assertNotNull("precondition", embeddedJar); - EasyMock.expect(implBundle.getResource("embedded.jar")).andReturn(embeddedJar).anyTimes(); + EasyMock.expect(implBundle.getEntry("embedded.jar")).andReturn(embeddedJar).anyTimes(); URL embedded2Jar = getClass().getResource("/embedded2.jar"); assertNotNull("precondition", embedded2Jar); - EasyMock.expect(implBundle.getResource("embedded2.jar")).andReturn(embedded2Jar).anyTimes(); + EasyMock.expect(implBundle.getEntry("embedded2.jar")).andReturn(embedded2Jar).anyTimes(); URL dir = new URL("jar:" + embeddedJar + "!/META-INF/services"); assertNotNull("precondition", dir); EasyMock.expect(implBundle.getResource("/META-INF/services")).andReturn(dir).anyTimes(); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java index 5dacedaff2..4e57857601 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java @@ -37,6 +37,7 @@ import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import org.apache.aries.mytest.MySPI; import org.easymock.EasyMock; @@ -188,8 +189,12 @@ public void explicitLoaderCannotAddProviderConfigurations() throws Exception { assertNotNull("precondition", selected); assertNotNull("precondition", forbidden); Bundle provider = mockProviderBundle(42L, selected); - activator.registerProviderBundle( - MySPI.class.getName(), provider, new HashMap()); + activator.registerProviderBundle(MySPI.class.getName(), + "org.apache.aries.spifly.impl2.MySPIImpl2a", provider, + new HashMap()); + activator.registerProviderBundle(MySPI.class.getName(), + "org.apache.aries.spifly.impl2.MySPIImpl2b", provider, + new HashMap()); ClassLoader specified = new URLClassLoader( new URL[] {forbidden}, getClass().getClassLoader()) { @@ -251,8 +256,9 @@ public void consumerPermissionIsCheckedAtLazyIteration() throws Exception { URL selected = getClass().getResource("/embedded2.jar"); assertNotNull("precondition", selected); Bundle provider = mockProviderBundle(42L, selected); - activator.registerProviderBundle( - MySPI.class.getName(), provider, new HashMap()); + activator.registerProviderBundle(MySPI.class.getName(), + "org.apache.aries.spifly.impl3.MySPIImpl3", provider, + new HashMap()); ServiceLoader loader = Util.serviceLoaderLoad( MySPI.class, callerClass(consumer)); @@ -286,8 +292,9 @@ public void providerPermissionChangesDoNotRequireReindexing() throws Exception { URL selected = getClass().getResource("/embedded2.jar"); assertNotNull("precondition", selected); Bundle provider = mockProviderBundle(42L, selected, providerPermission); - activator.registerProviderBundle( - MySPI.class.getName(), provider, new HashMap()); + activator.registerProviderBundle(MySPI.class.getName(), + "org.apache.aries.spifly.impl3.MySPIImpl3", provider, + new HashMap()); assertFalse(Util.serviceLoaderLoad(MySPI.class, callerClass(consumer)) .iterator().hasNext()); @@ -303,6 +310,122 @@ public void providerPermissionChangesDoNotRequireReindexing() throws Exception { .iterator().hasNext()); } + @Test + public void providerPermissionCannotBeBorrowedFromAnotherAdvertiser() + throws Exception { + BaseActivator activator = newActivator(); + Bundle consumer = mockPermissionBundle(new AtomicBoolean(true)); + BundleWiring consumerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(consumerWiring.getRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.replay(consumerWiring); + activator.registerStandardConsumer(consumer, consumerWiring); + + AtomicBoolean firstPermission = new AtomicBoolean(true); + Bundle first = mockProviderBundle(41L, + getClass().getResource("/embedded2.jar"), firstPermission); + Bundle second = mockProviderBundle(42L, + getClass().getResource("/embedded.jar")); + activator.registerProviderBundle(MySPI.class.getName(), + "org.apache.aries.spifly.impl3.MySPIImpl3", first, + new HashMap()); + activator.registerProviderBundle(MySPI.class.getName(), + "org.apache.aries.spifly.impl2.MySPIImpl2a", second, + new HashMap()); + + Iterator iterator = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)).iterator(); + assertTrue(iterator.hasNext()); + firstPermission.set(false); + + assertThrows(ServiceConfigurationError.class, iterator::next); + } + + @Test + public void duplicateAdvertisementCanUseAnotherActualAdvertiser() + throws Exception { + BaseActivator activator = newActivator(); + Bundle consumer = mockPermissionBundle(new AtomicBoolean(true)); + BundleWiring consumerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(consumerWiring.getRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.replay(consumerWiring); + activator.registerStandardConsumer(consumer, consumerWiring); + + AtomicBoolean firstPermission = new AtomicBoolean(false); + URL providerJar = getClass().getResource("/embedded2.jar"); + Bundle first = mockProviderBundle(41L, providerJar, firstPermission); + Bundle second = mockProviderBundle(42L, providerJar); + String implementation = "org.apache.aries.spifly.impl3.MySPIImpl3"; + activator.registerProviderBundle(MySPI.class.getName(), + implementation, first, new HashMap()); + activator.registerProviderBundle(MySPI.class.getName(), + implementation, second, new HashMap()); + + Iterator iterator = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)).iterator(); + assertTrue(iterator.hasNext()); + assertEquals(implementation, iterator.next().getClass().getName()); + assertFalse(iterator.hasNext()); + } + + @Test + public void stoppedProviderCannotServeExistingLoader() throws Exception { + BaseActivator activator = newActivator(); + Bundle consumer = mockPermissionBundle(new AtomicBoolean(true)); + BundleWiring consumerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(consumerWiring.getRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.replay(consumerWiring); + activator.registerStandardConsumer(consumer, consumerWiring); + + AtomicInteger providerState = new AtomicInteger(Bundle.ACTIVE); + Bundle provider = mockProviderBundle(42L, + getClass().getResource("/embedded2.jar"), + new AtomicBoolean(true), providerState, new AtomicBoolean(true)); + activator.registerProviderBundle(MySPI.class.getName(), + "org.apache.aries.spifly.impl3.MySPIImpl3", provider, + new HashMap()); + + Iterator iterator = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)).iterator(); + assertTrue(iterator.hasNext()); + providerState.set(Bundle.RESOLVED); + + assertThrows(ServiceConfigurationError.class, iterator::next); + } + + @Test + public void replacedProviderRevisionCannotServeExistingLoader() throws Exception { + BaseActivator activator = newActivator(); + Bundle consumer = mockPermissionBundle(new AtomicBoolean(true)); + BundleWiring consumerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(consumerWiring.getRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.replay(consumerWiring); + activator.registerStandardConsumer(consumer, consumerWiring); + + AtomicBoolean originalRevision = new AtomicBoolean(true); + Bundle provider = mockProviderBundle(42L, + getClass().getResource("/embedded2.jar"), + new AtomicBoolean(true), new AtomicInteger(Bundle.ACTIVE), + originalRevision); + activator.registerProviderBundle(MySPI.class.getName(), + "org.apache.aries.spifly.impl3.MySPIImpl3", provider, + new HashMap()); + + Iterator iterator = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)).iterator(); + assertTrue(iterator.hasNext()); + originalRevision.set(false); + + assertThrows(ServiceConfigurationError.class, iterator::next); + } + @Test public void providerFactoryRechecksRegisterPermission() { AtomicBoolean providerPermission = new AtomicBoolean(true); @@ -343,15 +466,35 @@ private Bundle mockProviderBundle(long bundleId, URL providerJar) throws Excepti @SuppressWarnings({ "unchecked", "rawtypes" }) private Bundle mockProviderBundle(long bundleId, URL providerJar, final AtomicBoolean permission) throws Exception { + return mockProviderBundle(bundleId, providerJar, permission, + new AtomicInteger(Bundle.ACTIVE), new AtomicBoolean(true)); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Bundle mockProviderBundle(long bundleId, URL providerJar, + final AtomicBoolean permission, final AtomicInteger state, + final AtomicBoolean originalRevision) throws Exception { Bundle providerBundle = EasyMock.createMock(Bundle.class); final ClassLoader providerCL = new TestBundleClassLoader( new URL[] {providerJar}, getClass().getClassLoader(), providerBundle); BundleWiring providerWiring = EasyMock.createNiceMock(BundleWiring.class); EasyMock.expect(providerWiring.getClassLoader()).andReturn(providerCL).anyTimes(); - EasyMock.replay(providerWiring); BundleRevision providerRevision = EasyMock.createNiceMock(BundleRevision.class); + BundleRevision replacementRevision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.expect(providerWiring.getRevision()).andAnswer( + new IAnswer() { + @Override + public BundleRevision answer() throws Throwable { + return originalRevision.get() + ? providerRevision : replacementRevision; + } + }).anyTimes(); EasyMock.expect(providerRevision.getWiring()).andReturn(providerWiring).anyTimes(); EasyMock.replay(providerRevision); + EasyMock.expect(replacementRevision.getWiring()) + .andReturn(providerWiring).anyTimes(); + EasyMock.replay(replacementRevision); + EasyMock.replay(providerWiring); Bundle systemBundle = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(systemBundle.loadClass(BundleRevision.class.getName())) .andReturn((Class) BundleRevision.class).anyTimes(); @@ -364,8 +507,22 @@ private Bundle mockProviderBundle(long bundleId, URL providerJar, EasyMock.expect(providerBundle.getBundleContext()) .andReturn(providerContext).anyTimes(); EasyMock.expect(providerBundle.adapt(BundleRevision.class)) - .andReturn(providerRevision).anyTimes(); + .andAnswer(new IAnswer() { + @Override + public BundleRevision answer() throws Throwable { + return originalRevision.get() + ? providerRevision : replacementRevision; + } + }).anyTimes(); + EasyMock.expect(providerBundle.adapt(BundleWiring.class)) + .andReturn(providerWiring).anyTimes(); EasyMock.expect(providerBundle.getBundleId()).andReturn(bundleId).anyTimes(); + EasyMock.expect(providerBundle.getState()).andAnswer(new IAnswer() { + @Override + public Integer answer() throws Throwable { + return state.get(); + } + }).anyTimes(); EasyMock.expect(providerBundle.hasPermission( EasyMock.isA(ServicePermission.class))).andAnswer(new IAnswer() { @Override @@ -393,6 +550,7 @@ public Class answer() throws Throwable { private Bundle mockPermissionBundle(final AtomicBoolean permission) { Bundle bundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(bundle.getState()).andReturn(Bundle.ACTIVE).anyTimes(); EasyMock.expect(bundle.hasPermission(EasyMock.isA(ServicePermission.class))) .andAnswer(new IAnswer() { @Override diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java index 569c9d38b1..96cee9389e 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java @@ -803,7 +803,8 @@ private Bundle mockProviderBundle(String subdir, long id, Version version) throw bsn = bsn.substring(0, idx); } EasyMock.expect(providerBundle.getSymbolicName()).andReturn(bsn).anyTimes(); - EasyMock.expect(providerBundle.getBundleId()).andReturn(id).anyTimes(); + EasyMock.expect(providerBundle.getBundleId()).andReturn(id).anyTimes(); + EasyMock.expect(providerBundle.getState()).andReturn(Bundle.ACTIVE).anyTimes(); EasyMock.expect(providerBundle.getBundleContext()).andReturn(bc).anyTimes(); EasyMock.expect(providerBundle.getVersion()).andReturn(version).anyTimes(); EasyMock.expect(providerBundle.hasPermission(EasyMock.isA(ServicePermission.class))).andReturn(true).anyTimes(); From 527d92d430a6363c670d4225c82a246359082cac Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 7 Aug 2026 00:35:03 +0200 Subject: [PATCH 14/26] Refresh consumers after late mediator start --- .../apache/aries/spifly/BaseActivator.java | 78 ++++- .../aries/spifly/ResolvedWiringTest.java | 83 +++++ .../dynamic/LateMediatorStartupTest.java | 325 ++++++++++++++++++ 3 files changed, 473 insertions(+), 13 deletions(-) create mode 100644 spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 68e4b9d1ca..565d35e710 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -26,6 +26,7 @@ import java.util.HashSet; import java.util.Hashtable; import java.util.Iterator; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -90,7 +91,7 @@ public abstract class BaseActivator implements BundleActivator { private final ConcurrentMap standardConsumerWirings = new ConcurrentHashMap(); - private final Set> refreshedConsumerHosts = + private final Set> requestedConsumerRefreshes = Collections.newSetFromMap( new ConcurrentHashMap, Boolean>()); @@ -126,6 +127,10 @@ public synchronized void start(BundleContext context, final String consumerHeade } activator = this; + + if (SpiFlyConstants.SPI_CONSUMER_HEADER.equals(consumerHeaderName)) { + refreshActiveConsumers(); + } } public void addConsumerWeavingData(Bundle bundle, String consumerHeaderName) throws Exception { @@ -290,32 +295,79 @@ private void refreshConsumerHost(Bundle host, BundleRevision fragmentRevision) { if (fragmentRevision == null) { return; } + requestConsumerRefreshes( + Collections.singleton(new Pair(host, fragmentRevision)), + "after processor fragment attachment"); + } + + void refreshActiveConsumers() { + if (bundleContext == null) { + return; + } + + Bundle mediatorBundle = bundleContext.getBundle(); + List> refreshes = + new ArrayList>(); + for (Bundle bundle : bundleContext.getBundles()) { + int state = bundle.getState(); + if ((state & (Bundle.ACTIVE | Bundle.STARTING)) == 0 + || bundle.equals(mediatorBundle) + || !isStandardConsumer(bundle)) { + continue; + } + + BundleRevision revision = bundle.adapt(BundleRevision.class); + if (revision == null + || (revision.getTypes() & BundleRevision.TYPE_FRAGMENT) != 0) { + continue; + } + refreshes.add(new Pair(bundle, revision)); + } + + Collections.sort(refreshes, (left, right) -> Long.compare( + left.getLeft().getBundleId(), right.getLeft().getBundleId())); + requestConsumerRefreshes(refreshes, "after dynamic mediator startup"); + } + + private void requestConsumerRefreshes( + Collection> refreshes, String reason) { + if (refreshes.isEmpty()) { + return; + } Bundle systemBundle = bundleContext == null ? null : bundleContext.getBundle(0); FrameworkWiring frameworkWiring = systemBundle == null ? null : systemBundle.adapt(FrameworkWiring.class); if (frameworkWiring == null) { - log(Level.WARNING, "Cannot refresh consumer host " + host - + " after processor fragment attachment: FrameworkWiring is unavailable"); + log(Level.WARNING, "Cannot refresh consumers " + reason + + ": FrameworkWiring is unavailable"); return; } - Pair refreshKey = - new Pair(host, fragmentRevision); - if (!refreshedConsumerHosts.add(refreshKey)) { + + List> newRefreshes = + new ArrayList>(); + Set bundles = new LinkedHashSet(); + for (Pair refresh : refreshes) { + if (requestedConsumerRefreshes.add(refresh)) { + newRefreshes.add(refresh); + bundles.add(refresh.getLeft()); + } + } + if (bundles.isEmpty()) { return; } try { - frameworkWiring.refreshBundles(Collections.singleton(host), event -> { + frameworkWiring.refreshBundles(bundles, event -> { if (event.getType() == FrameworkEvent.ERROR) { - log(Level.WARNING, "Could not refresh consumer host " + host - + " after processor fragment attachment", event.getThrowable()); + log(Level.WARNING, "Could not refresh consumers " + bundles + " " + + reason, event.getThrowable()); } }); } catch (RuntimeException e) { - refreshedConsumerHosts.remove(refreshKey); - log(Level.WARNING, "Could not request refresh of consumer host " + host - + " after processor fragment attachment", e); + requestedConsumerRefreshes.removeAll(newRefreshes); + log(Level.WARNING, "Could not request refresh of consumers " + bundles + " " + + reason, e); } } @@ -325,7 +377,7 @@ public synchronized void stop(BundleContext context) throws Exception { consumerBundleTracker.close(); providerBundleTracker.close(); - refreshedConsumerHosts.clear(); + requestedConsumerRefreshes.clear(); } public boolean isLogEnabled(Level level) { diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java index b77f2887cc..8c9eb7a024 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import java.lang.reflect.Field; @@ -34,6 +35,7 @@ import java.util.Map; import java.util.ServiceLoader; +import org.easymock.Capture; import org.easymock.EasyMock; import org.junit.Before; import org.junit.Test; @@ -375,6 +377,68 @@ public void lateProcessorFragmentDoesNotRefreshStaticConsumer() throws Exception EasyMock.verify(frameworkWiring); } + @Test + public void lateDynamicMediatorStartupRefreshesExistingStandardConsumersOnce() + throws Exception { + Bundle activeConsumer = mockRefreshBundle(7L, Bundle.ACTIVE, 0, null); + Bundle startingConsumer = mockRefreshBundle(8L, Bundle.STARTING, 0, null); + Bundle resolvedConsumer = mockRefreshBundle(9L, Bundle.RESOLVED, 0, null); + Bundle fragmentConsumer = mockRefreshBundle( + 10L, Bundle.ACTIVE, BundleRevision.TYPE_FRAGMENT, null); + Bundle unrelatedConsumer = mockRefreshBundle(11L, Bundle.ACTIVE, 0, null); + Bundle proprietaryConsumer = mockRefreshBundle( + 12L, Bundle.ACTIVE, 0, SpiFlyConstants.SPI_CONSUMER_HEADER); + + Capture> refreshedBundles = EasyMock.newCapture(); + FrameworkWiring frameworkWiring = EasyMock.createMock(FrameworkWiring.class); + frameworkWiring.refreshBundles( + EasyMock.capture(refreshedBundles), + EasyMock.anyObject()); + EasyMock.expectLastCall().andAnswer(() -> { + assertSame(activator, BaseActivator.activator); + return null; + }).once(); + EasyMock.replay(frameworkWiring); + + Bundle systemBundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(systemBundle.adapt(FrameworkWiring.class)) + .andReturn(frameworkWiring).anyTimes(); + EasyMock.replay(systemBundle); + BundleContext context = EasyMock.createNiceMock(BundleContext.class); + EasyMock.expect(context.getBundle()).andReturn(mediator).anyTimes(); + EasyMock.expect(context.getBundle(0)).andReturn(systemBundle).anyTimes(); + EasyMock.expect(context.getBundles()).andReturn(new Bundle[] { + unrelatedConsumer, startingConsumer, mediator, fragmentConsumer, + resolvedConsumer, proprietaryConsumer, activeConsumer + }).anyTimes(); + EasyMock.replay(context); + setBundleContext(context); + + activator.registerStandardConsumer(activeConsumer, null); + activator.registerStandardConsumer(startingConsumer, null); + activator.registerStandardConsumer(resolvedConsumer, null); + activator.registerStandardConsumer(fragmentConsumer, null); + activator.registerStandardConsumer(mediator, null); + activator.addConsumerWeavingData( + proprietaryConsumer, SpiFlyConstants.SPI_CONSUMER_HEADER); + assertNotNull(activator.getWeavingData(proprietaryConsumer)); + + BaseActivator.activator = activator; + try { + activator.refreshActiveConsumers(); + // A repeated startup callback must not cause a refresh loop for the + // same consumer revisions. + activator.refreshActiveConsumers(); + } + finally { + BaseActivator.activator = null; + } + + assertEquals(Arrays.asList(activeConsumer, startingConsumer), + new java.util.ArrayList(refreshedBundles.getValue())); + EasyMock.verify(frameworkWiring); + } + private BundleWiring mockConsumerWiring(List extenderWires, List serviceRequirements, List serviceWires) { return mockConsumerWiring(extenderWires, serviceRequirements, serviceWires, @@ -449,6 +513,25 @@ private Bundle mockConsumer(BundleWiring wiring, boolean processed, boolean proc return consumer; } + private Bundle mockRefreshBundle(long id, int state, int revisionTypes, + String proprietaryHeader) { + BundleRevision revision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.expect(revision.getTypes()).andReturn(revisionTypes).anyTimes(); + EasyMock.replay(revision); + + Dictionary headers = new Hashtable(); + if (proprietaryHeader != null) { + headers.put(proprietaryHeader, "*"); + } + Bundle bundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(bundle.getBundleId()).andReturn(id).anyTimes(); + EasyMock.expect(bundle.getState()).andReturn(state).anyTimes(); + EasyMock.expect(bundle.getHeaders()).andReturn(headers).anyTimes(); + EasyMock.expect(bundle.adapt(BundleRevision.class)).andReturn(revision).anyTimes(); + EasyMock.replay(bundle); + return bundle; + } + private void setBundleContext(BundleContext context) throws Exception { Field contextField = BaseActivator.class.getDeclaredField("bundleContext"); contextField.setAccessible(true); diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java new file mode 100644 index 0000000000..52b43ac9f0 --- /dev/null +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java @@ -0,0 +1,325 @@ +/** + * 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.aries.spifly.dynamic; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import org.apache.aries.mytest.MySPI; +import org.apache.aries.spifly.dynamic.impl1.MySPIImpl1; +import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.commons.AdviceAdapter; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.util.CheckClassAdapter; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; +import org.osgi.framework.BundleEvent; +import org.osgi.framework.BundleListener; +import org.osgi.framework.Constants; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.launch.FrameworkFactory; +import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.framework.wiring.FrameworkWiring; +import org.osgi.util.tracker.BundleTracker; + +public class LateMediatorStartupTest { + private static final String SERVICE_TYPE = "org.apache.aries.mytest.MySPI"; + private static final String IMPLEMENTATION = + "org.apache.aries.spifly.dynamic.impl1.MySPIImpl1"; + + @Test + public void startingMediatorRefreshesAlreadyLoadedConsumer() throws Exception { + Path storage = Files.createTempDirectory("spifly-late-mediator-"); + Framework framework = null; + try { + Map configuration = new HashMap(); + configuration.put(Constants.FRAMEWORK_STORAGE, storage.resolve("framework").toString()); + configuration.put(Constants.FRAMEWORK_STORAGE_CLEAN, + Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT); + framework = newFramework(configuration); + framework.start(); + + BundleContext context = framework.getBundleContext(); + installDependency(context, ClassReader.class); + installDependency(context, AdviceAdapter.class); + installDependency(context, ClassNode.class); + installDependency(context, Analyzer.class); + installDependency(context, CheckClassAdapter.class); + installDependency(context, BundleTracker.class); + + Bundle mediator = context.installBundle( + bundleFromClasses(storage, "spifly-dynamic.jar").toUri().toString()); + Bundle api = context.installBundle(createApiBundle(storage).toUri().toString()); + Bundle provider = context.installBundle(createProviderBundle(storage).toUri().toString()); + Bundle consumer = context.installBundle(createConsumerBundle(storage).toUri().toString()); + + FrameworkWiring frameworkWiring = framework.adapt(FrameworkWiring.class); + assertTrue(frameworkWiring.resolveBundles( + java.util.Arrays.asList(mediator, api, provider, consumer))); + assertEquals(Bundle.RESOLVED, mediator.getState()); + + provider.start(); + consumer.start(); + Class classBeforeMediator = consumer.loadClass(TestClient.class.getName()); + assertEquals(Collections.emptySet(), invokeConsumer(classBeforeMediator)); + + CountDownLatch consumerRestarted = new CountDownLatch(1); + BundleListener listener = event -> { + if (event.getType() == BundleEvent.STARTED + && consumer.equals(event.getBundle())) { + consumerRestarted.countDown(); + } + }; + context.addBundleListener(listener); + try { + mediator.start(); + assertTrue("The late mediator should refresh and restart the consumer", + consumerRestarted.await(30, TimeUnit.SECONDS)); + } + finally { + context.removeBundleListener(listener); + } + + Class classAfterMediator = consumer.loadClass(TestClient.class.getName()); + assertNotSame(classBeforeMediator, classAfterMediator); + assertEquals(Collections.singleton("olleh"), invokeConsumer(classAfterMediator)); + } + finally { + if (framework != null) { + framework.stop(); + framework.waitForStop(30000); + } + deleteRecursively(storage); + } + } + + private Framework newFramework(Map configuration) throws Exception { + String factoryName = System.getProperty( + "spifly.test.frameworkFactory", + "org.apache.felix.framework.FrameworkFactory"); + FrameworkFactory factory = (FrameworkFactory) Class.forName(factoryName) + .getDeclaredConstructor().newInstance(); + return factory.newFramework(configuration); + } + + @SuppressWarnings("unchecked") + private Set invokeConsumer(Class consumerClass) throws Exception { + Method method = consumerClass.getMethod("test", String.class); + return (Set) method.invoke( + consumerClass.getDeclaredConstructor().newInstance(), "hello"); + } + + private void installDependency(BundleContext context, Class type) throws Exception { + BundleWiring systemWiring = context.getBundle(0).adapt(BundleWiring.class); + if (systemWiring != null) { + for (BundleCapability capability : systemWiring.getCapabilities( + PackageNamespace.PACKAGE_NAMESPACE)) { + if (type.getPackage().getName().equals(capability.getAttributes().get( + PackageNamespace.PACKAGE_NAMESPACE))) { + return; + } + } + } + URI location = type.getProtectionDomain().getCodeSource().getLocation().toURI(); + context.installBundle(location.toString()); + } + + private Path createApiBundle(Path directory) throws IOException { + Manifest manifest = bundleManifest("spifly.test.api"); + manifest.getMainAttributes().putValue( + Constants.EXPORT_PACKAGE, "org.apache.aries.mytest;version=1.0.0"); + Map entries = new LinkedHashMap(); + addClass(entries, MySPI.class); + return writeBundle(directory.resolve("api.jar"), manifest, entries); + } + + private Path createProviderBundle(Path directory) throws IOException { + Manifest manifest = bundleManifest("spifly.test.provider"); + manifest.getMainAttributes().putValue( + Constants.IMPORT_PACKAGE, "org.apache.aries.mytest;version=\"[1,2)\""); + manifest.getMainAttributes().putValue(Constants.REQUIRE_CAPABILITY, + "osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.registrar)\""); + manifest.getMainAttributes().putValue(Constants.PROVIDE_CAPABILITY, + "osgi.serviceloader;osgi.serviceloader=\"" + SERVICE_TYPE + "\""); + Map entries = new LinkedHashMap(); + addClass(entries, MySPIImpl1.class); + entries.put("META-INF/services/" + SERVICE_TYPE, + (IMPLEMENTATION + "\n").getBytes(StandardCharsets.UTF_8)); + return writeBundle(directory.resolve("provider.jar"), manifest, entries); + } + + private Path createConsumerBundle(Path directory) throws IOException { + Manifest manifest = bundleManifest("spifly.test.consumer"); + manifest.getMainAttributes().putValue( + Constants.IMPORT_PACKAGE, "org.apache.aries.mytest;version=\"[1,2)\""); + manifest.getMainAttributes().putValue(Constants.REQUIRE_CAPABILITY, + "osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.processor)\"," + + "osgi.serviceloader;filter:=\"(osgi.serviceloader=" + SERVICE_TYPE + ")\""); + Map entries = new LinkedHashMap(); + addClass(entries, TestClient.class); + return writeBundle(directory.resolve("consumer.jar"), manifest, entries); + } + + private Manifest bundleManifest(String symbolicName) { + Manifest manifest = new Manifest(); + Attributes attributes = manifest.getMainAttributes(); + attributes.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + attributes.putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); + attributes.putValue(Constants.BUNDLE_SYMBOLICNAME, symbolicName); + attributes.putValue(Constants.BUNDLE_VERSION, "1.0.0"); + return manifest; + } + + private void addClass(Map entries, Class type) throws IOException { + String resource = type.getName().replace('.', '/') + ".class"; + InputStream stream = type.getClassLoader().getResourceAsStream(resource); + if (stream == null) { + throw new IOException("Cannot find test class " + resource); + } + try { + entries.put(resource, readAllBytes(stream)); + } + finally { + stream.close(); + } + } + + private byte[] readAllBytes(InputStream stream) throws IOException { + java.io.ByteArrayOutputStream output = new java.io.ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + for (int read; (read = stream.read(buffer)) >= 0;) { + output.write(buffer, 0, read); + } + return output.toByteArray(); + } + + private Path writeBundle(Path path, Manifest manifest, Map entries) + throws IOException { + JarOutputStream output = new JarOutputStream(Files.newOutputStream( + path, StandardOpenOption.CREATE_NEW), manifest); + try { + for (Map.Entry entry : entries.entrySet()) { + output.putNextEntry(new JarEntry(entry.getKey())); + output.write(entry.getValue()); + output.closeEntry(); + } + } + finally { + output.close(); + } + return path; + } + + private Path bundleFromClasses(Path directory, String fileName) throws IOException { + Path testClasses; + try { + testClasses = Paths.get(LateMediatorStartupTest.class.getProtectionDomain() + .getCodeSource().getLocation().toURI()); + } + catch (java.net.URISyntaxException e) { + throw new IOException(e); + } + Path classes = testClasses.resolveSibling("classes"); + Manifest manifest; + InputStream manifestStream = Files.newInputStream( + classes.resolve("META-INF").resolve("MANIFEST.MF")); + try { + manifest = new Manifest(manifestStream); + } + finally { + manifestStream.close(); + } + + Path bundle = directory.resolve(fileName); + JarOutputStream output = new JarOutputStream(Files.newOutputStream( + bundle, StandardOpenOption.CREATE_NEW), manifest); + try { + List files = new ArrayList(); + Stream stream = Files.walk(classes); + try { + stream.filter(Files::isRegularFile).forEach(files::add); + } + finally { + stream.close(); + } + Collections.sort(files, Comparator.comparing(path -> classes.relativize(path).toString())); + for (Path file : files) { + String name = classes.relativize(file).toString().replace('\\', '/'); + if ("META-INF/MANIFEST.MF".equals(name)) { + continue; + } + output.putNextEntry(new JarEntry(name)); + Files.copy(file, output); + output.closeEntry(); + } + } + finally { + output.close(); + } + return bundle; + } + + private void deleteRecursively(Path directory) throws IOException { + if (!Files.exists(directory)) { + return; + } + List paths = new ArrayList(); + Stream stream = Files.walk(directory); + try { + stream.forEach(paths::add); + } + finally { + stream.close(); + } + Collections.sort(paths, Comparator.reverseOrder()); + for (Path path : paths) { + Files.deleteIfExists(path); + } + } +} From c26c7aef7dbdbdf6fddd0ffbbb814e28041a32bb Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 7 Aug 2026 07:15:23 +0200 Subject: [PATCH 15/26] Mediate indirect ServiceLoader calls --- .../spifly/dynamic/ClientWeavingHookTest.java | 32 ++++- .../aries/spifly/dynamic/TestClient.java | 26 +++- .../spifly/statictool/RequirementTest.java | 47 +++++++ .../spifly/statictool/bundle/Test5Class.java | 37 +++++ .../spifly/weaver/TCCLSetterVisitor.java | 127 +++++++++++++++--- 5 files changed, 250 insertions(+), 19 deletions(-) create mode 100644 spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/bundle/Test5Class.java diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java index 96cee9389e..19a1618d73 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/ClientWeavingHookTest.java @@ -219,7 +219,6 @@ public void testServiceLoaderLoadInstalled() throws Exception { WovenClass wc = new MyWovenClass(clsUrl, "org.apache.aries.spifly.dynamic.TestClient", consumerBundle); wh.weave(wc); - Class cls = wc.getDefinedClass(); assertTrue(activator.getWeavingData(consumerBundle).toString(), activator.getWeavingData(consumerBundle).stream() @@ -228,6 +227,37 @@ public void testServiceLoaderLoadInstalled() throws Exception { Object result = method.invoke(cls.getDeclaredConstructor().newInstance(), "hello"); assertEquals(Collections.singleton("olleh"), result); } + + @Test + public void testServiceLoaderMethodReferences() throws Exception { + Dictionary consumerHeaders = new Hashtable(); + consumerHeaders.put(SpiFlyConstants.SPI_CONSUMER_HEADER, "*"); + + Bundle providerBundle = mockProviderBundle("impl1", 1); + activator.registerProviderBundle("org.apache.aries.mytest.MySPI", + providerBundle, new HashMap()); + Bundle consumerBundle = mockConsumerBundle(consumerHeaders, providerBundle); + activator.addConsumerWeavingData( + consumerBundle, SpiFlyConstants.SPI_CONSUMER_HEADER); + + Bundle spiFlyBundle = mockSpiFlyBundle( + "spifly", Version.parseVersion("1.9.4"), + consumerBundle, providerBundle); + WeavingHook wh = new ClientWeavingHook( + spiFlyBundle.getBundleContext(), activator); + URL clsUrl = getClass().getResource("TestClient.class"); + assertNotNull("Precondition", clsUrl); + WovenClass wc = new MyWovenClass(clsUrl, + "org.apache.aries.spifly.dynamic.TestClient", consumerBundle); + wh.weave(wc); + Class cls = wc.getDefinedClass(); + Method method = cls.getMethod("testMethodReferences", + String.class, ClassLoader.class); + Object result = method.invoke(cls.getDeclaredConstructor().newInstance(), + "hello", getClass().getClassLoader()); + assertEquals(new HashSet(Arrays.asList( + "load:olleh", "loader:olleh", "installed:olleh")), result); + } @Test public void testBasicServiceLoaderUsage3() throws Exception { diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/TestClient.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/TestClient.java index 5e9931c897..e74c8107c6 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/TestClient.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/TestClient.java @@ -20,7 +20,9 @@ import java.util.HashSet; import java.util.ServiceLoader; -import java.util.Set; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; import org.apache.aries.mytest.MySPI; @@ -77,4 +79,26 @@ public Set testInstalled(String input) { } return results; } + + public Set testMethodReferences(String input, + ClassLoader specifiedClassLoader) { + Set results = new HashSet(); + Function, ServiceLoader> load = ServiceLoader::load; + BiFunction, ClassLoader, ServiceLoader> loadWithLoader = + ServiceLoader::load; + Function, ServiceLoader> loadInstalled = + ServiceLoader::loadInstalled; + + for (MySPI mySPI : load.apply(MySPI.class)) { + results.add("load:" + mySPI.someMethod(input)); + } + for (MySPI mySPI : loadWithLoader.apply( + MySPI.class, specifiedClassLoader)) { + results.add("loader:" + mySPI.someMethod(input)); + } + for (MySPI mySPI : loadInstalled.apply(MySPI.class)) { + results.add("installed:" + mySPI.someMethod(input)); + } + return results; + } } diff --git a/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java index 785a1eeb53..31f5aa09bc 100644 --- a/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java +++ b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/RequirementTest.java @@ -28,6 +28,8 @@ import java.io.FileOutputStream; import java.net.URL; import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; import java.util.jar.Attributes; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; @@ -39,8 +41,14 @@ import org.apache.aries.spifly.statictool.bundle.Test2Class; import org.apache.aries.spifly.statictool.bundle.Test3Class; import org.apache.aries.spifly.statictool.bundle.Test4Class; +import org.apache.aries.spifly.statictool.bundle.Test5Class; import org.apache.aries.spifly.statictool.bundle.TestClass; import org.junit.Test; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.Handle; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; import aQute.bnd.header.Parameters; @@ -55,6 +63,8 @@ public void testConsumerBundle() throws Exception { URL test3ClassURL = getClass().getResource("/" + test3ClassFileName); String test4ClassFileName = Test4Class.class.getName().replace('.', '/') + ".class"; URL test4ClassURL = getClass().getResource("/" + test4ClassFileName); + String test5ClassFileName = Test5Class.class.getName().replace('.', '/') + ".class"; + URL test5ClassURL = getClass().getResource("/" + test5ClassFileName); File jarFile = new File(System.getProperty("java.io.tmpdir") + "/testjar_" + System.currentTimeMillis() + ".jar"); File expectedFile = null; @@ -81,6 +91,8 @@ public void testConsumerBundle() throws Exception { Streams.pump(test3ClassURL.openStream(), jos); jos.putNextEntry(new ZipEntry(test4ClassFileName)); Streams.pump(test4ClassURL.openStream(), jos); + jos.putNextEntry(new ZipEntry(test5ClassFileName)); + Streams.pump(test5ClassURL.openStream(), jos); jos.close(); Main.main(jarFile.getCanonicalPath()); @@ -135,6 +147,11 @@ public void testConsumerBundle() throws Exception { byte[] transBytes4 = Streams.suck(transformedJarFile.getInputStream(new ZipEntry(test4ClassFileName))); assertFalse("The loadInstalled class should be transformed", Arrays.equals(orgBytes4, transBytes4)); + byte[] orgBytes5 = Streams.suck(initialJarFile.getInputStream(new ZipEntry(test5ClassFileName))); + byte[] transBytes5 = Streams.suck(transformedJarFile.getInputStream(new ZipEntry(test5ClassFileName))); + assertFalse("ServiceLoader method references should be transformed", Arrays.equals(orgBytes5, transBytes5)); + assertServiceLoaderHandlesRewritten(transBytes5, Test5Class.class.getName()); + initialJarFile.close(); transformedJarFile.close(); } finally { @@ -144,4 +161,34 @@ public void testConsumerBundle() throws Exception { expectedFile.delete(); } } + + private static void assertServiceLoaderHandlesRewritten( + byte[] classBytes, String className) { + Set bridgeTargets = new HashSet(); + new ClassReader(classBytes).accept(new ClassVisitor(Opcodes.ASM9) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + return new MethodVisitor(Opcodes.ASM9) { + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, + Handle bootstrapMethodHandle, + Object... bootstrapMethodArguments) { + for (Object argument : bootstrapMethodArguments) { + if (argument instanceof Handle) { + Handle handle = (Handle) argument; + assertFalse("ServiceLoader handle was not rewritten", + "java/util/ServiceLoader".equals(handle.getOwner())); + if (className.replace('.', '/').equals(handle.getOwner())) { + bridgeTargets.add(handle.getName()); + } + } + } + } + }; + } + }, 0); + assertEquals("All ServiceLoader method references should use bridges", + 3, bridgeTargets.size()); + } } diff --git a/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/bundle/Test5Class.java b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/bundle/Test5Class.java new file mode 100644 index 0000000000..8f0069f343 --- /dev/null +++ b/spi-fly/spi-fly-static-tool/src/test/java/org/apache/aries/spifly/statictool/bundle/Test5Class.java @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.aries.spifly.statictool.bundle; + +import java.util.ServiceLoader; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class Test5Class { + public Function, ServiceLoader> load() { + return ServiceLoader::load; + } + + public BiFunction, ClassLoader, ServiceLoader> loadWithLoader() { + return ServiceLoader::load; + } + + public Function, ServiceLoader> loadInstalled() { + return ServiceLoader::loadInstalled; + } +} diff --git a/spi-fly/spi-fly-weaver/src/main/java/org/apache/aries/spifly/weaver/TCCLSetterVisitor.java b/spi-fly/spi-fly-weaver/src/main/java/org/apache/aries/spifly/weaver/TCCLSetterVisitor.java index 05468ad49b..31427f9011 100644 --- a/spi-fly/spi-fly-weaver/src/main/java/org/apache/aries/spifly/weaver/TCCLSetterVisitor.java +++ b/spi-fly/spi-fly-weaver/src/main/java/org/apache/aries/spifly/weaver/TCCLSetterVisitor.java @@ -24,9 +24,11 @@ import java.util.Set; import org.apache.aries.spifly.Util; -import org.apache.aries.spifly.WeavingData; -import org.objectweb.asm.ClassVisitor; -import org.objectweb.asm.Label; +import org.apache.aries.spifly.WeavingData; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ConstantDynamic; +import org.objectweb.asm.Handle; +import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -54,8 +56,10 @@ public class TCCLSetterVisitor extends ClassVisitor implements Opcodes { private static final Type SERVICELOADER_TYPE = Type.getType(ServiceLoader.class); - private final Type targetClass; - private final Set weavingData; + private final Type targetClass; + private final Set weavingData; + private final Set serviceLoaderBridges = + new HashSet(); // Set to true when the weaving code has changed the client such that an additional import // (to the Util.class.getPackage()) is needed. @@ -102,9 +106,12 @@ public void visitEnd() { methodNames.add(methodName); - if (ServiceLoader.class.getName().equals(wd.getClassName())) { - continue; - } + if (ServiceLoader.class.getName().equals(wd.getClassName())) { + if (serviceLoaderBridges.contains(wd)) { + addServiceLoaderBridge(wd, methodName); + } + continue; + } /* Equivalent to: * private static void $$FCCL$$$(Class cls) { @@ -136,8 +143,44 @@ public void visitEnd() { mv.endMethod(); } - super.visitEnd(); - } + super.visitEnd(); + } + + private void addServiceLoaderBridge(WeavingData wd, String methodName) { + Type[] argumentTypes; + String utilMethod; + if ("loadInstalled".equals(wd.getMethodName())) { + argumentTypes = new Type[] {CLASS_TYPE}; + utilMethod = "serviceLoaderLoadInstalled"; + } + else if (Arrays.equals( + new String[] {Class.class.getName(), ClassLoader.class.getName()}, + wd.getArgClasses())) { + argumentTypes = new Type[] {CLASS_TYPE, CLASSLOADER_TYPE}; + utilMethod = "serviceLoaderLoad"; + } + else { + argumentTypes = new Type[] {CLASS_TYPE}; + utilMethod = "serviceLoaderLoad"; + } + + Method bridge = new Method(methodName, SERVICELOADER_TYPE, argumentTypes); + GeneratorAdapter mv = new GeneratorAdapter(cv.visitMethod( + ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC, methodName, + bridge.getDescriptor(), null, null), + ACC_PRIVATE | ACC_STATIC | ACC_SYNTHETIC, + methodName, bridge.getDescriptor()); + mv.loadArgs(); + mv.visitLdcInsn(targetClass); + + Type[] utilityArguments = Arrays.copyOf(argumentTypes, + argumentTypes.length + 1); + utilityArguments[argumentTypes.length] = CLASS_TYPE; + mv.invokeStatic(UTIL_CLASS, new Method( + utilMethod, SERVICELOADER_TYPE, utilityArguments)); + mv.returnValue(); + mv.endMethod(); + } private String getGeneratedMethodName(WeavingData wd) { StringBuilder name = new StringBuilder(GENERATED_METHOD_NAME); @@ -169,13 +212,63 @@ public TCCLSetterMethodVisitor(MethodVisitor mv, int access, String name, String * contains the class being passed in. We need to pass this class to * $$FCCL$$ as well so we can copy the value found in here. */ - @Override - public void visitLdcInsn(Object cst) { - if (cst instanceof Type) { - lastLDCType = ((Type) cst); - } - super.visitLdcInsn(cst); - } + @Override + public void visitLdcInsn(Object cst) { + if (cst instanceof Type) { + lastLDCType = ((Type) cst); + } + super.visitLdcInsn(rewriteServiceLoaderConstant(cst)); + } + + @Override + public void visitInvokeDynamicInsn(String name, String descriptor, + Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { + Object[] rewrittenArguments = new Object[bootstrapMethodArguments.length]; + for (int i = 0; i < bootstrapMethodArguments.length; i++) { + rewrittenArguments[i] = rewriteServiceLoaderConstant( + bootstrapMethodArguments[i]); + } + super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, + rewrittenArguments); + } + + private Object rewriteServiceLoaderConstant(Object constant) { + if (constant instanceof Handle) { + Handle handle = (Handle) constant; + if (handle.getTag() != H_INVOKESTATIC) { + return handle; + } + WeavingData wd = findWeavingData( + handle.getOwner(), handle.getName(), handle.getDesc()); + if (wd == null || !ServiceLoader.class.getName().equals( + wd.getClassName())) { + return handle; + } + + serviceLoaderBridges.add(wd); + additionalImportRequired = true; + woven = true; + return new Handle(H_INVOKESTATIC, targetClass.getInternalName(), + getGeneratedMethodName(wd), handle.getDesc(), false); + } + if (constant instanceof ConstantDynamic) { + ConstantDynamic dynamic = (ConstantDynamic) constant; + Object[] arguments = new Object[ + dynamic.getBootstrapMethodArgumentCount()]; + boolean changed = false; + for (int i = 0; i < arguments.length; i++) { + Object argument = dynamic.getBootstrapMethodArgument(i); + arguments[i] = rewriteServiceLoaderConstant(argument); + changed |= arguments[i] != argument; + } + if (changed) { + return new ConstantDynamic(dynamic.getName(), + dynamic.getDescriptor(), + dynamic.getBootstrapMethod(), arguments); + } + } + return constant; + } /** * Store the last ALOAD call. When ServiceLoader.load(Class cls) is called From 81f4411f98c5d3ce19f281168ae2a9f916c2ead4 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 7 Aug 2026 07:29:27 +0200 Subject: [PATCH 16/26] Bind provider discovery to effective wiring --- .../apache/aries/spifly/BaseActivator.java | 14 +- .../ProviderBundleTrackerCustomizer.java | 217 ++++++++++++++---- .../java/org/apache/aries/spifly/Util.java | 8 +- ...rackerCustomizerGenericCapabilityTest.java | 47 +++- .../ProviderBundleTrackerCustomizerTest.java | 142 ++++++++++++ .../dynamic/LateMediatorStartupTest.java | 160 +++++++++++++ 6 files changed, 531 insertions(+), 57 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 565d35e710..52608092c0 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -473,7 +473,7 @@ void registerProviderBundle(String serviceType, String implementationName, if (advertisement == null) { BundleWiring wiring = WiringUtils.getWiring(bundle); BundleRevision revision = wiring == null ? null : wiring.getRevision(); - advertisement = new ProviderAdvertisement(bundle, revision); + advertisement = new ProviderAdvertisement(bundle, revision, wiring); advertisements.put(bundle.getBundleId(), advertisement); } advertisement.addImplementation(implementationName); @@ -749,12 +749,15 @@ Collection getProviders(String serviceType) { static final class ProviderAdvertisement { private final Bundle bundle; private final BundleRevision revision; + private final BundleWiring wiring; private final Set implementationNames = new java.util.LinkedHashSet(); - private ProviderAdvertisement(Bundle bundle, BundleRevision revision) { + private ProviderAdvertisement(Bundle bundle, BundleRevision revision, + BundleWiring wiring) { this.bundle = bundle; this.revision = revision; + this.wiring = wiring; } private synchronized void addImplementation(String implementationName) { @@ -762,7 +765,8 @@ private synchronized void addImplementation(String implementationName) { } private synchronized ProviderAdvertisement snapshot() { - ProviderAdvertisement snapshot = new ProviderAdvertisement(bundle, revision); + ProviderAdvertisement snapshot = new ProviderAdvertisement( + bundle, revision, wiring); snapshot.implementationNames.addAll(implementationNames); return snapshot; } @@ -775,6 +779,10 @@ BundleRevision getRevision() { return revision; } + BundleWiring getWiring() { + return wiring; + } + synchronized List getImplementationNames() { return new ArrayList(implementationNames); } diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index c36400fd34..ec2cab5210 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -33,6 +33,7 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -40,9 +41,11 @@ import java.util.Map.Entry; import java.util.Objects; import java.util.Set; -import java.util.jar.JarEntry; -import java.util.jar.JarInputStream; -import java.util.logging.Level; +import java.util.jar.JarEntry; +import java.util.jar.JarInputStream; +import java.util.logging.Level; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import java.util.stream.Stream; import org.osgi.framework.Bundle; @@ -73,8 +76,13 @@ public class ProviderBundleTrackerCustomizer implements BundleTrackerCustomizer Constants.IMPORT_PACKAGE, Constants.REQUIRE_BUNDLE, Constants.EXPORT_PACKAGE, Constants.PROVIDE_CAPABILITY, Constants.REQUIRE_CAPABILITY); - final BaseActivator activator; - final Bundle spiBundle; + final BaseActivator activator; + final Bundle spiBundle; + private final Map>> wiringServiceFiles = + Collections.synchronizedMap( + new IdentityHashMap>>()); + private final ConcurrentMap processedWirings = + new ConcurrentHashMap(); public ProviderBundleTrackerCustomizer(BaseActivator activator, Bundle spiBundle) { this.activator = activator; @@ -138,6 +146,7 @@ public List addingBundle(final Bundle bundle, BundleEvent e + bundle.getSymbolicName()); // Keep active hosts tracked so a fragment attached later can add provider // capabilities and configuration resources to them. + recordProcessedWiring(bundle, wiring); return new ArrayList(); } else { log(Level.FINE, "Examining bundle for SPI provider: " @@ -195,9 +204,19 @@ && hasRegisterPermission(bundle, details.serviceType)) { } } + recordProcessedWiring(bundle, wiring); return registrations; } + private void recordProcessedWiring(Bundle bundle, BundleWiring wiring) { + if (wiring == null) { + processedWirings.remove(bundle); + } + else { + processedWirings.put(bundle, wiring); + } + } + private boolean hasRegisterPermission(Bundle bundle, String serviceType) { boolean permitted = bundle.hasPermission( new ServicePermission(serviceType, ServicePermission.REGISTER)); @@ -334,45 +353,13 @@ List getServiceFileUrls(Bundle bundle, List serviceTypes) { } Set requestedTypes = new LinkedHashSet(serviceTypes); + Map> compatibilityEntries = + getBundleRootServiceFiles(bundle, requestedTypes); Set serviceFileURLs = new LinkedHashSet(); for (String serviceType : requestedTypes) { - List entries = wiring.findEntries( - METAINF_SERVICES, serviceType, 0); - if (entries != null) { - serviceFileURLs.addAll(entries); - } - } - if (serviceFileURLs.isEmpty()) { - Enumeration entries = bundle.findEntries( - METAINF_SERVICES, "*", false); - if (entries != null) { - for (URL entry : Collections.list(entries)) { - String path = entry.getPath(); - int separator = path.lastIndexOf('/'); - String serviceType = separator < 0 - ? path : path.substring(separator + 1); - if (requestedTypes.contains(serviceType)) { - serviceFileURLs.add(entry); - } - } - } - } - - addBundleClassPathServiceFiles( - bundle, requestedTypes, serviceFileURLs); - List hostWires = wiring.getProvidedWires( - HostNamespace.HOST_NAMESPACE); - if (hostWires != null) { - for (BundleWire hostWire : hostWires) { - BundleRevision fragmentRevision = hostWire.getRequirement() == null - ? null : hostWire.getRequirement().getRevision(); - Bundle fragment = fragmentRevision == null - ? null : fragmentRevision.getBundle(); - if (fragment != null) { - addBundleClassPathServiceFiles( - fragment, requestedTypes, serviceFileURLs); - } - } + serviceFileURLs.addAll(getServiceFileUrls( + bundle, wiring, serviceType, + compatibilityEntries.get(serviceType))); } return new ArrayList(serviceFileURLs); } @@ -401,6 +388,143 @@ List getServiceFileUrls(Bundle bundle, List serviceTypes) { return serviceFileURLs; } + private Map> getBundleRootServiceFiles(Bundle bundle, + Set serviceTypes) { + Map> result = new HashMap>(); + Enumeration entries = bundle.findEntries( + METAINF_SERVICES, "*", false); + if (entries == null) { + return result; + } + while (entries.hasMoreElements()) { + URL entry = entries.nextElement(); + String path = entry.getPath(); + int separator = path.lastIndexOf('/'); + String serviceType = separator < 0 + ? path : path.substring(separator + 1); + if (serviceTypes.contains(serviceType)) { + result.computeIfAbsent(serviceType, + key -> new ArrayList()).add(entry); + } + } + return result; + } + + private List getServiceFileUrls(Bundle bundle, BundleWiring wiring, + String serviceType, List compatibilityEntries) { + synchronized (wiringServiceFiles) { + for (java.util.Iterator iterator = + wiringServiceFiles.keySet().iterator(); iterator.hasNext();) { + BundleWiring cachedWiring = iterator.next(); + if (cachedWiring != wiring && !cachedWiring.isInUse()) { + iterator.remove(); + } + } + Map> byService = wiringServiceFiles.get(wiring); + if (byService != null && byService.containsKey(serviceType)) { + return byService.get(serviceType); + } + } + + Set urls = new LinkedHashSet(); + List rootEntries = wiring.findEntries( + METAINF_SERVICES, serviceType, 0); + if (rootEntries != null) { + urls.addAll(rootEntries); + } + if (urls.isEmpty() && compatibilityEntries != null) { + urls.addAll(compatibilityEntries); + } + + addExactWiringServiceFile(wiring, serviceType, urls); + + List result = Collections.unmodifiableList( + new ArrayList(urls)); + synchronized (wiringServiceFiles) { + Map> byService = wiringServiceFiles.get(wiring); + if (byService == null) { + byService = new HashMap>(); + wiringServiceFiles.put(wiring, byService); + } + List cached = byService.get(serviceType); + if (cached == null) { + byService.put(serviceType, result); + cached = result; + } + return cached; + } + } + + private void addExactWiringServiceFile(BundleWiring wiring, + String serviceType, Set serviceFileURLs) { + String resourceName = METAINF_SERVICES + "/" + serviceType; + java.util.Collection localResources = wiring.listResources( + METAINF_SERVICES, serviceType, BundleWiring.LISTRESOURCES_LOCAL); + if (localResources == null || !localResources.contains(resourceName)) { + return; + } + + ClassLoader classLoader = wiring.getClassLoader(); + if (classLoader == null) { + return; + } + Set foreignUrls = getForeignResourceUrls(wiring, resourceName); + try { + Enumeration resources = classLoader.getResources(resourceName); + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + if (!foreignUrls.contains(resource.toExternalForm())) { + serviceFileURLs.add(resource); + } + } + } + catch (IOException | RuntimeException e) { + log(Level.FINE, "Could not read local SPI resource " + resourceName + + " from exact provider wiring", e); + } + } + + private Set getForeignResourceUrls(BundleWiring wiring, + String resourceName) { + Set foreignUrls = new LinkedHashSet(); + try { + Enumeration systemResources = + ClassLoader.getSystemResources(resourceName); + while (systemResources.hasMoreElements()) { + foreignUrls.add(systemResources.nextElement().toExternalForm()); + } + } + catch (IOException | RuntimeException e) { + log(Level.FINE, "Could not identify system SPI resource " + + resourceName, e); + } + List requiredWires = wiring.getRequiredWires(null); + if (requiredWires == null) { + return foreignUrls; + } + for (BundleWire wire : requiredWires) { + BundleWiring providerWiring = wire.getProviderWiring(); + if (providerWiring == null || providerWiring == wiring) { + continue; + } + ClassLoader providerLoader = providerWiring.getClassLoader(); + if (providerLoader == null) { + continue; + } + try { + Enumeration resources = providerLoader.getResources(resourceName); + while (resources.hasMoreElements()) { + foreignUrls.add(resources.nextElement().toExternalForm()); + } + } + catch (IOException | RuntimeException e) { + log(Level.FINE, "Could not identify non-local SPI resource " + + resourceName, e); + } + } + return foreignUrls; + } + private void addBundleClassPathServiceFiles(Bundle bundle, Set serviceTypes, Set serviceFileURLs) { Object bcp = bundle.getHeaders().get(Constants.BUNDLE_CLASSPATH); @@ -536,7 +660,13 @@ private List getMetaInfServiceURLsFromJar( @Override public void modifiedBundle(Bundle bundle, BundleEvent event, Object registrations) { - // implementation is unnecessary for this use case + if (event != null && event.getType() == BundleEvent.STARTED + && registrations != null + && processedWirings.get(bundle) != WiringUtils.getWiring(bundle)) { + // Some frameworks deliver STARTING before publishing the refreshed host + // wiring. Reprocess at STARTED when the effective wiring changed. + reprocessBundle(bundle, registrations); + } } @SuppressWarnings("unchecked") @@ -557,6 +687,7 @@ void reprocessBundle(Bundle bundle, Object registrations) { @Override @SuppressWarnings("unchecked") public void removedBundle(Bundle bundle, BundleEvent event, Object registrations) { + processedWirings.remove(bundle); activator.unregisterProviderBundle(bundle); if (registrations == null) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java index cb47dae2b4..640fa76c04 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java @@ -688,11 +688,12 @@ private boolean hasRegisterPermission() { private static boolean isProviderAvailable( BaseActivator.ProviderAdvertisement advertisement, String serviceType) { return isProviderAvailable(advertisement.getBundle(), - advertisement.getRevision(), serviceType); + advertisement.getRevision(), advertisement.getWiring(), serviceType); } private static boolean isProviderAvailable(Bundle providerBundle, - BundleRevision providerRevision, String serviceType) { + BundleRevision providerRevision, BundleWiring providerWiring, + String serviceType) { if (providerBundle.getState() != Bundle.ACTIVE) { BaseActivator.activator.log(Level.FINE, "Bundle " + providerBundle + " is not active and cannot provide services of type: " @@ -708,7 +709,8 @@ private static boolean isProviderAvailable(Bundle providerBundle, } if (providerRevision != null) { BundleWiring wiring = WiringUtils.getWiring(providerBundle); - if (wiring == null || wiring.getRevision() != providerRevision) { + if (wiring == null || wiring != providerWiring + || wiring.getRevision() != providerRevision) { BaseActivator.activator.log(Level.FINE, "Bundle " + providerBundle + " no longer has the revision that advertised service type: " + serviceType); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java index 9098952d55..835d5eb18b 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerGenericCapabilityTest.java @@ -698,14 +698,15 @@ public void start(BundleContext context) throws Exception {} SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE + "=org.apache.aries.mytest.MySPI"); headers.put(Constants.BUNDLE_CLASSPATH, ".,non-jar.jar,embedded.jar,embedded2.jar"); EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); - EasyMock.expect(implBundle.adapt(BundleWiring.class)).andReturn( - mockProviderWiring(headers, null)).anyTimes(); - - URL embeddedJar = getClass().getResource("/embedded.jar"); + URL embeddedJar = getClass().getResource("/embedded.jar"); assertNotNull("precondition", embeddedJar); EasyMock.expect(implBundle.getEntry("embedded.jar")).andReturn(embeddedJar).anyTimes(); - URL embedded2Jar = getClass().getResource("/embedded2.jar"); - assertNotNull("precondition", embedded2Jar); + URL embedded2Jar = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", embedded2Jar); + ClassLoader providerClassLoader = new URLClassLoader( + new URL[] {embeddedJar, embedded2Jar}, getClass().getClassLoader()); + EasyMock.expect(implBundle.adapt(BundleWiring.class)).andReturn( + mockProviderWiring(headers, null, providerClassLoader)).anyTimes(); EasyMock.expect(implBundle.getEntry("embedded2.jar")).andReturn(embedded2Jar).anyTimes(); URL dir = new URL("jar:" + embeddedJar + "!/META-INF/services"); assertNotNull("precondition", dir); @@ -914,11 +915,23 @@ private BundleRevision mockHostRevision(Bundle... fragments) { } private BundleWiring mockProviderWiring(Dictionary hostHeaders, BundleRevision hostRevision) { - return mockProviderWiring(hostHeaders, hostRevision, 42L); + return mockProviderWiring(hostHeaders, hostRevision, 42L, null); + } + + private BundleWiring mockProviderWiring(Dictionary hostHeaders, + BundleRevision hostRevision, ClassLoader classLoader) { + return mockProviderWiring(hostHeaders, hostRevision, 42L, classLoader); } private BundleWiring mockProviderWiring(Dictionary hostHeaders, BundleRevision hostRevision, long mediatorBundleId) { + return mockProviderWiring( + hostHeaders, hostRevision, mediatorBundleId, null); + } + + private BundleWiring mockProviderWiring(Dictionary hostHeaders, + BundleRevision hostRevision, long mediatorBundleId, + ClassLoader classLoader) { if (hostHeaders == null) { hostHeaders = new Hashtable(); } @@ -965,11 +978,19 @@ private BundleWiring mockProviderWiring(Dictionary hostHeaders, } } - return mockProviderWiring(capabilities, registrarRequired, mediatorBundleId); + return mockProviderWiring( + capabilities, registrarRequired, mediatorBundleId, classLoader); } private BundleWiring mockProviderWiring(List capabilities, boolean registrarRequired, long mediatorBundleId) { + return mockProviderWiring(capabilities, registrarRequired, + mediatorBundleId, null); + } + + private BundleWiring mockProviderWiring(List capabilities, + boolean registrarRequired, long mediatorBundleId, + ClassLoader classLoader) { List extenderWires = registrarRequired ? Collections.singletonList(mockExtenderWire( SpiFlyConstants.REGISTRAR_EXTENDER_NAME, mediatorBundleId)) @@ -979,6 +1000,16 @@ private BundleWiring mockProviderWiring(List capabilities, .andReturn(capabilities).anyTimes(); EasyMock.expect(wiring.getRequiredWires(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE)) .andReturn(extenderWires).anyTimes(); + if (classLoader != null) { + EasyMock.expect(wiring.getClassLoader()).andReturn(classLoader).anyTimes(); + EasyMock.expect(wiring.getRequiredWires(null)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.expect(wiring.listResources("META-INF/services", + "org.apache.aries.mytest.MySPI", + BundleWiring.LISTRESOURCES_LOCAL)).andReturn(Collections.singleton( + "META-INF/services/org.apache.aries.mytest.MySPI")) + .anyTimes(); + } EasyMock.replay(wiring); return wiring; } diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index 08eec99c7a..5c4d0dd22b 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -33,6 +33,7 @@ import java.util.Hashtable; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import org.easymock.EasyMock; import org.junit.Test; @@ -43,6 +44,8 @@ import org.osgi.framework.ServicePermission; import org.osgi.framework.ServiceRegistration; import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; @@ -128,6 +131,145 @@ public void testStandardDiscoveryUsesBundleLocalEntries() throws Exception { customizer.getServiceFileUrls(bundle, Arrays.asList(serviceType, serviceType))); } + + @Test + public void testStandardDiscoveryRetainsAttachedFragmentRevisionUntilRefresh() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + final URL embeddedF1 = getClass().getResource("/embedded.jar"); + final URL embeddedF2 = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", embeddedF1); + assertNotNull("precondition", embeddedF2); + + BundleRevision fragmentF1 = EasyMock.createNiceMock(BundleRevision.class); + BundleRevision fragmentF2 = EasyMock.createNiceMock(BundleRevision.class); + AtomicReference currentFragment = + new AtomicReference(fragmentF1); + Bundle fragment = EasyMock.createNiceMock(Bundle.class); + Dictionary fragmentHeaders = + new Hashtable(); + fragmentHeaders.put(Constants.BUNDLE_CLASSPATH, "embedded.jar"); + EasyMock.expect(fragment.getHeaders()).andReturn(fragmentHeaders).anyTimes(); + EasyMock.expect(fragment.adapt(BundleRevision.class)) + .andAnswer(() -> currentFragment.get()).anyTimes(); + EasyMock.expect(fragment.getEntry("embedded.jar")) + .andAnswer(() -> currentFragment.get() == fragmentF1 + ? embeddedF1 : embeddedF2).anyTimes(); + EasyMock.replay(fragment); + EasyMock.expect(fragmentF1.getBundle()).andReturn(fragment).anyTimes(); + EasyMock.replay(fragmentF1); + EasyMock.expect(fragmentF2.getBundle()).andReturn(fragment).anyTimes(); + EasyMock.replay(fragmentF2); + + BundleRevision hostRevision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.replay(hostRevision); + ClassLoader classLoaderF1 = new URLClassLoader(new URL[] {embeddedF1}, null); + ClassLoader classLoaderF2 = new URLClassLoader(new URL[] {embeddedF2}, null); + BundleWiring wiringF1 = mockProviderWiring( + hostRevision, mockHostWire(fragmentF1), classLoaderF1, serviceType); + BundleWiring wiringF2 = mockProviderWiring( + hostRevision, mockHostWire(fragmentF2), classLoaderF2, serviceType); + AtomicReference currentWiring = + new AtomicReference(wiringF1); + Bundle host = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(host.adapt(BundleWiring.class)) + .andAnswer(() -> currentWiring.get()).anyTimes(); + EasyMock.expect(host.adapt(BundleRevision.class)) + .andReturn(hostRevision).anyTimes(); + EasyMock.expect(host.getHeaders()) + .andReturn(new Hashtable()).anyTimes(); + EasyMock.replay(host); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + List f1 = customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType)); + assertEquals(Collections.singletonList( + new URL("jar:" + embeddedF1 + "!/META-INF/services/" + serviceType)), f1); + + currentFragment.set(fragmentF2); + assertEquals("The unchanged host wiring must retain F1", f1, + customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + + currentWiring.set(wiringF2); + assertEquals(Collections.singletonList( + new URL("jar:" + embeddedF2 + "!/META-INF/services/" + serviceType)), + customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + } + + @Test + public void testStandardDiscoveryUsesExactWiringForStaleFragment() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + final String resourceName = "META-INF/services/" + serviceType; + final URL embeddedF1 = getClass().getResource("/embedded.jar"); + assertNotNull("precondition", embeddedF1); + + Bundle fragment = EasyMock.createNiceMock(Bundle.class); + BundleRevision fragmentF1 = EasyMock.createNiceMock(BundleRevision.class); + BundleRevision fragmentF2 = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.expect(fragment.adapt(BundleRevision.class)) + .andReturn(fragmentF2).anyTimes(); + EasyMock.replay(fragment); + EasyMock.expect(fragmentF1.getBundle()).andReturn(fragment).anyTimes(); + EasyMock.replay(fragmentF1); + EasyMock.replay(fragmentF2); + + BundleRevision hostRevision = EasyMock.createNiceMock(BundleRevision.class); + EasyMock.replay(hostRevision); + ClassLoader exactClassLoader = new URLClassLoader( + new URL[] {embeddedF1}, null); + BundleWiring wiring = mockProviderWiring(hostRevision, + mockHostWire(fragmentF1), exactClassLoader, serviceType); + + Bundle host = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(host.adapt(BundleWiring.class)).andReturn(wiring).anyTimes(); + EasyMock.expect(host.adapt(BundleRevision.class)) + .andReturn(hostRevision).anyTimes(); + EasyMock.expect(host.getHeaders()) + .andReturn(new Hashtable()).anyTimes(); + EasyMock.replay(host); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals(Collections.singletonList( + new URL("jar:" + embeddedF1 + "!/" + resourceName)), + customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + } + + private BundleWire mockHostWire(BundleRevision fragmentRevision) { + BundleRequirement requirement = EasyMock.createNiceMock(BundleRequirement.class); + EasyMock.expect(requirement.getRevision()) + .andReturn(fragmentRevision).anyTimes(); + EasyMock.replay(requirement); + BundleWire wire = EasyMock.createNiceMock(BundleWire.class); + EasyMock.expect(wire.getRequirement()).andReturn(requirement).anyTimes(); + EasyMock.replay(wire); + return wire; + } + + private BundleWiring mockProviderWiring(BundleRevision hostRevision, + BundleWire hostWire, ClassLoader classLoader, String serviceType) { + BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(wiring.getRevision()).andReturn(hostRevision).anyTimes(); + EasyMock.expect(wiring.getProvidedWires("osgi.wiring.host")) + .andReturn(Collections.singletonList(hostWire)).anyTimes(); + EasyMock.expect(wiring.findEntries("META-INF/services", serviceType, 0)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.expect(wiring.getClassLoader()).andReturn(classLoader).anyTimes(); + EasyMock.expect(wiring.getRequiredWires(null)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.expect(wiring.listResources("META-INF/services", serviceType, + BundleWiring.LISTRESOURCES_LOCAL)).andReturn(classLoader == null + ? Collections.emptyList() + : Collections.singleton("META-INF/services/" + serviceType)) + .anyTimes(); + EasyMock.replay(wiring); + return wiring; + } @Test @SuppressWarnings("unchecked") diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java index 52b43ac9f0..9e2ebf626b 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java @@ -20,10 +20,12 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.io.InputStream; +import java.io.ByteArrayOutputStream; import java.lang.reflect.Method; import java.net.URI; import java.nio.charset.StandardCharsets; @@ -49,6 +51,7 @@ import org.apache.aries.mytest.MySPI; import org.apache.aries.spifly.dynamic.impl1.MySPIImpl1; +import org.apache.aries.spifly.dynamic.impl2.MySPIImpl2; import org.junit.Test; import org.objectweb.asm.ClassReader; import org.objectweb.asm.commons.AdviceAdapter; @@ -60,10 +63,13 @@ import org.osgi.framework.BundleEvent; import org.osgi.framework.BundleListener; import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; import org.osgi.framework.launch.Framework; import org.osgi.framework.launch.FrameworkFactory; import org.osgi.framework.namespace.PackageNamespace; +import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.wiring.BundleCapability; +import org.osgi.framework.wiring.BundleRevision; import org.osgi.framework.wiring.BundleWiring; import org.osgi.framework.wiring.FrameworkWiring; import org.osgi.util.tracker.BundleTracker; @@ -139,6 +145,102 @@ public void startingMediatorRefreshesAlreadyLoadedConsumer() throws Exception { } } + @Test + public void providerDiscoveryUsesAttachedFragmentRevisionUntilRefresh() + throws Exception { + Path storage = Files.createTempDirectory("spifly-fragment-revision-"); + Framework framework = null; + try { + Map configuration = new HashMap(); + configuration.put(Constants.FRAMEWORK_STORAGE, + storage.resolve("framework").toString()); + configuration.put(Constants.FRAMEWORK_STORAGE_CLEAN, + Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT); + framework = newFramework(configuration); + framework.start(); + + BundleContext context = framework.getBundleContext(); + installDependency(context, ClassReader.class); + installDependency(context, AdviceAdapter.class); + installDependency(context, ClassNode.class); + installDependency(context, Analyzer.class); + installDependency(context, CheckClassAdapter.class); + installDependency(context, BundleTracker.class); + + Bundle mediator = context.installBundle( + bundleFromClasses(storage, "spifly-dynamic.jar").toUri().toString()); + Bundle api = context.installBundle(createApiBundle(storage).toUri().toString()); + Path fragmentF1 = createProviderFragment( + storage.resolve("fragment-f1.jar"), MySPIImpl1.class, "1.0.0"); + Path fragmentF2 = createProviderFragment( + storage.resolve("fragment-f2.jar"), MySPIImpl2.class, "2.0.0"); + Bundle fragment = context.installBundle(fragmentF1.toUri().toString()); + Bundle provider = context.installBundle( + createProviderHost(storage).toUri().toString()); + Bundle consumer = context.installBundle( + createConsumerBundle(storage).toUri().toString()); + + FrameworkWiring frameworkWiring = framework.adapt(FrameworkWiring.class); + assertTrue(frameworkWiring.resolveBundles(java.util.Arrays.asList( + mediator, api, fragment, provider, consumer))); + mediator.start(); + provider.start(); + consumer.start(); + assertEquals(Collections.singleton("olleh"), + invokeConsumer(consumer.loadClass(TestClient.class.getName()))); + + BundleWiring originalWiring = provider.adapt(BundleWiring.class); + BundleRevision attachedF1 = originalWiring.getProvidedWires( + HostNamespace.HOST_NAMESPACE).get(0).getRequirement().getRevision(); + InputStream update = Files.newInputStream(fragmentF2); + try { + fragment.update(update); + } + finally { + update.close(); + } + assertNotSame("The host must remain attached to F1 before refresh", + fragment.adapt(BundleRevision.class), attachedF1); + + provider.stop(); + provider.start(); + assertSame("A stop/start must not change the effective host wiring", + originalWiring, provider.adapt(BundleWiring.class)); + assertEquals(Collections.singleton("olleh"), + invokeConsumer(consumer.loadClass(TestClient.class.getName()))); + + CountDownLatch refreshed = new CountDownLatch(1); + frameworkWiring.refreshBundles( + java.util.Arrays.asList(provider, fragment), event -> { + if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) { + refreshed.countDown(); + } + }); + assertTrue("Provider and fragment refresh did not complete", + refreshed.await(30, TimeUnit.SECONDS)); + BundleWiring refreshedWiring = provider.adapt(BundleWiring.class); + assertNotSame(originalWiring, refreshedWiring); + BundleRevision attachedF2 = refreshedWiring.getProvidedWires( + HostNamespace.HOST_NAMESPACE).get(0).getRequirement().getRevision(); + assertSame("The refreshed host must attach the current fragment revision", + fragment.adapt(BundleRevision.class), attachedF2); + assertTrue(refreshedWiring.listResources("META-INF/services", SERVICE_TYPE, + BundleWiring.LISTRESOURCES_LOCAL).contains( + "META-INF/services/" + SERVICE_TYPE)); + assertEquals(MySPIImpl2.class.getName(), + provider.loadClass(MySPIImpl2.class.getName()).getName()); + assertEquals(Collections.singleton("HELLO"), + invokeConsumer(consumer.loadClass(TestClient.class.getName()))); + } + finally { + if (framework != null) { + framework.stop(); + framework.waitForStop(30000); + } + deleteRecursively(storage); + } + } + private Framework newFramework(Map configuration) throws Exception { String factoryName = System.getProperty( "spifly.test.frameworkFactory", @@ -194,6 +296,64 @@ private Path createProviderBundle(Path directory) throws IOException { return writeBundle(directory.resolve("provider.jar"), manifest, entries); } + private Path createProviderHost(Path directory) throws IOException { + Manifest manifest = bundleManifest("spifly.test.fragment.provider"); + manifest.getMainAttributes().putValue( + Constants.IMPORT_PACKAGE, "org.apache.aries.mytest;version=\"[1,2)\""); + return writeBundle(directory.resolve("provider-host.jar"), manifest, + Collections.emptyMap()); + } + + private Path createProviderFragment(Path path, Class implementation, + String version) throws IOException { + Manifest manifest = bundleManifest("spifly.test.fragment"); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue(Constants.BUNDLE_VERSION, version); + attributes.putValue(Constants.FRAGMENT_HOST, + "spifly.test.fragment.provider;bundle-version=\"[1,2)\""); + attributes.putValue(Constants.BUNDLE_CLASSPATH, ".,embedded.jar"); + attributes.putValue(Constants.REQUIRE_CAPABILITY, + "osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.registrar)\""); + attributes.putValue(Constants.PROVIDE_CAPABILITY, + "osgi.serviceloader;osgi.serviceloader=\"" + SERVICE_TYPE + "\""); + Map entries = new LinkedHashMap(); + entries.put("embedded.jar", createEmbeddedProviderJar(implementation)); + return writeBundle(path, manifest, entries); + } + + private byte[] createEmbeddedProviderJar(Class implementation) + throws IOException { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + JarOutputStream output = new JarOutputStream(bytes); + try { + String classResource = implementation.getName().replace('.', '/') + ".class"; + output.putNextEntry(new JarEntry(classResource)); + InputStream classBytes = implementation.getClassLoader() + .getResourceAsStream(classResource); + if (classBytes == null) { + throw new IOException("Cannot find test class " + classResource); + } + try { + byte[] buffer = new byte[8192]; + for (int read; (read = classBytes.read(buffer)) >= 0;) { + output.write(buffer, 0, read); + } + } + finally { + classBytes.close(); + } + output.closeEntry(); + output.putNextEntry(new JarEntry("META-INF/services/" + SERVICE_TYPE)); + output.write((implementation.getName() + "\n") + .getBytes(StandardCharsets.UTF_8)); + output.closeEntry(); + } + finally { + output.close(); + } + return bytes.toByteArray(); + } + private Path createConsumerBundle(Path directory) throws IOException { Manifest manifest = bundleManifest("spifly.test.consumer"); manifest.getMainAttributes().putValue( From 90e21739183e96aa431d6dcf8087dd9be7dbf373 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 7 Aug 2026 07:40:03 +0200 Subject: [PATCH 17/26] Close ServiceLoader views with the mediator --- .../apache/aries/spifly/BaseActivator.java | 29 +- .../java/org/apache/aries/spifly/Util.java | 324 ++++++++++++------ .../org/apache/aries/spifly/UtilTest.java | 72 +++- .../dynamic/LateMediatorStartupTest.java | 18 + 4 files changed, 340 insertions(+), 103 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 52608092c0..6aa8b82d06 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -65,7 +65,16 @@ public abstract class BaseActivator implements BundleActivator { // Static access to the activator used by the woven code, therefore // this bundle must be a singleton. // TODO see if we can get rid of the static access. - public static BaseActivator activator; + public static volatile BaseActivator activator; + + /* + * A ServiceLoader is lazy, so clearing activator alone is not sufficient: + * a loader created before stop could otherwise discover or instantiate a + * provider after this mediator instance has stopped. Each activation gets + * a distinct token which is captured by the class loaders backing that + * ServiceLoader view. + */ + private volatile Object activeSession = new Object(); private BundleContext bundleContext; @SuppressWarnings("rawtypes") @@ -126,6 +135,7 @@ public synchronized void start(BundleContext context, final String consumerHeade addConsumerWeavingData(bundle, consumerHeaderName); } + activeSession = new Object(); activator = this; if (SpiFlyConstants.SPI_CONSUMER_HEADER.equals(consumerHeaderName)) { @@ -373,13 +383,26 @@ private void requestConsumerRefreshes( @Override public synchronized void stop(BundleContext context) throws Exception { + activeSession = null; activator = null; - consumerBundleTracker.close(); - providerBundleTracker.close(); + if (consumerBundleTracker != null) { + consumerBundleTracker.close(); + } + if (providerBundleTracker != null) { + providerBundleTracker.close(); + } requestedConsumerRefreshes.clear(); } + Object getActiveSession() { + return activeSession; + } + + boolean isSessionActive(Object session) { + return session != null && activeSession == session && activator == this; + } + public boolean isLogEnabled(Level level) { return logger.isLoggable(level); } diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java index 640fa76c04..1923e4db13 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java @@ -57,8 +57,19 @@ * Methods used from ASM-generated code. They store, change and reset the thread context classloader. * The methods are static to make it easy to access them from generated code. */ -public class Util { - static ThreadLocal storedClassLoaders = new ThreadLocal(); +public class Util { + static ThreadLocal storedClassLoaders = new ThreadLocal(); + private static final ClassLoader EMPTY_PROVIDER_CLASSLOADER = new ClassLoader(null) { + @Override + public URL getResource(String name) { + return null; + } + + @Override + public Enumeration getResources(String name) { + return java.util.Collections.emptyEnumeration(); + } + }; // Provided as static method to make it easier to call from ASM-modified code public static void storeContextClassloader() { @@ -84,20 +95,27 @@ public Void run() { } public static ServiceLoader serviceLoaderLoad(Class service, Class caller) { - if (BaseActivator.activator == null) { - // The system is not yet initialized. We can't do anything. - return null; - } - - Bundle consumerBundle = getConsumerBundle(caller); + final BaseActivator activator = BaseActivator.activator; + final Object session = activator == null ? null : activator.getActiveSession(); + if (!isSessionActive(activator, session)) { + return emptyServiceLoader(service); + } + + final Bundle consumerBundle = getConsumerBundle(caller, activator); if (consumerBundle == null) { - return ServiceLoader.load(service); + return isSessionActive(activator, session) + ? ServiceLoader.load(service) : emptyServiceLoader(service); } final ClassLoader bundleClassloader = findContextClassloader( - consumerBundle, ServiceLoader.class.getName(), "load", service, true); + activator, session, consumerBundle, ServiceLoader.class.getName(), + "load", service, true); + + if (!isSessionActive(activator, session)) { + return emptyServiceLoader(service); + } if (bundleClassloader == null - && !BaseActivator.activator.isStandardConsumer(consumerBundle)) { + && !activator.isStandardConsumer(consumerBundle)) { return ServiceLoader.load(service); } @@ -109,7 +127,7 @@ public ServiceLoader run() { Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, new ProviderViewClassLoader( contextClassLoader, bundleClassloader, consumerBundle, - service.getName())); + service.getName(), activator, session)); } } ); @@ -119,58 +137,83 @@ public ServiceLoader run() { public static ServiceLoader serviceLoaderLoad( Class service, ClassLoader specifiedClassLoader, Class caller) { - if (BaseActivator.activator == null) { - // The system is not yet initialized. We can't do anything. - return null; - } - - Bundle consumerBundle = getConsumerBundle(caller); + final BaseActivator activator = BaseActivator.activator; + final Object session = activator == null ? null : activator.getActiveSession(); + if (!isSessionActive(activator, session)) { + return emptyServiceLoader(service); + } + + Bundle consumerBundle = getConsumerBundle(caller, activator); if (consumerBundle == null) { - return ServiceLoader.load(service, specifiedClassLoader); + return isSessionActive(activator, session) + ? ServiceLoader.load(service, specifiedClassLoader) + : emptyServiceLoader(service); } final ClassLoader bundleClassloader = findContextClassloader( - consumerBundle, ServiceLoader.class.getName(), "load", service, true); + activator, session, consumerBundle, ServiceLoader.class.getName(), + "load", service, true); + + if (!isSessionActive(activator, session)) { + return emptyServiceLoader(service); + } if (bundleClassloader == null) { - if (BaseActivator.activator.isStandardConsumer(consumerBundle)) { + if (activator.isStandardConsumer(consumerBundle)) { return ServiceLoader.load(service, new ProviderViewClassLoader( - specifiedClassLoader, null, consumerBundle, service.getName())); + specifiedClassLoader, null, consumerBundle, service.getName(), + activator, session)); } return ServiceLoader.load(service, specifiedClassLoader); } - if (BaseActivator.activator.isStandardConsumer(consumerBundle)) { + if (activator.isStandardConsumer(consumerBundle)) { return ServiceLoader.load(service, new ProviderViewClassLoader( specifiedClassLoader, bundleClassloader, consumerBundle, - service.getName())); + service.getName(), activator, session)); } - return ServiceLoader.load(service, new WrapperCL(specifiedClassLoader, bundleClassloader)); + return ServiceLoader.load(service, new WrapperCL(specifiedClassLoader, + bundleClassloader, activator, session)); } @BaselineIgnore("1.4.0") public static ServiceLoader serviceLoaderLoadInstalled( Class service, Class caller) { - if (BaseActivator.activator == null) { - return null; + final BaseActivator activator = BaseActivator.activator; + final Object session = activator == null ? null : activator.getActiveSession(); + if (!isSessionActive(activator, session)) { + return emptyServiceLoader(service); } - Bundle consumerBundle = getConsumerBundle(caller); + Bundle consumerBundle = getConsumerBundle(caller, activator); if (consumerBundle == null) { - return ServiceLoader.loadInstalled(service); + return isSessionActive(activator, session) + ? ServiceLoader.loadInstalled(service) : emptyServiceLoader(service); } ClassLoader bundleClassloader = findContextClassloader( - consumerBundle, ServiceLoader.class.getName(), + activator, session, consumerBundle, ServiceLoader.class.getName(), "loadInstalled", service, true); + + if (!isSessionActive(activator, session)) { + return emptyServiceLoader(service); + } if (bundleClassloader == null - && !BaseActivator.activator.isStandardConsumer(consumerBundle)) { + && !activator.isStandardConsumer(consumerBundle)) { return ServiceLoader.loadInstalled(service); } ClassLoader installedClassLoader = getInstalledClassLoader(); return ServiceLoader.load(service, new ProviderViewClassLoader( installedClassLoader, bundleClassloader, consumerBundle, - service.getName())); + service.getName(), activator, session)); + } + + private static ServiceLoader emptyServiceLoader(Class service) { + return ServiceLoader.load(service, EMPTY_PROVIDER_CLASSLOADER); + } + + private static boolean isSessionActive(BaseActivator activator, Object session) { + return activator != null && activator.isSessionActive(session); } private static ClassLoader getInstalledClassLoader() { @@ -186,7 +229,8 @@ public ClassLoader run() { }); } - private static Bundle getConsumerBundle(final Class caller) { + private static Bundle getConsumerBundle(final Class caller, + BaseActivator activator) { Bundle bundle = FrameworkUtil.getBundle(caller); if (bundle != null) { return bundle; @@ -203,22 +247,31 @@ public ClassLoader run() { return ((BundleReference) bundleLoader).getBundle(); } - BaseActivator.activator.log(Level.FINE, + activator.log(Level.FINE, "Could not identify consuming bundle for class " + caller.getName()); return null; } - - public static void fixContextClassloader(String cls, String method, Class clsArg, ClassLoader bundleLoader) { - BundleReference br = getBundleReference(bundleLoader); - - if (br == null) { - return; - } - + + public static void fixContextClassloader(String cls, String method, Class clsArg, ClassLoader bundleLoader) { + final BaseActivator activator = BaseActivator.activator; + final Object session = activator == null ? null : activator.getActiveSession(); + if (!isSessionActive(activator, session)) { + return; + } + + BundleReference br = getBundleReference(bundleLoader, activator); + + if (br == null) { + return; + } + final ClassLoader cl = findContextClassloader( - br.getBundle(), cls, method, clsArg, false); - if (cl != null) { - BaseActivator.activator.log(Level.FINE, "Temporarily setting Thread Context Classloader to: " + cl); + activator, session, br.getBundle(), cls, method, clsArg, false); + if (!isSessionActive(activator, session)) { + return; + } + if (cl != null) { + activator.log(Level.FINE, "Temporarily setting Thread Context Classloader to: " + cl); AccessController.doPrivileged(new PrivilegedAction() { @Override public Void run() { @@ -226,14 +279,17 @@ public Void run() { return null; } }); - } else { - BaseActivator.activator.log(Level.FINE, "No classloader found for " + cls + ":" + method + "(" + clsArg + ")"); - } - } - - private static ClassLoader findContextClassloader(Bundle consumerBundle, String className, + } else { + activator.log(Level.FINE, "No classloader found for " + cls + ":" + method + "(" + clsArg + ")"); + } + } + + private static ClassLoader findContextClassloader(BaseActivator activator, + Object session, Bundle consumerBundle, String className, String methodName, Class clsArg, boolean permissionAware) { - BaseActivator activator = BaseActivator.activator; + if (!isSessionActive(activator, session)) { + return null; + } String requestedClass; Map, String> args; @@ -272,7 +328,11 @@ private static ClassLoader findContextClassloader(Bundle consumerBundle, String && !bundles.isEmpty()) { return new ProviderAdvertisementClassLoader( activator.findProviderAdvertisements(requestedClass, bundles), - requestedClass); + requestedClass, activator, session); + } + + if (!isSessionActive(activator, session)) { + return null; } switch (bundles.size()) { @@ -281,35 +341,38 @@ private static ClassLoader findContextClassloader(Bundle consumerBundle, String case 1: Bundle bundle = bundles.iterator().next(); return serviceLoaderCall && permissionAware - ? getProviderClassLoader(bundle, requestedClass) - : getBundleClassLoader(bundle); + ? getProviderClassLoader(bundle, requestedClass, activator, session) + : getBundleClassLoader(bundle, activator); default: List loaders = new ArrayList(); for (Bundle b : bundles) { loaders.add(serviceLoaderCall && permissionAware - ? getProviderClassLoader(b, requestedClass) - : getBundleClassLoader(b)); + ? getProviderClassLoader(b, requestedClass, activator, session) + : getBundleClassLoader(b, activator)); } return new MultiDelegationClassloader(loaders.toArray(new ClassLoader[loaders.size()])); } } - private static ClassLoader getBundleClassLoader(final Bundle b) { + private static ClassLoader getBundleClassLoader(final Bundle b, + final BaseActivator activator) { return AccessController.doPrivileged(new PrivilegedAction() { @Override public ClassLoader run() { - return getBundleClassLoaderPrivileged(b); + return getBundleClassLoaderPrivileged(b, activator); } }); } private static ClassLoader getProviderClassLoader(Bundle providerBundle, - String serviceType) { + String serviceType, BaseActivator activator, Object session) { return new ProviderBundleClassLoader(providerBundle, - getBundleClassLoader(providerBundle), serviceType); + getBundleClassLoader(providerBundle, activator), serviceType, + activator, session); } - - private static ClassLoader getBundleClassLoaderPrivileged(Bundle b) { + + private static ClassLoader getBundleClassLoaderPrivileged(Bundle b, + BaseActivator activator) { // In 4.3 this can be done much easier by using the BundleWiring, but we want this code to // be 4.2 compliant. // Here we're just finding any class in the bundle, load that and then use its classloader. @@ -352,7 +415,7 @@ private static ClassLoader getBundleClassLoaderPrivileged(Bundle b) { URL url = b.getResource(entry); if (url != null) { - ClassLoader cl = getClassLoaderViaBundleClassPath(b, url); + ClassLoader cl = getClassLoaderViaBundleClassPath(b, url, activator); if (cl != null) return cl; } @@ -382,21 +445,18 @@ private static ClassLoader getBundleClassLoaderViaAdapt(Bundle b, Method adaptMe } } - private static BundleReference getBundleReference(ClassLoader bundleLoader) { - if (BaseActivator.activator == null) { - // The system is not yet initialized. We can't do anything. - return null; - } - - if (!(bundleLoader instanceof BundleReference)) { - BaseActivator.activator.log(Level.FINE, "Classloader of consuming bundle doesn't implement BundleReference: " + bundleLoader); - return null; - } + private static BundleReference getBundleReference(ClassLoader bundleLoader, + BaseActivator activator) { + if (!(bundleLoader instanceof BundleReference)) { + activator.log(Level.FINE, "Classloader of consuming bundle doesn't implement BundleReference: " + bundleLoader); + return null; + } return (BundleReference) bundleLoader; } - private static ClassLoader getClassLoaderViaBundleClassPath(Bundle b, URL url) { + private static ClassLoader getClassLoaderViaBundleClassPath(Bundle b, URL url, + BaseActivator activator) { try { JarInputStream jis = null; try { @@ -416,8 +476,8 @@ private static ClassLoader getClassLoaderViaBundleClassPath(Bundle b, URL url) { jis.close(); } } catch (IOException e) { - BaseActivator.activator.log(Level.FINE, "Problem loading class from embedded jar file: " + url + - " in bundle " + b.getSymbolicName(), e); + activator.log(Level.FINE, "Problem loading class from embedded jar file: " + url + + " in bundle " + b.getSymbolicName(), e); } return null; } @@ -438,11 +498,38 @@ private static ClassLoader getClassLoaderFromClassResource(Bundle b, String path } private static class WrapperCL extends ClassLoader { - private final ClassLoader bundleClassloader; - public WrapperCL(ClassLoader specifiedClassLoader, ClassLoader bundleClassloader) { - super(specifiedClassLoader); - this.bundleClassloader = bundleClassloader; - } + private final ClassLoader bundleClassloader; + private final BaseActivator activator; + private final Object session; + + WrapperCL(ClassLoader specifiedClassLoader, ClassLoader bundleClassloader, + BaseActivator activator, Object session) { + super(specifiedClassLoader); + this.bundleClassloader = bundleClassloader; + this.activator = activator; + this.session = session; + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (!isSessionActive(activator, session)) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name, resolve); + } + + @Override + public URL getResource(String name) { + return isSessionActive(activator, session) ? super.getResource(name) : null; + } + + @Override + public Enumeration getResources(String name) throws IOException { + return isSessionActive(activator, session) + ? super.getResources(name) + : java.util.Collections.emptyEnumeration(); + } @Override protected Class findClass(String name) throws ClassNotFoundException { @@ -465,18 +552,26 @@ private static class ProviderViewClassLoader extends ClassLoader { private final Bundle consumerBundle; private final String serviceType; private final String providerConfiguration; + private final BaseActivator activator; + private final Object session; ProviderViewClassLoader(ClassLoader parent, ClassLoader providerClassLoader, - Bundle consumerBundle, String serviceType) { + Bundle consumerBundle, String serviceType, BaseActivator activator, + Object session) { super(parent); this.providerClassLoader = providerClassLoader; this.consumerBundle = consumerBundle; this.serviceType = serviceType; + this.activator = activator; + this.session = session; providerConfiguration = "META-INF/services/" + serviceType; } @Override public URL getResource(String name) { + if (!isSessionActive(activator, session)) { + return null; + } if (providerConfiguration.equals(name)) { return !hasGetPermission() || providerClassLoader == null ? null : providerClassLoader.getResource(name); @@ -486,6 +581,9 @@ public URL getResource(String name) { @Override public Enumeration getResources(String name) throws IOException { + if (!isSessionActive(activator, session)) { + return java.util.Collections.emptyEnumeration(); + } if (providerConfiguration.equals(name)) { return !hasGetPermission() || providerClassLoader == null ? java.util.Collections.emptyEnumeration() @@ -497,7 +595,8 @@ public Enumeration getResources(String name) throws IOException { @Override protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException { - if (!hasGetPermission() || providerClassLoader == null) { + if (!isSessionActive(activator, session) + || !hasGetPermission() || providerClassLoader == null) { throw new ClassNotFoundException(name); } Class cls = providerClassLoader.loadClass(name); @@ -509,13 +608,15 @@ protected synchronized Class loadClass(String name, boolean resolve) @Override protected URL findResource(String name) { - return !hasGetPermission() || providerClassLoader == null + return !isSessionActive(activator, session) + || !hasGetPermission() || providerClassLoader == null ? null : providerClassLoader.getResource(name); } @Override protected Enumeration findResources(String name) throws IOException { - return !hasGetPermission() || providerClassLoader == null + return !isSessionActive(activator, session) + || !hasGetPermission() || providerClassLoader == null ? java.util.Collections.emptyEnumeration() : providerClassLoader.getResources(name); } @@ -524,7 +625,7 @@ private boolean hasGetPermission() { boolean permitted = consumerBundle.hasPermission( new ServicePermission(serviceType, ServicePermission.GET)); if (!permitted) { - BaseActivator.activator.log(Level.FINE, "Bundle " + consumerBundle + activator.log(Level.FINE, "Bundle " + consumerBundle + " does not have permission to obtain services of type: " + serviceType); } @@ -535,15 +636,19 @@ private boolean hasGetPermission() { private static class ProviderAdvertisementClassLoader extends ClassLoader { private final String serviceType; private final String providerConfiguration; + private final BaseActivator activator; + private final Object session; private final Map> advertisersByImplementation = new LinkedHashMap>(); ProviderAdvertisementClassLoader( List advertisements, - String serviceType) { + String serviceType, BaseActivator activator, Object session) { super(null); this.serviceType = serviceType; + this.activator = activator; + this.session = session; providerConfiguration = "META-INF/services/" + serviceType; for (BaseActivator.ProviderAdvertisement advertisement : advertisements) { for (String implementation : advertisement.getImplementationNames()) { @@ -556,7 +661,8 @@ private static class ProviderAdvertisementClassLoader extends ClassLoader { @Override public URL getResource(String name) { - if (!providerConfiguration.equals(name)) { + if (!isSessionActive(activator, session) + || !providerConfiguration.equals(name)) { return null; } return createProviderConfiguration(); @@ -573,6 +679,9 @@ public Enumeration getResources(String name) throws IOException { @Override public Class loadClass(String name) throws ClassNotFoundException { + if (!isSessionActive(activator, session)) { + throw new ClassNotFoundException(name); + } List advertisements = advertisersByImplementation.get(name); if (advertisements == null) { @@ -581,7 +690,8 @@ public Class loadClass(String name) throws ClassNotFoundException { ClassNotFoundException last = null; for (BaseActivator.ProviderAdvertisement advertisement : advertisements) { - if (!isProviderAvailable(advertisement, serviceType)) { + if (!isProviderAvailable(advertisement, serviceType, + activator, session)) { continue; } try { @@ -595,11 +705,15 @@ public Class loadClass(String name) throws ClassNotFoundException { } private URL createProviderConfiguration() { + if (!isSessionActive(activator, session)) { + return null; + } Set implementations = new LinkedHashSet(); for (Map.Entry> entry : advertisersByImplementation.entrySet()) { for (BaseActivator.ProviderAdvertisement advertisement : entry.getValue()) { - if (isProviderAvailable(advertisement, serviceType)) { + if (isProviderAvailable(advertisement, serviceType, + activator, session)) { implementations.add(entry.getKey()); break; } @@ -644,13 +758,17 @@ private static class ProviderBundleClassLoader extends ClassLoader { private final Bundle providerBundle; private final ClassLoader delegate; private final String serviceType; + private final BaseActivator activator; + private final Object session; ProviderBundleClassLoader(Bundle providerBundle, ClassLoader delegate, - String serviceType) { + String serviceType, BaseActivator activator, Object session) { super(null); this.providerBundle = providerBundle; this.delegate = delegate; this.serviceType = serviceType; + this.activator = activator; + this.session = session; } @Override @@ -674,10 +792,13 @@ public Enumeration getResources(String name) throws IOException { } private boolean hasRegisterPermission() { + if (!isSessionActive(activator, session)) { + return false; + } boolean permitted = providerBundle.hasPermission( new ServicePermission(serviceType, ServicePermission.REGISTER)); if (!permitted) { - BaseActivator.activator.log(Level.FINE, "Bundle " + providerBundle + activator.log(Level.FINE, "Bundle " + providerBundle + " does not have permission to provide services of type: " + serviceType); } @@ -686,23 +807,28 @@ private boolean hasRegisterPermission() { } private static boolean isProviderAvailable( - BaseActivator.ProviderAdvertisement advertisement, String serviceType) { + BaseActivator.ProviderAdvertisement advertisement, String serviceType, + BaseActivator activator, Object session) { return isProviderAvailable(advertisement.getBundle(), - advertisement.getRevision(), advertisement.getWiring(), serviceType); + advertisement.getRevision(), advertisement.getWiring(), serviceType, + activator, session); } private static boolean isProviderAvailable(Bundle providerBundle, BundleRevision providerRevision, BundleWiring providerWiring, - String serviceType) { + String serviceType, BaseActivator activator, Object session) { + if (!isSessionActive(activator, session)) { + return false; + } if (providerBundle.getState() != Bundle.ACTIVE) { - BaseActivator.activator.log(Level.FINE, "Bundle " + providerBundle + activator.log(Level.FINE, "Bundle " + providerBundle + " is not active and cannot provide services of type: " + serviceType); return false; } if (!providerBundle.hasPermission( new ServicePermission(serviceType, ServicePermission.REGISTER))) { - BaseActivator.activator.log(Level.FINE, "Bundle " + providerBundle + activator.log(Level.FINE, "Bundle " + providerBundle + " does not have permission to provide services of type: " + serviceType); return false; @@ -711,7 +837,7 @@ private static boolean isProviderAvailable(Bundle providerBundle, BundleWiring wiring = WiringUtils.getWiring(providerBundle); if (wiring == null || wiring != providerWiring || wiring.getRevision() != providerRevision) { - BaseActivator.activator.log(Level.FINE, "Bundle " + providerBundle + activator.log(Level.FINE, "Bundle " + providerBundle + " no longer has the revision that advertised service type: " + serviceType); return false; diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java index 4e57857601..b69032870c 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java @@ -122,7 +122,7 @@ public Class answer() throws Throwable { @Test public void testNotInitialized() throws Exception { - BaseActivator.activator = null; + BaseActivator.activator = null; URL url = getClass().getResource("/embedded3.jar"); assertNotNull("precondition", url); @@ -154,6 +154,44 @@ public Class answer() throws Throwable { null, Thread.currentThread().getContextClassLoader()); } + @Test + public void serviceLoaderFactoriesReturnClosedViewWhenMediatorUnavailable() + throws Exception { + BaseActivator.activator = null; + + URL provider = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", provider); + ClassLoader providerLoader = new URLClassLoader( + new URL[] {provider}, getClass().getClassLoader()); + Thread.currentThread().setContextClassLoader(providerLoader); + + ServiceLoader contextLoader = Util.serviceLoaderLoad( + MySPI.class, UtilTest.class); + ServiceLoader specifiedLoader = Util.serviceLoaderLoad( + MySPI.class, providerLoader, UtilTest.class); + ServiceLoader installedLoader = Util.serviceLoaderLoadInstalled( + MySPI.class, UtilTest.class); + + assertNotNull(contextLoader); + assertNotNull(specifiedLoader); + assertNotNull(installedLoader); + assertFalse(contextLoader.iterator().hasNext()); + assertFalse(specifiedLoader.iterator().hasNext()); + assertFalse(installedLoader.iterator().hasNext()); + } + + @Test + public void closedServiceLoaderFactoriesRetainNullValidation() { + BaseActivator.activator = null; + + assertThrows(NullPointerException.class, () -> Util.serviceLoaderLoad( + (Class) null, UtilTest.class)); + assertThrows(NullPointerException.class, () -> Util.serviceLoaderLoad( + (Class) null, getClass().getClassLoader(), UtilTest.class)); + assertThrows(NullPointerException.class, () -> Util.serviceLoaderLoadInstalled( + (Class) null, UtilTest.class)); + } + @Test public void standardConsumerWithNoProvidersHasClosedView() throws Exception { BaseActivator activator = newActivator(); @@ -398,6 +436,38 @@ public void stoppedProviderCannotServeExistingLoader() throws Exception { assertThrows(ServiceConfigurationError.class, iterator::next); } + @Test + public void stoppedMediatorClosesExistingLazyLoaderAcrossRestart() + throws Exception { + BaseActivator activator = newActivator(); + Bundle consumer = mockPermissionBundle(new AtomicBoolean(true)); + BundleWiring consumerWiring = EasyMock.createNiceMock(BundleWiring.class); + EasyMock.expect(consumerWiring.getRequirements( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .andReturn(Collections.emptyList()).anyTimes(); + EasyMock.replay(consumerWiring); + activator.registerStandardConsumer(consumer, consumerWiring); + + Bundle provider = mockProviderBundle(42L, + getClass().getResource("/embedded2.jar")); + activator.registerProviderBundle(MySPI.class.getName(), + "org.apache.aries.spifly.impl3.MySPIImpl3", provider, + new HashMap()); + + ServiceLoader activeLoader = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)); + ServiceLoader deferredLoader = Util.serviceLoaderLoad( + MySPI.class, callerClass(consumer)); + assertTrue(activeLoader.iterator().hasNext()); + + activator.stop(null); + assertFalse(deferredLoader.iterator().hasNext()); + + newActivator(); + deferredLoader.reload(); + assertFalse(deferredLoader.iterator().hasNext()); + } + @Test public void replacedProviderRevisionCannotServeExistingLoader() throws Exception { BaseActivator activator = newActivator(); diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java index 9e2ebf626b..ef9fe728f4 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java @@ -135,6 +135,14 @@ public void startingMediatorRefreshesAlreadyLoadedConsumer() throws Exception { Class classAfterMediator = consumer.loadClass(TestClient.class.getName()); assertNotSame(classBeforeMediator, classAfterMediator); assertEquals(Collections.singleton("olleh"), invokeConsumer(classAfterMediator)); + assertEquals(new java.util.HashSet(java.util.Arrays.asList( + "load:olleh", "loader:olleh", "installed:olleh")), + invokeMethodReferences(classAfterMediator)); + + mediator.stop(); + assertEquals(Collections.emptySet(), invokeConsumer(classAfterMediator)); + assertEquals(Collections.emptySet(), + invokeMethodReferences(classAfterMediator)); } finally { if (framework != null) { @@ -257,6 +265,16 @@ private Set invokeConsumer(Class consumerClass) throws Exception { consumerClass.getDeclaredConstructor().newInstance(), "hello"); } + @SuppressWarnings("unchecked") + private Set invokeMethodReferences(Class consumerClass) + throws Exception { + Method method = consumerClass.getMethod("testMethodReferences", + String.class, ClassLoader.class); + return (Set) method.invoke( + consumerClass.getDeclaredConstructor().newInstance(), "hello", + consumerClass.getClassLoader()); + } + private void installDependency(BundleContext context, Class type) throws Exception { BundleWiring systemWiring = context.getBundle(0).adapt(BundleWiring.class); if (systemWiring != null) { From 3a2c9fee5bf65a46f33eaa941ae0b856b4cdfbb3 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Fri, 7 Aug 2026 07:54:46 +0200 Subject: [PATCH 18/26] Refresh consumers after provider stops --- .../apache/aries/spifly/BaseActivator.java | 102 ++++++++++++++ .../ConsumerBundleTrackerCustomizer.java | 19 +-- .../ProviderBundleTrackerCustomizer.java | 1 + .../java/org/apache/aries/spifly/Util.java | 16 ++- .../aries/spifly/ResolvedWiringTest.java | 76 +++++++++++ .../dynamic/LateMediatorStartupTest.java | 129 ++++++++++++++++-- 6 files changed, 322 insertions(+), 21 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 6aa8b82d06..4466dc6899 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -104,6 +104,13 @@ public abstract class BaseActivator implements BundleActivator { Collections.newSetFromMap( new ConcurrentHashMap, Boolean>()); + private final ConcurrentMap> consumersByProvider = + new ConcurrentHashMap>(); + + private final Set requestedProviderStopRefreshes = + Collections.newSetFromMap( + new ConcurrentHashMap()); + @SuppressWarnings({ "unchecked", "rawtypes" }) public synchronized void start(BundleContext context, final String consumerHeaderName) throws Exception { bundleContext = context; @@ -339,6 +346,99 @@ void refreshActiveConsumers() { requestConsumerRefreshes(refreshes, "after dynamic mediator startup"); } + void recordProviderUse(Bundle consumer, Bundle provider, Object session) { + if (!isSessionActive(session) || consumer == null || provider == null + || !isStandardConsumer(consumer)) { + return; + } + consumersByProvider.computeIfAbsent(provider, + key -> Collections.newSetFromMap( + new ConcurrentHashMap())).add(consumer); + } + + void forgetConsumerProviderUses(Bundle consumer) { + for (Map.Entry> entry : consumersByProvider.entrySet()) { + Set consumers = entry.getValue(); + consumers.remove(consumer); + if (consumers.isEmpty()) { + consumersByProvider.remove(entry.getKey(), consumers); + } + } + } + + void providerBundleStopped(Bundle provider) { + Object session = getActiveSession(); + if (!isSessionActive(session)) { + return; + } + int state = provider.getState(); + if ((state & (Bundle.ACTIVE | Bundle.STARTING)) != 0) { + return; + } + + Set consumers = consumersByProvider.remove(provider); + if (consumers == null || consumers.isEmpty()) { + return; + } + + List refreshes = new ArrayList(); + for (Bundle consumer : consumers) { + int consumerState = consumer.getState(); + BundleRevision revision = consumer.adapt(BundleRevision.class); + if ((consumerState & (Bundle.ACTIVE | Bundle.STARTING)) != 0 + && isStandardConsumer(consumer) + && revision != null + && (revision.getTypes() & BundleRevision.TYPE_FRAGMENT) == 0) { + refreshes.add(consumer); + } + } + Collections.sort(refreshes, (left, right) -> Long.compare( + left.getBundleId(), right.getBundleId())); + requestProviderStopRefreshes(refreshes, provider); + } + + private void requestProviderStopRefreshes( + Collection refreshes, Bundle provider) { + if (refreshes.isEmpty()) { + return; + } + Bundle systemBundle = bundleContext == null ? null : bundleContext.getBundle(0); + FrameworkWiring frameworkWiring = systemBundle == null + ? null : systemBundle.adapt(FrameworkWiring.class); + if (frameworkWiring == null) { + log(Level.WARNING, "Cannot refresh consumers after provider " + provider + + " stopped: FrameworkWiring is unavailable"); + return; + } + + List newRefreshes = new ArrayList(); + for (Bundle consumer : refreshes) { + if (requestedProviderStopRefreshes.add(consumer)) { + newRefreshes.add(consumer); + } + } + if (newRefreshes.isEmpty()) { + return; + } + + Set bundles = new LinkedHashSet(newRefreshes); + try { + frameworkWiring.refreshBundles(bundles, event -> { + requestedProviderStopRefreshes.removeAll(newRefreshes); + if (event.getType() == FrameworkEvent.ERROR) { + log(Level.WARNING, "Could not refresh consumers " + bundles + + " after provider " + provider + " stopped", + event.getThrowable()); + } + }); + } + catch (RuntimeException e) { + requestedProviderStopRefreshes.removeAll(newRefreshes); + log(Level.WARNING, "Could not request refresh of consumers " + bundles + + " after provider " + provider + " stopped", e); + } + } + private void requestConsumerRefreshes( Collection> refreshes, String reason) { if (refreshes.isEmpty()) { @@ -393,6 +493,8 @@ public synchronized void stop(BundleContext context) throws Exception { providerBundleTracker.close(); } requestedConsumerRefreshes.clear(); + consumersByProvider.clear(); + requestedProviderStopRefreshes.clear(); } Object getActiveSession() { diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerBundleTrackerCustomizer.java index 5fd783b05e..25b1224374 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerBundleTrackerCustomizer.java @@ -48,14 +48,15 @@ public Object addingBundle(Bundle bundle, BundleEvent event) { return bundle; } - @Override - public void modifiedBundle(Bundle bundle, BundleEvent event, Object object) { - removedBundle(bundle, event, object); - addingBundle(bundle, event); - } + @Override + public void modifiedBundle(Bundle bundle, BundleEvent event, Object object) { + activator.removeWeavingData(bundle); + addingBundle(bundle, event); + } @Override - public void removedBundle(Bundle bundle, BundleEvent event, Object object) { - activator.removeWeavingData(bundle); - } -} + public void removedBundle(Bundle bundle, BundleEvent event, Object object) { + activator.removeWeavingData(bundle); + activator.forgetConsumerProviderUses(bundle); + } +} diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index ec2cab5210..42681efc8d 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -688,6 +688,7 @@ void reprocessBundle(Bundle bundle, Object registrations) { @SuppressWarnings("unchecked") public void removedBundle(Bundle bundle, BundleEvent event, Object registrations) { processedWirings.remove(bundle); + activator.providerBundleStopped(bundle); activator.unregisterProviderBundle(bundle); if (registrations == null) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java index 1923e4db13..8a5957fcb7 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/Util.java @@ -328,7 +328,7 @@ private static ClassLoader findContextClassloader(BaseActivator activator, && !bundles.isEmpty()) { return new ProviderAdvertisementClassLoader( activator.findProviderAdvertisements(requestedClass, bundles), - requestedClass, activator, session); + requestedClass, consumerBundle, activator, session); } if (!isSessionActive(activator, session)) { @@ -636,6 +636,7 @@ private boolean hasGetPermission() { private static class ProviderAdvertisementClassLoader extends ClassLoader { private final String serviceType; private final String providerConfiguration; + private final Bundle consumerBundle; private final BaseActivator activator; private final Object session; private final Map> @@ -644,9 +645,11 @@ private static class ProviderAdvertisementClassLoader extends ClassLoader { ProviderAdvertisementClassLoader( List advertisements, - String serviceType, BaseActivator activator, Object session) { + String serviceType, Bundle consumerBundle, + BaseActivator activator, Object session) { super(null); this.serviceType = serviceType; + this.consumerBundle = consumerBundle; this.activator = activator; this.session = session; providerConfiguration = "META-INF/services/" + serviceType; @@ -695,7 +698,14 @@ public Class loadClass(String name) throws ClassNotFoundException { continue; } try { - return advertisement.getBundle().loadClass(name); + Class providerClass = advertisement.getBundle().loadClass(name); + if (!isProviderAvailable(advertisement, serviceType, + activator, session)) { + throw new ClassNotFoundException(name); + } + activator.recordProviderUse(consumerBundle, + advertisement.getBundle(), session); + return providerClass; } catch (ClassNotFoundException e) { last = e; diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java index 8c9eb7a024..b735f29689 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java @@ -41,6 +41,7 @@ import org.junit.Test; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkEvent; import org.osgi.framework.FrameworkListener; import org.osgi.framework.namespace.HostNamespace; import org.osgi.framework.wiring.BundleCapability; @@ -439,6 +440,81 @@ public void lateDynamicMediatorStartupRefreshesExistingStandardConsumersOnce() EasyMock.verify(frameworkWiring); } + @Test + public void providerStopRefreshesActiveStandardConsumerPerUse() + throws Exception { + Bundle consumer = mockRefreshBundle(7L, Bundle.ACTIVE, 0, null); + Bundle resolvedConsumer = mockRefreshBundle(8L, Bundle.RESOLVED, 0, null); + Bundle firstProvider = mockRefreshBundle(9L, Bundle.RESOLVED, 0, null); + Bundle secondProvider = mockRefreshBundle(10L, Bundle.RESOLVED, 0, null); + List refreshInvocations = new java.util.ArrayList(); + + FrameworkWiring frameworkWiring = EasyMock.createMock(FrameworkWiring.class); + frameworkWiring.refreshBundles( + EasyMock.>anyObject(), + EasyMock.anyObject()); + EasyMock.expectLastCall().andAnswer(() -> { + refreshInvocations.add(EasyMock.getCurrentArguments().clone()); + return null; + }).times(2); + EasyMock.replay(frameworkWiring); + + Bundle systemBundle = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(systemBundle.adapt(FrameworkWiring.class)) + .andReturn(frameworkWiring).anyTimes(); + EasyMock.replay(systemBundle); + BundleContext context = EasyMock.createNiceMock(BundleContext.class); + EasyMock.expect(context.getBundle()).andReturn(mediator).anyTimes(); + EasyMock.expect(context.getBundle(0)).andReturn(systemBundle).anyTimes(); + EasyMock.replay(context); + setBundleContext(context); + + activator.registerStandardConsumer(consumer, null); + activator.registerStandardConsumer(resolvedConsumer, null); + BaseActivator.activator = activator; + try { + Object session = activator.getActiveSession(); + activator.recordProviderUse(consumer, firstProvider, session); + activator.recordProviderUse(consumer, firstProvider, session); + activator.recordProviderUse(resolvedConsumer, firstProvider, session); + activator.recordProviderUse(consumer, secondProvider, session); + activator.providerBundleStopped(firstProvider); + assertEquals(1, refreshInvocations.size()); + + // The second stopped provider does not start a concurrent refresh of + // the same consumer. The pending refresh covers both stale objects. + activator.providerBundleStopped(secondProvider); + assertEquals(1, refreshInvocations.size()); + notifyRefreshListener(refreshInvocations.get(0)[1]); + + // A later provider use can request another refresh after the first + // callback has cleared the in-flight marker. + activator.recordProviderUse(consumer, secondProvider, session); + activator.providerBundleStopped(secondProvider); + notifyRefreshListener(refreshInvocations.get(1)[1]); + } + finally { + BaseActivator.activator = null; + } + + assertEquals(Collections.singleton(consumer), refreshInvocations.get(0)[0]); + assertEquals(Collections.singleton(consumer), refreshInvocations.get(1)[0]); + EasyMock.verify(frameworkWiring); + } + + private void notifyRefreshListener(Object listenerArgument) { + FrameworkEvent event = new FrameworkEvent( + FrameworkEvent.PACKAGES_REFRESHED, mediator, null); + if (listenerArgument instanceof FrameworkListener[]) { + for (FrameworkListener listener : (FrameworkListener[]) listenerArgument) { + listener.frameworkEvent(event); + } + } + else { + ((FrameworkListener) listenerArgument).frameworkEvent(event); + } + } + private BundleWiring mockConsumerWiring(List extenderWires, List serviceRequirements, List serviceWires) { return mockConsumerWiring(extenderWires, serviceRequirements, serviceWires, diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java index ef9fe728f4..debd26e0ae 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java @@ -153,6 +153,87 @@ public void startingMediatorRefreshesAlreadyLoadedConsumer() throws Exception { } } + @Test + public void stoppedProviderRefreshesConsumerThatLoadedItsProvider() + throws Exception { + Path storage = Files.createTempDirectory("spifly-provider-stop-"); + Framework framework = null; + try { + Map configuration = new HashMap(); + configuration.put(Constants.FRAMEWORK_STORAGE, + storage.resolve("framework").toString()); + configuration.put(Constants.FRAMEWORK_STORAGE_CLEAN, + Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT); + framework = newFramework(configuration); + framework.start(); + + BundleContext context = framework.getBundleContext(); + installDependency(context, ClassReader.class); + installDependency(context, AdviceAdapter.class); + installDependency(context, ClassNode.class); + installDependency(context, Analyzer.class); + installDependency(context, CheckClassAdapter.class); + installDependency(context, BundleTracker.class); + + Bundle mediator = context.installBundle( + bundleFromClasses(storage, "spifly-dynamic.jar").toUri().toString()); + Bundle api = context.installBundle(createApiBundle(storage).toUri().toString()); + Bundle provider = context.installBundle( + createProviderBundle(storage).toUri().toString()); + Bundle consumer = context.installBundle( + createConsumerBundle(storage).toUri().toString()); + + FrameworkWiring frameworkWiring = framework.adapt(FrameworkWiring.class); + assertTrue(frameworkWiring.resolveBundles( + java.util.Arrays.asList(mediator, api, provider, consumer))); + mediator.start(); + provider.start(); + consumer.start(); + + Class originalConsumerClass = consumer.loadClass(TestClient.class.getName()); + assertEquals(Collections.singleton("olleh"), + invokeConsumer(originalConsumerClass)); + + CountDownLatch consumerStopped = new CountDownLatch(1); + CountDownLatch consumerRestarted = new CountDownLatch(1); + BundleListener listener = event -> { + if (consumer.equals(event.getBundle())) { + if (event.getType() == BundleEvent.STOPPED) { + consumerStopped.countDown(); + } + else if (event.getType() == BundleEvent.STARTED) { + consumerRestarted.countDown(); + } + } + }; + context.addBundleListener(listener); + try { + provider.stop(); + assertTrue("The stale consumer should be stopped for refresh", + consumerStopped.await(30, TimeUnit.SECONDS)); + assertTrue("The stale consumer should restart after refresh", + consumerRestarted.await(30, TimeUnit.SECONDS)); + } + finally { + context.removeBundleListener(listener); + } + + assertEquals(Bundle.RESOLVED, provider.getState()); + Class refreshedConsumerClass = consumer.loadClass( + TestClient.class.getName()); + assertNotSame(originalConsumerClass, refreshedConsumerClass); + assertEquals(Collections.emptySet(), + invokeConsumer(refreshedConsumerClass)); + } + finally { + if (framework != null) { + framework.stop(); + framework.waitForStop(30000); + } + deleteRecursively(storage); + } + } + @Test public void providerDiscoveryUsesAttachedFragmentRevisionUntilRefresh() throws Exception { @@ -210,7 +291,22 @@ public void providerDiscoveryUsesAttachedFragmentRevisionUntilRefresh() assertNotSame("The host must remain attached to F1 before refresh", fragment.adapt(BundleRevision.class), attachedF1); - provider.stop(); + CountDownLatch consumerRefreshedAfterStop = new CountDownLatch(1); + BundleListener stopListener = event -> { + if (event.getType() == BundleEvent.STARTED + && consumer.equals(event.getBundle())) { + consumerRefreshedAfterStop.countDown(); + } + }; + context.addBundleListener(stopListener); + try { + provider.stop(); + assertTrue("Consumer refresh after provider stop did not complete", + consumerRefreshedAfterStop.await(30, TimeUnit.SECONDS)); + } + finally { + context.removeBundleListener(stopListener); + } provider.start(); assertSame("A stop/start must not change the effective host wiring", originalWiring, provider.adapt(BundleWiring.class)); @@ -218,14 +314,29 @@ public void providerDiscoveryUsesAttachedFragmentRevisionUntilRefresh() invokeConsumer(consumer.loadClass(TestClient.class.getName()))); CountDownLatch refreshed = new CountDownLatch(1); - frameworkWiring.refreshBundles( - java.util.Arrays.asList(provider, fragment), event -> { - if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) { - refreshed.countDown(); - } - }); - assertTrue("Provider and fragment refresh did not complete", - refreshed.await(30, TimeUnit.SECONDS)); + CountDownLatch consumerRefreshedWithProvider = new CountDownLatch(1); + BundleListener refreshListener = event -> { + if (event.getType() == BundleEvent.STARTED + && consumer.equals(event.getBundle())) { + consumerRefreshedWithProvider.countDown(); + } + }; + context.addBundleListener(refreshListener); + try { + frameworkWiring.refreshBundles( + java.util.Arrays.asList(provider, fragment), event -> { + if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) { + refreshed.countDown(); + } + }); + assertTrue("Provider and fragment refresh did not complete", + refreshed.await(30, TimeUnit.SECONDS)); + assertTrue("Consumer refresh with provider did not complete", + consumerRefreshedWithProvider.await(30, TimeUnit.SECONDS)); + } + finally { + context.removeBundleListener(refreshListener); + } BundleWiring refreshedWiring = provider.adapt(BundleWiring.class); assertNotSame(originalWiring, refreshedWiring); BundleRevision attachedF2 = refreshedWiring.getProvidedWires( From 3ff096fef66c46bd747824b2fdd5770364f2248a Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sat, 8 Aug 2026 17:32:45 +0200 Subject: [PATCH 19/26] Stabilize fragment refresh integration test --- .../dynamic/LateMediatorStartupTest.java | 63 +++++++------------ 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java index debd26e0ae..0608c4d2fc 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java @@ -64,6 +64,7 @@ import org.osgi.framework.BundleListener; import org.osgi.framework.Constants; import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; import org.osgi.framework.launch.Framework; import org.osgi.framework.launch.FrameworkFactory; import org.osgi.framework.namespace.PackageNamespace; @@ -196,6 +197,7 @@ public void stoppedProviderRefreshesConsumerThatLoadedItsProvider() CountDownLatch consumerStopped = new CountDownLatch(1); CountDownLatch consumerRestarted = new CountDownLatch(1); + CountDownLatch refreshCompleted = new CountDownLatch(1); BundleListener listener = event -> { if (consumer.equals(event.getBundle())) { if (event.getType() == BundleEvent.STOPPED) { @@ -206,16 +208,25 @@ else if (event.getType() == BundleEvent.STARTED) { } } }; + FrameworkListener frameworkListener = event -> { + if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) { + refreshCompleted.countDown(); + } + }; context.addBundleListener(listener); + context.addFrameworkListener(frameworkListener); try { provider.stop(); assertTrue("The stale consumer should be stopped for refresh", consumerStopped.await(30, TimeUnit.SECONDS)); assertTrue("The stale consumer should restart after refresh", consumerRestarted.await(30, TimeUnit.SECONDS)); + assertTrue("The consumer refresh should complete", + refreshCompleted.await(30, TimeUnit.SECONDS)); } finally { context.removeBundleListener(listener); + context.removeFrameworkListener(frameworkListener); } assertEquals(Bundle.RESOLVED, provider.getState()); @@ -291,52 +302,25 @@ public void providerDiscoveryUsesAttachedFragmentRevisionUntilRefresh() assertNotSame("The host must remain attached to F1 before refresh", fragment.adapt(BundleRevision.class), attachedF1); - CountDownLatch consumerRefreshedAfterStop = new CountDownLatch(1); - BundleListener stopListener = event -> { - if (event.getType() == BundleEvent.STARTED - && consumer.equals(event.getBundle())) { - consumerRefreshedAfterStop.countDown(); - } - }; - context.addBundleListener(stopListener); - try { - provider.stop(); - assertTrue("Consumer refresh after provider stop did not complete", - consumerRefreshedAfterStop.await(30, TimeUnit.SECONDS)); - } - finally { - context.removeBundleListener(stopListener); - } + consumer.stop(); + provider.stop(); provider.start(); assertSame("A stop/start must not change the effective host wiring", originalWiring, provider.adapt(BundleWiring.class)); + consumer.start(); assertEquals(Collections.singleton("olleh"), invokeConsumer(consumer.loadClass(TestClient.class.getName()))); + consumer.stop(); CountDownLatch refreshed = new CountDownLatch(1); - CountDownLatch consumerRefreshedWithProvider = new CountDownLatch(1); - BundleListener refreshListener = event -> { - if (event.getType() == BundleEvent.STARTED - && consumer.equals(event.getBundle())) { - consumerRefreshedWithProvider.countDown(); - } - }; - context.addBundleListener(refreshListener); - try { - frameworkWiring.refreshBundles( - java.util.Arrays.asList(provider, fragment), event -> { - if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) { - refreshed.countDown(); - } - }); - assertTrue("Provider and fragment refresh did not complete", - refreshed.await(30, TimeUnit.SECONDS)); - assertTrue("Consumer refresh with provider did not complete", - consumerRefreshedWithProvider.await(30, TimeUnit.SECONDS)); - } - finally { - context.removeBundleListener(refreshListener); - } + frameworkWiring.refreshBundles( + java.util.Arrays.asList(provider, fragment, consumer), event -> { + if (event.getType() == FrameworkEvent.PACKAGES_REFRESHED) { + refreshed.countDown(); + } + }); + assertTrue("Provider, fragment, and consumer refresh did not complete", + refreshed.await(30, TimeUnit.SECONDS)); BundleWiring refreshedWiring = provider.adapt(BundleWiring.class); assertNotSame(originalWiring, refreshedWiring); BundleRevision attachedF2 = refreshedWiring.getProvidedWires( @@ -348,6 +332,7 @@ public void providerDiscoveryUsesAttachedFragmentRevisionUntilRefresh() "META-INF/services/" + SERVICE_TYPE)); assertEquals(MySPIImpl2.class.getName(), provider.loadClass(MySPIImpl2.class.getName()).getName()); + consumer.start(); assertEquals(Collections.singleton("HELLO"), invokeConsumer(consumer.loadClass(TestClient.class.getName()))); } From 6244c054b6e187100486a7823ff6a57a40078823 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sat, 8 Aug 2026 17:35:34 +0200 Subject: [PATCH 20/26] Select complete provider bundles --- .../apache/aries/spifly/BaseActivator.java | 39 ++++++++----------- .../aries/spifly/ResolvedWiringTest.java | 32 +++++++++++++++ 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java index 4466dc6899..6024b0f65c 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/BaseActivator.java @@ -699,9 +699,7 @@ public Collection findConsumerRestrictions(Bundle consumer, String class StandardConsumerWiring standardWiring = standardConsumerWirings.get(consumer); if (standardWiring != null && ServiceLoader.class.getName().equals(className) && isServiceLoaderMethod(methodName)) { - String serviceType = args == null ? null - : args.get(new Pair(0, Class.class.getName())); - return standardWiring.getProviders(serviceType); + return standardWiring.getProviders(); } Map> restrictions = consumerRestrictions.get(consumer); @@ -782,40 +780,36 @@ private static boolean isServiceLoaderMethod(String methodName) { private static final class StandardConsumerWiring { private final boolean restricted; - private final Map> providersByServiceType; + private final Set providers; - private StandardConsumerWiring(boolean restricted, Map> providersByServiceType) { + private StandardConsumerWiring(boolean restricted, Set providers) { this.restricted = restricted; - this.providersByServiceType = providersByServiceType; + this.providers = providers; } static StandardConsumerWiring from(BundleWiring wiring) { if (wiring == null) { - return new StandardConsumerWiring(true, Collections.>emptyMap()); + return new StandardConsumerWiring(true, Collections.emptySet()); } if (!hasDeclaredServiceLoaderRequirement(wiring)) { - return new StandardConsumerWiring(false, Collections.>emptyMap()); + return new StandardConsumerWiring(false, Collections.emptySet()); } - Map> providers = new HashMap>(); + Set providers = new LinkedHashSet(); for (BundleWire wire : wiring.getRequiredWires( SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) { - Object serviceType = wire.getCapability().getAttributes().get( - SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE); BundleWiring providerWiring = wire.getProviderWiring(); - if (serviceType instanceof String && providerWiring != null) { - providers.computeIfAbsent((String) serviceType, key -> new HashSet()) - .add(providerWiring.getBundle()); + if (providerWiring != null) { + Bundle provider = providerWiring.getBundle(); + if (provider != null) { + providers.add(provider); + } } } - Map> immutableProviders = new HashMap>(); - for (Map.Entry> entry : providers.entrySet()) { - immutableProviders.put(entry.getKey(), Collections.unmodifiableSet(entry.getValue())); - } return new StandardConsumerWiring( - true, Collections.unmodifiableMap(immutableProviders)); + true, Collections.unmodifiableSet(providers)); } private static boolean hasDeclaredServiceLoaderRequirement(BundleWiring wiring) { @@ -859,15 +853,14 @@ private static boolean hasDeclaredServiceLoaderRequirement( static StandardConsumerWiring denied() { return new StandardConsumerWiring( - true, Collections.>emptyMap()); + true, Collections.emptySet()); } - Collection getProviders(String serviceType) { + Collection getProviders() { if (!restricted) { return null; } - Set providers = providersByServiceType.get(serviceType); - return providers == null ? Collections.emptySet() : providers; + return providers; } } diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java index b735f29689..dac9c167e8 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java @@ -123,6 +123,38 @@ public void restrictsConsumerToActuallyWiredProvider() throws Exception { assertEquals(Collections.singleton(selectedProvider), providers); } + @Test + public void wiredProviderBundleContributesEveryPublishedServiceType() + throws Exception { + Bundle firstProvider = mockBundle(7L); + Bundle secondProvider = mockBundle(8L); + BundleRequirement serviceRequirement = + EasyMock.createNiceMock(BundleRequirement.class); + EasyMock.replay(serviceRequirement); + BundleWiring wiring = mockConsumerWiring( + Collections.singletonList(mockWire( + SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE, + SpiFlyConstants.PROCESSOR_EXTENDER_NAME, mediator)), + Collections.singletonList(serviceRequirement), + Arrays.asList( + mockWire(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, + SERVICE_TYPE, firstProvider), + mockWire(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, + "org.example.OtherService", secondProvider), + mockWire(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, + SERVICE_TYPE, firstProvider))); + Bundle consumer = mockConsumer(wiring); + + activator.addConsumerWeavingData( + consumer, SpiFlyConstants.SPI_CONSUMER_HEADER); + + Collection selected = activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "load", + serviceArguments("org.example.ThirdService")); + assertEquals(new java.util.HashSet( + Arrays.asList(firstProvider, secondProvider)), selected); + } + @Test public void declaredButUnwiredServiceRequirementAllowsNoProviders() throws Exception { BundleRequirement serviceRequirement = EasyMock.createNiceMock(BundleRequirement.class); From b55df50c2e6e03e71a1ab00dbbd12f9ef26e9b40 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sat, 8 Aug 2026 17:50:00 +0200 Subject: [PATCH 21/26] Discover providers from exact bundle content --- .../ProviderBundleTrackerCustomizer.java | 150 +++++------- .../ProviderBundleTrackerCustomizerTest.java | 216 ++++++++++-------- 2 files changed, 179 insertions(+), 187 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index 42681efc8d..36bcf6617f 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -21,19 +21,20 @@ import static java.util.stream.Collectors.toList; import static org.osgi.framework.wiring.BundleRevision.TYPE_FRAGMENT; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.Hashtable; -import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -43,6 +44,7 @@ import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarInputStream; +import java.util.jar.Manifest; import java.util.logging.Level; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -78,9 +80,6 @@ public class ProviderBundleTrackerCustomizer implements BundleTrackerCustomizer final BaseActivator activator; final Bundle spiBundle; - private final Map>> wiringServiceFiles = - Collections.synchronizedMap( - new IdentityHashMap>>()); private final ConcurrentMap processedWirings = new ConcurrentHashMap(); @@ -412,20 +411,6 @@ private Map> getBundleRootServiceFiles(Bundle bundle, private List getServiceFileUrls(Bundle bundle, BundleWiring wiring, String serviceType, List compatibilityEntries) { - synchronized (wiringServiceFiles) { - for (java.util.Iterator iterator = - wiringServiceFiles.keySet().iterator(); iterator.hasNext();) { - BundleWiring cachedWiring = iterator.next(); - if (cachedWiring != wiring && !cachedWiring.isInUse()) { - iterator.remove(); - } - } - Map> byService = wiringServiceFiles.get(wiring); - if (byService != null && byService.containsKey(serviceType)) { - return byService.get(serviceType); - } - } - Set urls = new LinkedHashSet(); List rootEntries = wiring.findEntries( METAINF_SERVICES, serviceType, 0); @@ -435,99 +420,74 @@ private List getServiceFileUrls(Bundle bundle, BundleWiring wiring, if (urls.isEmpty() && compatibilityEntries != null) { urls.addAll(compatibilityEntries); } - - addExactWiringServiceFile(wiring, serviceType, urls); - - List result = Collections.unmodifiableList( - new ArrayList(urls)); - synchronized (wiringServiceFiles) { - Map> byService = wiringServiceFiles.get(wiring); - if (byService == null) { - byService = new HashMap>(); - wiringServiceFiles.put(wiring, byService); - } - List cached = byService.get(serviceType); - if (cached == null) { - byService.put(serviceType, result); - cached = result; - } - return cached; + if (!addExactBundleClassPathServiceFiles(wiring, serviceType, urls)) { + addBundleClassPathServiceFiles(bundle, + Collections.singleton(serviceType), urls); } + return new ArrayList(urls); } - private void addExactWiringServiceFile(BundleWiring wiring, + private boolean addExactBundleClassPathServiceFiles(BundleWiring wiring, String serviceType, Set serviceFileURLs) { - String resourceName = METAINF_SERVICES + "/" + serviceType; - java.util.Collection localResources = wiring.listResources( - METAINF_SERVICES, serviceType, BundleWiring.LISTRESOURCES_LOCAL); - if (localResources == null || !localResources.contains(resourceName)) { - return; + List manifests = wiring.findEntries("META-INF", "MANIFEST.MF", 0); + if (manifests == null || manifests.isEmpty()) { + return false; } - - ClassLoader classLoader = wiring.getClassLoader(); - if (classLoader == null) { - return; - } - Set foreignUrls = getForeignResourceUrls(wiring, resourceName); - try { - Enumeration resources = classLoader.getResources(resourceName); - while (resources.hasMoreElements()) { - URL resource = resources.nextElement(); - if (!foreignUrls.contains(resource.toExternalForm())) { - serviceFileURLs.add(resource); + for (URL manifestUrl : manifests) { + try (InputStream stream = manifestUrl.openStream()) { + String bundleClassPath = new Manifest(stream).getMainAttributes() + .getValue(Constants.BUNDLE_CLASSPATH); + if (bundleClassPath == null) { + continue; + } + Parameters entries = new Parameters(bundleClassPath); + for (String key : entries.keySet()) { + String entry = ConsumerHeaderProcessor.removeDuplicateMarker(key).trim(); + if (!".".equals(entry)) { + addExactBundleClassPathEntry(manifestUrl, entry, + serviceType, serviceFileURLs); + } } } + catch (IOException | RuntimeException e) { + log(Level.FINE, "Could not read exact bundle manifest " + + manifestUrl, e); + } } - catch (IOException | RuntimeException e) { - log(Level.FINE, "Could not read local SPI resource " + resourceName - + " from exact provider wiring", e); - } + return true; } - private Set getForeignResourceUrls(BundleWiring wiring, - String resourceName) { - Set foreignUrls = new LinkedHashSet(); + private void addExactBundleClassPathEntry(URL manifestUrl, String entry, + String serviceType, Set serviceFileURLs) { try { - Enumeration systemResources = - ClassLoader.getSystemResources(resourceName); - while (systemResources.hasMoreElements()) { - foreignUrls.add(systemResources.nextElement().toExternalForm()); + URL entryUrl = new URL(manifestUrl, "../" + entry); + List embedded = getMetaInfServiceURLsFromJar( + entryUrl, Collections.singleton(serviceType)); + if (!embedded.isEmpty()) { + serviceFileURLs.addAll(embedded); + return; + } + + String separator = entry.endsWith("/") ? "" : "/"; + URL resource = new URL(manifestUrl, "../" + entry + separator + + METAINF_SERVICES + "/" + serviceType); + try (InputStream stream = resource.openStream()) { + serviceFileURLs.add(resource); } } catch (IOException | RuntimeException e) { - log(Level.FINE, "Could not identify system SPI resource " - + resourceName, e); - } - List requiredWires = wiring.getRequiredWires(null); - if (requiredWires == null) { - return foreignUrls; - } - for (BundleWire wire : requiredWires) { - BundleWiring providerWiring = wire.getProviderWiring(); - if (providerWiring == null || providerWiring == wiring) { - continue; - } - ClassLoader providerLoader = providerWiring.getClassLoader(); - if (providerLoader == null) { - continue; - } - try { - Enumeration resources = providerLoader.getResources(resourceName); - while (resources.hasMoreElements()) { - foreignUrls.add(resources.nextElement().toExternalForm()); - } - } - catch (IOException | RuntimeException e) { - log(Level.FINE, "Could not identify non-local SPI resource " - + resourceName, e); - } + log(Level.FINE, "Could not read SPI resource for " + serviceType + + " from exact Bundle-ClassPath entry " + entry, e); } - return foreignUrls; } private void addBundleClassPathServiceFiles(Bundle bundle, Set serviceTypes, Set serviceFileURLs) { - Object bcp = bundle.getHeaders().get(Constants.BUNDLE_CLASSPATH); + Dictionary headers = bundle.getHeaders(); + if (headers == null) { + return; + } + Object bcp = headers.get(Constants.BUNDLE_CLASSPATH); if (!(bcp instanceof String)) { return; } diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index 5c4d0dd22b..26d8827af3 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -21,10 +21,15 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertSame; - +import static org.junit.Assert.assertSame; + +import java.io.InputStream; +import java.io.OutputStream; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -34,9 +39,13 @@ import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; - -import org.easymock.EasyMock; -import org.junit.Test; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.easymock.EasyMock; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; @@ -49,7 +58,10 @@ import org.osgi.framework.wiring.BundleWire; import org.osgi.framework.wiring.BundleWiring; -public class ProviderBundleTrackerCustomizerTest { +public class ProviderBundleTrackerCustomizerTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private BaseActivator activator = new BaseActivator() { @Override @@ -140,44 +152,17 @@ public void testStandardDiscoveryRetainsAttachedFragmentRevisionUntilRefresh() final URL embeddedF2 = getClass().getResource("/embedded2.jar"); assertNotNull("precondition", embeddedF1); assertNotNull("precondition", embeddedF2); - - BundleRevision fragmentF1 = EasyMock.createNiceMock(BundleRevision.class); - BundleRevision fragmentF2 = EasyMock.createNiceMock(BundleRevision.class); - AtomicReference currentFragment = - new AtomicReference(fragmentF1); - Bundle fragment = EasyMock.createNiceMock(Bundle.class); - Dictionary fragmentHeaders = - new Hashtable(); - fragmentHeaders.put(Constants.BUNDLE_CLASSPATH, "embedded.jar"); - EasyMock.expect(fragment.getHeaders()).andReturn(fragmentHeaders).anyTimes(); - EasyMock.expect(fragment.adapt(BundleRevision.class)) - .andAnswer(() -> currentFragment.get()).anyTimes(); - EasyMock.expect(fragment.getEntry("embedded.jar")) - .andAnswer(() -> currentFragment.get() == fragmentF1 - ? embeddedF1 : embeddedF2).anyTimes(); - EasyMock.replay(fragment); - EasyMock.expect(fragmentF1.getBundle()).andReturn(fragment).anyTimes(); - EasyMock.replay(fragmentF1); - EasyMock.expect(fragmentF2.getBundle()).andReturn(fragment).anyTimes(); - EasyMock.replay(fragmentF2); - - BundleRevision hostRevision = EasyMock.createNiceMock(BundleRevision.class); - EasyMock.replay(hostRevision); - ClassLoader classLoaderF1 = new URLClassLoader(new URL[] {embeddedF1}, null); - ClassLoader classLoaderF2 = new URLClassLoader(new URL[] {embeddedF2}, null); + URL manifestF1 = createRevisionContent("fragment-f1", embeddedF1); + URL manifestF2 = createRevisionContent("fragment-f2", embeddedF2); BundleWiring wiringF1 = mockProviderWiring( - hostRevision, mockHostWire(fragmentF1), classLoaderF1, serviceType); + Collections.singletonList(manifestF1), serviceType); BundleWiring wiringF2 = mockProviderWiring( - hostRevision, mockHostWire(fragmentF2), classLoaderF2, serviceType); + Collections.singletonList(manifestF2), serviceType); AtomicReference currentWiring = new AtomicReference(wiringF1); Bundle host = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(host.adapt(BundleWiring.class)) .andAnswer(() -> currentWiring.get()).anyTimes(); - EasyMock.expect(host.adapt(BundleRevision.class)) - .andReturn(hostRevision).anyTimes(); - EasyMock.expect(host.getHeaders()) - .andReturn(new Hashtable()).anyTimes(); EasyMock.replay(host); ProviderBundleTrackerCustomizer customizer = @@ -185,91 +170,133 @@ public void testStandardDiscoveryRetainsAttachedFragmentRevisionUntilRefresh() List f1 = customizer.getServiceFileUrls( host, Collections.singletonList(serviceType)); assertEquals(Collections.singletonList( - new URL("jar:" + embeddedF1 + "!/META-INF/services/" + serviceType)), f1); + embeddedServiceFile(manifestF1, serviceType)), f1); - currentFragment.set(fragmentF2); assertEquals("The unchanged host wiring must retain F1", f1, customizer.getServiceFileUrls( host, Collections.singletonList(serviceType))); currentWiring.set(wiringF2); assertEquals(Collections.singletonList( - new URL("jar:" + embeddedF2 + "!/META-INF/services/" + serviceType)), + embeddedServiceFile(manifestF2, serviceType)), customizer.getServiceFileUrls( host, Collections.singletonList(serviceType))); } @Test - public void testStandardDiscoveryUsesExactWiringForStaleFragment() + public void testStandardDiscoveryRecomputesSameEffectiveWiring() throws Exception { final String serviceType = "org.apache.aries.mytest.MySPI"; - final String resourceName = "META-INF/services/" + serviceType; final URL embeddedF1 = getClass().getResource("/embedded.jar"); + final URL embeddedF2 = getClass().getResource("/embedded2.jar"); assertNotNull("precondition", embeddedF1); + assertNotNull("precondition", embeddedF2); + URL manifestF1 = createRevisionContent("attached-f1", embeddedF1); + URL manifestF2 = createRevisionContent("attached-f2", embeddedF2); + + AtomicReference> manifests = new AtomicReference>( + Collections.singletonList(manifestF1)); + BundleWiring wiring = mockProviderWiring(manifests, serviceType); - Bundle fragment = EasyMock.createNiceMock(Bundle.class); - BundleRevision fragmentF1 = EasyMock.createNiceMock(BundleRevision.class); - BundleRevision fragmentF2 = EasyMock.createNiceMock(BundleRevision.class); - EasyMock.expect(fragment.adapt(BundleRevision.class)) - .andReturn(fragmentF2).anyTimes(); - EasyMock.replay(fragment); - EasyMock.expect(fragmentF1.getBundle()).andReturn(fragment).anyTimes(); - EasyMock.replay(fragmentF1); - EasyMock.replay(fragmentF2); + Bundle host = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(host.adapt(BundleWiring.class)).andReturn(wiring).anyTimes(); + EasyMock.replay(host); - BundleRevision hostRevision = EasyMock.createNiceMock(BundleRevision.class); - EasyMock.replay(hostRevision); - ClassLoader exactClassLoader = new URLClassLoader( - new URL[] {embeddedF1}, null); - BundleWiring wiring = mockProviderWiring(hostRevision, - mockHostWire(fragmentF1), exactClassLoader, serviceType); + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals(Collections.singletonList( + embeddedServiceFile(manifestF1, serviceType)), + customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + + manifests.set(Arrays.asList(manifestF1, manifestF2)); + assertEquals(Arrays.asList( + embeddedServiceFile(manifestF1, serviceType), + embeddedServiceFile(manifestF2, serviceType)), + customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + } + + @Test + public void testStandardDiscoveryDoesNotConsultDependencyClassLoaders() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + final URL embedded = getClass().getResource("/embedded.jar"); + assertNotNull("precondition", embedded); + URL manifest = createRevisionContent("cycle-local", embedded); + BundleWiring wiring = mockProviderWiring( + Collections.singletonList(manifest), serviceType); Bundle host = EasyMock.createNiceMock(Bundle.class); EasyMock.expect(host.adapt(BundleWiring.class)).andReturn(wiring).anyTimes(); - EasyMock.expect(host.adapt(BundleRevision.class)) - .andReturn(hostRevision).anyTimes(); - EasyMock.expect(host.getHeaders()) - .andReturn(new Hashtable()).anyTimes(); EasyMock.replay(host); ProviderBundleTrackerCustomizer customizer = new ProviderBundleTrackerCustomizer(activator, null); assertEquals(Collections.singletonList( - new URL("jar:" + embeddedF1 + "!/" + resourceName)), + embeddedServiceFile(manifest, serviceType)), customizer.getServiceFileUrls( host, Collections.singletonList(serviceType))); } - private BundleWire mockHostWire(BundleRevision fragmentRevision) { - BundleRequirement requirement = EasyMock.createNiceMock(BundleRequirement.class); - EasyMock.expect(requirement.getRevision()) - .andReturn(fragmentRevision).anyTimes(); - EasyMock.replay(requirement); - BundleWire wire = EasyMock.createNiceMock(BundleWire.class); - EasyMock.expect(wire.getRequirement()).andReturn(requirement).anyTimes(); - EasyMock.replay(wire); - return wire; + @Test + public void testStandardDiscoveryExcludesForeignOnlyServiceFile() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + BundleWiring wiring = mockProviderWiring( + Collections.emptyList(), serviceType); + Bundle host = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(host.adapt(BundleWiring.class)).andReturn(wiring).anyTimes(); + EasyMock.replay(host); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals(Collections.emptyList(), customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); } - private BundleWiring mockProviderWiring(BundleRevision hostRevision, - BundleWire hostWire, ClassLoader classLoader, String serviceType) { - BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); - EasyMock.expect(wiring.getRevision()).andReturn(hostRevision).anyTimes(); - EasyMock.expect(wiring.getProvidedWires("osgi.wiring.host")) - .andReturn(Collections.singletonList(hostWire)).anyTimes(); + private BundleWiring mockProviderWiring( + List manifests, String serviceType) { + return mockProviderWiring( + new AtomicReference>(manifests), serviceType); + } + + private BundleWiring mockProviderWiring( + AtomicReference> manifests, String serviceType) { + BundleWiring wiring = EasyMock.createMock(BundleWiring.class); EasyMock.expect(wiring.findEntries("META-INF/services", serviceType, 0)) .andReturn(Collections.emptyList()).anyTimes(); - EasyMock.expect(wiring.getClassLoader()).andReturn(classLoader).anyTimes(); - EasyMock.expect(wiring.getRequiredWires(null)) - .andReturn(Collections.emptyList()).anyTimes(); - EasyMock.expect(wiring.listResources("META-INF/services", serviceType, - BundleWiring.LISTRESOURCES_LOCAL)).andReturn(classLoader == null - ? Collections.emptyList() - : Collections.singleton("META-INF/services/" + serviceType)) - .anyTimes(); + EasyMock.expect(wiring.findEntries("META-INF", "MANIFEST.MF", 0)) + .andAnswer(() -> manifests.get()).anyTimes(); EasyMock.replay(wiring); return wiring; } + + private URL createRevisionContent(String name, URL embedded) + throws Exception { + Path root = temporaryFolder.newFolder(name).toPath(); + Path metaInf = Files.createDirectories(root.resolve("META-INF")); + Manifest manifest = new Manifest(); + manifest.getMainAttributes().put( + Attributes.Name.MANIFEST_VERSION, "1.0"); + manifest.getMainAttributes().putValue( + Constants.BUNDLE_CLASSPATH, "embedded.jar"); + try (OutputStream output = Files.newOutputStream( + metaInf.resolve("MANIFEST.MF"))) { + manifest.write(output); + } + try (InputStream input = embedded.openStream()) { + Files.copy(input, root.resolve("embedded.jar"), + StandardCopyOption.REPLACE_EXISTING); + } + return metaInf.resolve("MANIFEST.MF").toUri().toURL(); + } + + private URL embeddedServiceFile(URL manifest, String serviceType) + throws Exception { + URL embedded = new URL(manifest, "../embedded.jar"); + return new URL("jar:" + embedded + "!/META-INF/services/" + serviceType); + } @Test @SuppressWarnings("unchecked") @@ -400,17 +427,17 @@ private Bundle mockMultiSPIBundle(BundleContext implBC) throws ClassNotFoundExce "osgi.serviceloader;osgi.serviceloader='org.apache.aries.mytest.MySPI2';register:='org.apache.aries.spifly.impl4.MySPIImpl4c';foo='ccc'" ); EasyMock.expect(implBundle.getHeaders()).andReturn(headers).anyTimes(); - EasyMock.expect(implBundle.adapt(BundleWiring.class)).andReturn( - mockStandardProviderWiring("org.apache.aries.mytest.MySPI2", 25L)).anyTimes(); - - // List the resources found at META-INF/services in the test bundle + // List the resources found at META-INF/services in the test bundle URL dir = getClass().getResource("impl4/META-INF/services"); assertNotNull("precondition", dir); EasyMock.expect(implBundle.getResource("/META-INF/services")).andReturn(dir).anyTimes(); URL resA = getClass().getResource("impl4/META-INF/services/org.apache.aries.mytest.MySPI"); assertNotNull("precondition", resA); - URL resB = getClass().getResource("impl4/META-INF/services/org.apache.aries.mytest.MySPI2"); - assertNotNull("precondition", resB); + URL resB = getClass().getResource("impl4/META-INF/services/org.apache.aries.mytest.MySPI2"); + assertNotNull("precondition", resB); + EasyMock.expect(implBundle.adapt(BundleWiring.class)).andReturn( + mockStandardProviderWiring("org.apache.aries.mytest.MySPI2", + 25L, resB)).anyTimes(); EasyMock.expect(implBundle.findEntries("META-INF/services", "*", false)).andReturn( Collections.enumeration(Arrays.asList(resA, resB))).anyTimes(); Class cls = getClass().getClassLoader().loadClass("org.apache.aries.spifly.impl4.MySPIImpl4b"); @@ -421,7 +448,8 @@ private Bundle mockMultiSPIBundle(BundleContext implBC) throws ClassNotFoundExce return implBundle; } - private BundleWiring mockStandardProviderWiring(String serviceType, long mediatorBundleId) { + private BundleWiring mockStandardProviderWiring( + String serviceType, long mediatorBundleId, URL serviceFile) { Map serviceAttributes = new HashMap(); serviceAttributes.put(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, serviceType); BundleCapability serviceCapability = EasyMock.createNiceMock(BundleCapability.class); @@ -454,6 +482,10 @@ private BundleWiring mockStandardProviderWiring(String serviceType, long mediato .andReturn(Collections.singletonList(serviceCapability)).anyTimes(); EasyMock.expect(wiring.getRequiredWires(SpiFlyConstants.EXTENDER_CAPABILITY_NAMESPACE)) .andReturn(Collections.singletonList(extenderWire)).anyTimes(); + EasyMock.expect(wiring.findEntries("META-INF/services", serviceType, 0)) + .andReturn(Collections.singletonList(serviceFile)).anyTimes(); + EasyMock.expect(wiring.findEntries("META-INF", "MANIFEST.MF", 0)) + .andReturn(Collections.emptyList()).anyTimes(); EasyMock.replay(wiring); return wiring; } From 67a4a103b07ff0c8712838813b304a52227c7f56 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sat, 8 Aug 2026 18:03:13 +0200 Subject: [PATCH 22/26] Recover revision-ambiguous provider URLs --- .../ProviderBundleTrackerCustomizer.java | 103 ++++++++++++++++-- .../ProviderBundleTrackerCustomizerTest.java | 90 +++++++++++++++ 2 files changed, 186 insertions(+), 7 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index 36bcf6617f..e0ca023e85 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -30,6 +30,7 @@ import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Dictionary; import java.util.Enumeration; @@ -88,9 +89,9 @@ public ProviderBundleTrackerCustomizer(BaseActivator activator, Bundle spiBundle this.spiBundle = spiBundle; } - @Override - public List addingBundle(final Bundle bundle, BundleEvent event) { - BundleRevision bundleRevision = bundle.adapt(BundleRevision.class); + @Override + public List addingBundle(final Bundle bundle, BundleEvent event) { + BundleRevision bundleRevision = bundle.adapt(BundleRevision.class); if (bundle.equals(spiBundle) || ((bundleRevision != null) && ((bundleRevision.getTypes() & TYPE_FRAGMENT) == TYPE_FRAGMENT))) return null; // don't process the SPI bundle itself @@ -163,7 +164,11 @@ public List addingBundle(final Bundle bundle, BundleEvent e serviceFileURLs = getServiceFileUrls(bundle, discoveryMode == DiscoveryMode.SERVICELOADER_CAPABILITIES ? providedServices : null); - } + if (discoveryMode == DiscoveryMode.SERVICELOADER_CAPABILITIES) { + serviceFileURLs = replaceUnusableExactServiceFiles( + wiring, providedServices, serviceFileURLs); + } + } final List registrations = new ArrayList(); for (ServiceDetails details : collectServiceDetails(bundle, serviceFileURLs, discoveryMode, @@ -196,9 +201,9 @@ && hasRegisterPermission(bundle, details.serviceType)) { details.instanceType, bundle, details.properties == null ? Collections.emptyMap() : details.properties); - log(Level.INFO, "Registered provider " + details.instanceType + " of service " + details.serviceType + " in bundle " + bundle.getSymbolicName()); - } catch (Exception | NoClassDefFoundError e) { - log(Level.FINE, + log(Level.INFO, "Registered provider " + details.instanceType + " of service " + details.serviceType + " in bundle " + bundle.getSymbolicName()); + } catch (Exception | NoClassDefFoundError e) { + log(Level.FINE, "Could not load provider " + details.instanceType + " of service " + details.serviceType, e); } } @@ -278,6 +283,90 @@ else if (registerServiceLoaderServices) { return serviceDetails; } + List replaceUnusableExactServiceFiles(BundleWiring wiring, + List serviceTypes, List exactFiles) { + if (wiring == null || serviceTypes == null || serviceTypes.isEmpty()) { + return exactFiles; + } + ClassLoader classLoader = wiring.getClassLoader(); + if (classLoader == null) { + return exactFiles; + } + + Set result = new LinkedHashSet(exactFiles); + for (String serviceType : new LinkedHashSet(serviceTypes)) { + List typeFiles = serviceFilesForType(result, serviceType); + if (hasLoadableProvider(typeFiles, serviceType, classLoader)) { + continue; + } + + String resourceName = METAINF_SERVICES + "/" + serviceType; + Collection localResources = wiring.listResources( + METAINF_SERVICES, serviceType, + BundleWiring.LISTRESOURCES_LOCAL); + if (localResources == null || !localResources.contains(resourceName)) { + continue; + } + + List replacements = new ArrayList(); + try { + Enumeration resources = classLoader.getResources(resourceName); + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + if (hasLoadableProvider(Collections.singletonList(resource), + serviceType, classLoader)) { + replacements.add(resource); + } + } + } + catch (IOException | RuntimeException e) { + log(Level.FINE, "Could not recover local SPI resource " + + resourceName + " from the effective wiring", e); + } + if (!replacements.isEmpty()) { + // Some framework URL handlers do not retain a revision in the URL + // returned by findEntries. Only replace an unusable exact result + // after the wiring confirms that this resource name is local. + result.removeAll(typeFiles); + result.addAll(replacements); + } + } + return new ArrayList(result); + } + + private List serviceFilesForType( + Collection serviceFiles, String serviceType) { + List result = new ArrayList(); + for (URL serviceFile : serviceFiles) { + String path = serviceFile.getPath(); + int separator = path.lastIndexOf('/'); + if (serviceType.equals(separator < 0 + ? path : path.substring(separator + 1))) { + result.add(serviceFile); + } + } + return result; + } + + private boolean hasLoadableProvider(List serviceFiles, + String serviceType, ClassLoader classLoader) { + List providers = readServiceProviderFiles(serviceFiles) + .get(serviceType); + if (providers == null) { + return false; + } + for (String provider : providers) { + try { + classLoader.loadClass(provider); + return true; + } + catch (ClassNotFoundException | LinkageError e) { + // Try the next provider configuration before using the fallback. + } + } + return false; + } + Map> readServiceProviderFiles(List serviceFileURLs) { Map> providers = new LinkedHashMap>(); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index 26d8827af3..7c357aed46 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -42,6 +42,7 @@ import java.util.jar.Attributes; import java.util.jar.Manifest; +import org.apache.aries.mytest.MySPI; import org.easymock.EasyMock; import org.junit.Rule; import org.junit.Test; @@ -255,6 +256,95 @@ public void testStandardDiscoveryExcludesForeignOnlyServiceFile() host, Collections.singletonList(serviceType))); } + @Test + public void testStandardDiscoveryRecoversRevisionAmbiguousResourceUrl() + throws Exception { + final String serviceType = MySPI.class.getName(); + final String resourceName = "META-INF/services/" + serviceType; + URL staleJar = getClass().getResource("/embedded.jar"); + URL currentJar = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", staleJar); + assertNotNull("precondition", currentJar); + URL staleServiceFile = new URL( + "jar:" + staleJar + "!/" + resourceName); + URL currentServiceFile = new URL( + "jar:" + currentJar + "!/" + resourceName); + + ClassLoader apiLoader = new ClassLoader(null) { + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (MySPI.class.getName().equals(name)) { + return MySPI.class; + } + return super.loadClass(name, resolve); + } + }; + URLClassLoader currentWiringLoader = new URLClassLoader( + new URL[] {currentJar}, apiLoader); + try { + BundleWiring wiring = EasyMock.createMock(BundleWiring.class); + EasyMock.expect(wiring.getClassLoader()) + .andReturn(currentWiringLoader).anyTimes(); + EasyMock.expect(wiring.listResources( + "META-INF/services", serviceType, + BundleWiring.LISTRESOURCES_LOCAL)) + .andReturn(Collections.singleton(resourceName)).anyTimes(); + EasyMock.replay(wiring); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals(Collections.singletonList(currentServiceFile), + customizer.replaceUnusableExactServiceFiles( + wiring, Collections.singletonList(serviceType), + Collections.singletonList(staleServiceFile))); + } + finally { + currentWiringLoader.close(); + } + } + + @Test + public void testStandardDiscoveryKeepsUsableExactResourceWithoutFallback() + throws Exception { + final String serviceType = MySPI.class.getName(); + final String resourceName = "META-INF/services/" + serviceType; + URL currentJar = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", currentJar); + URL currentServiceFile = new URL( + "jar:" + currentJar + "!/" + resourceName); + + ClassLoader apiLoader = new ClassLoader(null) { + @Override + protected Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (MySPI.class.getName().equals(name)) { + return MySPI.class; + } + return super.loadClass(name, resolve); + } + }; + URLClassLoader currentWiringLoader = new URLClassLoader( + new URL[] {currentJar}, apiLoader); + try { + BundleWiring wiring = EasyMock.createMock(BundleWiring.class); + EasyMock.expect(wiring.getClassLoader()) + .andReturn(currentWiringLoader); + EasyMock.replay(wiring); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals(Collections.singletonList(currentServiceFile), + customizer.replaceUnusableExactServiceFiles( + wiring, Collections.singletonList(serviceType), + Collections.singletonList(currentServiceFile))); + EasyMock.verify(wiring); + } + finally { + currentWiringLoader.close(); + } + } + private BundleWiring mockProviderWiring( List manifests, String serviceType) { return mockProviderWiring( From 16acb1daeb95d073a95e7ca7581637bae738240d Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sat, 8 Aug 2026 18:39:09 +0200 Subject: [PATCH 23/26] Keep provider advertisements revision-local --- .../ProviderBundleTrackerCustomizer.java | 88 ----------- .../ProviderBundleTrackerCustomizerTest.java | 143 ++++++++---------- 2 files changed, 61 insertions(+), 170 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index e0ca023e85..a4e89097fb 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -164,10 +164,6 @@ public List addingBundle(final Bundle bundle, BundleEvent e serviceFileURLs = getServiceFileUrls(bundle, discoveryMode == DiscoveryMode.SERVICELOADER_CAPABILITIES ? providedServices : null); - if (discoveryMode == DiscoveryMode.SERVICELOADER_CAPABILITIES) { - serviceFileURLs = replaceUnusableExactServiceFiles( - wiring, providedServices, serviceFileURLs); - } } final List registrations = new ArrayList(); @@ -283,90 +279,6 @@ else if (registerServiceLoaderServices) { return serviceDetails; } - List replaceUnusableExactServiceFiles(BundleWiring wiring, - List serviceTypes, List exactFiles) { - if (wiring == null || serviceTypes == null || serviceTypes.isEmpty()) { - return exactFiles; - } - ClassLoader classLoader = wiring.getClassLoader(); - if (classLoader == null) { - return exactFiles; - } - - Set result = new LinkedHashSet(exactFiles); - for (String serviceType : new LinkedHashSet(serviceTypes)) { - List typeFiles = serviceFilesForType(result, serviceType); - if (hasLoadableProvider(typeFiles, serviceType, classLoader)) { - continue; - } - - String resourceName = METAINF_SERVICES + "/" + serviceType; - Collection localResources = wiring.listResources( - METAINF_SERVICES, serviceType, - BundleWiring.LISTRESOURCES_LOCAL); - if (localResources == null || !localResources.contains(resourceName)) { - continue; - } - - List replacements = new ArrayList(); - try { - Enumeration resources = classLoader.getResources(resourceName); - while (resources.hasMoreElements()) { - URL resource = resources.nextElement(); - if (hasLoadableProvider(Collections.singletonList(resource), - serviceType, classLoader)) { - replacements.add(resource); - } - } - } - catch (IOException | RuntimeException e) { - log(Level.FINE, "Could not recover local SPI resource " - + resourceName + " from the effective wiring", e); - } - if (!replacements.isEmpty()) { - // Some framework URL handlers do not retain a revision in the URL - // returned by findEntries. Only replace an unusable exact result - // after the wiring confirms that this resource name is local. - result.removeAll(typeFiles); - result.addAll(replacements); - } - } - return new ArrayList(result); - } - - private List serviceFilesForType( - Collection serviceFiles, String serviceType) { - List result = new ArrayList(); - for (URL serviceFile : serviceFiles) { - String path = serviceFile.getPath(); - int separator = path.lastIndexOf('/'); - if (serviceType.equals(separator < 0 - ? path : path.substring(separator + 1))) { - result.add(serviceFile); - } - } - return result; - } - - private boolean hasLoadableProvider(List serviceFiles, - String serviceType, ClassLoader classLoader) { - List providers = readServiceProviderFiles(serviceFiles) - .get(serviceType); - if (providers == null) { - return false; - } - for (String provider : providers) { - try { - classLoader.loadClass(provider); - return true; - } - catch (ClassNotFoundException | LinkageError e) { - // Try the next provider configuration before using the fallback. - } - } - return false; - } - Map> readServiceProviderFiles(List serviceFileURLs) { Map> providers = new LinkedHashMap>(); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index 7c357aed46..2c72ebaf66 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -42,7 +42,6 @@ import java.util.jar.Attributes; import java.util.jar.Manifest; -import org.apache.aries.mytest.MySPI; import org.easymock.EasyMock; import org.junit.Rule; import org.junit.Test; @@ -257,91 +256,54 @@ public void testStandardDiscoveryExcludesForeignOnlyServiceFile() } @Test - public void testStandardDiscoveryRecoversRevisionAmbiguousResourceUrl() + public void testStandardDiscoveryDoesNotReplaceExactResourceFromDependency() throws Exception { - final String serviceType = MySPI.class.getName(); - final String resourceName = "META-INF/services/" + serviceType; - URL staleJar = getClass().getResource("/embedded.jar"); - URL currentJar = getClass().getResource("/embedded2.jar"); - assertNotNull("precondition", staleJar); - assertNotNull("precondition", currentJar); - URL staleServiceFile = new URL( - "jar:" + staleJar + "!/" + resourceName); - URL currentServiceFile = new URL( - "jar:" + currentJar + "!/" + resourceName); - - ClassLoader apiLoader = new ClassLoader(null) { - @Override - protected Class loadClass(String name, boolean resolve) - throws ClassNotFoundException { - if (MySPI.class.getName().equals(name)) { - return MySPI.class; - } - return super.loadClass(name, resolve); - } - }; - URLClassLoader currentWiringLoader = new URLClassLoader( - new URL[] {currentJar}, apiLoader); - try { - BundleWiring wiring = EasyMock.createMock(BundleWiring.class); - EasyMock.expect(wiring.getClassLoader()) - .andReturn(currentWiringLoader).anyTimes(); - EasyMock.expect(wiring.listResources( - "META-INF/services", serviceType, - BundleWiring.LISTRESOURCES_LOCAL)) - .andReturn(Collections.singleton(resourceName)).anyTimes(); - EasyMock.replay(wiring); - - ProviderBundleTrackerCustomizer customizer = - new ProviderBundleTrackerCustomizer(activator, null); - assertEquals(Collections.singletonList(currentServiceFile), - customizer.replaceUnusableExactServiceFiles( - wiring, Collections.singletonList(serviceType), - Collections.singletonList(staleServiceFile))); - } - finally { - currentWiringLoader.close(); - } - } - - @Test - public void testStandardDiscoveryKeepsUsableExactResourceWithoutFallback() - throws Exception { - final String serviceType = MySPI.class.getName(); + final String serviceType = "org.apache.aries.mytest.MySPI"; final String resourceName = "META-INF/services/" + serviceType; - URL currentJar = getClass().getResource("/embedded2.jar"); - assertNotNull("precondition", currentJar); - URL currentServiceFile = new URL( - "jar:" + currentJar + "!/" + resourceName); - - ClassLoader apiLoader = new ClassLoader(null) { - @Override - protected Class loadClass(String name, boolean resolve) - throws ClassNotFoundException { - if (MySPI.class.getName().equals(name)) { - return MySPI.class; - } - return super.loadClass(name, resolve); - } - }; - URLClassLoader currentWiringLoader = new URLClassLoader( - new URL[] {currentJar}, apiLoader); - try { - BundleWiring wiring = EasyMock.createMock(BundleWiring.class); - EasyMock.expect(wiring.getClassLoader()) - .andReturn(currentWiringLoader); - EasyMock.replay(wiring); + Path localRoot = temporaryFolder.newFolder("local-provider").toPath(); + Path localService = Files.createDirectories( + localRoot.resolve("META-INF/services")).resolve(serviceType); + Files.write(localService, Collections.singletonList("p.Missing")); + URL exactLocal = localService.toUri().toURL(); + + URL foreignJar = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", foreignJar); + try (URLClassLoader foreignLoader = new URLClassLoader( + new URL[] {foreignJar}, getClass().getClassLoader())) { + BundleWiring wiring = mockStandardProviderWiring( + serviceType, 25L, exactLocal, foreignLoader, resourceName); + + BundleContext providerContext = EasyMock.createMock(BundleContext.class); + EasyMock.replay(providerContext); + + Bundle provider = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(provider.adapt(BundleWiring.class)) + .andReturn(wiring).anyTimes(); + EasyMock.expect(provider.getHeaders()) + .andReturn(new Hashtable()).anyTimes(); + EasyMock.expect(provider.getBundleContext()) + .andReturn(providerContext).anyTimes(); + EasyMock.expect(provider.hasPermission( + EasyMock.isA(ServicePermission.class))) + .andReturn(true).anyTimes(); + EasyMock.expect(provider.loadClass("p.Missing")) + .andThrow(new ClassNotFoundException("p.Missing")); + EasyMock.expect(provider.loadClass( + "org.apache.aries.spifly.impl3.MySPIImpl3")) + .andReturn(foreignLoader.loadClass( + "org.apache.aries.spifly.impl3.MySPIImpl3")) + .anyTimes(); + EasyMock.replay(provider); + + Bundle mediator = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(mediator.getBundleId()).andReturn(25L).anyTimes(); + EasyMock.replay(mediator); ProviderBundleTrackerCustomizer customizer = - new ProviderBundleTrackerCustomizer(activator, null); - assertEquals(Collections.singletonList(currentServiceFile), - customizer.replaceUnusableExactServiceFiles( - wiring, Collections.singletonList(serviceType), - Collections.singletonList(currentServiceFile))); - EasyMock.verify(wiring); - } - finally { - currentWiringLoader.close(); + new ProviderBundleTrackerCustomizer(activator, mediator); + assertEquals("Dependency-visible metadata must not be registered", + Collections.emptyList(), customizer.addingBundle(provider, null)); + EasyMock.verify(providerContext); } } @@ -540,6 +502,13 @@ private Bundle mockMultiSPIBundle(BundleContext implBC) throws ClassNotFoundExce private BundleWiring mockStandardProviderWiring( String serviceType, long mediatorBundleId, URL serviceFile) { + return mockStandardProviderWiring( + serviceType, mediatorBundleId, serviceFile, null, null); + } + + private BundleWiring mockStandardProviderWiring( + String serviceType, long mediatorBundleId, URL serviceFile, + ClassLoader classLoader, String localResource) { Map serviceAttributes = new HashMap(); serviceAttributes.put(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE, serviceType); BundleCapability serviceCapability = EasyMock.createNiceMock(BundleCapability.class); @@ -576,6 +545,16 @@ private BundleWiring mockStandardProviderWiring( .andReturn(Collections.singletonList(serviceFile)).anyTimes(); EasyMock.expect(wiring.findEntries("META-INF", "MANIFEST.MF", 0)) .andReturn(Collections.emptyList()).anyTimes(); + if (classLoader != null) { + EasyMock.expect(wiring.getClassLoader()) + .andReturn(classLoader).anyTimes(); + } + if (localResource != null) { + EasyMock.expect(wiring.listResources( + "META-INF/services", serviceType, + BundleWiring.LISTRESOURCES_LOCAL)) + .andReturn(Collections.singleton(localResource)).anyTimes(); + } EasyMock.replay(wiring); return wiring; } From e1c15788bad987f67fa266b169da0deeec8cfdc4 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sat, 8 Aug 2026 18:53:48 +0200 Subject: [PATCH 24/26] Reconstruct the effective provider bundle class path --- .../ProviderBundleTrackerCustomizer.java | 212 +++++++++++++--- .../ProviderBundleTrackerCustomizerTest.java | 227 +++++++++++++++++- .../dynamic/LateMediatorStartupTest.java | 75 +++++- 3 files changed, 475 insertions(+), 39 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index a4e89097fb..1a6e6433b1 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -19,8 +19,9 @@ package org.apache.aries.spifly; import static java.util.stream.Collectors.toList; -import static org.osgi.framework.wiring.BundleRevision.TYPE_FRAGMENT; - +import static org.osgi.framework.wiring.BundleRevision.TYPE_FRAGMENT; + +import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -413,6 +414,10 @@ private Map> getBundleRootServiceFiles(Bundle bundle, private List getServiceFileUrls(Bundle bundle, BundleWiring wiring, String serviceType, List compatibilityEntries) { Set urls = new LinkedHashSet(); + if (addExactBundleClassPathServiceFiles(wiring, serviceType, urls)) { + return new ArrayList(urls); + } + List rootEntries = wiring.findEntries( METAINF_SERVICES, serviceType, 0); if (rootEntries != null) { @@ -421,10 +426,8 @@ private List getServiceFileUrls(Bundle bundle, BundleWiring wiring, if (urls.isEmpty() && compatibilityEntries != null) { urls.addAll(compatibilityEntries); } - if (!addExactBundleClassPathServiceFiles(wiring, serviceType, urls)) { - addBundleClassPathServiceFiles(bundle, - Collections.singleton(serviceType), urls); - } + addBundleClassPathServiceFiles(bundle, + Collections.singleton(serviceType), urls); return new ArrayList(urls); } @@ -434,51 +437,173 @@ private boolean addExactBundleClassPathServiceFiles(BundleWiring wiring, if (manifests == null || manifests.isEmpty()) { return false; } + + List containers = + new ArrayList(); for (URL manifestUrl : manifests) { try (InputStream stream = manifestUrl.openStream()) { String bundleClassPath = new Manifest(stream).getMainAttributes() .getValue(Constants.BUNDLE_CLASSPATH); - if (bundleClassPath == null) { - continue; - } - Parameters entries = new Parameters(bundleClassPath); - for (String key : entries.keySet()) { - String entry = ConsumerHeaderProcessor.removeDuplicateMarker(key).trim(); - if (!".".equals(entry)) { - addExactBundleClassPathEntry(manifestUrl, entry, - serviceType, serviceFileURLs); - } - } + containers.add(new ExactBundleContainer( + manifestUrl, parseBundleClassPath(bundleClassPath))); } catch (IOException | RuntimeException e) { log(Level.FINE, "Could not read exact bundle manifest " + manifestUrl, e); + containers.add(new ExactBundleContainer( + manifestUrl, Collections.emptyList())); + } + } + + ExactBundleContainer host = containers.get(0); + for (String entry : host.bundleClassPath) { + if (isRootClassPathEntry(entry)) { + addExactRootServiceFile( + host, serviceType, serviceFileURLs); + continue; + } + for (ExactBundleContainer candidate : containers) { + ExactClassPathEntry match = findExactBundleClassPathEntry( + wiring, candidate, entry, serviceType); + if (match.exists) { + serviceFileURLs.addAll(match.serviceFiles); + break; + } + } + } + + for (int i = 1; i < containers.size(); i++) { + ExactBundleContainer fragment = containers.get(i); + for (String entry : fragment.bundleClassPath) { + if (isRootClassPathEntry(entry)) { + addExactRootServiceFile( + fragment, serviceType, serviceFileURLs); + continue; + } + ExactClassPathEntry match = findExactBundleClassPathEntry( + wiring, fragment, entry, serviceType); + if (match.exists) { + serviceFileURLs.addAll(match.serviceFiles); + } } } return true; } - private void addExactBundleClassPathEntry(URL manifestUrl, String entry, + private List parseBundleClassPath(String bundleClassPath) { + if (bundleClassPath == null) { + return Collections.singletonList("."); + } + + List result = new ArrayList(); + Parameters entries = new Parameters(bundleClassPath); + for (String key : entries.keySet()) { + result.add(ConsumerHeaderProcessor.removeDuplicateMarker(key).trim()); + } + return result; + } + + private boolean isRootClassPathEntry(String entry) { + return ".".equals(entry) || "/".equals(entry); + } + + private void addExactRootServiceFile(ExactBundleContainer container, String serviceType, Set serviceFileURLs) { try { - URL entryUrl = new URL(manifestUrl, "../" + entry); - List embedded = getMetaInfServiceURLsFromJar( - entryUrl, Collections.singleton(serviceType)); - if (!embedded.isEmpty()) { - serviceFileURLs.addAll(embedded); - return; + URL resource = exactEntry(container, + METAINF_SERVICES + "/" + serviceType); + if (canOpen(resource)) { + serviceFileURLs.add(resource); + } + } + catch (IOException | RuntimeException e) { + log(Level.FINE, "Could not resolve exact root SPI resource for " + + serviceType + " from " + container.manifest, e); + } + } + + private ExactClassPathEntry findExactBundleClassPathEntry( + BundleWiring wiring, ExactBundleContainer container, String entry, + String serviceType) { + try { + URL entryUrl = exactEntry(container, entry); + if (isZip(entryUrl)) { + return new ExactClassPathEntry(true, + getMetaInfServiceURLsFromJar( + entryUrl, Collections.singleton(serviceType))); } String separator = entry.endsWith("/") ? "" : "/"; - URL resource = new URL(manifestUrl, "../" + entry + separator + URL resource = exactEntry(container, entry + separator + METAINF_SERVICES + "/" + serviceType); - try (InputStream stream = resource.openStream()) { - serviceFileURLs.add(resource); + if (canOpen(resource)) { + return new ExactClassPathEntry( + true, Collections.singletonList(resource)); } + + return new ExactClassPathEntry( + exactDirectoryExists(wiring, container, entry), + Collections.emptyList()); } catch (IOException | RuntimeException e) { log(Level.FINE, "Could not read SPI resource for " + serviceType + " from exact Bundle-ClassPath entry " + entry, e); + return ExactClassPathEntry.NOT_FOUND; + } + } + + private boolean exactDirectoryExists(BundleWiring wiring, + ExactBundleContainer container, String entry) throws IOException { + List contents = wiring.findEntries( + trimLeadingSlash(entry), "*", BundleWiring.FINDENTRIES_RECURSE); + if (contents == null) { + return false; + } + for (URL content : contents) { + String path = trimLeadingSlash(content.getPath()); + if (canOpen(exactEntry(container, path))) { + return true; + } + } + return false; + } + + private URL exactEntry(ExactBundleContainer container, String path) + throws IOException { + return new URL(container.manifest, "../" + trimLeadingSlash(path)); + } + + private String trimLeadingSlash(String path) { + int start = 0; + while (start < path.length() && path.charAt(start) == '/') { + start++; + } + return path.substring(start); + } + + private boolean canOpen(URL resource) { + try (InputStream stream = resource.openStream()) { + return true; + } + catch (IOException | RuntimeException e) { + return false; + } + } + + private boolean isZip(URL resource) { + try (InputStream raw = resource.openStream(); + BufferedInputStream stream = new BufferedInputStream(raw)) { + int first = stream.read(); + int second = stream.read(); + int third = stream.read(); + int fourth = stream.read(); + return first == 0x50 && second == 0x4b + && ((third == 0x03 && fourth == 0x04) + || (third == 0x05 && fourth == 0x06) + || (third == 0x07 && fourth == 0x08)); + } + catch (IOException | RuntimeException e) { + return false; } } @@ -680,11 +805,34 @@ private void log(Level level, String message) { activator.log(level, message); } - private void log(Level level, String message, Throwable th) { - activator.log(level, message, th); - } - - enum DiscoveryMode { + private void log(Level level, String message, Throwable th) { + activator.log(level, message, th); + } + + private static final class ExactBundleContainer { + private final URL manifest; + private final List bundleClassPath; + + private ExactBundleContainer(URL manifest, List bundleClassPath) { + this.manifest = manifest; + this.bundleClassPath = bundleClassPath; + } + } + + private static final class ExactClassPathEntry { + private static final ExactClassPathEntry NOT_FOUND = + new ExactClassPathEntry(false, Collections.emptyList()); + + private final boolean exists; + private final List serviceFiles; + + private ExactClassPathEntry(boolean exists, List serviceFiles) { + this.exists = exists; + this.serviceFiles = serviceFiles; + } + } + + enum DiscoveryMode { SPI_PROVIDER_HEADER, AUTO_PROVIDERS_PROPERTY, SERVICELOADER_CAPABILITIES diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index 2c72ebaf66..00e4140971 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -307,6 +307,170 @@ public void testStandardDiscoveryDoesNotReplaceExactResourceFromDependency() } } + @Test + public void testStandardDiscoveryUsesFragmentSuppliedHostClassPathEntry() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + URL embedded = getClass().getResource("/embedded.jar"); + assertNotNull("precondition", embedded); + + URL hostManifest = createRevisionContent( + "host-missing-entry", "embedded.jar", + Collections.emptyMap()); + Map fragmentEntries = new HashMap(); + fragmentEntries.put("embedded.jar", readBytes(embedded)); + URL fragmentManifest = createRevisionContent( + "fragment-supplied-entry", null, fragmentEntries); + + Bundle host = mockProviderBundle(Arrays.asList( + hostManifest, fragmentManifest), serviceType); + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + + assertEquals(Collections.singletonList( + embeddedServiceFile(fragmentManifest, serviceType)), + customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + } + + @Test + public void testStandardDiscoveryUsesFirstHostClassPathEntryMatch() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + URL embedded1 = getClass().getResource("/embedded.jar"); + URL embedded2 = getClass().getResource("/embedded2.jar"); + assertNotNull("precondition", embedded1); + assertNotNull("precondition", embedded2); + + Map hostEntries = new HashMap(); + hostEntries.put("embedded.jar", readBytes(embedded1)); + URL hostManifest = createRevisionContent( + "host-first-entry", "embedded.jar", hostEntries); + Map fragmentEntries = new HashMap(); + fragmentEntries.put("embedded.jar", readBytes(embedded2)); + URL fragmentManifest = createRevisionContent( + "fragment-shadowed-entry", null, fragmentEntries); + + Bundle host = mockProviderBundle(Arrays.asList( + hostManifest, fragmentManifest), serviceType); + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals("The host entry must shadow the fragment entry", + Collections.singletonList( + embeddedServiceFile(hostManifest, serviceType)), + customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + + URL missingHost = createRevisionContent( + "host-first-fragment", "embedded.jar", + Collections.emptyMap()); + URL firstFragment = createRevisionContent( + "first-fragment-entry", null, hostEntries); + URL secondFragment = createRevisionContent( + "second-fragment-entry", null, fragmentEntries); + Bundle fragmentedHost = mockProviderBundle(Arrays.asList( + missingHost, firstFragment, secondFragment), serviceType); + assertEquals("The first attached fragment entry must shadow later fragments", + Collections.singletonList( + embeddedServiceFile(firstFragment, serviceType)), + customizer.getServiceFileUrls( + fragmentedHost, Collections.singletonList(serviceType))); + } + + @Test + public void testStandardDiscoveryKeepsFirstMatchingDirectory() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + Map hostEntries = new HashMap(); + hostEntries.put("classes/host.txt", new byte[] {1}); + URL hostManifest = createRevisionContent( + "host-directory", "classes", hostEntries); + Map fragmentEntries = new HashMap(); + fragmentEntries.put("classes/META-INF/services/" + serviceType, + "org.apache.aries.spifly.impl3.MySPIImpl3\n".getBytes("UTF-8")); + URL fragmentManifest = createRevisionContent( + "fragment-directory", null, fragmentEntries); + + Map> directoryContents = + new HashMap>(); + directoryContents.put("classes", Arrays.asList( + new URL("file:/classes/host.txt"), + new URL("file:/classes/META-INF/services/" + serviceType))); + BundleWiring wiring = mockProviderWiring( + new AtomicReference>(Arrays.asList( + hostManifest, fragmentManifest)), + serviceType, directoryContents); + Bundle host = mockProviderBundle(wiring); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals("An existing host directory must shadow a later fragment directory", + Collections.emptyList(), customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + } + + @Test + public void testStandardDiscoveryHonorsRevisionRootClassPathEntries() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + Map hostEntries = providerRootEntry( + serviceType, "org.apache.aries.spifly.impl2.MySPIImpl2a"); + Map fragmentEntries = providerRootEntry( + serviceType, "org.apache.aries.spifly.impl3.MySPIImpl3"); + + URL excludedHost = createRevisionContent( + "excluded-host-root", "missing.jar", hostEntries); + URL excludedFragment = createRevisionContent( + "excluded-fragment-root", "missing.jar", fragmentEntries); + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals("Roots omitted from Bundle-ClassPath must not be advertised", + Collections.emptyList(), customizer.getServiceFileUrls( + mockProviderBundle(Arrays.asList( + excludedHost, excludedFragment), serviceType), + Collections.singletonList(serviceType))); + + URL defaultHost = createRevisionContent( + "default-host-root", null, hostEntries); + URL defaultFragment = createRevisionContent( + "default-fragment-root", null, fragmentEntries); + assertEquals("A missing Bundle-ClassPath must default to each revision root", + Arrays.asList( + new URL(defaultHost, "../META-INF/services/" + serviceType), + new URL(defaultFragment, "../META-INF/services/" + serviceType)), + customizer.getServiceFileUrls( + mockProviderBundle(Arrays.asList( + defaultHost, defaultFragment), serviceType), + Collections.singletonList(serviceType))); + } + + @Test + public void testFragmentClassPathEntryStaysFragmentLocal() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + URL embedded = getClass().getResource("/embedded.jar"); + assertNotNull("precondition", embedded); + + URL hostManifest = createRevisionContent( + "fragment-local-host", ".", + Collections.emptyMap()); + URL firstFragment = createRevisionContent( + "fragment-local-missing", "embedded.jar", + Collections.emptyMap()); + Map secondEntries = new HashMap(); + secondEntries.put("embedded.jar", readBytes(embedded)); + URL secondFragment = createRevisionContent( + "fragment-local-later", null, secondEntries); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals("A fragment entry must not search another fragment container", + Collections.emptyList(), customizer.getServiceFileUrls( + mockProviderBundle(Arrays.asList( + hostManifest, firstFragment, secondFragment), serviceType), + Collections.singletonList(serviceType))); + } + private BundleWiring mockProviderWiring( List manifests, String serviceType) { return mockProviderWiring( @@ -315,35 +479,86 @@ private BundleWiring mockProviderWiring( private BundleWiring mockProviderWiring( AtomicReference> manifests, String serviceType) { - BundleWiring wiring = EasyMock.createMock(BundleWiring.class); + return mockProviderWiring(manifests, serviceType, + Collections.>emptyMap()); + } + + private BundleWiring mockProviderWiring( + AtomicReference> manifests, String serviceType, + Map> directoryContents) { + BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); EasyMock.expect(wiring.findEntries("META-INF/services", serviceType, 0)) .andReturn(Collections.emptyList()).anyTimes(); EasyMock.expect(wiring.findEntries("META-INF", "MANIFEST.MF", 0)) .andAnswer(() -> manifests.get()).anyTimes(); + for (Map.Entry> entry : directoryContents.entrySet()) { + EasyMock.expect(wiring.findEntries( + entry.getKey(), "*", BundleWiring.FINDENTRIES_RECURSE)) + .andReturn(entry.getValue()).anyTimes(); + } EasyMock.replay(wiring); return wiring; } + private Bundle mockProviderBundle(List manifests, String serviceType) { + return mockProviderBundle(mockProviderWiring(manifests, serviceType)); + } + + private Bundle mockProviderBundle(BundleWiring wiring) { + Bundle host = EasyMock.createNiceMock(Bundle.class); + EasyMock.expect(host.adapt(BundleWiring.class)).andReturn(wiring).anyTimes(); + EasyMock.replay(host); + return host; + } + private URL createRevisionContent(String name, URL embedded) throws Exception { + Map entries = new HashMap(); + entries.put("embedded.jar", readBytes(embedded)); + return createRevisionContent(name, "embedded.jar", entries); + } + + private URL createRevisionContent(String name, String bundleClassPath, + Map entries) throws Exception { Path root = temporaryFolder.newFolder(name).toPath(); Path metaInf = Files.createDirectories(root.resolve("META-INF")); Manifest manifest = new Manifest(); manifest.getMainAttributes().put( Attributes.Name.MANIFEST_VERSION, "1.0"); - manifest.getMainAttributes().putValue( - Constants.BUNDLE_CLASSPATH, "embedded.jar"); + if (bundleClassPath != null) { + manifest.getMainAttributes().putValue( + Constants.BUNDLE_CLASSPATH, bundleClassPath); + } try (OutputStream output = Files.newOutputStream( metaInf.resolve("MANIFEST.MF"))) { manifest.write(output); } - try (InputStream input = embedded.openStream()) { - Files.copy(input, root.resolve("embedded.jar"), - StandardCopyOption.REPLACE_EXISTING); + for (Map.Entry entry : entries.entrySet()) { + Path target = root.resolve(entry.getKey()); + if (target.getParent() != null) { + Files.createDirectories(target.getParent()); + } + Files.write(target, entry.getValue()); } return metaInf.resolve("MANIFEST.MF").toUri().toURL(); } + private byte[] readBytes(URL resource) throws Exception { + try (InputStream input = resource.openStream()) { + Path copy = temporaryFolder.newFile().toPath(); + Files.copy(input, copy, StandardCopyOption.REPLACE_EXISTING); + return Files.readAllBytes(copy); + } + } + + private Map providerRootEntry( + String serviceType, String provider) throws Exception { + Map entries = new HashMap(); + entries.put("META-INF/services/" + serviceType, + (provider + "\n").getBytes("UTF-8")); + return entries; + } + private URL embeddedServiceFile(URL manifest, String serviceType) throws Exception { URL embedded = new URL(manifest, "../embedded.jar"); diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java index 0608c4d2fc..aad2413381 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java @@ -345,6 +345,63 @@ public void providerDiscoveryUsesAttachedFragmentRevisionUntilRefresh() } } + @Test + public void providerDiscoveryUsesHostClassPathEntryFromFragment() + throws Exception { + Path storage = Files.createTempDirectory("spifly-fragment-host-entry-"); + Framework framework = null; + try { + Map configuration = new HashMap(); + configuration.put(Constants.FRAMEWORK_STORAGE, + storage.resolve("framework").toString()); + configuration.put(Constants.FRAMEWORK_STORAGE_CLEAN, + Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT); + framework = newFramework(configuration); + framework.start(); + + BundleContext context = framework.getBundleContext(); + installDependency(context, ClassReader.class); + installDependency(context, AdviceAdapter.class); + installDependency(context, ClassNode.class); + installDependency(context, Analyzer.class); + installDependency(context, CheckClassAdapter.class); + installDependency(context, BundleTracker.class); + + Bundle mediator = context.installBundle( + bundleFromClasses(storage, "spifly-dynamic.jar").toUri().toString()); + Bundle api = context.installBundle(createApiBundle(storage).toUri().toString()); + Bundle fragment = context.installBundle(createProviderFragment( + storage.resolve("host-entry-fragment.jar"), + MySPIImpl1.class, "1.0.0", false).toUri().toString()); + Bundle provider = context.installBundle( + createProviderHost(storage, true).toUri().toString()); + Bundle consumer = context.installBundle( + createConsumerBundle(storage).toUri().toString()); + + FrameworkWiring frameworkWiring = framework.adapt(FrameworkWiring.class); + assertTrue(frameworkWiring.resolveBundles(java.util.Arrays.asList( + mediator, api, fragment, provider, consumer))); + mediator.start(); + provider.start(); + consumer.start(); + + assertEquals("The mediated ServiceLoader must see the fragment entry", + Collections.singleton("olleh"), + invokeConsumer(consumer.loadClass(TestClient.class.getName()))); + org.osgi.framework.ServiceReference[] registrations = + context.getAllServiceReferences(SERVICE_TYPE, null); + assertEquals("The registrar must publish the fragment provider", + 1, registrations == null ? 0 : registrations.length); + } + finally { + if (framework != null) { + framework.stop(); + framework.waitForStop(30000); + } + deleteRecursively(storage); + } + } + private Framework newFramework(Map configuration) throws Exception { String factoryName = System.getProperty( "spifly.test.frameworkFactory", @@ -411,21 +468,37 @@ private Path createProviderBundle(Path directory) throws IOException { } private Path createProviderHost(Path directory) throws IOException { + return createProviderHost(directory, false); + } + + private Path createProviderHost(Path directory, + boolean fragmentSuppliesClassPathEntry) throws IOException { Manifest manifest = bundleManifest("spifly.test.fragment.provider"); manifest.getMainAttributes().putValue( Constants.IMPORT_PACKAGE, "org.apache.aries.mytest;version=\"[1,2)\""); + if (fragmentSuppliesClassPathEntry) { + manifest.getMainAttributes().putValue( + Constants.BUNDLE_CLASSPATH, "embedded.jar"); + } return writeBundle(directory.resolve("provider-host.jar"), manifest, Collections.emptyMap()); } private Path createProviderFragment(Path path, Class implementation, String version) throws IOException { + return createProviderFragment(path, implementation, version, true); + } + + private Path createProviderFragment(Path path, Class implementation, + String version, boolean declaresClassPath) throws IOException { Manifest manifest = bundleManifest("spifly.test.fragment"); Attributes attributes = manifest.getMainAttributes(); attributes.putValue(Constants.BUNDLE_VERSION, version); attributes.putValue(Constants.FRAGMENT_HOST, "spifly.test.fragment.provider;bundle-version=\"[1,2)\""); - attributes.putValue(Constants.BUNDLE_CLASSPATH, ".,embedded.jar"); + if (declaresClassPath) { + attributes.putValue(Constants.BUNDLE_CLASSPATH, ".,embedded.jar"); + } attributes.putValue(Constants.REQUIRE_CAPABILITY, "osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.registrar)\""); attributes.putValue(Constants.PROVIDE_CAPABILITY, From 920d233024a786c97f504a39e22cf5b384be6105 Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sun, 9 Aug 2026 19:11:55 +0200 Subject: [PATCH 25/26] Honor empty Bundle-ClassPath directory shadowing --- .../ProviderBundleTrackerCustomizer.java | 76 ++++++++++--- .../ProviderBundleTrackerCustomizerTest.java | 103 ++++++++++++++++- .../dynamic/LateMediatorStartupTest.java | 104 ++++++++++++++++++ 3 files changed, 268 insertions(+), 15 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index 1a6e6433b1..d973eb7d4a 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -533,17 +533,12 @@ private ExactClassPathEntry findExactBundleClassPathEntry( entryUrl, Collections.singleton(serviceType))); } - String separator = entry.endsWith("/") ? "" : "/"; - URL resource = exactEntry(container, entry + separator - + METAINF_SERVICES + "/" + serviceType); - if (canOpen(resource)) { - return new ExactClassPathEntry( - true, Collections.singletonList(resource)); + if (exactDirectoryExists(wiring, container, entry)) { + return new ExactClassPathEntry(true, + exactDirectoryServiceFiles( + wiring, container, entry, serviceType)); } - - return new ExactClassPathEntry( - exactDirectoryExists(wiring, container, entry), - Collections.emptyList()); + return ExactClassPathEntry.NOT_FOUND; } catch (IOException | RuntimeException e) { log(Level.FINE, "Could not read SPI resource for " + serviceType @@ -554,20 +549,75 @@ private ExactClassPathEntry findExactBundleClassPathEntry( private boolean exactDirectoryExists(BundleWiring wiring, ExactBundleContainer container, String entry) throws IOException { + String path = normalizedDirectoryPath(entry); + if (path.isEmpty()) { + return false; + } + + int separator = path.lastIndexOf('/'); + String parent = separator < 0 ? "/" : path.substring(0, separator); + String name = path.substring(separator + 1); + List directories = wiring.findEntries(parent, name, 0); + if (directories != null) { + URL expected = exactEntry(container, path + "/"); + for (URL directory : directories) { + if (directory.getPath().endsWith("/") + && directory.sameFile(expected)) { + return true; + } + } + } + List contents = wiring.findEntries( - trimLeadingSlash(entry), "*", BundleWiring.FINDENTRIES_RECURSE); + path, "*", BundleWiring.FINDENTRIES_RECURSE); if (contents == null) { return false; } for (URL content : contents) { - String path = trimLeadingSlash(content.getPath()); - if (canOpen(exactEntry(container, path))) { + if (isFromExactContainer(container, content)) { return true; } } return false; } + private List exactDirectoryServiceFiles(BundleWiring wiring, + ExactBundleContainer container, String entry, String serviceType) + throws IOException { + String path = normalizedDirectoryPath(entry) + "/" + + METAINF_SERVICES + "/" + serviceType; + int separator = path.lastIndexOf('/'); + List resources = wiring.findEntries( + path.substring(0, separator), path.substring(separator + 1), 0); + if (resources == null) { + return Collections.emptyList(); + } + + URL expected = exactEntry(container, path); + List exact = new ArrayList(); + for (URL resource : resources) { + if (resource.sameFile(expected) && canOpen(resource)) { + exact.add(resource); + } + } + return exact; + } + + private String normalizedDirectoryPath(String entry) { + String path = trimLeadingSlash(entry); + int end = path.length(); + while (end > 0 && path.charAt(end - 1) == '/') { + end--; + } + return path.substring(0, end); + } + + private boolean isFromExactContainer( + ExactBundleContainer container, URL resource) throws IOException { + return resource.toExternalForm().startsWith( + exactEntry(container, "").toExternalForm()); + } + private URL exactEntry(ExactBundleContainer container, String path) throws IOException { return new URL(container.manifest, "../" + trimLeadingSlash(path)); diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index 00e4140971..512b5cdccc 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -394,8 +394,9 @@ public void testStandardDiscoveryKeepsFirstMatchingDirectory() Map> directoryContents = new HashMap>(); directoryContents.put("classes", Arrays.asList( - new URL("file:/classes/host.txt"), - new URL("file:/classes/META-INF/services/" + serviceType))); + new URL(hostManifest, "../classes/host.txt"), + new URL(fragmentManifest, + "../classes/META-INF/services/" + serviceType))); BundleWiring wiring = mockProviderWiring( new AtomicReference>(Arrays.asList( hostManifest, fragmentManifest)), @@ -409,6 +410,78 @@ public void testStandardDiscoveryKeepsFirstMatchingDirectory() host, Collections.singletonList(serviceType))); } + @Test + public void testStandardDiscoveryKeepsExplicitEmptyHostDirectory() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + URL hostManifest = createRevisionContent( + "empty-host-directory", "classes", + Collections.emptyMap(), + Collections.singletonList("classes")); + Map fragmentEntries = new HashMap(); + fragmentEntries.put("classes/META-INF/services/" + serviceType, + "org.apache.aries.spifly.impl3.MySPIImpl3\n".getBytes("UTF-8")); + URL fragmentManifest = createRevisionContent( + "populated-fragment-directory", null, fragmentEntries); + + Map> directoryContents = + new HashMap>(); + directoryContents.put("classes", Collections.singletonList( + new URL(fragmentManifest, + "../classes/META-INF/services/" + serviceType))); + Map> exactEntries = + new HashMap>(); + exactEntries.put("classes", Arrays.asList( + new URL(hostManifest, "../classes/"), + new URL(fragmentManifest, "../classes/"))); + exactEntries.put("classes/META-INF/services/" + serviceType, + Collections.singletonList(new URL(fragmentManifest, + "../classes/META-INF/services/" + serviceType))); + BundleWiring wiring = mockProviderWiring( + new AtomicReference>(Arrays.asList( + hostManifest, fragmentManifest)), + serviceType, directoryContents, exactEntries); + Bundle host = mockProviderBundle(wiring); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals("An explicit empty host directory must shadow a fragment directory", + Collections.emptyList(), customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + } + + @Test + public void testStandardDiscoveryUsesDirectoryWithoutExplicitEntry() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + Map entries = new HashMap(); + entries.put("classes/META-INF/services/" + serviceType, + "org.apache.aries.spifly.impl3.MySPIImpl3\n".getBytes("UTF-8")); + URL manifest = createRevisionContent( + "omitted-directory-entry", "classes", entries); + URL serviceFile = new URL(manifest, + "../classes/META-INF/services/" + serviceType); + + Map> directoryContents = + new HashMap>(); + directoryContents.put("classes", Collections.singletonList(serviceFile)); + Map> exactEntries = + new HashMap>(); + exactEntries.put("classes/META-INF/services/" + serviceType, + Collections.singletonList(serviceFile)); + BundleWiring wiring = mockProviderWiring( + new AtomicReference>(Collections.singletonList(manifest)), + serviceType, directoryContents, exactEntries); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals("Descendants must establish a directory whose entry is omitted", + Collections.singletonList(serviceFile), + customizer.getServiceFileUrls( + mockProviderBundle(wiring), + Collections.singletonList(serviceType))); + } + @Test public void testStandardDiscoveryHonorsRevisionRootClassPathEntries() throws Exception { @@ -486,6 +559,14 @@ private BundleWiring mockProviderWiring( private BundleWiring mockProviderWiring( AtomicReference> manifests, String serviceType, Map> directoryContents) { + return mockProviderWiring(manifests, serviceType, directoryContents, + Collections.>emptyMap()); + } + + private BundleWiring mockProviderWiring( + AtomicReference> manifests, String serviceType, + Map> directoryContents, + Map> exactEntries) { BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); EasyMock.expect(wiring.findEntries("META-INF/services", serviceType, 0)) .andReturn(Collections.emptyList()).anyTimes(); @@ -496,6 +577,14 @@ private BundleWiring mockProviderWiring( entry.getKey(), "*", BundleWiring.FINDENTRIES_RECURSE)) .andReturn(entry.getValue()).anyTimes(); } + for (Map.Entry> entry : exactEntries.entrySet()) { + String path = entry.getKey(); + int separator = path.lastIndexOf('/'); + String parent = separator < 0 ? "/" : path.substring(0, separator); + String name = path.substring(separator + 1); + EasyMock.expect(wiring.findEntries(parent, name, 0)) + .andReturn(entry.getValue()).anyTimes(); + } EasyMock.replay(wiring); return wiring; } @@ -520,6 +609,13 @@ private URL createRevisionContent(String name, URL embedded) private URL createRevisionContent(String name, String bundleClassPath, Map entries) throws Exception { + return createRevisionContent(name, bundleClassPath, entries, + Collections.emptyList()); + } + + private URL createRevisionContent(String name, String bundleClassPath, + Map entries, List directories) + throws Exception { Path root = temporaryFolder.newFolder(name).toPath(); Path metaInf = Files.createDirectories(root.resolve("META-INF")); Manifest manifest = new Manifest(); @@ -533,6 +629,9 @@ private URL createRevisionContent(String name, String bundleClassPath, metaInf.resolve("MANIFEST.MF"))) { manifest.write(output); } + for (String directory : directories) { + Files.createDirectories(root.resolve(directory)); + } for (Map.Entry entry : entries.entrySet()) { Path target = root.resolve(entry.getKey()); if (target.getParent() != null) { diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java index aad2413381..9a33e6c3a6 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java @@ -20,6 +20,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; @@ -402,6 +403,68 @@ public void providerDiscoveryUsesHostClassPathEntryFromFragment() } } + @Test + public void explicitEmptyHostDirectoryShadowsFragmentProvider() + throws Exception { + Path storage = Files.createTempDirectory("spifly-empty-host-directory-"); + Framework framework = null; + try { + Map configuration = new HashMap(); + configuration.put(Constants.FRAMEWORK_STORAGE, + storage.resolve("framework").toString()); + configuration.put(Constants.FRAMEWORK_STORAGE_CLEAN, + Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT); + framework = newFramework(configuration); + framework.start(); + + BundleContext context = framework.getBundleContext(); + installDependency(context, ClassReader.class); + installDependency(context, AdviceAdapter.class); + installDependency(context, ClassNode.class); + installDependency(context, Analyzer.class); + installDependency(context, CheckClassAdapter.class); + installDependency(context, BundleTracker.class); + + Bundle mediator = context.installBundle( + bundleFromClasses(storage, "spifly-dynamic.jar").toUri().toString()); + Bundle api = context.installBundle(createApiBundle(storage).toUri().toString()); + Bundle fragment = context.installBundle( + createEmptyDirectoryProviderFragment(storage).toUri().toString()); + Bundle provider = context.installBundle( + createEmptyDirectoryProviderHost(storage).toUri().toString()); + Bundle consumer = context.installBundle( + createConsumerBundle(storage).toUri().toString()); + + FrameworkWiring frameworkWiring = framework.adapt(FrameworkWiring.class); + assertTrue(frameworkWiring.resolveBundles(java.util.Arrays.asList( + mediator, api, fragment, provider, consumer))); + assertEquals(IMPLEMENTATION, + provider.loadClass(IMPLEMENTATION).getName()); + assertNull("The framework class path must stop at the empty host directory", + provider.adapt(BundleWiring.class).getClassLoader().getResource( + "META-INF/services/" + SERVICE_TYPE)); + + mediator.start(); + provider.start(); + consumer.start(); + + assertEquals("The mediated ServiceLoader must not see the fragment entry", + Collections.emptySet(), + invokeConsumer(consumer.loadClass(TestClient.class.getName()))); + org.osgi.framework.ServiceReference[] registrations = + context.getAllServiceReferences(SERVICE_TYPE, null); + assertEquals("The registrar must not publish the shadowed fragment provider", + 0, registrations == null ? 0 : registrations.length); + } + finally { + if (framework != null) { + framework.stop(); + framework.waitForStop(30000); + } + deleteRecursively(storage); + } + } + private Framework newFramework(Map configuration) throws Exception { String factoryName = System.getProperty( "spifly.test.frameworkFactory", @@ -484,6 +547,36 @@ private Path createProviderHost(Path directory, Collections.emptyMap()); } + private Path createEmptyDirectoryProviderHost(Path directory) + throws IOException { + Manifest manifest = bundleManifest("spifly.test.empty.directory.provider"); + manifest.getMainAttributes().putValue( + Constants.IMPORT_PACKAGE, "org.apache.aries.mytest;version=\"[1,2)\""); + manifest.getMainAttributes().putValue( + Constants.BUNDLE_CLASSPATH, "classes,."); + Map entries = new LinkedHashMap(); + addClass(entries, MySPIImpl1.class); + return writeBundle(directory.resolve("empty-directory-provider-host.jar"), + manifest, Collections.singletonList("classes/"), entries); + } + + private Path createEmptyDirectoryProviderFragment(Path directory) + throws IOException { + Manifest manifest = bundleManifest("spifly.test.empty.directory.fragment"); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue(Constants.FRAGMENT_HOST, + "spifly.test.empty.directory.provider;bundle-version=\"[1,2)\""); + attributes.putValue(Constants.REQUIRE_CAPABILITY, + "osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.registrar)\""); + attributes.putValue(Constants.PROVIDE_CAPABILITY, + "osgi.serviceloader;osgi.serviceloader=\"" + SERVICE_TYPE + "\""); + Map entries = new LinkedHashMap(); + entries.put("classes/META-INF/services/" + SERVICE_TYPE, + (IMPLEMENTATION + "\n").getBytes(StandardCharsets.UTF_8)); + return writeBundle(directory.resolve("empty-directory-provider-fragment.jar"), + manifest, entries); + } + private Path createProviderFragment(Path path, Class implementation, String version) throws IOException { return createProviderFragment(path, implementation, version, true); @@ -588,9 +681,20 @@ private byte[] readAllBytes(InputStream stream) throws IOException { private Path writeBundle(Path path, Manifest manifest, Map entries) throws IOException { + return writeBundle(path, manifest, Collections.emptyList(), entries); + } + + private Path writeBundle(Path path, Manifest manifest, + List directories, Map entries) + throws IOException { JarOutputStream output = new JarOutputStream(Files.newOutputStream( path, StandardOpenOption.CREATE_NEW), manifest); try { + for (String directory : directories) { + String name = directory.endsWith("/") ? directory : directory + "/"; + output.putNextEntry(new JarEntry(name)); + output.closeEntry(); + } for (Map.Entry entry : entries.entrySet()) { output.putNextEntry(new JarEntry(entry.getKey())); output.write(entry.getValue()); From 4e7692632a9b1a6bd8472c6a3e4363063d089d3b Mon Sep 17 00:00:00 2001 From: Vaclav Haisman Date: Sun, 9 Aug 2026 19:44:17 +0200 Subject: [PATCH 26/26] Honor unusable Bundle-ClassPath first matches --- .../ProviderBundleTrackerCustomizer.java | 67 +++++++++---- .../ProviderBundleTrackerCustomizerTest.java | 85 ++++++++++++++++- .../dynamic/LateMediatorStartupTest.java | 93 +++++++++++++++++++ 3 files changed, 228 insertions(+), 17 deletions(-) diff --git a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java index d973eb7d4a..16858ccaa2 100644 --- a/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java +++ b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizer.java @@ -526,17 +526,23 @@ private ExactClassPathEntry findExactBundleClassPathEntry( BundleWiring wiring, ExactBundleContainer container, String entry, String serviceType) { try { - URL entryUrl = exactEntry(container, entry); - if (isZip(entryUrl)) { + URL entryUrl = findExactRawEntry(wiring, container, entry); + if (entryUrl != null) { + if (entryUrl.getPath().endsWith("/")) { + return exactDirectoryClassPathEntry( + wiring, container, entry, serviceType); + } + if (!isZip(entryUrl)) { + return ExactClassPathEntry.FOUND_EMPTY; + } return new ExactClassPathEntry(true, getMetaInfServiceURLsFromJar( entryUrl, Collections.singleton(serviceType))); } - if (exactDirectoryExists(wiring, container, entry)) { - return new ExactClassPathEntry(true, - exactDirectoryServiceFiles( - wiring, container, entry, serviceType)); + if (inferredDirectoryExists(wiring, container, entry)) { + return exactDirectoryClassPathEntry( + wiring, container, entry, serviceType); } return ExactClassPathEntry.NOT_FOUND; } @@ -547,26 +553,53 @@ private ExactClassPathEntry findExactBundleClassPathEntry( } } - private boolean exactDirectoryExists(BundleWiring wiring, + private URL findExactRawEntry(BundleWiring wiring, ExactBundleContainer container, String entry) throws IOException { String path = normalizedDirectoryPath(entry); if (path.isEmpty()) { - return false; + return null; } int separator = path.lastIndexOf('/'); String parent = separator < 0 ? "/" : path.substring(0, separator); String name = path.substring(separator + 1); - List directories = wiring.findEntries(parent, name, 0); - if (directories != null) { - URL expected = exactEntry(container, path + "/"); - for (URL directory : directories) { - if (directory.getPath().endsWith("/") - && directory.sameFile(expected)) { - return true; - } + List entries = wiring.findEntries(parent, name, 0); + if (entries == null) { + return null; + } + + URL expected = exactEntry(container, path); + URL expectedDirectory = exactEntry(container, path + "/"); + for (URL candidate : entries) { + if (candidate.sameFile(expected) + || candidate.sameFile(expectedDirectory)) { + return candidate; } } + return null; + } + + private ExactClassPathEntry exactDirectoryClassPathEntry( + BundleWiring wiring, ExactBundleContainer container, String entry, + String serviceType) { + try { + return new ExactClassPathEntry(true, + exactDirectoryServiceFiles( + wiring, container, entry, serviceType)); + } + catch (IOException | RuntimeException e) { + log(Level.FINE, "Could not enumerate SPI resource for " + serviceType + + " from selected Bundle-ClassPath directory " + entry, e); + return ExactClassPathEntry.FOUND_EMPTY; + } + } + + private boolean inferredDirectoryExists(BundleWiring wiring, + ExactBundleContainer container, String entry) throws IOException { + String path = normalizedDirectoryPath(entry); + if (path.isEmpty()) { + return false; + } List contents = wiring.findEntries( path, "*", BundleWiring.FINDENTRIES_RECURSE); @@ -872,6 +905,8 @@ private ExactBundleContainer(URL manifest, List bundleClassPath) { private static final class ExactClassPathEntry { private static final ExactClassPathEntry NOT_FOUND = new ExactClassPathEntry(false, Collections.emptyList()); + private static final ExactClassPathEntry FOUND_EMPTY = + new ExactClassPathEntry(true, Collections.emptyList()); private final boolean exists; private final List serviceFiles; diff --git a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java index 512b5cdccc..2a4a1743be 100644 --- a/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ProviderBundleTrackerCustomizerTest.java @@ -23,6 +23,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; +import java.io.File; import java.io.InputStream; import java.io.OutputStream; import java.net.URL; @@ -30,7 +31,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Dictionary; @@ -450,6 +452,72 @@ public void testStandardDiscoveryKeepsExplicitEmptyHostDirectory() host, Collections.singletonList(serviceType))); } + @Test + public void testStandardDiscoveryKeepsFirstMatchingOrdinaryFile() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + Map hostEntries = new HashMap(); + hostEntries.put("classes", new byte[] {1}); + URL hostManifest = createRevisionContent( + "host-ordinary-file", "classes", hostEntries); + Map fragmentEntries = new HashMap(); + fragmentEntries.put("classes/META-INF/services/" + serviceType, + "org.apache.aries.spifly.impl3.MySPIImpl3\n".getBytes("UTF-8")); + URL fragmentManifest = createRevisionContent( + "fragment-provider-directory", null, fragmentEntries); + + Map> directoryContents = + new HashMap>(); + URL fragmentServiceFile = new URL(fragmentManifest, + "../classes/META-INF/services/" + serviceType); + directoryContents.put("classes", + Collections.singletonList(fragmentServiceFile)); + Map> exactEntries = + new HashMap>(); + exactEntries.put("classes", Arrays.asList( + new URL(hostManifest, "../classes"), + new URL(fragmentManifest, "../classes/"))); + exactEntries.put("classes/META-INF/services/" + serviceType, + Collections.singletonList(fragmentServiceFile)); + BundleWiring wiring = mockProviderWiring( + new AtomicReference>(Arrays.asList( + hostManifest, fragmentManifest)), + serviceType, directoryContents, exactEntries); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals("An ordinary host entry must shadow a fragment directory", + Collections.emptyList(), customizer.getServiceFileUrls( + mockProviderBundle(wiring), + Collections.singletonList(serviceType))); + + URL missingHostManifest = createRevisionContent( + "host-missing-ordinary-file", "classes", + Collections.emptyMap()); + URL ordinaryFragmentManifest = createRevisionContent( + "first-fragment-ordinary-file", null, hostEntries); + URL providerFragmentManifest = createRevisionContent( + "second-fragment-provider-directory", null, fragmentEntries); + URL providerFragmentServiceFile = new URL(providerFragmentManifest, + "../classes/META-INF/services/" + serviceType); + directoryContents.put("classes", + Collections.singletonList(providerFragmentServiceFile)); + exactEntries.put("classes", Arrays.asList( + new URL(ordinaryFragmentManifest, "../classes"), + new URL(providerFragmentManifest, "../classes/"))); + exactEntries.put("classes/META-INF/services/" + serviceType, + Collections.singletonList(providerFragmentServiceFile)); + wiring = mockProviderWiring( + new AtomicReference>(Arrays.asList( + missingHostManifest, ordinaryFragmentManifest, + providerFragmentManifest)), + serviceType, directoryContents, exactEntries); + assertEquals("An ordinary first-fragment entry must shadow later fragments", + Collections.emptyList(), customizer.getServiceFileUrls( + mockProviderBundle(wiring), + Collections.singletonList(serviceType))); + } + @Test public void testStandardDiscoveryUsesDirectoryWithoutExplicitEntry() throws Exception { @@ -572,6 +640,9 @@ private BundleWiring mockProviderWiring( .andReturn(Collections.emptyList()).anyTimes(); EasyMock.expect(wiring.findEntries("META-INF", "MANIFEST.MF", 0)) .andAnswer(() -> manifests.get()).anyTimes(); + EasyMock.expect(wiring.findEntries("/", "embedded.jar", 0)) + .andAnswer(() -> exactExistingEntries( + manifests.get(), "embedded.jar")).anyTimes(); for (Map.Entry> entry : directoryContents.entrySet()) { EasyMock.expect(wiring.findEntries( entry.getKey(), "*", BundleWiring.FINDENTRIES_RECURSE)) @@ -589,6 +660,18 @@ private BundleWiring mockProviderWiring( return wiring; } + private List exactExistingEntries( + List manifests, String path) throws Exception { + List result = new ArrayList(); + for (URL manifest : manifests) { + URL entry = new URL(manifest, "../" + path); + if (new File(entry.toURI()).exists()) { + result.add(entry); + } + } + return result; + } + private Bundle mockProviderBundle(List manifests, String serviceType) { return mockProviderBundle(mockProviderWiring(manifests, serviceType)); } diff --git a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java index 9a33e6c3a6..7576625e9e 100644 --- a/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java @@ -465,6 +465,68 @@ public void explicitEmptyHostDirectoryShadowsFragmentProvider() } } + @Test + public void ordinaryHostClassPathEntryShadowsFragmentProvider() + throws Exception { + Path storage = Files.createTempDirectory("spifly-ordinary-host-entry-"); + Framework framework = null; + try { + Map configuration = new HashMap(); + configuration.put(Constants.FRAMEWORK_STORAGE, + storage.resolve("framework").toString()); + configuration.put(Constants.FRAMEWORK_STORAGE_CLEAN, + Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT); + framework = newFramework(configuration); + framework.start(); + + BundleContext context = framework.getBundleContext(); + installDependency(context, ClassReader.class); + installDependency(context, AdviceAdapter.class); + installDependency(context, ClassNode.class); + installDependency(context, Analyzer.class); + installDependency(context, CheckClassAdapter.class); + installDependency(context, BundleTracker.class); + + Bundle mediator = context.installBundle( + bundleFromClasses(storage, "spifly-dynamic.jar").toUri().toString()); + Bundle api = context.installBundle(createApiBundle(storage).toUri().toString()); + Bundle fragment = context.installBundle( + createOrdinaryEntryProviderFragment(storage).toUri().toString()); + Bundle provider = context.installBundle( + createOrdinaryEntryProviderHost(storage).toUri().toString()); + Bundle consumer = context.installBundle( + createConsumerBundle(storage).toUri().toString()); + + FrameworkWiring frameworkWiring = framework.adapt(FrameworkWiring.class); + assertTrue(frameworkWiring.resolveBundles(java.util.Arrays.asList( + mediator, api, fragment, provider, consumer))); + assertEquals(IMPLEMENTATION, + provider.loadClass(IMPLEMENTATION).getName()); + assertNull("The framework class path must stop at the ordinary host entry", + provider.adapt(BundleWiring.class).getClassLoader().getResource( + "META-INF/services/" + SERVICE_TYPE)); + + mediator.start(); + provider.start(); + consumer.start(); + + assertEquals("The mediated ServiceLoader must not see the fragment entry", + Collections.emptySet(), + invokeConsumer(consumer.loadClass(TestClient.class.getName()))); + org.osgi.framework.ServiceReference[] registrations = + context.getAllServiceReferences(SERVICE_TYPE, null); + assertEquals("The registrar must not publish the shadowed fragment provider", + 0, registrations == null ? 0 : registrations.length); + } + finally { + if (framework != null) { + framework.stop(); + framework.waitForStop(30000); + } + deleteRecursively(storage); + } + } + private Framework newFramework(Map configuration) throws Exception { String factoryName = System.getProperty( "spifly.test.frameworkFactory", @@ -577,6 +639,37 @@ private Path createEmptyDirectoryProviderFragment(Path directory) manifest, entries); } + private Path createOrdinaryEntryProviderHost(Path directory) + throws IOException { + Manifest manifest = bundleManifest("spifly.test.ordinary.entry.provider"); + manifest.getMainAttributes().putValue( + Constants.IMPORT_PACKAGE, "org.apache.aries.mytest;version=\"[1,2)\""); + manifest.getMainAttributes().putValue( + Constants.BUNDLE_CLASSPATH, "classes,."); + Map entries = new LinkedHashMap(); + entries.put("classes", new byte[] {1}); + addClass(entries, MySPIImpl1.class); + return writeBundle(directory.resolve("ordinary-entry-provider-host.jar"), + manifest, entries); + } + + private Path createOrdinaryEntryProviderFragment(Path directory) + throws IOException { + Manifest manifest = bundleManifest("spifly.test.ordinary.entry.fragment"); + Attributes attributes = manifest.getMainAttributes(); + attributes.putValue(Constants.FRAGMENT_HOST, + "spifly.test.ordinary.entry.provider;bundle-version=\"[1,2)\""); + attributes.putValue(Constants.REQUIRE_CAPABILITY, + "osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.registrar)\""); + attributes.putValue(Constants.PROVIDE_CAPABILITY, + "osgi.serviceloader;osgi.serviceloader=\"" + SERVICE_TYPE + "\""); + Map entries = new LinkedHashMap(); + entries.put("classes/META-INF/services/" + SERVICE_TYPE, + (IMPLEMENTATION + "\n").getBytes(StandardCharsets.UTF_8)); + return writeBundle(directory.resolve("ordinary-entry-provider-fragment.jar"), + manifest, entries); + } + private Path createProviderFragment(Path path, Class implementation, String version) throws IOException { return createProviderFragment(path, implementation, version, true);