You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@felix.apache.org by pa...@apache.org on 2021/02/17 13:56:16 UTC

[felix-atomos] branch master updated: Add a headers provider hook (#46)

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 <tj...@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;