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..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 @@ -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; @@ -42,9 +43,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; @@ -52,19 +57,31 @@ 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()); // 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") private BundleTracker consumerBundleTracker; @SuppressWarnings("rawtypes") private BundleTracker providerBundleTracker; + private ProviderBundleTrackerCustomizer providerBundleTrackerCustomizer; private Optional autoConsumerInstructions; private Optional autoProviderInstructions; @@ -74,9 +91,26 @@ public abstract class BaseActivator implements BundleActivator { private final ConcurrentMap>>> registeredProviders = new ConcurrentHashMap>>>(); + private final ConcurrentMap> providerAdvertisements = + new ConcurrentHashMap>(); + private final ConcurrentMap>> consumerRestrictions = new ConcurrentHashMap>>(); + private final ConcurrentMap standardConsumerWirings = + new ConcurrentHashMap(); + + private final Set> requestedConsumerRefreshes = + 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; @@ -94,8 +128,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, @@ -106,7 +142,12 @@ public synchronized void start(BundleContext context, final String consumerHeade addConsumerWeavingData(bundle, consumerHeaderName); } + activeSession = new Object(); activator = this; + + if (SpiFlyConstants.SPI_CONSUMER_HEADER.equals(consumerHeaderName)) { + refreshActiveConsumers(); + } } public void addConsumerWeavingData(Bundle bundle, String consumerHeaderName) throws Exception { @@ -115,17 +156,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 +228,16 @@ public void addConsumerWeavingData(Bundle bundle, String consumerHeaderName) thr } } + 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); @@ -183,14 +266,243 @@ private List getAllHeaders(String headerName, Bundle bundle) { public void removeWeavingData(Bundle bundle) { bundleWeavingData.remove(bundle); consumerRestrictions.remove(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) { + 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; + } + 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"); + } + + 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()) { + 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 " + reason + + ": FrameworkWiring is unavailable"); + return; + } + + 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(bundles, event -> { + if (event.getType() == FrameworkEvent.ERROR) { + log(Level.WARNING, "Could not refresh consumers " + bundles + " " + + reason, event.getThrowable()); + } + }); + } + catch (RuntimeException e) { + requestedConsumerRefreshes.removeAll(newRefreshes); + log(Level.WARNING, "Could not request refresh of consumers " + bundles + " " + + reason, e); + } } @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(); + consumersByProvider.clear(); + requestedProviderStopRefreshes.clear(); + } + + Object getActiveSession() { + return activeSession; + } + + boolean isSessionActive(Object session) { + return session != null && activeSession == session && activator == this; } public boolean isLogEnabled(Level level) { @@ -274,6 +586,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, wiring); + 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(); ) { @@ -283,6 +614,10 @@ public void unregisterProviderBundle(Bundle bundle) { } } } + for (Map advertisements + : providerAdvertisements.values()) { + advertisements.remove(bundle.getBundleId()); + } } public Collection findProviderBundles(String name) { @@ -298,6 +633,46 @@ 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; + } + + 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) @@ -321,6 +696,12 @@ 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) + && isServiceLoaderMethod(methodName)) { + return standardWiring.getProviders(); + } + Map> restrictions = consumerRestrictions.get(consumer); if (restrictions == null) { // Null means: no restrictions @@ -371,8 +752,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); @@ -393,4 +774,136 @@ 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 Set providers; + + private StandardConsumerWiring(boolean restricted, Set providers) { + this.restricted = restricted; + this.providers = providers; + } + + static StandardConsumerWiring from(BundleWiring wiring) { + if (wiring == null) { + return new StandardConsumerWiring(true, Collections.emptySet()); + } + + if (!hasDeclaredServiceLoaderRequirement(wiring)) { + return new StandardConsumerWiring(false, Collections.emptySet()); + } + + Set providers = new LinkedHashSet(); + for (BundleWire wire : wiring.getRequiredWires( + SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) { + BundleWiring providerWiring = wire.getProviderWiring(); + if (providerWiring != null) { + Bundle provider = providerWiring.getBundle(); + if (provider != null) { + providers.add(provider); + } + } + } + + return new StandardConsumerWiring( + true, Collections.unmodifiableSet(providers)); + } + + 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.emptySet()); + } + + Collection getProviders() { + if (!restricted) { + return null; + } + return providers; + } + } + + 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, + BundleWiring wiring) { + this.bundle = bundle; + this.revision = revision; + this.wiring = wiring; + } + + private synchronized void addImplementation(String implementationName) { + implementationNames.add(implementationName); + } + + private synchronized ProviderAdvertisement snapshot() { + ProviderAdvertisement snapshot = new ProviderAdvertisement( + bundle, revision, wiring); + snapshot.implementationNames.addAll(implementationNames); + return snapshot; + } + + Bundle getBundle() { + return bundle; + } + + 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/ConsumerBundleTrackerCustomizer.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerBundleTrackerCustomizer.java index c5e98d8205..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 @@ -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,24 +32,31 @@ 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); } 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/ConsumerHeaderProcessor.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/ConsumerHeaderProcessor.java index b3cb8a96b4..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 @@ -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; @@ -177,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; } @@ -197,8 +204,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 +214,47 @@ 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)); + } + + // ServiceLoader.loadInstalled(Class) + { + ArgRestrictions ar = new ArgRestrictions(); + ar.addRestriction(0, Class.class.getName()); + MethodRestriction mr = new MethodRestriction("loadInstalled", ar); + weavingData.add(createWeavingData( + ServiceLoader.class.getName(), "loadInstalled", 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 1dc7e6befe..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 @@ -19,41 +19,52 @@ package org.apache.aries.spifly; 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.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.util.jar.JarEntry; -import java.util.jar.JarInputStream; -import java.util.logging.Level; +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; +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.Collection; +import java.util.Collections; +import java.util.Dictionary; +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.jar.Manifest; +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; 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.namespace.HostNamespace; +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; 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; @@ -63,38 +74,53 @@ */ @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); - final BaseActivator activator; - final Bundle spiBundle; + final BaseActivator activator; + final Bundle spiBundle; + private final ConcurrentMap processedWirings = + new ConcurrentHashMap(); public ProviderBundleTrackerCustomizer(BaseActivator activator, Bundle spiBundle) { this.activator = activator; 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 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; + List serviceLoaderCapabilities = Collections.emptyList(); + boolean registerServiceLoaderServices = false; + Map customAttributes = new HashMap(); + BundleWiring wiring = WiringUtils.getWiring(bundle); + if (wiring != null) { + 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) { @@ -116,134 +142,181 @@ 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. + recordProcessedWiring(bundle, wiring); + return new ArrayList(); } else { log(Level.FINE, "Examining bundle for SPI provider: " + 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 + // 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) { - serviceFileURLs = getServiceFileUrls(bundle); - } + if (serviceFileURLs == null) { + serviceFileURLs = getServiceFileUrls(bundle, + discoveryMode == DiscoveryMode.SERVICELOADER_CAPABILITIES + ? providedServices : null); + } final List registrations = new ArrayList(); - for (ServiceDetails details : collectServiceDetails(bundle, serviceFileURLs, discoveryMode)) { - if (providedServices.size() > 0 && !providedServices.contains(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 = - (details.properties.containsKey("service.scope") && - "prototype".equalsIgnoreCase(String.valueOf(details.properties.get("service.scope")))) ? - new ProviderPrototypeServiceFactory(cls) : - 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) { + for (ServiceDetails details : collectServiceDetails(bundle, serviceFileURLs, discoveryMode, + serviceLoaderCapabilities, registerServiceLoaderServices)) { + if ((discoveryMode == DiscoveryMode.SERVICELOADER_CAPABILITIES + || providedServices.size() > 0) + && !providedServices.contains(details.serviceType)) + continue; + + try { + 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); + + if (reg != null) { registrations.add(reg); log(Level.FINE, "Registered service: " + reg); } } - activator.registerProviderBundle(details.serviceType, bundle, details.properties); - log(Level.INFO, "Registered provider " + details.instanceType + " of service " + details.serviceType + " in bundle " + bundle.getSymbolicName()); - } catch (Exception | NoClassDefFoundError e) { - log(Level.FINE, + 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()); + } catch (Exception | NoClassDefFoundError e) { + log(Level.FINE, "Could not load provider " + details.instanceType + " of service " + details.serviceType, 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; - } + 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)); + 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, + boolean registerServiceLoaderServices) { + List serviceDetails = new ArrayList<>(); + + for (Entry> providerFile : readServiceProviderFiles(serviceFileURLs).entrySet()) { + String registrationClassName = providerFile.getKey(); + for (String className : providerFile.getValue()) { + try { + final List> registrations; + if (discoveryMode == DiscoveryMode.SPI_PROVIDER_HEADER) { + registrations = Collections.singletonList(new Hashtable()); + } + else if (discoveryMode == DiscoveryMode.AUTO_PROVIDERS_PROPERTY) { + 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) { + registrations = findServiceRegistrationProperties( + serviceLoaderCapabilities, registrationClassName, className); + } + else { + registrations = Collections.emptyList(); + } + + 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)); + } + } 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 @@ -254,7 +327,9 @@ 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, + Collections.emptyList(), false); collectServiceDetails.stream().map(ServiceDetails::getProperties).filter(Objects::nonNull).forEach( hashtable -> hashtable.forEach(customAttributes::put) @@ -267,8 +342,30 @@ 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); + if (wiring == null) { + return Collections.emptyList(); + } + + Set requestedTypes = new LinkedHashSet(serviceTypes); + Map> compatibilityEntries = + getBundleRootServiceFiles(bundle, requestedTypes); + Set serviceFileURLs = new LinkedHashSet(); + for (String serviceType : requestedTypes) { + serviceFileURLs.addAll(getServiceFileUrls( + bundle, wiring, serviceType, + compatibilityEntries.get(serviceType))); + } + return new ArrayList(serviceFileURLs); + } + + List serviceFileURLs = new ArrayList(); Enumeration entries = bundle.findEntries(METAINF_SERVICES, "*", false); if (entries != null) { @@ -282,15 +379,340 @@ private List getServiceFileUrls(Bundle bundle) { 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 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) { + Set urls = new LinkedHashSet(); + if (addExactBundleClassPathServiceFiles(wiring, serviceType, urls)) { + return new ArrayList(urls); + } + + List rootEntries = wiring.findEntries( + METAINF_SERVICES, serviceType, 0); + if (rootEntries != null) { + urls.addAll(rootEntries); + } + if (urls.isEmpty() && compatibilityEntries != null) { + urls.addAll(compatibilityEntries); + } + addBundleClassPathServiceFiles(bundle, + Collections.singleton(serviceType), urls); + return new ArrayList(urls); + } + + private boolean addExactBundleClassPathServiceFiles(BundleWiring wiring, + String serviceType, Set serviceFileURLs) { + List manifests = wiring.findEntries("META-INF", "MANIFEST.MF", 0); + 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); + 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 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 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 = 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 (inferredDirectoryExists(wiring, container, entry)) { + return exactDirectoryClassPathEntry( + wiring, container, entry, serviceType); + } + return ExactClassPathEntry.NOT_FOUND; + } + 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 URL findExactRawEntry(BundleWiring wiring, + ExactBundleContainer container, String entry) throws IOException { + String path = normalizedDirectoryPath(entry); + if (path.isEmpty()) { + return null; + } + + int separator = path.lastIndexOf('/'); + String parent = separator < 0 ? "/" : path.substring(0, separator); + String name = path.substring(separator + 1); + 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); + if (contents == null) { + return false; + } + for (URL content : contents) { + 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)); + } + + 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; + } + } + + private void addBundleClassPathServiceFiles(Bundle bundle, + Set serviceTypes, Set serviceFileURLs) { + Dictionary headers = bundle.getHeaders(); + if (headers == null) { + return; + } + Object bcp = headers.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); @@ -342,85 +764,44 @@ 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) { - 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) { - 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 { @@ -428,8 +809,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())); } } @@ -444,20 +827,50 @@ 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) { + 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") + 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) { + processedWirings.remove(bundle); + activator.providerBundleStopped(bundle); + 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); @@ -475,11 +888,36 @@ 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 static final ExactClassPathEntry FOUND_EMPTY = + new ExactClassPathEntry(true, 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/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..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 @@ -20,17 +20,33 @@ 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) { + if (providerBundle.getState() != Bundle.ACTIVE + || !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/SpiFlyConstants.java b/spi-fly/spi-fly-core/src/main/java/org/apache/aries/spifly/SpiFlyConstants.java index afdf77bc95..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"; @@ -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..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 @@ -18,35 +18,58 @@ */ package org.apache.aries.spifly; -import java.io.IOException; -import java.lang.reflect.Method; -import java.net.URL; -import java.security.AccessControlException; -import java.security.AccessController; +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; import org.osgi.framework.Bundle; -import org.osgi.framework.BundleReference; -import org.osgi.framework.Constants; -import org.osgi.framework.ServicePermission; +import org.osgi.framework.BundleReference; +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; /** * 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() { @@ -71,101 +94,184 @@ 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; - } - - 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); - } - - 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); - } - } - } - ); + public static ServiceLoader serviceLoaderLoad(Class service, Class 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 isSessionActive(activator, session) + ? ServiceLoader.load(service) : emptyServiceLoader(service); + } + final ClassLoader bundleClassloader = findContextClassloader( + activator, session, consumerBundle, ServiceLoader.class.getName(), + "load", service, true); + + if (!isSessionActive(activator, session)) { + return emptyServiceLoader(service); + } + + if (bundleClassloader == null + && !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, consumerBundle, + service.getName(), activator, session)); + } + } + ); } 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; - } - - 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; - - 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)); - } - - public static void fixContextClassloader(String cls, String method, Class clsArg, ClassLoader bundleLoader) { - BundleReference br = getBundleReference(bundleLoader); - - if (br == null) { - return; - } - - final ClassLoader cl = findContextClassloader(br.getBundle(), cls, method, clsArg); - if (cl != null) { - BaseActivator.activator.log(Level.FINE, "Temporarily setting Thread Context Classloader to: " + cl); + 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 isSessionActive(activator, session) + ? ServiceLoader.load(service, specifiedClassLoader) + : emptyServiceLoader(service); + } + final ClassLoader bundleClassloader = findContextClassloader( + activator, session, consumerBundle, ServiceLoader.class.getName(), + "load", service, true); + + if (!isSessionActive(activator, session)) { + return emptyServiceLoader(service); + } + + if (bundleClassloader == null) { + if (activator.isStandardConsumer(consumerBundle)) { + return ServiceLoader.load(service, new ProviderViewClassLoader( + specifiedClassLoader, null, consumerBundle, service.getName(), + activator, session)); + } + return ServiceLoader.load(service, specifiedClassLoader); + } + + if (activator.isStandardConsumer(consumerBundle)) { + return ServiceLoader.load(service, new ProviderViewClassLoader( + specifiedClassLoader, bundleClassloader, consumerBundle, + service.getName(), activator, session)); + } + return ServiceLoader.load(service, new WrapperCL(specifiedClassLoader, + bundleClassloader, activator, session)); + } + + @BaselineIgnore("1.4.0") + public static ServiceLoader serviceLoaderLoadInstalled( + Class service, Class 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 isSessionActive(activator, session) + ? ServiceLoader.loadInstalled(service) : emptyServiceLoader(service); + } + + ClassLoader bundleClassloader = findContextClassloader( + activator, session, consumerBundle, ServiceLoader.class.getName(), + "loadInstalled", service, true); + + if (!isSessionActive(activator, session)) { + return emptyServiceLoader(service); + } + if (bundleClassloader == null + && !activator.isStandardConsumer(consumerBundle)) { + return ServiceLoader.loadInstalled(service); + } + + ClassLoader installedClassLoader = getInstalledClassLoader(); + return ServiceLoader.load(service, new ProviderViewClassLoader( + installedClassLoader, bundleClassloader, consumerBundle, + 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() { + 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, + BaseActivator activator) { + 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(); + } + + 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) { + 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( + 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() { @@ -173,32 +279,28 @@ 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, String methodName, Class clsArg) { - BaseActivator activator = BaseActivator.activator; - - String requestedClass; - Map, String> args; - if (ServiceLoader.class.getName().equals(className) && "load".equals(methodName)) { + } 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) { + if (!isSessionActive(activator, session)) { + return null; + } + + String requestedClass; + Map, String> args; + boolean serviceLoaderCall = ServiceLoader.class.getName().equals(className) + && ("load".equals(methodName) || "loadInstalled".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 } @@ -208,39 +310,69 @@ 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()) { - 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)); - } + if (allowedBundles != null) { + for (Iterator it = bundles.iterator(); it.hasNext(); ) { + if (!allowedBundles.contains(it.next())) { + it.remove(); + } + } + } + + if (serviceLoaderCall) { + bundles = activator.filterCompatibleProviderBundles( + consumerBundle, clsArg, bundles); + } + + if (serviceLoaderCall && permissionAware + && activator.isStandardConsumer(consumerBundle) + && !bundles.isEmpty()) { + return new ProviderAdvertisementClassLoader( + activator.findProviderAdvertisements(requestedClass, bundles), + requestedClass, consumerBundle, activator, session); + } + + if (!isSessionActive(activator, session)) { + return null; + } + + switch (bundles.size()) { + case 0: + return null; + case 1: + Bundle bundle = bundles.iterator().next(); + return serviceLoaderCall && permissionAware + ? getProviderClassLoader(bundle, requestedClass, activator, session) + : getBundleClassLoader(bundle, activator); + default: + List loaders = new ArrayList(); + for (Bundle b : bundles) { + loaders.add(serviceLoaderCall && permissionAware + ? 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 getBundleClassLoaderPrivileged(Bundle b) { + }); + } + + private static ClassLoader getProviderClassLoader(Bundle providerBundle, + String serviceType, BaseActivator activator, Object session) { + return new ProviderBundleClassLoader(providerBundle, + getBundleClassLoader(providerBundle, activator), serviceType, + activator, session); + } + + 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. @@ -283,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; } @@ -313,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 { @@ -347,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; } @@ -368,12 +497,39 @@ private static ClassLoader getClassLoaderFromClassResource(Bundle b, String path return null; } - private static class WrapperCL extends ClassLoader { - private final ClassLoader bundleClassloader; - public WrapperCL(ClassLoader specifiedClassLoader, ClassLoader bundleClassloader) { - super(specifiedClassLoader); - this.bundleClassloader = bundleClassloader; - } + private static class WrapperCL extends ClassLoader { + 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 { @@ -386,8 +542,317 @@ 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 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, 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); + } + return super.getResource(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() + : providerClassLoader.getResources(name); + } + return super.getResources(name); + } + + @Override + protected synchronized Class loadClass(String name, boolean resolve) + throws ClassNotFoundException { + if (!isSessionActive(activator, session) + || !hasGetPermission() || providerClassLoader == null) { + throw new ClassNotFoundException(name); + } + Class cls = providerClassLoader.loadClass(name); + if (resolve) { + resolveClass(cls); + } + return cls; + } + + @Override + protected URL findResource(String name) { + return !isSessionActive(activator, session) + || !hasGetPermission() || providerClassLoader == null + ? null : providerClassLoader.getResource(name); + } + + @Override + protected Enumeration findResources(String name) throws IOException { + return !isSessionActive(activator, session) + || !hasGetPermission() || providerClassLoader == null + ? java.util.Collections.emptyEnumeration() + : providerClassLoader.getResources(name); + } + + private boolean hasGetPermission() { + boolean permitted = consumerBundle.hasPermission( + new ServicePermission(serviceType, ServicePermission.GET)); + if (!permitted) { + activator.log(Level.FINE, "Bundle " + consumerBundle + + " does not have permission to obtain services of type: " + + serviceType); + } + return permitted; + } + } + + 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> + advertisersByImplementation = + new LinkedHashMap>(); + + ProviderAdvertisementClassLoader( + List advertisements, + 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; + 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 (!isSessionActive(activator, session) + || !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 { + if (!isSessionActive(activator, session)) { + throw new ClassNotFoundException(name); + } + List advertisements = + advertisersByImplementation.get(name); + if (advertisements == null) { + throw new ClassNotFoundException(name); + } + + ClassNotFoundException last = null; + for (BaseActivator.ProviderAdvertisement advertisement : advertisements) { + if (!isProviderAvailable(advertisement, serviceType, + activator, session)) { + continue; + } + try { + 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; + } + } + throw last == null ? new ClassNotFoundException(name) : last; + } + + 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, + activator, session)) { + 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; + private final String serviceType; + private final BaseActivator activator; + private final Object session; + + ProviderBundleClassLoader(Bundle providerBundle, ClassLoader delegate, + String serviceType, BaseActivator activator, Object session) { + super(null); + this.providerBundle = providerBundle; + this.delegate = delegate; + this.serviceType = serviceType; + this.activator = activator; + this.session = session; + } + + @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() { + if (!isSessionActive(activator, session)) { + return false; + } + boolean permitted = providerBundle.hasPermission( + new ServicePermission(serviceType, ServicePermission.REGISTER)); + if (!permitted) { + activator.log(Level.FINE, "Bundle " + providerBundle + + " does not have permission to provide services of type: " + + serviceType); + } + return permitted; + } + } + + private static boolean isProviderAvailable( + BaseActivator.ProviderAdvertisement advertisement, String serviceType, + BaseActivator activator, Object session) { + return isProviderAvailable(advertisement.getBundle(), + advertisement.getRevision(), advertisement.getWiring(), serviceType, + activator, session); + } + + private static boolean isProviderAvailable(Bundle providerBundle, + BundleRevision providerRevision, BundleWiring providerWiring, + String serviceType, BaseActivator activator, Object session) { + if (!isSessionActive(activator, session)) { + return false; + } + if (providerBundle.getState() != Bundle.ACTIVE) { + 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))) { + 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 != providerWiring + || wiring.getRevision() != providerRevision) { + 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/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..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 @@ -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; @@ -31,8 +32,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; @@ -50,15 +52,18 @@ import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; -import org.osgi.framework.ServiceFactory; -import org.osgi.framework.ServiceReference; -import org.osgi.framework.ServiceRegistration; +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; +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 +159,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,14 +345,84 @@ 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 - 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(Constants.SERVICE_SCOPE, Constants.SCOPE_PROTOTYPE); + 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) { + 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( + 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 @@ -498,7 +572,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 +588,81 @@ 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); + } + + @Test + public void testProviderWithoutRegisterPermissionRemainsCandidate() 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()); + assertProviderBundle(activator, + "org.apache.aries.mytest.MySPI", implBundle); + EasyMock.verify(implBC); + } @SuppressWarnings({ "resource", "unchecked" }) @Test @@ -545,23 +685,29 @@ 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.getState()).andReturn(Bundle.ACTIVE).anyTimes(); + EasyMock.expect(implBundle.hasPermission(EasyMock.isA(ServicePermission.class))) + .andReturn(true).anyTimes(); 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(); - - URL embeddedJar = getClass().getResource("/embedded.jar"); + 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(); + URL embeddedJar = getClass().getResource("/embedded.jar"); assertNotNull("precondition", embeddedJar); - EasyMock.expect(implBundle.getResource("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("embedded.jar")).andReturn(embeddedJar).anyTimes(); + 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); EasyMock.expect(implBundle.getResource("/META-INF/services")).andReturn(dir).anyTimes(); @@ -610,12 +756,26 @@ 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(); - - Bundle implBundle = EasyMock.createNiceMock(Bundle.class); - EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + 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 { + 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(); + 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(); EasyMock.expect(implBundle.getSymbolicName()).andReturn("bsn").anyTimes(); @@ -630,8 +790,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; @@ -678,12 +839,21 @@ 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); - EasyMock.expect(implBundle.getBundleContext()).andReturn(implBC).anyTimes(); + 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.getState()).andReturn(Bundle.ACTIVE).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(); + if (rev != null) + EasyMock.expect(implBundle.adapt(BundleRevision.class)).andReturn(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"); @@ -716,7 +886,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 +911,138 @@ 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, 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(); + } + 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(); + 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, 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)) + : 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(); + 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; + } + + 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); + 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..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 @@ -21,26 +21,49 @@ 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 java.net.URL; -import java.net.URLClassLoader; -import java.util.Arrays; +import static org.junit.Assert.assertSame; + +import java.io.File; +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.ArrayList; +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 org.easymock.EasyMock; -import org.junit.Test; +import java.util.Dictionary; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +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; -import org.osgi.framework.ServiceFactory; -import org.osgi.framework.ServiceRegistration; - -public class ProviderBundleTrackerCustomizerTest { +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.BundleRequirement; +import org.osgi.framework.wiring.BundleRevision; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +public class ProviderBundleTrackerCustomizerTest { + + @Rule + public final TemporaryFolder temporaryFolder = new TemporaryFolder(); private BaseActivator activator = new BaseActivator() { @Override @@ -94,8 +117,635 @@ 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 testStandardDiscoveryUsesBundleLocalEntries() throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + final URL serviceFile = getClass().getResource( + "impl1/META-INF/services/" + serviceType); + assertNotNull("precondition", serviceFile); + + BundleWiring wiring = EasyMock.createNiceMock(BundleWiring.class); + 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 = + new ProviderBundleTrackerCustomizer(activator, null); + + assertEquals(Collections.singletonList(serviceFile), + 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); + URL manifestF1 = createRevisionContent("fragment-f1", embeddedF1); + URL manifestF2 = createRevisionContent("fragment-f2", embeddedF2); + BundleWiring wiringF1 = mockProviderWiring( + Collections.singletonList(manifestF1), serviceType); + BundleWiring wiringF2 = mockProviderWiring( + 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.replay(host); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + List f1 = customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType)); + assertEquals(Collections.singletonList( + embeddedServiceFile(manifestF1, serviceType)), f1); + + assertEquals("The unchanged host wiring must retain F1", f1, + customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + + currentWiring.set(wiringF2); + assertEquals(Collections.singletonList( + embeddedServiceFile(manifestF2, serviceType)), + customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + } + + @Test + public void testStandardDiscoveryRecomputesSameEffectiveWiring() + 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); + 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 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.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.replay(host); + + ProviderBundleTrackerCustomizer customizer = + new ProviderBundleTrackerCustomizer(activator, null); + assertEquals(Collections.singletonList( + embeddedServiceFile(manifest, serviceType)), + customizer.getServiceFileUrls( + host, Collections.singletonList(serviceType))); + } + + @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))); + } + + @Test + public void testStandardDiscoveryDoesNotReplaceExactResourceFromDependency() + throws Exception { + final String serviceType = "org.apache.aries.mytest.MySPI"; + final String resourceName = "META-INF/services/" + serviceType; + 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, mediator); + assertEquals("Dependency-visible metadata must not be registered", + Collections.emptyList(), customizer.addingBundle(provider, null)); + EasyMock.verify(providerContext); + } + } + + @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(hostManifest, "../classes/host.txt"), + new URL(fragmentManifest, + "../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 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 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 { + 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 { + 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( + new AtomicReference>(manifests), serviceType); + } + + private BundleWiring mockProviderWiring( + AtomicReference> manifests, String serviceType) { + return mockProviderWiring(manifests, serviceType, + Collections.>emptyMap()); + } + + 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(); + 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)) + .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; + } + + 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)); + } + + 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 { + 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(); + manifest.getMainAttributes().put( + Attributes.Name.MANIFEST_VERSION, "1.0"); + if (bundleClassPath != null) { + manifest.getMainAttributes().putValue( + Constants.BUNDLE_CLASSPATH, bundleClassPath); + } + try (OutputStream output = Files.newOutputStream( + 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) { + 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"); + return new URL("jar:" + embedded + "!/META-INF/services/" + serviceType); + } @Test @SuppressWarnings("unchecked") @@ -114,20 +764,22 @@ 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, "*"); - 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); - 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(); @@ -183,8 +835,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) @@ -206,36 +860,99 @@ 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( 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(); - - // List the resources found at META-INF/services in the test bundle + 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(); + // 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"); 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, 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); + EasyMock.expect(serviceCapability.getAttributes()).andReturn(serviceAttributes).anyTimes(); + EasyMock.expect(serviceCapability.getDirectives()).andReturn( + Collections.emptyMap()).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.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(); + 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; + } +} 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..dac9c167e8 --- /dev/null +++ b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/ResolvedWiringTest.java @@ -0,0 +1,674 @@ +/** + * 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 static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +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.Capture; +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.FrameworkEvent; +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; +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"; + 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); + + setBundleContext(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 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); + 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))); + assertEquals(Collections.emptySet(), activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "loadInstalled", + 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( + 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))); + assertNull(activator.findConsumerRestrictions( + consumer, ServiceLoader.class.getName(), "loadInstalled", + 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))); + } + + @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]); + } + + @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); + } + + @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); + } + + @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, + 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(); + EasyMock.expect(wiring.getRequirements(SpiFlyConstants.SERVICELOADER_CAPABILITY_NAMESPACE)) + .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); + 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 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); + contextField.set(activator, context); + } + + 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-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/java/org/apache/aries/spifly/UtilTest.java b/spi-fly/spi-fly-core/src/test/java/org/apache/aries/spifly/UtilTest.java index f2ef9cfde1..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 @@ -18,15 +18,26 @@ */ 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.assertThrows; +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.Iterator; +import java.util.List; +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; @@ -36,8 +47,12 @@ 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.ServicePermission; +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 +63,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,8 +121,8 @@ public Class answer() throws Throwable { } @Test - public void testNotInitialized() throws Exception { - BaseActivator.activator = null; + public void testNotInitialized() throws Exception { + BaseActivator.activator = null; URL url = getClass().getResource("/embedded3.jar"); assertNotNull("precondition", url); @@ -134,11 +150,489 @@ 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 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(); + Bundle consumer = mockPermissionBundle(new AtomicBoolean(true)); + 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()); + assertFalse(Util.serviceLoaderLoadInstalled( + MySPI.class, callerClass(consumer)).iterator().hasNext()); + } + + @Test + public void explicitLoaderCannotAddProviderConfigurations() 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); + + 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(), + "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()) { + @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)); + + 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()); + } + + @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(), + "org.apache.aries.spifly.impl3.MySPIImpl3", 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(), + "org.apache.aries.spifly.impl3.MySPIImpl3", 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 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 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(); + 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); + 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 + 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 { + return mockProviderBundle(bundleId, providerJar, new AtomicBoolean(true)); + } + + @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(); + 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(); + 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)) + .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 + public Boolean answer() throws Throwable { + return permission.get(); + } + }).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 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 + 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; public TestBundleClassLoader(URL[] urls, ClassLoader parent, Bundle bundle) { 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 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..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 @@ -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; @@ -159,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, "*"); @@ -191,8 +192,72 @@ 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 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 { @@ -768,10 +833,12 @@ 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.getBundleContext()).andReturn(bc).anyTimes(); - EasyMock.expect(providerBundle.getVersion()).andReturn(version).anyTimes(); - EasyMock.expect(providerBundle.getEntryPaths("/")).andAnswer(new IAnswer>() { + 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(); + EasyMock.expect(providerBundle.getEntryPaths("/")).andAnswer(new IAnswer>() { @Override public Enumeration answer() throws Throwable { return Collections.enumeration(classResources); @@ -817,10 +884,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); 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..7576625e9e --- /dev/null +++ b/spi-fly/spi-fly-dynamic-bundle/src/test/java/org/apache/aries/spifly/dynamic/LateMediatorStartupTest.java @@ -0,0 +1,869 @@ +/** + * 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.assertNull; +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; +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.apache.aries.spifly.dynamic.impl2.MySPIImpl2; +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.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +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; + +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)); + 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) { + framework.stop(); + framework.waitForStop(30000); + } + deleteRecursively(storage); + } + } + + @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); + CountDownLatch refreshCompleted = 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(); + } + } + }; + 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()); + 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 { + 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); + + 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); + 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( + 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()); + consumer.start(); + assertEquals(Collections.singleton("HELLO"), + invokeConsumer(consumer.loadClass(TestClient.class.getName()))); + } + finally { + if (framework != null) { + framework.stop(); + framework.waitForStop(30000); + } + deleteRecursively(storage); + } + } + + @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); + } + } + + @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); + } + } + + @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", + "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"); + } + + @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) { + 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 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 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 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); + } + + 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)\""); + 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, + "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( + 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 { + 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()); + 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); + } + } +} 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..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; @@ -58,7 +60,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 +68,37 @@ 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; + } + + 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-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..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,8 +68,20 @@ org.apache.aries.spifly.mysvc.impl5 - osgi.extender; filter:="(osgi.extender=osgi.serviceloader.registrar)" - 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-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 386fd6eb9d..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 @@ -28,6 +28,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +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; @@ -37,9 +42,13 @@ 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; 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; @@ -109,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() @@ -139,21 +175,37 @@ 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); - 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(3).extracting(reference -> reference.getProperty("decorator")) + .containsExactlyInAnyOrder("first", "second", "fragment-embedded"); + + 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) 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..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,9 +48,16 @@ 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"; private static final String MODIFIED_BUNDLE_SUFFIX = "_spifly.jar"; private static final String IMPORT_PACKAGE = "Import-Package"; @@ -100,18 +111,17 @@ 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); + 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); } // TODO if new packages needed then... @@ -125,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); @@ -352,4 +397,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..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; @@ -38,8 +40,17 @@ 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.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; public class RequirementTest { @Test @@ -50,6 +61,10 @@ 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); + 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; @@ -64,7 +79,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)); @@ -73,6 +89,10 @@ 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.putNextEntry(new ZipEntry(test5ClassFileName)); + Streams.pump(test5ClassURL.openStream(), jos); jos.close(); Main.main(jarFile.getCanonicalPath()); @@ -86,8 +106,22 @@ 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)(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)); 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"); @@ -109,6 +143,15 @@ 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)); + + 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 { @@ -118,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/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-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 9a05deec5a..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 @@ -213,10 +306,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())) {