diff --git a/framework/pom.xml b/framework/pom.xml index 065cb19569..fb4c66b35b 100644 --- a/framework/pom.xml +++ b/framework/pom.xml @@ -68,6 +68,7 @@ exports org.osgi.framework.connect; exports org.osgi.framework.dto; exports org.osgi.framework.hooks.bundle; + exports org.osgi.framework.hooks.delegation; exports org.osgi.framework.hooks.resolver; exports org.osgi.framework.hooks.service; exports org.osgi.framework.hooks.weaving; diff --git a/framework/src/main/java/org/apache/felix/framework/BundleRevisionDependencies.java b/framework/src/main/java/org/apache/felix/framework/BundleRevisionDependencies.java index 2d7cb33fc3..89a5db62fa 100644 --- a/framework/src/main/java/org/apache/felix/framework/BundleRevisionDependencies.java +++ b/framework/src/main/java/org/apache/felix/framework/BundleRevisionDependencies.java @@ -40,6 +40,21 @@ class BundleRevisionDependencies private final Map>> m_dependentsMap = new HashMap<>(); + // Revisions that got classes from a provider through a DelegationHook. + // There is no wire for this, so it is tracked separately. + private final Map> + m_delegationDependentsMap = new HashMap<>(); + + public synchronized void addDelegationDependent( + BundleRevision provider, BundleRevision requirer) + { + if (provider != requirer) + { + m_delegationDependentsMap.computeIfAbsent(provider, k -> new HashSet<>()) + .add(requirer); + } + } + public synchronized void addDependent(BundleWire bw) { BundleRevision provider = bw.getProvider(); @@ -85,6 +100,7 @@ public synchronized void removeDependent( public synchronized void removeDependents(BundleRevision provider) { m_dependentsMap.remove(provider); + m_delegationDependentsMap.remove(provider); } public synchronized Map> @@ -197,6 +213,14 @@ public synchronized Set getDependentBundles(Bundle bundle) } } } + Set delegating = m_delegationDependentsMap.get(revision); + if (delegating != null) + { + for (BundleRevision requirer : delegating) + { + result.add(requirer.getBundle()); + } + } } } @@ -275,6 +299,10 @@ public synchronized Set getRequiringBundles(Bundle bundle) public synchronized void removeDependencies(Bundle bundle) { List revs = bundle.adapt(BundleRevisions.class).getRevisions(); + for (Set delegating : m_delegationDependentsMap.values()) + { + delegating.removeAll(revs); + } for (BundleRevision rev : revs) { BundleWiring wiring = rev.getWiring(); diff --git a/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java b/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java index e9df5707a5..8b626cb9e3 100644 --- a/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java +++ b/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java @@ -1186,10 +1186,73 @@ private Enumeration findResourcesByDelegation(String name) } } + Enumeration delegated = findResourcesByDelegationHooks(pkgName, name); + if (delegated != null) + { + completeUrlList.add(delegated); + } + return new CompoundEnumeration((Enumeration[]) completeUrlList.toArray(new Enumeration[completeUrlList.size()])); } + private boolean isDelegationHookCandidate(String pkgName) + { + // Hooks are not asked for java.*, nor for packages that are + // imported, required or exported. + if (pkgName.startsWith("java.") || hasPackageSource(pkgName)) + { + return false; + } + for (BundleCapability cap : m_resolvedCaps) + { + if (BundleRevision.PACKAGE_NAMESPACE.equals(cap.getNamespace()) + && pkgName.equals(cap.getAttributes().get(BundleRevision.PACKAGE_NAMESPACE))) + { + return false; + } + } + return true; + } + + private DelegationHookSupport getDelegationHooks() + { + DelegationHookSupport hooks = + m_revision.getBundle().getFramework().getDelegationHooks(); + return hooks.hasHooks() ? hooks : null; + } + + private Enumeration findResourcesByDelegationHooks(String pkgName, String name) + { + if (isDelegationHookCandidate(pkgName)) + { + DelegationHookSupport hooks = getDelegationHooks(); + if (hooks != null) + { + List urls = hooks.findResources(this, name); + if (!urls.isEmpty()) + { + return Collections.enumeration(urls); + } + } + } + return null; + } + + private Object findByDelegationHooks(String pkgName, String name, boolean isClass) + { + if (isDelegationHookCandidate(pkgName)) + { + DelegationHookSupport hooks = getDelegationHooks(); + if (hooks != null) + { + return isClass ? (Object) hooks.findClass(this, name) + : (Object) hooks.findResource(this, name); + } + } + return null; + } + private ClassLoader determineParentClassLoader() { // Determine the class loader's parent based on the @@ -1520,6 +1583,12 @@ private Object findClassOrResourceByDelegation(String name, boolean isClass) { result = searchDynamicImports(pkgName, name, isClass); } + + // As the last step ask the delegation hooks. + if (result == null) + { + result = findByDelegationHooks(pkgName, name, isClass); + } } } finally @@ -1880,6 +1949,11 @@ public BundleImpl getBundle() return m_wiring.getBundle(); } + BundleWiringImpl getBundleWiring() + { + return m_wiring; + } + @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException diff --git a/framework/src/main/java/org/apache/felix/framework/DelegationHookSupport.java b/framework/src/main/java/org/apache/felix/framework/DelegationHookSupport.java new file mode 100644 index 0000000000..b84f2366f1 --- /dev/null +++ b/framework/src/main/java/org/apache/felix/framework/DelegationHookSupport.java @@ -0,0 +1,170 @@ +/* + * 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.felix.framework; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.ServiceReference; +import org.osgi.framework.hooks.delegation.DelegationHook; +import org.osgi.framework.wiring.BundleWiring; + +/** + * Calls the registered {@link DelegationHook} services on behalf of a + * {@link BundleWiringImpl}. Hooks are called in service ranking order without + * holding any framework lock. A hook that throws is reported as a + * {@link FrameworkEvent#ERROR} and is not called again. + */ +class DelegationHookSupport +{ + private final Felix m_felix; + + DelegationHookSupport(Felix felix) + { + m_felix = felix; + } + + boolean hasHooks() + { + return !hooks().isEmpty(); + } + + Class findClass(BundleWiringImpl wiring, String name) + { + for (ServiceReference sr : hooks()) + { + DelegationHook hook = m_felix.getService(m_felix, sr, false); + if (hook == null) + { + continue; + } + try + { + Class clazz = hook.findClass(wiring, name); + if (clazz != null) + { + ClassLoader cl = clazz.getClassLoader(); + if (cl instanceof BundleWiringImpl.BundleClassLoader) + { + BundleWiringImpl provider = + ((BundleWiringImpl.BundleClassLoader) cl).getBundleWiring(); + m_felix.getDependencies().addDelegationDependent( + provider.getRevision(), wiring.getRevision()); + } + return clazz; + } + } + catch (ClassNotFoundException ex) + { + // The hook knows the class but cannot load it, treat as not provided. + } + catch (Throwable th) + { + failed(sr, th); + } + finally + { + m_felix.ungetService(m_felix, sr, null); + } + } + return null; + } + + URL findResource(BundleWiringImpl wiring, String name) + { + for (ServiceReference sr : hooks()) + { + DelegationHook hook = m_felix.getService(m_felix, sr, false); + if (hook == null) + { + continue; + } + try + { + URL url = hook.findResource(wiring, name); + if (url != null) + { + return url; + } + } + catch (Throwable th) + { + failed(sr, th); + } + finally + { + m_felix.ungetService(m_felix, sr, null); + } + } + return null; + } + + List findResources(BundleWiring wiring, String name) + { + List result = new ArrayList<>(); + for (ServiceReference sr : hooks()) + { + DelegationHook hook = m_felix.getService(m_felix, sr, false); + if (hook == null) + { + continue; + } + try + { + Collection urls = hook.findResources(wiring, name); + if (urls != null) + { + result.addAll(urls); + } + } + catch (Throwable th) + { + failed(sr, th); + } + finally + { + m_felix.ungetService(m_felix, sr, null); + } + } + return result; + } + + private List> hooks() + { + List> result = new ArrayList<>(); + for (ServiceReference sr : + m_felix.getHookRegistry().getHooks(DelegationHook.class)) + { + if (!m_felix.getHookRegistry().isHookBlackListed(sr)) + { + result.add(sr); + } + } + return result; + } + + private void failed(ServiceReference sr, Throwable th) + { + m_felix.getHookRegistry().blackListHook(sr); + m_felix.fireFrameworkEvent(FrameworkEvent.ERROR, sr.getBundle(), th); + } +} diff --git a/framework/src/main/java/org/apache/felix/framework/Felix.java b/framework/src/main/java/org/apache/felix/framework/Felix.java index f2104d2aa5..79720bda2f 100644 --- a/framework/src/main/java/org/apache/felix/framework/Felix.java +++ b/framework/src/main/java/org/apache/felix/framework/Felix.java @@ -166,6 +166,7 @@ public class Felix extends BundleImpl implements Framework // but no lock for reads since it is copy on write. private volatile List m_uninstalledBundles; // Object to keep track of dependencies among bundle revisions. + private final DelegationHookSupport m_delegationHooks = new DelegationHookSupport(this); private final BundleRevisionDependencies m_dependencies = new BundleRevisionDependencies(); @@ -3924,6 +3925,11 @@ else if (Util.isFragment(adapt(BundleRevision.class))) // Hook service management methods. // + DelegationHookSupport getDelegationHooks() + { + return m_delegationHooks; + } + HookRegistry getHookRegistry() { return m_registry.getHookRegistry(); diff --git a/framework/src/main/java/org/apache/felix/framework/HookRegistry.java b/framework/src/main/java/org/apache/felix/framework/HookRegistry.java index 4fb6949744..32807d676d 100644 --- a/framework/src/main/java/org/apache/felix/framework/HookRegistry.java +++ b/framework/src/main/java/org/apache/felix/framework/HookRegistry.java @@ -44,6 +44,7 @@ public class HookRegistry addHookClass(org.osgi.framework.hooks.bundle.CollisionHook.class); addHookClass(org.osgi.framework.hooks.bundle.FindHook.class); addHookClass(org.osgi.framework.hooks.bundle.EventHook.class); + addHookClass(org.osgi.framework.hooks.delegation.DelegationHook.class); addHookClass(org.osgi.framework.hooks.service.EventHook.class); addHookClass(org.osgi.framework.hooks.service.EventListenerHook.class); addHookClass(org.osgi.framework.hooks.service.FindHook.class); diff --git a/framework/src/main/java/org/osgi/framework/hooks/delegation/DelegationHook.java b/framework/src/main/java/org/osgi/framework/hooks/delegation/DelegationHook.java new file mode 100644 index 0000000000..377dccce9b --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/delegation/DelegationHook.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ + +package org.osgi.framework.hooks.delegation; + +import java.net.URL; +import java.util.Collection; + +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.framework.wiring.BundleWiring; + +/** + * OSGi Framework Delegation Hook Service. + * + *

+ * Bundles registering this service are called by the framework as the last step + * of the search order of a bundle class loader, to contribute classes and + * resources the search order did not provide. A hook can only add; it cannot + * replace or hide what the wiring of the asking bundle provides. + *

+ * + *

+ * The framework does not call a hook for classes and resources in + * {@code java.*} packages, nor for packages the asking bundle imports, + * requires or exports. + *

+ * + *

+ * Hooks are called in service ranking order. The framework must not hold any + * locks while calling a hook, and must not call a hook for the same wiring and + * name while the hook is already running on the same thread. If a hook throws + * an exception, the framework publishes a + * {@link org.osgi.framework.FrameworkEvent#ERROR} and continues as if the hook + * had returned nothing. + *

+ * + *

+ * If a class returned by {@link #findClass(BundleWiring, String)} is defined by + * the class loader of another bundle, the asking bundle depends on that bundle + * for the purposes of refreshing, like it does for a dynamic import. + *

+ * + * @ThreadSafe + * @since 1.0 + */ +@ConsumerType +public interface DelegationHook { + + /** + * Find a class the search order of the asking bundle did not find. + * + *

+ * This method is only called if the search order found no class. + *

+ * + * @param wiring The wiring of the asking bundle. Not {@code null}. + * @param name The binary name of the class. Not {@code null}. + * @return The class, or {@code null} if this hook does not provide it. The + * class may be defined by any class loader. + * @throws ClassNotFoundException If the hook knows the class but cannot load + * it. + */ + Class findClass(BundleWiring wiring, String name) throws ClassNotFoundException; + + /** + * Find a resource the search order of the asking bundle did not find. + * + *

+ * This method is only called if the search order found no resource. + *

+ * + * @param wiring The wiring of the asking bundle. Not {@code null}. + * @param name The resource name. Not {@code null}. + * @return The URL of the resource, or {@code null} if this hook does not + * provide it. + */ + URL findResource(BundleWiring wiring, String name); + + /** + * Find resources to add to those the search order of the asking bundle + * found. + * + *

+ * This method is called whether or not the search order found resources. + * The returned URLs are appended after those the search order found, and + * after those of hooks earlier in the call order. + *

+ * + * @param wiring The wiring of the asking bundle. Not {@code null}. + * @param name The resource name. Not {@code null}. + * @return The URLs of the resources to add. May be {@code null} or empty. + */ + Collection findResources(BundleWiring wiring, String name); +} diff --git a/framework/src/main/java/org/osgi/framework/hooks/delegation/package-info.java b/framework/src/main/java/org/osgi/framework/hooks/delegation/package-info.java new file mode 100644 index 0000000000..6585d49f02 --- /dev/null +++ b/framework/src/main/java/org/osgi/framework/hooks/delegation/package-info.java @@ -0,0 +1,38 @@ +/******************************************************************************* + * Copyright (c) Contributors to the Eclipse Foundation + * + * Licensed 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. + * + * SPDX-License-Identifier: Apache-2.0 + *******************************************************************************/ + +/** + * Framework Delegation Hooks Package Version 1.0. + * + *

+ * Bundles wishing to use this package must list the package in the + * Import-Package header of the bundle's manifest. + *

+ * + *

+ * Example import for consumers using the API in this package: + *

+ *

+ * {@code Import-Package: org.osgi.framework.hooks.delegation; version="[1.0,2.0)"} + *

+ */ + +@Version("1.0") +package org.osgi.framework.hooks.delegation; + +import org.osgi.annotation.versioning.Version; diff --git a/framework/src/main/resources/default.properties b/framework/src/main/resources/default.properties index 9d5c4fe54c..2d636d1c99 100644 --- a/framework/src/main/resources/default.properties +++ b/framework/src/main/resources/default.properties @@ -110,6 +110,7 @@ osgi-exports= \ org.osgi.framework.connect;version="1.0.0", \ org.osgi.framework.dto;version="1.8";uses:="org.osgi.dto", \ org.osgi.framework.hooks.bundle;version="1.1";uses:="org.osgi.framework", \ + org.osgi.framework.hooks.delegation;version="1.0";uses:="org.osgi.framework.wiring", \ org.osgi.framework.hooks.resolver;version="1.0";uses:="org.osgi.framework.wiring", \ org.osgi.framework.hooks.service;version="1.1";uses:="org.osgi.framework", \ org.osgi.framework.hooks.weaving;version="1.1";uses:="org.osgi.framework.wiring", \ diff --git a/framework/src/test/java/org/apache/felix/framework/DelegationHookTest.java b/framework/src/test/java/org/apache/felix/framework/DelegationHookTest.java new file mode 100644 index 0000000000..ff9bcfbee4 --- /dev/null +++ b/framework/src/test/java/org/apache/felix/framework/DelegationHookTest.java @@ -0,0 +1,254 @@ +/* + * 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.felix.framework; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; +import java.util.zip.ZipEntry; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.osgi.framework.Bundle; +import org.osgi.framework.Constants; +import org.osgi.framework.FrameworkEvent; +import org.osgi.framework.FrameworkListener; +import org.osgi.framework.hooks.delegation.DelegationHook; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.wiring.BundleWiring; +import org.osgi.framework.wiring.FrameworkWiring; + +class DelegationHookTest +{ + private static final String PROVIDED = DelegationHookTest.class.getName() + "$Provided"; + + private File tempDir; + private Framework felix; + + @BeforeEach + void setUp() throws Exception + { + tempDir = Files.createTempDirectory("felix-delegation").toFile(); + Map params = new HashMap<>(); + params.put(Constants.FRAMEWORK_STORAGE, new File(tempDir, "cache").getPath()); + params.put(Constants.FRAMEWORK_STORAGE_CLEAN, Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT); + felix = new Felix(params); + felix.start(); + } + + @AfterEach + void tearDown() throws Exception + { + felix.stop(); + felix.waitForStop(10000); + deleteRecursive(tempDir); + } + + @Test + void classIsFoundAndAskingBundleDependsOnProvider() throws Exception + { + Bundle provider = install("provider", true, "data/ignored.txt"); + Bundle asker = install("asker", false); + provider.start(); + asker.start(); + + TestHook hook = new TestHook(); + hook.clazz = provider.loadClass(PROVIDED); + felix.getBundleContext().registerService(DelegationHook.class, hook, null); + + assertThat(asker.loadClass(PROVIDED)).isSameAs(hook.clazz); + assertThat(hook.classRequests).containsExactly(PROVIDED); + + Collection closure = felix.adapt(FrameworkWiring.class) + .getDependencyClosure(Collections.singleton(provider)); + assertThat(closure).contains(provider, asker); + } + + @Test + void hookIsNotCalledForJavaPackages() throws Exception + { + Bundle asker = install("asker", false); + asker.start(); + TestHook hook = new TestHook(); + felix.getBundleContext().registerService(DelegationHook.class, hook, null); + + assertThat(asker.loadClass("java.lang.String")).isSameAs(String.class); + try + { + asker.loadClass("java.lang.DoesNotExist"); + } + catch (ClassNotFoundException ex) + { + // expected + } + assertThat(hook.classRequests).isEmpty(); + } + + @Test + void resourcesAreAddedAndSingleResourceOnlyIfNothingFound() throws Exception + { + Bundle asker = install("asker", false, "data/own.txt"); + asker.start(); + URL extra = new URL("file:/extra"); + TestHook hook = new TestHook(); + hook.resource = extra; + hook.resources = Collections.singletonList(extra); + felix.getBundleContext().registerService(DelegationHook.class, hook, null); + + assertThat(asker.getResource("data/missing.txt")).isEqualTo(extra); + + URL own = asker.getResource("data/own.txt"); + assertThat(own).isNotNull().isNotEqualTo(extra); + assertThat(hook.resourceRequests).containsExactly("data/missing.txt"); + + List all = Collections.list(asker.adapt(BundleWiring.class) + .getClassLoader().getResources("data/own.txt")); + assertThat(all).containsExactly(own, extra); + } + + @Test + void throwingHookIsReportedAndIgnored() throws Exception + { + Bundle asker = install("asker", false); + asker.start(); + List events = new CopyOnWriteArrayList<>(); + felix.getBundleContext().addFrameworkListener((FrameworkListener) events::add); + + TestHook hook = new TestHook(); + hook.failure = new IllegalStateException("boom"); + felix.getBundleContext().registerService(DelegationHook.class, hook, null); + + try + { + asker.loadClass("does.not.Exist"); + } + catch (ClassNotFoundException ex) + { + // expected + } + assertThat(events).anyMatch(e -> e.getType() == FrameworkEvent.ERROR + && e.getThrowable() == hook.failure); + + hook.classRequests.clear(); + try + { + asker.loadClass("does.not.Exist2"); + } + catch (ClassNotFoundException ex) + { + // expected + } + assertThat(hook.classRequests).isEmpty(); + } + + private Bundle install(String name, boolean withProvided, String... resources) throws Exception + { + Manifest mf = new Manifest(); + mf.getMainAttributes().putValue("Manifest-Version", "1.0"); + mf.getMainAttributes().putValue(Constants.BUNDLE_MANIFESTVERSION, "2"); + mf.getMainAttributes().putValue(Constants.BUNDLE_SYMBOLICNAME, name); + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + try (JarOutputStream jar = new JarOutputStream(bout, mf)) + { + for (String resource : resources) + { + jar.putNextEntry(new ZipEntry(resource)); + jar.write(1); + jar.closeEntry(); + } + if (withProvided) + { + String path = PROVIDED.replace('.', '/') + ".class"; + jar.putNextEntry(new ZipEntry(path)); + try (InputStream in = getClass().getClassLoader().getResourceAsStream(path)) + { + in.transferTo(jar); + } + jar.closeEntry(); + } + } + return felix.getBundleContext().installBundle(name, new ByteArrayInputStream(bout.toByteArray())); + } + + private static void deleteRecursive(File f) + { + File[] children = f.listFiles(); + if (children != null) + { + for (File c : children) + { + deleteRecursive(c); + } + } + f.delete(); + } + + public static class Provided + { + } + + static class TestHook implements DelegationHook + { + final List classRequests = new CopyOnWriteArrayList<>(); + final List resourceRequests = new CopyOnWriteArrayList<>(); + volatile Class clazz; + volatile URL resource; + volatile List resources = new ArrayList<>(); + volatile RuntimeException failure; + + @Override + public Class findClass(BundleWiring wiring, String name) throws ClassNotFoundException + { + classRequests.add(name); + if (failure != null) + { + throw failure; + } + return PROVIDED.equals(name) ? clazz : null; + } + + @Override + public URL findResource(BundleWiring wiring, String name) + { + resourceRequests.add(name); + return resource; + } + + @Override + public Collection findResources(BundleWiring wiring, String name) + { + return resources; + } + } +} diff --git a/main/pom.xml b/main/pom.xml index cd17f5847c..20a5236f53 100644 --- a/main/pom.xml +++ b/main/pom.xml @@ -137,6 +137,7 @@ exports org.osgi.framework.connect; exports org.osgi.framework.dto; exports org.osgi.framework.hooks.bundle; + exports org.osgi.framework.hooks.delegation; exports org.osgi.framework.hooks.resolver; exports org.osgi.framework.hooks.service; exports org.osgi.framework.hooks.weaving;