This is an automated email from the ASF dual-hosted git repository. pauls pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/felix-atomos.git
The following commit(s) were added to refs/heads/master by this push: new eb3a410 Add a headers provider hook (#46) eb3a410 is described below commit eb3a41073ea7c89e3f0a0d0297cd58d2c0a1cc86 Author: Karl Pauls <pa...@apache.org> AuthorDate: Wed Feb 17 14:56:07 2021 +0100 Add a headers provider hook (#46) * Add a new HeaderProvider that can be used to augment existing bundle manifest headers or add completely new bundle manifest headers that are not present in the existing headers. * Allow the HeaderProvider to provide headers for content that has no headers Co-authored-by: Thomas Watson <tjwat...@us.ibm.com> --- .../service/test/ClasspathLaunchTest.java | 103 +++++++ .../tests/index/bundles/IndexLaunchTest.java | 104 +++++++ .../modulepath/service/ModulepathLaunchTest.java | 132 ++++++++- atomos/pom.xml | 16 ++ .../main/java/org/apache/felix/atomos/Atomos.java | 100 ++++++- .../org/apache/felix/atomos/AtomosContent.java | 2 + .../java/org/apache/felix/atomos/AtomosLayer.java | 3 + .../apache/felix/atomos/impl/base/AtomosBase.java | 271 +++++++++++++----- .../felix/atomos/impl/base/AtomosClassPath.java | 4 +- .../impl/content/ConnectContentCloseableJar.java | 6 +- .../atomos/impl/content/ConnectContentFile.java | 8 +- .../atomos/impl/content/ConnectContentIndexed.java | 7 +- .../atomos/impl/content/ConnectContentJar.java | 11 +- .../felix/atomos/impl/modules/AtomosModules.java | 309 ++++++++++++++++----- .../atomos/impl/modules/ConnectContentModule.java | 191 +------------ 15 files changed, 901 insertions(+), 366 deletions(-) diff --git a/atomos.tests/atomos.tests.classpath.service/src/test/java/org/apache/felix/atomos/tests/classpath/service/test/ClasspathLaunchTest.java b/atomos.tests/atomos.tests.classpath.service/src/test/java/org/apache/felix/atomos/tests/classpath/service/test/ClasspathLaunchTest.java index 9851280..bf53a12 100644 --- a/atomos.tests/atomos.tests.classpath.service/src/test/java/org/apache/felix/atomos/tests/classpath/service/test/ClasspathLaunchTest.java +++ b/atomos.tests/atomos.tests.classpath.service/src/test/java/org/apache/felix/atomos/tests/classpath/service/test/ClasspathLaunchTest.java @@ -26,16 +26,20 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.felix.atomos.Atomos; +import org.apache.felix.atomos.Atomos.HeaderProvider; import org.apache.felix.atomos.AtomosContent; import org.apache.felix.atomos.AtomosLayer; import org.apache.felix.atomos.AtomosLayer.LoaderType; import org.apache.felix.atomos.impl.base.AtomosCommands; import org.apache.felix.atomos.tests.testbundles.service.contract.Echo; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.osgi.framework.Bundle; @@ -45,6 +49,7 @@ import org.osgi.framework.Constants; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; import org.osgi.framework.launch.Framework; import org.osgi.framework.namespace.PackageNamespace; import org.osgi.framework.wiring.BundleCapability; @@ -171,6 +176,104 @@ public class ClasspathLaunchTest assertNotNull(mf, "No manifest found."); } + @Test + void testUnmodifiableExistingHeaders(@TempDir Path storage) throws BundleException + { + AtomicBoolean fail = new AtomicBoolean(true); + HeaderProvider attemptModification = (location, headers) -> { + try + { + headers.put(Constants.BUNDLE_SYMBOLICNAME, "should.fail"); + fail.set(true); + } + catch (UnsupportedOperationException e) + { + // expected + fail.set(false); + } + return Optional.empty(); + }; + testFramework = Atomos.newAtomos(attemptModification).newFramework( + Map.of(Constants.FRAMEWORK_STORAGE, storage.toFile().getAbsolutePath())); + testFramework.start(); + if (fail.get()) + { + Assertions.fail("Was able to modify the existing headers"); + } + } + + @Test + void testBundleWithCustomHeader(@TempDir Path storage) throws BundleException, InterruptedException + { + HeaderProvider headerProvider = ( + location, headers) -> { + headers = new HashMap<>(headers); + headers.put("X-TEST", location); + return Optional.of(headers); + }; + testFramework = Atomos.newAtomos(headerProvider).newFramework( + Map.of(Constants.FRAMEWORK_STORAGE, storage.toFile().getAbsolutePath())); + testFramework.start(); + BundleContext bc = testFramework.getBundleContext(); + assertNotNull(bc, "No context found."); + + Atomos runtime = getRuntime(bc); + Bundle b = assertFindBundle(TESTBUNDLES_SERVICE_IMPL_A, runtime.getBootLayer(), + runtime.getBootLayer(), true).getBundle(); + assertEquals(b.getLocation(), "atomos:" + b.getHeaders().get("X-TEST")); + + testFramework.stop(); + testFramework.waitForStop(10000); + + // Bundles should already be installed, disable auto-install option + // and check the provider is still used to provide the custom header + // for the already installed bundle from persistence + testFramework = Atomos.newAtomos(Map.of(Atomos.ATOMOS_CONTENT_INSTALL, "false"), + headerProvider).newFramework( + Map.of(Constants.FRAMEWORK_STORAGE, storage.toFile().getAbsolutePath())); + testFramework.start(); + bc = testFramework.getBundleContext(); + assertNotNull(bc, "No context found."); + runtime = getRuntime(bc); + b = assertFindBundle(TESTBUNDLES_SERVICE_IMPL_A, runtime.getBootLayer(), + runtime.getBootLayer(), true).getBundle(); + assertEquals(b.getLocation(), "atomos:" + b.getHeaders().get("X-TEST")); + } + + @Test + void testHeaderProviderChangeBSN(@TempDir Path storage) throws BundleException + { + final String BSN_CONTRACT = "atomos.service.contract"; + final String CHANGED_BSN = "changed.bsn"; + HeaderProvider changeBSN = (location, headers) -> { + if (BSN_CONTRACT.equals(headers.get(Constants.BUNDLE_SYMBOLICNAME))) + { + headers = new HashMap<>(headers); + headers.put(Constants.BUNDLE_SYMBOLICNAME, CHANGED_BSN); + headers.put(Constants.BUNDLE_VERSION, "100"); + return Optional.of(headers); + } + return Optional.empty(); + }; + Atomos atomos = Atomos.newAtomos(changeBSN); + testFramework = atomos.newFramework( + Map.of(Constants.FRAMEWORK_STORAGE, storage.toFile().getAbsolutePath())); + testFramework.start(); + + // make sure the contract names are correct + Bundle contractBundle = FrameworkUtil.getBundle(Echo.class); + assertEquals(CHANGED_BSN, contractBundle.getSymbolicName(), + "Wrong BSN for contract bundle."); + assertEquals(Version.valueOf("100"), contractBundle.getVersion()); + + atomos.getBootLayer().findAtomosContent(CHANGED_BSN).ifPresentOrElse((c) -> { + assertEquals(CHANGED_BSN, c.getSymbolicName()); + assertEquals(Version.valueOf("100"), c.getVersion()); + }, () -> { + fail("Could not find the content: " + CHANGED_BSN); + }); + } + private AtomosContent assertFindBundle(String name, AtomosLayer layer, AtomosLayer expectedLayer, boolean expectedToFind) { diff --git a/atomos.tests/atomos.tests.index.bundles/src/test/java/org/apache/felix/atomos/tests/index/bundles/IndexLaunchTest.java b/atomos.tests/atomos.tests.index.bundles/src/test/java/org/apache/felix/atomos/tests/index/bundles/IndexLaunchTest.java index 1cd32bd..80fe000 100644 --- a/atomos.tests/atomos.tests.index.bundles/src/test/java/org/apache/felix/atomos/tests/index/bundles/IndexLaunchTest.java +++ b/atomos.tests/atomos.tests.index.bundles/src/test/java/org/apache/felix/atomos/tests/index/bundles/IndexLaunchTest.java @@ -18,6 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import java.io.BufferedReader; import java.io.IOException; @@ -25,16 +26,21 @@ import java.io.InputStreamReader; import java.net.URL; import java.nio.file.Path; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; import java.util.stream.Collectors; import org.apache.felix.atomos.Atomos; +import org.apache.felix.atomos.Atomos.HeaderProvider; import org.apache.felix.atomos.AtomosContent; import org.apache.felix.atomos.AtomosLayer; import org.apache.felix.atomos.impl.base.AtomosBase; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.osgi.framework.Bundle; @@ -44,6 +50,7 @@ import org.osgi.framework.BundleException; import org.osgi.framework.Constants; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; import org.osgi.framework.launch.Framework; import org.osgi.framework.wiring.BundleWiring; @@ -327,4 +334,101 @@ public class IndexLaunchTest } return result.orElse(null); } + + @Test + void testUnmodifiableExistingHeaders(@TempDir Path storage) throws BundleException + { + AtomicBoolean fail = new AtomicBoolean(true); + HeaderProvider attemptModification = (location, headers) -> { + try + { + headers.put(Constants.BUNDLE_SYMBOLICNAME, "should.fail"); + fail.set(true); + } + catch (UnsupportedOperationException e) + { + // expected + fail.set(false); + } + return Optional.empty(); + }; + testFramework = Atomos.newAtomos(attemptModification).newFramework( + Map.of(Constants.FRAMEWORK_STORAGE, storage.toFile().getAbsolutePath())); + testFramework.start(); + if (fail.get()) + { + Assertions.fail("Was able to modify the existing headers"); + } + } + + @Test + void testBundleWithCustomHeader(@TempDir Path storage) throws BundleException, InterruptedException + { + HeaderProvider provider = (location, headers) -> { + headers = new HashMap<>(headers); + headers.put("X-TEST", location); + return Optional.of(headers); + }; + testFramework = Atomos.newAtomos(provider).newFramework( + Map.of(Constants.FRAMEWORK_STORAGE, storage.toFile().getAbsolutePath())); + testFramework.start(); + BundleContext bc = testFramework.getBundleContext(); + assertNotNull(bc, "No context found."); + + Consumer<AtomosContent> verifyHeader = c -> { + if (c.getBundle().getBundleId() == 0) + { + return; + } + String customHeader = c.getBundle().getHeaders(null).get("X-TEST"); + assertEquals(c.getAtomosLocation(), customHeader, "Wrong header value"); + }; + + Atomos atomos = getRuntime(bc); + atomos.getBootLayer().getAtomosContents().forEach(verifyHeader); + + testFramework.stop(); + testFramework.waitForStop(10000); + + // Bundles should already be installed, disable auto-install option + // and check the provider is still used to provide the custom header + // for the already installed bundle from persistence + atomos = Atomos.newAtomos(Map.of(Atomos.ATOMOS_CONTENT_INSTALL, "false"), + provider); + testFramework = atomos.newFramework( + Map.of(Constants.FRAMEWORK_STORAGE, storage.toFile().getAbsolutePath())); + testFramework.start(); + atomos.getBootLayer().getAtomosContents().forEach(verifyHeader); + } + + @Test + void testHeaderProviderChangeBSN(@TempDir Path storage) throws BundleException + { + final String BSN_BUNDLE_1 = "bundle.1"; + final String CHANGED_BSN = "changed.bsn"; + HeaderProvider changeBSN = (location, headers) -> { + if (BSN_BUNDLE_1.equals(headers.get(Constants.BUNDLE_SYMBOLICNAME))) + { + headers = new HashMap<>(headers); + headers.put(Constants.BUNDLE_SYMBOLICNAME, CHANGED_BSN); + headers.put(Constants.BUNDLE_VERSION, "100"); + return Optional.of(headers); + } + return Optional.empty(); + }; + Atomos atomos = Atomos.newAtomos(changeBSN); + testFramework = atomos.newFramework( + Map.of(Constants.FRAMEWORK_STORAGE, storage.toFile().getAbsolutePath())); + testFramework.start(); + + atomos.getBootLayer().findAtomosContent(CHANGED_BSN).ifPresentOrElse((c) -> { + assertEquals(CHANGED_BSN, c.getBundle().getSymbolicName(), + "Bundle symbolic name is incorrect."); + assertEquals(CHANGED_BSN, c.getSymbolicName(), + "Atomos content symbolic name is incorrect."); + assertEquals(Version.valueOf("100"), c.getVersion()); + }, () -> { + fail("Could not find the content: " + CHANGED_BSN); + }); + } } diff --git a/atomos.tests/atomos.tests.modulepath.service/src/test/java/org/apache/felix/atomos/tests/modulepath/service/ModulepathLaunchTest.java b/atomos.tests/atomos.tests.modulepath.service/src/test/java/org/apache/felix/atomos/tests/modulepath/service/ModulepathLaunchTest.java index fe2dc65..0b0d31f 100644 --- a/atomos.tests/atomos.tests.modulepath.service/src/test/java/org/apache/felix/atomos/tests/modulepath/service/ModulepathLaunchTest.java +++ b/atomos.tests/atomos.tests.modulepath.service/src/test/java/org/apache/felix/atomos/tests/modulepath/service/ModulepathLaunchTest.java @@ -15,6 +15,7 @@ package org.apache.felix.atomos.tests.modulepath.service; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -30,6 +31,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -37,14 +39,17 @@ import java.util.Objects; import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; import org.apache.felix.atomos.Atomos; +import org.apache.felix.atomos.Atomos.HeaderProvider; import org.apache.felix.atomos.AtomosContent; import org.apache.felix.atomos.AtomosLayer; import org.apache.felix.atomos.AtomosLayer.LoaderType; import org.apache.felix.atomos.tests.testbundles.service.contract.Echo; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.osgi.framework.Bundle; @@ -55,6 +60,7 @@ import org.osgi.framework.Constants; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceReference; +import org.osgi.framework.Version; import org.osgi.framework.connect.ConnectFrameworkFactory; import org.osgi.framework.launch.Framework; import org.osgi.framework.namespace.BundleNamespace; @@ -249,6 +255,20 @@ public class ModulepathLaunchTest return framework; } + private Framework getFramework(Path modules, HeaderProvider provider, String... args) + throws BundleException + { + Map<String, String> config = Atomos.getConfiguration(args); + Atomos atomos = Atomos.newAtomos(config, provider); + if (modules != null) + { + atomos.getBootLayer().addModules("modules", modules); + } + Framework framework = atomos.newFramework(config); + framework.start(); + return framework; + } + private ClassLoader getCLForResourceTests(Path storage) throws BundleException { Path[] testBundle = findModulePaths(TESTBUNDLES_RESOURCE_A); @@ -897,8 +917,8 @@ public class ModulepathLaunchTest } } - private static String BSN_CONTRACT = "atomos.service.contract"; - private static String BSN_SERVICE_IMPL = "org.apache.felix.atomos.tests.testbundles.service.impl"; + private static final String BSN_CONTRACT = "atomos.service.contract"; + private static final String BSN_SERVICE_IMPL = "org.apache.felix.atomos.tests.testbundles.service.impl"; @Test void testConnectLocation(@TempDir Path storage) throws BundleException, InterruptedException @@ -1066,6 +1086,114 @@ public class ModulepathLaunchTest } @Test + void testUnmodifiableExistingHeaders(@TempDir Path storage) throws BundleException + { + AtomicBoolean fail = new AtomicBoolean(true); + HeaderProvider attemptModification = (location, headers) -> { + try + { + headers.put(Constants.BUNDLE_SYMBOLICNAME, "should.fail"); + fail.set(true); + } + catch (UnsupportedOperationException e) + { + // expected + fail.set(false); + } + return Optional.empty(); + }; + testFramework = Atomos.newAtomos(attemptModification).newFramework( + Map.of(Constants.FRAMEWORK_STORAGE, storage.toFile().getAbsolutePath())); + testFramework.start(); + if (fail.get()) + { + Assertions.fail("Was able to modify the existing headers"); + } + } + + @Test + void testModuleWithCustomerHeader(@TempDir Path storage) throws BundleException, InterruptedException + { + HeaderProvider provider = (location, headers) -> { + headers = new HashMap<>(headers); + headers.put("X-TEST", location); + return Optional.of(headers); + }; + testFramework = getFramework(null, provider, + Constants.FRAMEWORK_STORAGE + '=' + storage.toFile().getAbsolutePath()); + + // make sure the contract names are correct + Module contractModule = Echo.class.getModule(); + Bundle contractBundle = FrameworkUtil.getBundle(Echo.class); + assertEquals(BSN_CONTRACT, contractBundle.getSymbolicName(), + "Wrong BSN for contract bundle."); + assertEquals(Echo.class.getPackageName(), contractModule.getName(), + "Wrong module name for contract module."); + + assertEquals(contractBundle.getLocation(), "atomos:" + contractBundle.getHeaders().get("X-TEST")); + + testFramework.stop(); + testFramework.waitForStop(10000); + + // Bundles should already be installed, disable auto-install option + // and check the provider is still used to provide the custom header + // for the already installed bundle from persistence + testFramework = Atomos.newAtomos(Map.of(Atomos.ATOMOS_CONTENT_INSTALL, "false"), + provider).newFramework( + Map.of(Constants.FRAMEWORK_STORAGE, storage.toFile().getAbsolutePath())); + testFramework.start(); + Bundle contractBundle2 = FrameworkUtil.getBundle(Echo.class); + assertNotEquals(contractBundle, contractBundle2, "Expecting new bundle."); + assertEquals(contractBundle.getLocation(), + "atomos:" + contractBundle.getHeaders().get("X-TEST")); + } + + @Test + void testHeaderProviderChangeBSN(@TempDir Path storage) throws BundleException + { + final String CHANGED_BSN = "changed.bsn"; + HeaderProvider changeBSN = (location, headers) -> { + if (BSN_CONTRACT.equals(headers.get(Constants.BUNDLE_SYMBOLICNAME))) + { + headers = new HashMap<>(headers); + headers.put(Constants.BUNDLE_SYMBOLICNAME, CHANGED_BSN); + headers.put(Constants.BUNDLE_VERSION, "100"); + return Optional.of(headers); + } + return Optional.empty(); + }; + testFramework = getFramework(null, changeBSN, + Constants.FRAMEWORK_STORAGE + '=' + storage.toFile().getAbsolutePath()); + BundleContext bc = testFramework.getBundleContext(); + Atomos atomos = bc.getService(bc.getServiceReference(Atomos.class)); + + // make sure the contract names are correct + Module contractModule = Echo.class.getModule(); + Bundle contractBundle = FrameworkUtil.getBundle(Echo.class); + assertEquals(CHANGED_BSN, contractBundle.getSymbolicName(), + "Wrong BSN for contract bundle."); + assertEquals(Version.valueOf("100"), contractBundle.getVersion()); + assertEquals(Echo.class.getPackageName(), contractModule.getName(), + "Wrong module name for contract module."); + + // make sure the bundle wiring reflect the mapping correctly using the BSN + Bundle testBundle = FrameworkUtil.getBundle(ModulepathLaunch.class); + BundleWiring testWiring = testBundle.adapt(BundleWiring.class); + assertTrue(testWiring.getRequiredWires(BundleNamespace.BUNDLE_NAMESPACE).stream() // + .filter((w) -> CHANGED_BSN.equals( // + w.getCapability().getAttributes().get( // + BundleNamespace.BUNDLE_NAMESPACE))) // + .findFirst().isPresent(), "No wire for " + CHANGED_BSN); + + atomos.getBootLayer().findAtomosContent(CHANGED_BSN).ifPresentOrElse((c) -> { + assertEquals(CHANGED_BSN, c.getSymbolicName()); + assertEquals(Version.valueOf("100"), c.getVersion()); + }, () -> { + fail("Could not find the content: " + CHANGED_BSN); + }); + } + + @Test void testMultiParentResolve(@TempDir Path storage) throws BundleException { testFramework = getFramework(null, diff --git a/atomos/pom.xml b/atomos/pom.xml index a593ede..c0c7a18 100644 --- a/atomos/pom.xml +++ b/atomos/pom.xml @@ -47,6 +47,22 @@ </activation> </profile> <profile> + <id>unit-test</id> + <dependencies> + <dependency> + <groupId>org.eclipse.platform</groupId> + <artifactId>org.eclipse.osgi</artifactId> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>org.apache.felix.atomos.equinox</groupId> + <artifactId>osgi.core</artifactId> + <version>8.0.0-SNAPSHOT</version> + <scope>provided</scope> + </dependency> + </dependencies> + </profile> + <profile> <id>equinox</id> <dependencies> <dependency> diff --git a/atomos/src/main/java/org/apache/felix/atomos/Atomos.java b/atomos/src/main/java/org/apache/felix/atomos/Atomos.java index edd2000..3b9f495 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/Atomos.java +++ b/atomos/src/main/java/org/apache/felix/atomos/Atomos.java @@ -13,17 +13,24 @@ */ package org.apache.felix.atomos; +import static org.apache.felix.atomos.impl.base.AtomosBase.NO_OP_HEADER_PROVIDER; + import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.ServiceLoader; +import java.util.function.BiFunction; import org.apache.felix.atomos.AtomosLayer.LoaderType; import org.apache.felix.atomos.impl.base.AtomosBase; +import org.osgi.annotation.versioning.ConsumerType; +import org.osgi.annotation.versioning.ProviderType; import org.osgi.framework.BundleException; import org.osgi.framework.Constants; +import org.osgi.framework.connect.ConnectContent; import org.osgi.framework.connect.ConnectFrameworkFactory; import org.osgi.framework.connect.ModuleConnector; import org.osgi.framework.launch.Framework; @@ -134,6 +141,7 @@ import org.osgi.framework.launch.Framework; * service registered with its bundle context. */ @org.osgi.annotation.bundle.Header(name = "Main-Class", value = "org.apache.felix.atomos.Atomos") +@ProviderType public interface Atomos { /** @@ -205,7 +213,7 @@ public interface Atomos * @param frameworkConfig The framework configuration options, or {@code null} if the defaults should be used * @return The new uninitialized Framework instance which uses this Atomos instance */ - public Framework newFramework(Map<String, String> frameworkConfig); + Framework newFramework(Map<String, String> frameworkConfig); /** * A main method that can be used by executable jars to initialize and start @@ -236,7 +244,7 @@ public interface Atomos * cannot contain an '=' (equals) character. * @return a map of the configuration specified by the args */ - public static Map<String, String> getConfiguration(String... args) + static Map<String, String> getConfiguration(String... args) { Map<String, String> config = new HashMap<>(); if (args != null) @@ -264,7 +272,33 @@ public interface Atomos */ static Atomos newAtomos() { - return newAtomos(Collections.emptyMap()); + return newAtomos(NO_OP_HEADER_PROVIDER); + } + + /** + * Creates a new Atomos that can be used to create a new OSGi framework + * instance. Same as calling {@code newAtomos(Map,HeaderProvider)} with an empty + * configuration. + * + * @param headerProvider the header provider function + * @return a new Atomos. + */ + static Atomos newAtomos(HeaderProvider headerProvider) + { + return newAtomos(Collections.emptyMap(), headerProvider); + } + + /** + * Creates a new Atomos that can be used to create a new OSGi framework + * instance. Same as calling {@code newAtomos(Map,HeaderProvider)} with a + * no-op {@code headerProvider} function. + * + * @param configuration the properties to configure the new Atomos + * @return a new Atomos. + */ + static Atomos newAtomos(Map<String, String> configuration) + { + return newAtomos(configuration, NO_OP_HEADER_PROVIDER); } /** @@ -277,15 +311,61 @@ public interface Atomos * will be automatically installed and started according to the * {@link #ATOMOS_CONTENT_INSTALL} and {@link #ATOMOS_CONTENT_START} options. * <p> - * Note that this {@code Atomos} must be used for creating a new - * {@link ConnectFrameworkFactory#newFramework(Map, ModuleConnector)} instance to use - * the layers added to this {@code Atomos}. - * - * @param configuration the properties to configure the new runtime + * Note that this {@code Atomos} must be used for creating a new framework instance + * with the method {@link ConnectFrameworkFactory#newFramework(Map, ModuleConnector)} to use + * the layers added to this {@code Atomos} or the {@link #newFramework(Map)} method can + * be called on this {@code Atomos}. + * <p> + * The given headerProvider function maps each Atomos content + * {@link AtomosContent#getAtomosLocation() location} + * and existing headers of the content to a new optional map of headers. + * The resulting map will be used as the headers for the {@link ConnectContent#getHeaders()}. + * If the function returns an empty optional then the existing + * headers will be used. + * + * @param configuration the properties to configure the new Atomos + * @param headerProvider a function that will be called with the location and the existing headers for each Atomos content. * @return a new Atomos. */ - static Atomos newAtomos(Map<String, String> configuration) + static Atomos newAtomos(Map<String, String> configuration, + HeaderProvider headerProvider) { - return AtomosBase.newAtomos(configuration); + return AtomosBase.newAtomos(configuration, headerProvider); + } + + /** + * A function that maps each {@code AtomosContent} {@link AtomosContent#getAtomosLocation() location} + * and its existing headers to a new optional map of headers to be used for the + * {@link ConnectContent#getHeaders() headers} of the {@link AtomosContent#getConnectContent() ConnectContent}. + */ + @FunctionalInterface + @ConsumerType + interface HeaderProvider extends BiFunction<String, Map<String, String>, Optional<Map<String, String>>> + { + + /** + * Applies this header provider function to the specified + * {@code AtomosContent} {@link AtomosContent#getAtomosLocation() location} and + * map of existing headers. The returned {@code Optional} map of headers will + * be used by the {@link ConnectContent#getHeaders()} method for the + * {@code ConnectContent} {@link AtomosContent#getConnectContent() associated} + * with the {@code AtomosContent} that has the specified + * {@link AtomosContent#getAtomosLocation() location}. + * <p> + * This method allows a header provider to augment existing bundle manifest + * headers or add completely new bundle manifest headers that are not present + * in the existing headers. + * <p> + * This function may be applied before the instance of the {@code AtomosContent} + * instance is created which may result in the symbolic name and or version + * of the {@code AtomosContent} to be influenced by this function. + * @param location The {@code AtomosContent} {@link AtomosContent#getAtomosLocation() location} + * @param existingHeaders The existing headers found for the {@code AtomosContent} + * @return the {@code Optional} map of headers to use instead of the {@code existingHeaders}. If + * the existing headers should be used then an empty {@code Optional} may be returned. + */ + @Override + Optional<Map<String, String>> apply(String location, + Map<String, String> existingHeaders); } } diff --git a/atomos/src/main/java/org/apache/felix/atomos/AtomosContent.java b/atomos/src/main/java/org/apache/felix/atomos/AtomosContent.java index 660e50a..97e640c 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/AtomosContent.java +++ b/atomos/src/main/java/org/apache/felix/atomos/AtomosContent.java @@ -15,6 +15,7 @@ package org.apache.felix.atomos; import java.util.Optional; +import org.osgi.annotation.versioning.ProviderType; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.BundleException; @@ -26,6 +27,7 @@ import org.osgi.framework.connect.ConnectContent; * by the Atomos runtime which can be installed as a connected * bundle into an OSGi Framework. */ +@ProviderType public interface AtomosContent extends Comparable<AtomosContent> { diff --git a/atomos/src/main/java/org/apache/felix/atomos/AtomosLayer.java b/atomos/src/main/java/org/apache/felix/atomos/AtomosLayer.java index 0fc64da..d407248 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/AtomosLayer.java +++ b/atomos/src/main/java/org/apache/felix/atomos/AtomosLayer.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import org.osgi.annotation.versioning.ProviderType; import org.osgi.framework.BundleException; import org.osgi.framework.connect.ConnectFrameworkFactory; import org.osgi.framework.connect.ModuleConnector; @@ -31,11 +32,13 @@ import org.osgi.framework.connect.ModuleConnector; * then be used to {@link AtomosContent#install(String) install } them as OSGi connected bundles into the * {@link ConnectFrameworkFactory#newFramework(Map, ModuleConnector)} framework}. */ +@ProviderType public interface AtomosLayer { /** * The loader type used for the class loaders of an Atomos layer. */ + @ProviderType enum LoaderType { /** diff --git a/atomos/src/main/java/org/apache/felix/atomos/impl/base/AtomosBase.java b/atomos/src/main/java/org/apache/felix/atomos/impl/base/AtomosBase.java index 9ee0aa2..231786b 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/impl/base/AtomosBase.java +++ b/atomos/src/main/java/org/apache/felix/atomos/impl/base/AtomosBase.java @@ -16,8 +16,10 @@ package org.apache.felix.atomos.impl.base; import java.io.BufferedReader; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.UncheckedIOException; +import java.lang.reflect.InvocationTargetException; import java.net.JarURLConnection; import java.net.URISyntaxException; import java.net.URL; @@ -52,6 +54,7 @@ import org.apache.felix.atomos.AtomosLayer; import org.apache.felix.atomos.AtomosLayer.LoaderType; import org.apache.felix.atomos.impl.base.AtomosBase.AtomosLayerBase.AtomosContentBase; import org.apache.felix.atomos.impl.base.AtomosBase.AtomosLayerBase.AtomosContentIndexed; +import org.apache.felix.atomos.impl.base.AtomosBase.AtomosLayerBase.ManifestHolder; import org.apache.felix.atomos.impl.content.ConnectContentCloseableJar; import org.apache.felix.atomos.impl.content.ConnectContentFile; import org.apache.felix.atomos.impl.content.ConnectContentIndexed; @@ -67,6 +70,7 @@ import org.osgi.framework.ServiceRegistration; import org.osgi.framework.SynchronousBundleListener; import org.osgi.framework.Version; import org.osgi.framework.connect.ConnectContent; +import org.osgi.framework.connect.ConnectContent.ConnectEntry; import org.osgi.framework.connect.ConnectFrameworkFactory; import org.osgi.framework.connect.FrameworkUtilHelper; import org.osgi.framework.connect.ModuleConnector; @@ -98,6 +102,8 @@ public abstract class AtomosBase implements Atomos, SynchronousBundleListener, F public static final String ATOMOS_RUNTIME_MODULES_CLASS = "org.apache.felix.atomos.impl.modules.AtomosModules"; public static final String ATOMOS_LIB_DIR = "atomos_lib"; public static final String GRAAL_NATIVE_IMAGE_KIND = "org.graalvm.nativeimage.kind"; + public static final HeaderProvider NO_OP_HEADER_PROVIDER = ( + l, h) -> Optional.empty(); private final boolean DEBUG; private final boolean REPORT_RESOLUTION_ERRORS; @@ -132,47 +138,59 @@ public abstract class AtomosBase implements Atomos, SynchronousBundleListener, F protected final AtomicLong nextLayerId = new AtomicLong(0); + protected final HeaderProvider headerProvider; + public static enum Index { IGNORE, FIRST } - public static Atomos newAtomos(Map<String, String> config) + public static Atomos newAtomos(Map<String, String> config, + HeaderProvider headerProvider) { String runtimeClass = config.get(ATOMOS_CLASS_PROP); if (runtimeClass != null) { - return loadRuntime(runtimeClass, config); + return loadRuntime(runtimeClass, config, headerProvider); } if (config.get( ATOMOS_LIB_DIR_PROP) != null || System.getProperty(GRAAL_NATIVE_IMAGE_KIND) != null) { - return new AtomosClassPath(config); + return new AtomosClassPath(config, headerProvider); } try { Class.forName("java.lang.Module"); - return loadRuntime(ATOMOS_RUNTIME_MODULES_CLASS, config); + return loadRuntime(ATOMOS_RUNTIME_MODULES_CLASS, config, headerProvider); } catch (ClassNotFoundException e) { // ignore } // default to classpath - return new AtomosClassPath(config); + return new AtomosClassPath(config, headerProvider); } private static Atomos loadRuntime(String runtimeClass, - Map<String, String> config) + Map<String, String> config, HeaderProvider headerProvider) { try { return (AtomosBase) Class.forName( - runtimeClass).getConstructor(Map.class).newInstance(config); + runtimeClass).getConstructor(Map.class, HeaderProvider.class).newInstance( + config, headerProvider); } catch (Exception e) { + if (e instanceof InvocationTargetException) + { + Throwable cause = e.getCause(); + if (cause instanceof Exception) + { + e = (Exception) cause; + } + } throw e instanceof RuntimeException ? (RuntimeException) e : new RuntimeException(e); } @@ -184,9 +202,10 @@ public abstract class AtomosBase implements Atomos, SynchronousBundleListener, F return new File(libDirProp, ATOMOS_LIB_DIR); } - protected AtomosBase(Map<String, String> config) + protected AtomosBase(Map<String, String> config, HeaderProvider headerProvider) { saveConfig(config); + this.headerProvider = headerProvider; DEBUG = Boolean.parseBoolean(this.config.get(ATOMOS_DEBUG_PROP)); REPORT_RESOLUTION_ERRORS = Boolean.parseBoolean( this.config.get(ATOMOS_REPORT_RESOLUTION_PROP)); @@ -734,64 +753,69 @@ public abstract class AtomosBase implements Atomos, SynchronousBundleListener, F JarFile.MANIFEST_NAME); while (classpathManifests.hasMoreElements()) { - URL manifest = classpathManifests.nextElement(); - if (parentManifests.contains(manifest)) + URL manifestURL = classpathManifests.nextElement(); + if (parentManifests.contains(manifestURL)) { // ignore parent manifests continue; } - Attributes headers = new Manifest( - manifest.openStream()).getMainAttributes(); - String symbolicName = headers.getValue(Constants.BUNDLE_SYMBOLICNAME); - if (symbolicName != null) + + Object content = getBundleContent(manifestURL); + if (content != null) { - int semiColon = symbolicName.indexOf(';'); - if (semiColon != -1) + ManifestHolder holder = new ManifestHolder(); + + ConnectContent connectContent; + URL url; + if (content instanceof File) { - symbolicName = symbolicName.substring(0, semiColon); + connectContent = new ConnectContentFile((File) content, holder::getHeaders); + url = ((File) content).toURI().toURL(); } - symbolicName = symbolicName.trim(); - - Object content = getBundleContent(manifest); - if (content != null) + else { - ConnectContent connectContent; - URL url; - if (content instanceof File) - { - connectContent = new ConnectContentFile((File) content); - url = ((File) content).toURI().toURL(); - - } - else - { - connectContent = new ConnectContentJar( - () -> ((JarFile) content), null); - url = new File( + connectContent = new ConnectContentJar( + () -> ((JarFile) content), // + (dontClose) -> {}, // + holder::getHeaders); + url = new File( ((JarFile) content).getName()).toURI().toURL(); - } + } - String location; - if (connectContent.getEntry( + String location; + if (connectContent.getEntry( "META-INF/services/org.osgi.framework.launch.FrameworkFactory").isPresent()) + { + location = Constants.SYSTEM_BUNDLE_LOCATION; + } + else + { + location = content instanceof File + ? ((File) content).getPath() + : ((JarFile) content).getName(); + if (!getName().isEmpty()) { - location = Constants.SYSTEM_BUNDLE_LOCATION; + location = getName() + ":" + location; } - else + } + + Map<String, String> headers = getRawHeaders(connectContent); + headers = applyHeaderProvider(holder, location, headers); + + String symbolicName = headers.get(Constants.BUNDLE_SYMBOLICNAME); + if (symbolicName != null) + { + int semiColon = symbolicName.indexOf(';'); + if (semiColon != -1) { - location = content instanceof File - ? ((File) content).getPath() - : ((JarFile) content).getName(); - if (!getName().isEmpty()) - { - location = getName() + ":" + location; - } + symbolicName = symbolicName.substring(0, semiColon); } - Version version = Version.parseVersion( - headers.getValue(Constants.BUNDLE_VERSION)); + symbolicName = symbolicName.trim(); + + Version version = Version.parseVersion(headers.get(Constants.BUNDLE_VERSION)); result.add(new AtomosContentClassPath(location, symbolicName, - version, connectContent, url)); + version, connectContent, url)); } } } @@ -831,41 +855,52 @@ public abstract class AtomosBase implements Atomos, SynchronousBundleListener, F { try (JarFile jar = new JarFile(f)) { - Attributes headers = jar.getManifest().getMainAttributes(); - String symbolicName = headers.getValue( - Constants.BUNDLE_SYMBOLICNAME); - if (symbolicName != null) - { - int semiColon = symbolicName.indexOf(';'); - if (semiColon != -1) - { - symbolicName = symbolicName.substring(0, semiColon); - } - symbolicName = symbolicName.trim(); + ManifestHolder holder = new ManifestHolder(); ConnectContent connectContent = new ConnectContentCloseableJar( - f.getName(), () -> atomosLibDir); + f.getName(), () -> atomosLibDir, holder::getHeaders); connectContent.open(); String location; - if (connectContent.getEntry( - "META-INF/services/org.osgi.framework.launch.FrameworkFactory").isPresent()) + try { - location = Constants.SYSTEM_BUNDLE_LOCATION; + if (connectContent.getEntry( + "META-INF/services/org.osgi.framework.launch.FrameworkFactory").isPresent()) + { + location = Constants.SYSTEM_BUNDLE_LOCATION; + } + else + { + location = f.getName(); + if (!getName().isEmpty()) + { + location = getName() + ":" + location; + } + } } - else + finally + { + connectContent.close(); + } + Map<String, String> headers = toMap(jar.getManifest()); + headers = applyHeaderProvider(holder, location, headers); + + String symbolicName = headers.get( + Constants.BUNDLE_SYMBOLICNAME); + if (symbolicName != null) { - location = f.getName(); - if (!getName().isEmpty()) + int semiColon = symbolicName.indexOf(';'); + if (semiColon != -1) { - location = getName() + ":" + location; + symbolicName = symbolicName.substring(0, semiColon); } + symbolicName = symbolicName.trim(); + + Version version = Version.parseVersion( + headers.get(Constants.BUNDLE_VERSION)); + AtomosContentBase bundle = new AtomosContentIndexed(location, + symbolicName, version, connectContent); + bootBundles.add(bundle); } - Version version = Version.parseVersion( - headers.getValue(Constants.BUNDLE_VERSION)); - AtomosContentBase bundle = new AtomosContentIndexed(location, - symbolicName, version, connectContent); - bootBundles.add(bundle); - } } catch (IOException e) { @@ -879,13 +914,35 @@ public abstract class AtomosBase implements Atomos, SynchronousBundleListener, F String currentIndex, String currentBSN, Version currentVersion, List<String> currentPaths) { + ManifestHolder holder = new ManifestHolder(); String bundleIndexPath = indexRoot + currentIndex; ConnectContentIndexed content = new ConnectContentIndexed(bundleIndexPath, - currentPaths); + currentPaths, holder::getHeaders); debug("Found indexed content: %s %s %s %s", currentIndex, currentBSN, currentVersion, currentPaths); - return new AtomosContentIndexed(getIndexedLocation(content, currentBSN), - currentBSN, currentVersion, content); + String location = getIndexedLocation(content, currentBSN); + if (headerProvider != NO_OP_HEADER_PROVIDER) + { + Map<String, String> headers = applyHeaderProvider(holder, location, + getRawHeaders(content)); + String symbolicName = headers.get(Constants.BUNDLE_SYMBOLICNAME); + if (symbolicName == null) + { + throw new IllegalStateException( + "Expecting a symbolic name for index bundle: " + currentBSN); + } + int semiColon = symbolicName.indexOf(';'); + if (semiColon != -1) + { + symbolicName = symbolicName.substring(0, semiColon); + } + symbolicName = symbolicName.trim(); + currentBSN = symbolicName; + currentVersion = Version.parseVersion( + headers.get(Constants.BUNDLE_VERSION)); + } + return new AtomosContentIndexed(location, currentBSN, currentVersion, + content); } private void findAtomosIndexedContent(URL index, @@ -1395,6 +1452,52 @@ public abstract class AtomosBase implements Atomos, SynchronousBundleListener, F return this; } } + + public final class ManifestHolder { + private volatile Optional<Map<String, String>> headers = Optional.empty(); + + public Map<String, String> setHeaders(Optional<Map<String, String>> headers) + { + this.headers = headers; + return headers.get(); + } + + public Optional<Map<String, String>> getHeaders() { + return headers; + } + } + } + + static protected Map<String, String> getRawHeaders(ConnectContent content) + { + return content.getEntry("META-INF/MANIFEST.MF").map( + AtomosBase::getRawHeaders).orElse(new HashMap<>()); + } + + static protected Map<String, String> getRawHeaders(ConnectEntry mfEntry) + { + try (InputStream in = mfEntry.getInputStream()) + { + return toMap(new Manifest(in)); + } + catch (IOException e) + { + sneakyThrow(e); + return null; + } + } + + static protected Map<String, String> toMap(Manifest manifest) + { + + Map<String, String> result = new HashMap<>(); + Attributes attributes = manifest.getMainAttributes(); + for (Object key : attributes.keySet()) + { + String keyString = key.toString(); + result.put(keyString, manifest.getMainAttributes().getValue(keyString)); + } + return result; } @Override @@ -1726,4 +1829,18 @@ public abstract class AtomosBase implements Atomos, SynchronousBundleListener, F { // do nothing by default } + + protected Map<String, String> applyHeaderProvider(ManifestHolder holder, + String location, + Map<String, String> existingHeaders) + { + Optional<Map<String,String>> provided = headerProvider.apply(location, + Collections.unmodifiableMap(existingHeaders)); + Map<String, String> headers = existingHeaders; + if (provided.isPresent()) + { + headers = new HashMap<>(provided.get()); + } + return holder.setHeaders(Optional.of(headers)); + } } diff --git a/atomos/src/main/java/org/apache/felix/atomos/impl/base/AtomosClassPath.java b/atomos/src/main/java/org/apache/felix/atomos/impl/base/AtomosClassPath.java index fdf279c..82cc7ba 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/impl/base/AtomosClassPath.java +++ b/atomos/src/main/java/org/apache/felix/atomos/impl/base/AtomosClassPath.java @@ -34,9 +34,9 @@ public class AtomosClassPath extends AtomosBase private final AtomosLayer bootLayer = createBootLayer(); - public AtomosClassPath(Map<String, String> config) + public AtomosClassPath(Map<String, String> config, HeaderProvider headerProvider) { - super(config); + super(config, headerProvider); } private AtomosLayer createBootLayer() diff --git a/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentCloseableJar.java b/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentCloseableJar.java index e02bd9c..96f2b3b 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentCloseableJar.java +++ b/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentCloseableJar.java @@ -17,6 +17,8 @@ import static org.apache.felix.atomos.impl.base.AtomosBase.sneakyThrow; import java.io.File; import java.io.IOException; +import java.util.Map; +import java.util.Optional; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.zip.ZipFile; @@ -81,9 +83,9 @@ public class ConnectContentCloseableJar extends ConnectContentJar } - public ConnectContentCloseableJar(String fileName, Supplier<File> rootSupplier) + public ConnectContentCloseableJar(String fileName, Supplier<File> rootSupplier, Supplier<Optional<Map<String, String>>> headers) { super(new ZipFileHolder(fileName, rootSupplier), - z -> ((ZipFileHolder) z).accept(z)); + z -> ((ZipFileHolder) z).accept(z), headers); } } diff --git a/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentFile.java b/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentFile.java index 7069585..5194505 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentFile.java +++ b/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentFile.java @@ -21,6 +21,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Map; import java.util.Optional; +import java.util.function.Supplier; import java.util.stream.Collectors; import org.osgi.framework.connect.ConnectContent; @@ -77,9 +78,12 @@ public class ConnectContentFile implements ConnectContent final File root; - public ConnectContentFile(File root) + final Supplier<Optional<Map<String, String>>> headers; + + public ConnectContentFile(File root, Supplier<Optional<Map<String, String>>> headers) { this.root = root; + this.headers = headers; } @Override @@ -152,7 +156,7 @@ public class ConnectContentFile implements ConnectContent @Override public Optional<Map<String, String>> getHeaders() { - return Optional.empty(); + return headers.get(); } } diff --git a/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentIndexed.java b/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentIndexed.java index 964957d..9b35736 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentIndexed.java +++ b/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentIndexed.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Supplier; import org.osgi.framework.connect.ConnectContent; @@ -80,17 +81,19 @@ public class ConnectContentIndexed implements ConnectContent private final String index; private final Set<String> entries; + final Supplier<Optional<Map<String, String>>> headers; - public ConnectContentIndexed(String index, List<String> entries) + public ConnectContentIndexed(String index, List<String> entries, Supplier<Optional<Map<String, String>>> headers) { this.index = index; this.entries = Collections.unmodifiableSet(new LinkedHashSet<>(entries)); + this.headers = headers; } @Override public Optional<Map<String, String>> getHeaders() { - return Optional.empty(); + return headers.get(); } @Override diff --git a/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentJar.java b/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentJar.java index 1875a1c..95e53f5 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentJar.java +++ b/atomos/src/main/java/org/apache/felix/atomos/impl/content/ConnectContentJar.java @@ -30,11 +30,13 @@ public class ConnectContentJar implements ConnectContent { final Supplier<ZipFile> zipSupplier; final Consumer<Supplier<ZipFile>> closer; + final Supplier<Optional<Map<String, String>>> headers; - public ConnectContentJar(Supplier<ZipFile> zipSupplier, Consumer<Supplier<ZipFile>> closer) + public ConnectContentJar(Supplier<ZipFile> zipSupplier, Consumer<Supplier<ZipFile>> closer, Supplier<Optional<Map<String, String>>> headers) { this.zipSupplier = zipSupplier; this.closer = closer; + this.headers = headers; } @Override @@ -46,10 +48,7 @@ public class ConnectContentJar implements ConnectContent @Override public void close() throws IOException { - if (closer != null) - { - closer.accept(zipSupplier); - } + closer.accept(zipSupplier); } @Override @@ -93,7 +92,7 @@ public class ConnectContentJar implements ConnectContent @Override public Optional<Map<String, String>> getHeaders() { - return Optional.empty(); + return headers.get(); } class JarConnectEntry implements ConnectEntry diff --git a/atomos/src/main/java/org/apache/felix/atomos/impl/modules/AtomosModules.java b/atomos/src/main/java/org/apache/felix/atomos/impl/modules/AtomosModules.java index dc79eb9..e6adf68 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/impl/modules/AtomosModules.java +++ b/atomos/src/main/java/org/apache/felix/atomos/impl/modules/AtomosModules.java @@ -15,16 +15,14 @@ package org.apache.felix.atomos.impl.modules; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.io.UncheckedIOException; import java.lang.module.Configuration; import java.lang.module.ModuleDescriptor; +import java.lang.module.ModuleDescriptor.Requires; import java.lang.module.ModuleFinder; -import java.lang.module.ModuleReader; import java.lang.module.ResolvedModule; import java.net.URI; import java.nio.file.Path; -import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -33,40 +31,43 @@ import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Optional; import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; -import java.util.jar.Attributes; -import java.util.jar.Manifest; import java.util.stream.Collectors; +import org.apache.felix.atomos.Atomos; import org.apache.felix.atomos.AtomosContent; import org.apache.felix.atomos.AtomosLayer; -import org.apache.felix.atomos.Atomos; import org.apache.felix.atomos.AtomosLayer.LoaderType; import org.apache.felix.atomos.impl.base.AtomosBase; +import org.apache.felix.atomos.impl.base.JavaServiceNamespace; import org.osgi.framework.Bundle; import org.osgi.framework.BundleContext; import org.osgi.framework.Constants; import org.osgi.framework.Version; +import org.osgi.framework.connect.ConnectContent; import org.osgi.framework.connect.ConnectFrameworkFactory; import org.osgi.framework.launch.FrameworkFactory; +import org.osgi.framework.namespace.BundleNamespace; import org.osgi.framework.wiring.BundleCapability; +import org.osgi.resource.Namespace; public class AtomosModules extends AtomosBase { + private final static String ATOMOS_GENERATED = "Atomos-GeneratedManifest"; + private final static String ATOMOS_TEMPORARY_GENERATED_REQUIRES = "Atomos-TemporaryGeneratedRequires"; private final Module thisModule = AtomosModules.class.getModule(); private final Configuration thisConfig = thisModule.getLayer() == null ? null : thisModule.getLayer().configuration(); private final Map<Configuration, AtomosLayerBase> byConfig = new HashMap<>(); private final AtomosLayer bootLayer = createBootLayer(); - public AtomosModules(Map<String, String> config) + public AtomosModules(Map<String, String> config, HeaderProvider headerProvider) { - super(config); + super(config, headerProvider); } private AtomosLayer createBootLayer() @@ -284,67 +285,6 @@ public class AtomosModules extends AtomosBase return super.getAtomosKey(classFromBundle); } - private static Entry<String, Version> getBSNVersion(ResolvedModule m) - { - try (ModuleReader reader = m.reference().open()) - { - return reader.find("META-INF/MANIFEST.MF").map( - (mf) -> getManifestBSNVersion(mf, m)).orElseGet( - () -> new SimpleEntry<>(getBSN(m, null), getVersion(m, null))); - } - catch (IOException e) - { - return new SimpleEntry<>(getBSN(m, null), getVersion(m, null)); - } - } - - private static Entry<String, Version> getManifestBSNVersion(URI manifest, - ResolvedModule resolved) - { - try (InputStream is = manifest.toURL().openStream()) - { - Attributes headers = new Manifest(is).getMainAttributes(); - return new SimpleEntry<>(getBSN(resolved, headers), - getVersion(resolved, headers)); - } - catch (IOException e) - { - return null; - } - } - - private static String getBSN(ResolvedModule resolved, Attributes headers) - { - String bsnHeader = headers != null - ? headers.getValue(Constants.BUNDLE_SYMBOLICNAME) - : null; - if (bsnHeader == null) - { - return resolved.name(); - } - int bsnEnd = bsnHeader.indexOf(';'); - return bsnEnd < 0 ? bsnHeader.trim() : bsnHeader.substring(0, bsnEnd).trim(); - } - - private static Version getVersion(ResolvedModule resolved, Attributes headers) - { - String sVersion = headers != null ? headers.getValue(Constants.BUNDLE_VENDOR) - : null; - if (sVersion == null) - { - sVersion = resolved.reference().descriptor().version().map( - java.lang.module.ModuleDescriptor.Version::toString).orElse("0"); - } - try - { - return Version.valueOf(sVersion); - } - catch (IllegalArgumentException e) - { - return Version.emptyVersion; - } - } - @Override protected void filterBasedOnReadEdges(AtomosContent atomosContent, Collection<BundleCapability> candidates) @@ -397,6 +337,28 @@ public class AtomosModules extends AtomosBase a -> a.adapt(Module.class).isPresent()).collect( Collectors.toUnmodifiableMap((k) -> k.adapt(Module.class).get(), (v) -> v)); + for (AtomosContentBase content : atomosBundles) + { + final Module m = content.adapt(Module.class).orElse(null); + if (m == null) + { + continue; + } + content.getConnectContent().getHeaders().ifPresent(headers -> { + if (Boolean.parseBoolean(headers.get(ATOMOS_GENERATED)) && + Boolean.parseBoolean(headers.get(ATOMOS_TEMPORARY_GENERATED_REQUIRES))) + { + calculateRequires(headers, m, (requires) -> { + return m.getLayer().findModule(requires).map( + requiredModule -> { + return getAtomosContent( + requiredModule).getSymbolicName(); + }).orElse(requires); + }); + headers.remove(ATOMOS_TEMPORARY_GENERATED_REQUIRES); + } + }); + } } @Override @@ -450,6 +412,7 @@ public class AtomosModules extends AtomosBase Set<AtomosContentBase> found = new LinkedHashSet<>(); Map<ModuleDescriptor, Module> descriptorMap = searchLayer.modules().stream().collect( Collectors.toMap(Module::getDescriptor, m -> (m))); + for (ResolvedModule resolved : searchLayer.configuration().modules()) { // include only if it is not excluded @@ -483,15 +446,209 @@ public class AtomosModules extends AtomosBase continue; } - Entry<String, Version> bsnVersion = getBSNVersion(resolved); - found.add(new AtomosContentModule(resolved, m, location, - bsnVersion.getKey(), bsnVersion.getValue())); + ManifestHolder holder = new ManifestHolder(); + + ConnectContent content = new ConnectContentModule(m, resolved.reference(), AtomosLayerModules.this, holder::getHeaders); + + Map<String, String> headers; + try + { + content.open(); + try + { + headers = getRawHeaders(content); + } + finally + { + content.close(); + } + } + catch (IOException e) + { + throw new UncheckedIOException("Error reading connect manifest.", e); + } + + + + generateHeaders(headers, m); + + headers = applyHeaderProvider(holder, location, headers); + + String symbolicName = headers.get(Constants.BUNDLE_SYMBOLICNAME); + if (symbolicName != null) + { + int semiColon = symbolicName.indexOf(';'); + if (semiColon != -1) + { + symbolicName = symbolicName.substring(0, semiColon); + } + symbolicName = symbolicName.trim(); + + Version version = Version.parseVersion( + headers.get(Constants.BUNDLE_VERSION)); + found.add(new AtomosContentModule(m, location, + symbolicName, version, content)); + } } return Collections.unmodifiableSet(found); } + private void generateHeaders(Map<String, String> headers, Module m) + { + ModuleDescriptor desc = m.getDescriptor(); + StringBuilder capabilities = new StringBuilder(); + StringBuilder requirements = new StringBuilder(); + String bsn = headers.get(Constants.BUNDLE_SYMBOLICNAME); + if (bsn == null) + { + // NOTE that we depend on the framework connect implementation to allow connect bundles + // to export java.* packages + headers.put(Constants.BUNDLE_MANIFESTVERSION, "2"); + // set the symbolic name for the module; don't allow fragments to attach + headers.put(Constants.BUNDLE_SYMBOLICNAME, + desc.name() + "; " + Constants.FRAGMENT_ATTACHMENT_DIRECTIVE + ":=" + + Constants.FRAGMENT_ATTACHMENT_NEVER); + + // set the version + Version v; + try + { + v = Version.parseVersion(desc.version().map( + java.lang.module.ModuleDescriptor.Version::toString).orElse("0")); + } + catch (IllegalArgumentException e) + { + v = Version.emptyVersion; + } + headers.put(Constants.BUNDLE_VERSION, v.toString()); + + // only do exports for non bundle modules + // real OSGi bundles already have good export capabilities + StringBuilder exportPackageHeader = new StringBuilder(); + desc.exports().stream().sorted().forEach((exports) -> { + if (exportPackageHeader.length() > 0) + { + exportPackageHeader.append(", "); + } + exportPackageHeader.append(exports.source()); + // TODO map targets to x-friends directive? + }); + if (exportPackageHeader.length() > 0) + { + headers.put(Constants.EXPORT_PACKAGE, exportPackageHeader.toString()); + } + + // Note that for generated manifests based of module descriptor + // will have their requires calculated later. + // Place a header indicating this is generated + headers.put(ATOMOS_GENERATED, Boolean.TRUE.toString()); + if (calculateRequires(headers, m, Function.identity())) + { + // Set a temporary header if we need to recalculate later + headers.put(ATOMOS_TEMPORARY_GENERATED_REQUIRES, Boolean.TRUE.toString()); + } + } + else + { + String origCaps = headers.get(Constants.PROVIDE_CAPABILITY); + if (origCaps != null) + { + capabilities.append(origCaps); + } + String origReqs = headers.get(Constants.REQUIRE_CAPABILITY); + if (origReqs != null) + { + requirements.append(origReqs); + } + } + // map provides to a made up namespace only to give proper resolution errors + // (although JPMS will likely complain first + for (ModuleDescriptor.Provides provides : desc.provides()) + { + if (capabilities.length() > 0) + { + capabilities.append(", "); + } + capabilities.append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append( + "; "); + capabilities.append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append( + "=").append(provides.service()).append("; "); + capabilities.append( + JavaServiceNamespace.CAPABILITY_PROVIDES_WITH_ATTRIBUTE).append( + "=\"").append(String.join(",", provides.providers())).append( + "\""); + } + + // map uses to a made up namespace only to give proper resolution errors + // (although JPMS will likely complain first) + for (String uses : desc.uses()) + { + if (requirements.length() > 0) + { + requirements.append(", "); + } + requirements.append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append( + "; "); + requirements.append(Constants.RESOLUTION_DIRECTIVE).append(":=").append( + Constants.RESOLUTION_OPTIONAL).append("; "); + requirements.append(Constants.FILTER_DIRECTIVE).append(":=").append( + "\"(").append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append( + "=").append(uses).append(")\""); + } + + if (capabilities.length() > 0) + { + headers.put(Constants.PROVIDE_CAPABILITY, capabilities.toString()); + } + if (requirements.length() > 0) + { + headers.put(Constants.REQUIRE_CAPABILITY, requirements.toString()); + } + } + + private boolean calculateRequires(Map<String, String> headers, Module m, + Function<String, String> mapper) + { + // only do requires for non bundle modules + // map requires to require bundle + StringBuilder requireBundleHeader = new StringBuilder(); + for (Requires requires : m.getDescriptor().requires()) + { + if (requireBundleHeader.length() > 0) + { + requireBundleHeader.append(", "); + } + // before requiring based on the name make sure the required + // module has a BSN that is the same + String mapping = mapper.apply(requires.name()); + requireBundleHeader.append(mapping).append("; "); + // determine the resolution value based on the STATIC modifier + String resolution = requires.modifiers().contains( + Requires.Modifier.STATIC) ? Namespace.RESOLUTION_OPTIONAL + : Namespace.RESOLUTION_MANDATORY; + requireBundleHeader.append(Constants.RESOLUTION_DIRECTIVE).append( + ":=").append(resolution).append("; "); + // determine the visibility value based on the TRANSITIVE modifier + String visibility = requires.modifiers().contains( + Requires.Modifier.TRANSITIVE) ? BundleNamespace.VISIBILITY_REEXPORT + : BundleNamespace.VISIBILITY_PRIVATE; + requireBundleHeader.append(Constants.VISIBILITY_DIRECTIVE).append( + ":=").append(visibility); + + } + if (requireBundleHeader.length() > 0) + { + headers.put(Constants.REQUIRE_BUNDLE, requireBundleHeader.toString()); + return true; + } + else + { + return false; + } + } + @SuppressWarnings("unchecked") @Override public <T> Optional<T> adapt(Class<T> type) @@ -516,11 +673,9 @@ public class AtomosModules extends AtomosBase */ private final Module module; - public AtomosContentModule(ResolvedModule resolvedModule, Module module, String location, String symbolicName, Version version) + public AtomosContentModule(Module module, String location, String symbolicName, Version version, ConnectContent content) { - super(location, symbolicName, version, new ConnectContentModule(module, - resolvedModule.reference(), AtomosLayerModules.this, symbolicName, - version)); + super(location, symbolicName, version, content); this.module = module; } diff --git a/atomos/src/main/java/org/apache/felix/atomos/impl/modules/ConnectContentModule.java b/atomos/src/main/java/org/apache/felix/atomos/impl/modules/ConnectContentModule.java index 4f16ff7..aafaf6c 100644 --- a/atomos/src/main/java/org/apache/felix/atomos/impl/modules/ConnectContentModule.java +++ b/atomos/src/main/java/org/apache/felix/atomos/impl/modules/ConnectContentModule.java @@ -16,45 +16,29 @@ package org.apache.felix.atomos.impl.modules; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; -import java.lang.module.ModuleDescriptor; -import java.lang.module.ModuleDescriptor.Provides; -import java.lang.module.ModuleDescriptor.Requires; import java.lang.module.ModuleReader; import java.lang.module.ModuleReference; import java.net.URI; -import java.util.HashMap; import java.util.Map; import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; -import java.util.jar.Attributes; -import java.util.jar.Attributes.Name; -import java.util.jar.Manifest; - -import org.apache.felix.atomos.impl.base.JavaServiceNamespace; +import java.util.function.Supplier; import org.apache.felix.atomos.impl.modules.AtomosModules.AtomosLayerModules; -import org.osgi.framework.Constants; -import org.osgi.framework.Version; import org.osgi.framework.connect.ConnectContent; -import org.osgi.framework.namespace.BundleNamespace; -import org.osgi.resource.Namespace; public class ConnectContentModule implements ConnectContent { final Module module; final ModuleReference reference; final AtomosLayerModules atomosLayer; - final String symbolicName; - final Version version; - final AtomicReference<Optional<Map<String, String>>> headers = new AtomicReference<>(); + final Supplier<Optional<Map<String, String>>> headers; volatile ModuleReader reader = null; - public ConnectContentModule(Module module, ModuleReference reference, AtomosLayerModules atomosLayer, String symbolicName, Version version) + public ConnectContentModule(Module module, ModuleReference reference, AtomosLayerModules atomosLayer, Supplier<Optional<Map<String,String>>> headers) { this.module = module; this.reference = reference; this.atomosLayer = atomosLayer; - this.symbolicName = symbolicName; - this.version = version; + this.headers = headers; } @Override @@ -123,174 +107,9 @@ public class ConnectContentModule implements ConnectContent @Override public Optional<Map<String, String>> getHeaders() { - return headers.updateAndGet((h) -> { - if (h == null) - { - h = createManifest(); - } - return h; - }); - } - - private Optional<Map<String, String>> createManifest() - { - return Optional.of(getEntry("META-INF/MANIFEST.MF").map( - (mf) -> createManifest(mf)).orElseGet(() -> createManifest(null))); - - } - - private Map<String, String> createManifest(ConnectEntry mfEntry) - { - Map<String, String> result = new HashMap<>(); - if (mfEntry != null) - { - try - { - Manifest mf = new Manifest(mfEntry.getInputStream()); - Attributes mainAttrs = mf.getMainAttributes(); - for (Object key : mainAttrs.keySet()) - { - Name name = (Name) key; - result.put(name.toString(), mainAttrs.getValue(name)); - } - } - catch (IOException e) - { - throw new UncheckedIOException("Error reading connect manfest.", e); - } - } - - ModuleDescriptor desc = module.getDescriptor(); - StringBuilder capabilities = new StringBuilder(); - StringBuilder requirements = new StringBuilder(); - String bsn = result.get(Constants.BUNDLE_SYMBOLICNAME); - if (bsn == null) - { - // NOTE that we depend on the framework connect implementation to allow connect bundles - // to export java.* packages - result.put(Constants.BUNDLE_MANIFESTVERSION, "2"); - // set the symbolic name for the module; don't allow fragments to attach - result.put(Constants.BUNDLE_SYMBOLICNAME, - symbolicName + "; " + Constants.FRAGMENT_ATTACHMENT_DIRECTIVE + ":=" - + Constants.FRAGMENT_ATTACHMENT_NEVER); - - // set the version - result.put(Constants.BUNDLE_VERSION, version.toString()); - - // only do exports for non bundle modules - // real OSGi bundles already have good export capabilities - StringBuilder exportPackageHeader = new StringBuilder(); - desc.exports().stream().sorted().forEach((exports) -> - { - if (exportPackageHeader.length() > 0) - { - exportPackageHeader.append(", "); - } - exportPackageHeader.append(exports.source()); - // TODO map targets to x-friends directive? - }); - if (exportPackageHeader.length() > 0) - { - result.put(Constants.EXPORT_PACKAGE, exportPackageHeader.toString()); - } - - // only do requires for non bundle modules - // map requires to require bundle - StringBuilder requireBundleHeader = new StringBuilder(); - for (Requires requires : desc.requires()) - { - if (requireBundleHeader.length() > 0) - { - requireBundleHeader.append(", "); - } - - // before requiring based on the name make sure the required - // module has a BSN that is the same - String requiresBSN = getRequiresBSN(requires.name()); - requireBundleHeader.append(requiresBSN).append("; "); - // determine the resolution value based on the STATIC modifier - String resolution = requires.modifiers().contains( - Requires.Modifier.STATIC) ? Namespace.RESOLUTION_OPTIONAL - : Namespace.RESOLUTION_MANDATORY; - requireBundleHeader.append(Constants.RESOLUTION_DIRECTIVE).append( - ":=").append(resolution).append("; "); - // determine the visibility value based on the TRANSITIVE modifier - String visibility = requires.modifiers().contains( - Requires.Modifier.TRANSITIVE) ? BundleNamespace.VISIBILITY_REEXPORT - : BundleNamespace.VISIBILITY_PRIVATE; - requireBundleHeader.append(Constants.VISIBILITY_DIRECTIVE).append( - ":=").append(visibility); - - } - if (requireBundleHeader.length() > 0) - { - result.put(Constants.REQUIRE_BUNDLE, requireBundleHeader.toString()); - } - } - else - { - String origCaps = result.get(Constants.PROVIDE_CAPABILITY); - if (origCaps != null) - { - capabilities.append(origCaps); - } - String origReqs = result.get(Constants.REQUIRE_CAPABILITY); - if (origReqs != null) - { - requirements.append(origReqs); - } - } - // map provides to a made up namespace only to give proper resolution errors - // (although JPMS will likely complain first - for (Provides provides : desc.provides()) - { - if (capabilities.length() > 0) - { - capabilities.append(", "); - } - capabilities.append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append("; "); - capabilities.append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append( - "=").append(provides.service()).append("; "); - capabilities.append( - JavaServiceNamespace.CAPABILITY_PROVIDES_WITH_ATTRIBUTE).append( - "=\"").append(String.join(",", provides.providers())).append("\""); - } - - // map uses to a made up namespace only to give proper resolution errors - // (although JPMS will likely complain first) - for (String uses : desc.uses()) - { - if (requirements.length() > 0) - { - requirements.append(", "); - } - requirements.append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append("; "); - requirements.append(Constants.RESOLUTION_DIRECTIVE).append(":=").append( - Constants.RESOLUTION_OPTIONAL).append("; "); - requirements.append(Constants.FILTER_DIRECTIVE).append(":=").append( - "\"(").append(JavaServiceNamespace.JAVA_SERVICE_NAMESPACE).append( - "=").append(uses).append(")\""); - } - - if (capabilities.length() > 0) - { - result.put(Constants.PROVIDE_CAPABILITY, capabilities.toString()); - } - if (requirements.length() > 0) - { - result.put(Constants.REQUIRE_CAPABILITY, requirements.toString()); - } - return result; - } - - private String getRequiresBSN(String name) - { - return module.getLayer().findModule(name).map( - m -> atomosLayer.getAtomosContent(m).getSymbolicName()).orElse(name); - + return headers.get(); } - class ModuleConnectEntry implements ConnectEntry { final String name;