Introduce OSGi sub framework with shared class loaders
authorMathieu Baudier <mbaudier@argeo.org>
Fri, 8 Mar 2024 17:13:41 +0000 (18:13 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Fri, 8 Mar 2024 17:13:41 +0000 (18:13 +0100)
org.argeo.init/src/org/argeo/api/init/RuntimeManager.java
org.argeo.init/src/org/argeo/init/RuntimeManagerMain.java
org.argeo.init/src/org/argeo/init/osgi/OsgiBoot.java
org.argeo.init/src/org/argeo/init/osgi/OsgiBuilder.java
org.argeo.init/src/org/argeo/init/osgi/OsgiRuntimeContext.java
org.argeo.init/src/org/argeo/init/osgi/SubFrameworkActivator.java [new file with mode: 0644]

index da15b1ebccab4dc6d7f30a69edbec23d0bfad837..cb8caeda9366c9c8d47c5ac19e29e49ae4bae6da 100644 (file)
@@ -19,27 +19,54 @@ public interface RuntimeManager {
 
        public void closeRuntime(String relPath, boolean async);
 
+       /**
+        * Load configs recursively starting with the parent directories, until a
+        * jvm.args file is found.
+        */
        static void loadConfig(Path dir, Map<String, String> config) {
-                       try {
-       //                      System.out.println("Load from " + dir);
-                               Path jvmArgsPath = dir.resolve(RuntimeManager.JVM_ARGS);
-                               if (!Files.exists(jvmArgsPath)) {
-                                       // load from parent directory
-                                       loadConfig(dir.getParent(), config);
-                               }
-       
-                               if (Files.exists(dir))
-                                       for (Path p : Files.newDirectoryStream(dir, "*.ini")) {
-                                               Properties props = new Properties();
-                                               try (InputStream in = Files.newInputStream(p)) {
-                                                       props.load(in);
-                                               }
-                                               for (Object key : props.keySet()) {
-                                                       config.put(key.toString(), props.getProperty(key.toString()));
-                                               }
+               try {
+                       Path jvmArgsPath = dir.resolve(RuntimeManager.JVM_ARGS);
+                       if (!Files.exists(jvmArgsPath)) {
+                               // load from parent directory
+                               loadConfig(dir.getParent(), config);
+                       }
+
+                       if (Files.exists(dir))
+                               for (Path p : Files.newDirectoryStream(dir, "*.ini")) {
+                                       try (InputStream in = Files.newInputStream(p)) {
+                                               loadConfig(in, config);
                                        }
-                       } catch (IOException e) {
-                               throw new UncheckedIOException("Cannot load configuration from " + dir, e);
+                               }
+               } catch (IOException e) {
+                       throw new UncheckedIOException("Cannot load configuration from " + dir, e);
+               }
+       }
+
+       /**
+        * Load config from a {@link Properties} formatted stream. If a property value
+        * starts with a '+' character, itis expected that the last character is a
+        * separator and it will be prepended to the existing value.
+        */
+       static void loadConfig(InputStream in, Map<String, String> config) throws IOException {
+               Properties props = new Properties();
+               props.load(in);
+               for (Object k : props.keySet()) {
+                       String key = k.toString();
+                       String value = props.getProperty(key);
+                       if (value.length() > 1 && '+' == value.charAt(0)) {
+                               String currentValue = config.get(key);
+                               if (currentValue == null || "".equals(currentValue)) {
+                                       // remove the + and the trailing separator
+                                       value = value.substring(1, value.length() - 1);
+                                       config.put(key, value);
+                               } else {
+                                       // remove the + but keep the trailing separator
+                                       value = value.substring(1);
+                                       config.put(key, value + currentValue);
+                               }
+                       } else {
+                               config.put(key, value);
                        }
                }
+       }
 }
index 74560185d011729928c5e5d9476a6e086ea4b0ff..f4ed507c07b37229cd73b8ccbd376deadcef8f22 100644 (file)
@@ -17,11 +17,10 @@ import org.argeo.api.init.InitConstants;
 import org.argeo.api.init.RuntimeContext;
 import org.argeo.api.init.RuntimeManager;
 import org.argeo.init.logging.ThinLoggerFinder;
+import org.argeo.init.osgi.OsgiBoot;
 import org.argeo.init.osgi.OsgiRuntimeContext;
 import org.argeo.internal.init.InternalState;
-import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.BundleException;
 import org.osgi.framework.FrameworkEvent;
 import org.osgi.framework.launch.Framework;
 
@@ -69,16 +68,12 @@ public class RuntimeManagerMain implements RuntimeManager {
 
                        BundleContext bc = managerRuntimeContext.getFramework().getBundleContext();
                        // uninstall init as a bundle since it will be available via OSGi system
-                       for (Bundle b : bc.getBundles()) {
-                               if (b.getSymbolicName().equals(SYMBOLIC_NAME_INIT)) {
-                                       b.uninstall();
-                               }
-                       }
+                       OsgiBoot.uninstallBundles(bc, SYMBOLIC_NAME_INIT);
                        bc.registerService(RuntimeManager.class, this, new Hashtable<>(configuration));
                        logger.log(Level.DEBUG, "Registered runtime manager");
 
                        managerRuntimeContext.waitForStop(0);
-               } catch (InterruptedException | BundleException e) {
+               } catch (InterruptedException e) {
                        e.printStackTrace();
                        System.exit(1);
                }
index 9b9ed6a7a500f0f943f66c5333fa5e754ba45e85..963bab4d495e4349bcc7ba409141deb7e2fa94b7 100644 (file)
@@ -14,12 +14,12 @@ import java.nio.file.Path;
 import java.nio.file.PathMatcher;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Properties;
 import java.util.ServiceLoader;
 import java.util.Set;
 import java.util.SortedMap;
@@ -131,37 +131,15 @@ public class OsgiBoot {
         * with {@link BundleContext#getProperty(String)}. If these properties are
         * <code>null</code>, system properties are used instead.
         */
-       public void bootstrap(Map<String, String> properties) {
+       public void bootstrap() {
                try {
                        long begin = System.currentTimeMillis();
-
-                       // notify start
-                       String osgiInstancePath = getProperty(InitConstants.PROP_OSGI_INSTANCE_AREA);
-                       String osgiConfigurationPath = getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA);
-                       String osgiSharedConfigurationPath = getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA);
-                       logger.log(DEBUG, () -> "OSGi bootstrap starting" //
-                                       + (osgiInstancePath != null ? " data: " + osgiInstancePath + "" : "") //
-                                       + (osgiConfigurationPath != null ? " state: " + osgiConfigurationPath + "" : "") //
-                                       + (osgiSharedConfigurationPath != null ? " config: " + osgiSharedConfigurationPath + "" : "") //
-                       );
-
-                       // legacy install bundles
-                       installUrls(getBundlesUrls());
-                       installUrls(getDistributionUrls());
-
-                       // A2 install bundles
-                       provisioningManager.install(null);
-
+                       // Install bundles
+                       install();
                        // Make sure fragments are properly considered by refreshing
-                       refreshFramework();
-
-                       // start bundles
-//                     if (properties != null && !Boolean.parseBoolean(properties.get(PROP_OSGI_USE_SYSTEM_PROPERTIES)))
-                       startBundles(properties);
-//                     else
-//                             startBundles();
-
-                       // complete
+                       refresh();
+                       // Start bundles
+                       startBundles();
                        long duration = System.currentTimeMillis() - begin;
                        logger.log(DEBUG, () -> "OSGi bootstrap completed in " + Math.round(((double) duration) / 1000) + "s ("
                                        + duration + "ms), " + bundleContext.getBundles().length + " bundles");
@@ -191,14 +169,23 @@ public class OsgiBoot {
                System.out.println();
        }
 
-       /**
-        * Calls {@link #bootstrap(Map)} with <code>null</code>.
-        * 
-        * @see #bootstrap(Map)
-        */
-       @Deprecated
-       public void bootstrap() {
-               bootstrap(null);
+       public void install() {
+               String osgiInstancePath = getProperty(InitConstants.PROP_OSGI_INSTANCE_AREA);
+               String osgiConfigurationPath = getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA);
+               String osgiSharedConfigurationPath = getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA);
+               logger.log(DEBUG, () -> "OSGi bootstrap starting" //
+                               + (osgiInstancePath != null ? " data: " + osgiInstancePath + "" : "") //
+                               + (osgiConfigurationPath != null ? " state: " + osgiConfigurationPath + "" : "") //
+                               + (osgiSharedConfigurationPath != null ? " config: " + osgiSharedConfigurationPath + "" : "") //
+               );
+
+               // legacy install bundles
+               installUrls(getBundlesUrls());
+               installUrls(getDistributionUrls());
+
+               // A2 install bundles
+               provisioningManager.install(null);
+
        }
 
        public void update() {
@@ -305,17 +292,17 @@ public class OsgiBoot {
         * 
         * @see OsgiBoot#doStartBundles(Map)
         */
-       public void startBundles(Map<String, String> properties) {
+       public void startBundles() {
                Map<String, String> map = new TreeMap<>();
                // first use properties
-               if (properties != null) {
-                       for (String key : properties.keySet()) {
-                               String property = key;
-                               if (property.startsWith(InitConstants.PROP_ARGEO_OSGI_START)) {
-                                       map.put(property, properties.get(property));
-                               }
-                       }
-               }
+//             if (properties != null) {
+//                     for (String key : properties.keySet()) {
+//                             String property = key;
+//                             if (property.startsWith(InitConstants.PROP_ARGEO_OSGI_START)) {
+//                                     map.put(property, properties.get(property));
+//                             }
+//                     }
+//             }
                // then try all start level until a maximum
                int maxStartLevel = Integer
                                .parseInt(getProperty(InitConstants.PROP_ARGEO_OSGI_MAX_START_LEVEL, DEFAULT_MAX_START_LEVEL));
@@ -327,28 +314,28 @@ public class OsgiBoot {
 
                }
                // finally, override with system properties
-               for (Object key : System.getProperties().keySet()) {
-                       if (key.toString().startsWith(InitConstants.PROP_ARGEO_OSGI_START)) {
-                               map.put(key.toString(), System.getProperty(key.toString()));
-                       }
-               }
+//             for (Object key : System.getProperties().keySet()) {
+//                     if (key.toString().startsWith(InitConstants.PROP_ARGEO_OSGI_START)) {
+//                             map.put(key.toString(), System.getProperty(key.toString()));
+//                     }
+//             }
                // start
                doStartBundles(map);
        }
 
-       void startBundles(Properties properties) {
-               Map<String, String> map = new TreeMap<>();
-               // first use properties
-               if (properties != null) {
-                       for (Object key : properties.keySet()) {
-                               String property = key.toString();
-                               if (property.startsWith(InitConstants.PROP_ARGEO_OSGI_START)) {
-                                       map.put(property, properties.get(property).toString());
-                               }
-                       }
-               }
-               startBundles(map);
-       }
+//     void startBundles(Properties properties) {
+//             Map<String, String> map = new TreeMap<>();
+//             // first use properties
+//             if (properties != null) {
+//                     for (Object key : properties.keySet()) {
+//                             String property = key.toString();
+//                             if (property.startsWith(InitConstants.PROP_ARGEO_OSGI_START)) {
+//                                     map.put(property, properties.get(property).toString());
+//                             }
+//                     }
+//             }
+//             startBundles(map);
+//     }
 
        /**
         * Start bundle based on keys starting with
@@ -717,7 +704,7 @@ public class OsgiBoot {
                return (basePath + '/' + relativePath).replace('/', File.separatorChar);
        }
 
-       private void refreshFramework() {
+       public void refresh() {
                Bundle systemBundle = bundleContext.getBundle(0);
                FrameworkWiring frameworkWiring = systemBundle.adapt(FrameworkWiring.class);
                // TODO deal with refresh breaking native loading (e.g SWT)
@@ -741,6 +728,14 @@ public class OsgiBoot {
                return getProperty(name, null);
        }
 
+       /*
+        * BEAN METHODS
+        */
+
+       public BundleContext getBundleContext() {
+               return bundleContext;
+       }
+
        /*
         * PLAIN OSGI LAUNCHER
         */
@@ -765,11 +760,23 @@ public class OsgiBoot {
        }
 
        /*
-        * BEAN METHODS
+        * OSGI UTILITIES
         */
+       /** Uninstall all bundles with these symbolic names */
+       public static void uninstallBundles(BundleContext bc, String... symbolicNames) {
+               List<String> lst = Arrays.asList(symbolicNames);
+               for (Bundle b : bc.getBundles()) {
+                       String sn = b.getSymbolicName();
+                       if (sn == null)
+                               continue;
+                       if (lst.contains(sn)) {
+                               try {
+                                       b.uninstall();
+                               } catch (BundleException e) {
+                                       logger.log(ERROR, "Cannot uninstall " + sn, e);
+                               }
+                       }
+               }
 
-       public BundleContext getBundleContext() {
-               return bundleContext;
        }
-
 }
index d89535e03633d276c55648e442bc1b097d7b89c9..f1c16af91aa76110d59b5cbf764587df3f0bebe7 100644 (file)
@@ -12,7 +12,6 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Properties;
 import java.util.Set;
 import java.util.TreeMap;
 
@@ -53,6 +52,7 @@ public class OsgiBuilder {
        }
 
        public Framework launch() {
+               configuration.putAll(startLevelsToProperties());
                // start OSGi
                framework = OsgiBoot.defaultOsgiLaunch(configuration);
 
@@ -73,7 +73,7 @@ public class OsgiBuilder {
                        }
                }
                // start bundles
-               osgiBoot.startBundles(startLevelsToProperties());
+               osgiBoot.startBundles();
 
                // if (OsgiBootUtils.isDebug())
                // for (Bundle bundle : bc.getBundles()) {
@@ -283,8 +283,8 @@ public class OsgiBuilder {
        //
        // UTILITIES
        //
-       private Properties startLevelsToProperties() {
-               Properties properties = new Properties();
+       private Map<String, String> startLevelsToProperties() {
+               Map<String, String> properties = new HashMap<>();
                for (Integer startLevel : startLevels.keySet()) {
                        String property = InitConstants.PROP_ARGEO_OSGI_START + "." + startLevel;
                        StringBuilder value = new StringBuilder();
index 4ec5a04734bea64cabed71dd5ccf95534672b154..2914be38a75eac6af5e787a6fcca3820ff226b19 100644 (file)
@@ -100,7 +100,7 @@ public class OsgiRuntimeContext implements RuntimeContext, AutoCloseable {
                Thread osgiBootThread = new Thread("OSGi boot framework " + frameworkUuuid) {
                        @Override
                        public void run() {
-                               osgiBoot.bootstrap(config);
+                               osgiBoot.bootstrap();
                        }
                };
                osgiBootThread.start();
diff --git a/org.argeo.init/src/org/argeo/init/osgi/SubFrameworkActivator.java b/org.argeo.init/src/org/argeo/init/osgi/SubFrameworkActivator.java
new file mode 100644 (file)
index 0000000..3d6469d
--- /dev/null
@@ -0,0 +1,295 @@
+package org.argeo.init.osgi;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.argeo.api.init.InitConstants;
+import org.argeo.api.init.RuntimeManager;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleActivator;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.BundleReference;
+import org.osgi.framework.connect.ConnectContent;
+import org.osgi.framework.connect.ConnectFrameworkFactory;
+import org.osgi.framework.connect.ConnectModule;
+import org.osgi.framework.connect.ModuleConnector;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.wiring.BundleWiring;
+
+public class SubFrameworkActivator implements BundleActivator {
+//     private final static String EQUINOX_FRAMEWORK_CLASS = "org.eclipse.osgi.launch.Equinox";
+       private final static String EQUINOX_FRAMEWORK_FACTORY_CLASS = "org.eclipse.osgi.launch.EquinoxFactory";
+
+//     private ClassLoader bundleClassLoader;
+//     private ClassLoader subFrameworkClassLoader;
+       private BundleContext bundleContext;
+
+       private ConnectFrameworkFactory frameworkFactory;
+
+       @Override
+       public void start(BundleContext context) throws Exception {
+               this.bundleContext = context;
+
+               try {
+                       Bundle bundle = context.getBundle();
+                       ClassLoader bundleClassLoader = bundle.adapt(BundleWiring.class).getClassLoader();
+//                     subFrameworkClassLoader = new URLClassLoader(new URL[0], bundleClassLoader);
+
+                       @SuppressWarnings("unchecked")
+                       Class<? extends ConnectFrameworkFactory> frameworkFactoryClass = (Class<? extends ConnectFrameworkFactory>) bundleClassLoader
+                                       .loadClass(EQUINOX_FRAMEWORK_FACTORY_CLASS);
+                       frameworkFactory = frameworkFactoryClass.getConstructor().newInstance();
+
+                       new Thread() {
+
+                               @Override
+                               public void run() {
+                                       for (int i = 0; i < 5; i++) {
+                                               Map<String, String> config = new HashMap<>();
+                                               Path basePase = Paths.get(System.getProperty("user.home"), ".config/argeo/test/", "test" + i);
+                                               config.put(InitConstants.PROP_OSGI_CONFIGURATION_AREA,
+                                                               basePase.resolve(RuntimeManager.STATE).toString());
+                                               config.put(InitConstants.PROP_OSGI_INSTANCE_AREA,
+                                                               basePase.resolve(RuntimeManager.DATA).toString());
+                                               config.put("argeo.host", "host" + i);
+                                               config.put("osgi.console", "host" + i + ":2023");
+                                               startFramework(config);
+                                       }
+                               }
+
+                       }.start();
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       throw e;
+               }
+       }
+
+       Framework startFramework(Map<String, String> config) {
+               try {
+                       URL bundleConfigUrl = bundleContext.getBundle().getEntry("config.ini");
+                       try (InputStream in = bundleConfigUrl.openStream()) {
+                               RuntimeManager.loadConfig(in, config);
+                       }
+
+                       // Equinox
+//                     config.put("osgi.frameworkParentClassloader", "current");
+//                     config.put("osgi.parentClassLoader", "app");
+//                     config.put("osgi.contextClassLoaderParent", "app");
+
+                       ModuleConnector moduleConnector = new ParentBundleModuleConnector(bundleContext);
+
+//                     URL frameworkUrl = URI.create(bundleContext.getProperty("osgi.framework")).toURL();
+//                     URLClassLoader frameworkClassLoader = new URLClassLoader(new URL[] { frameworkUrl, });
+//                     Class<? extends Framework> frameworkClass = (Class<? extends Framework>) frameworkClassLoader
+//                                     .loadClass(EQUINOX_FRAMEWORK_CLASS);
+//                     Framework framework = frameworkClass.getConstructor(Map.class, ModuleConnector.class).newInstance(config,
+//                                     moduleConnector);
+
+                       Framework framework = frameworkFactory.newFramework(config, moduleConnector);
+
+                       framework.init();
+
+                       for (Bundle b : bundleContext.getBundles()) {
+                               if (b.getBundleId() == 0)
+                                       continue;
+                               String location = b.getLocation();
+                               if (location.contains("/org.argeo.tp/") //
+                                               || location.contains("/org.argeo.tp.sys/") //
+                                               || location.contains("/org.argeo.tp.httpd/") //
+                                               || location.contains("/org.argeo.tp.sshd/") //
+                                               || location.contains("/org.argeo.cms/org.argeo.init") //
+                               ) {
+                                       framework.getBundleContext().installBundle(b.getLocation());
+                               }
+                       }
+
+                       OsgiBoot osgiBoot = new OsgiBoot(framework.getBundleContext());
+                       osgiBoot.install();
+//                     OsgiBoot.uninstallBundles(osgiBoot.getBundleContext(), "org.argeo.api.cms");
+//                     OsgiBoot.uninstallBundles(osgiBoot.getBundleContext(), "org.osgi.service.useradmin");
+//                     osgiBoot.getBundleContext()
+//                                     .installBundle("initial@reference:file:../../../../../argeo-commons/org.argeo.api.cms/");
+//                     osgiBoot.getBundleContext().installBundle(
+//                                     "reference:file:/usr/local/share/a2/osgi/equinox/org.argeo.tp.osgi/org.osgi.service.useradmin.1.1.jar");
+                       osgiBoot.refresh();
+                       framework.start();
+                       osgiBoot.startBundles();
+
+//                     for (Bundle b : framework.getBundleContext().getBundles()) {
+//                             BundleContext bc = b.getBundleContext();
+//                             if (bc == null)
+//                                     System.err.println(b.getSymbolicName() + " BC null");
+//                     }
+                       return framework;
+               } catch (Exception e) {
+                       throw new IllegalStateException("Cannot start framework", e);
+               }
+       }
+
+       @Override
+       public void stop(BundleContext context) throws Exception {
+               bundleContext = null;
+               frameworkFactory = null;
+       }
+
+       class ParentBundleModuleConnector implements ModuleConnector {
+               private final BundleContext foreignBundleContext;
+               private BundleContext localBundleContext;
+
+               public ParentBundleModuleConnector(BundleContext foreignBundleContext) {
+                       this.foreignBundleContext = foreignBundleContext;
+               }
+
+               @Override
+               public Optional<BundleActivator> newBundleActivator() {
+                       return Optional.of(new BundleActivator() {
+                               @Override
+                               public void start(BundleContext context) throws Exception {
+                                       ParentBundleModuleConnector.this.localBundleContext = context;
+                               }
+
+                               @Override
+                               public void stop(BundleContext context) throws Exception {
+                                       ParentBundleModuleConnector.this.localBundleContext = null;
+                               }
+
+                       });
+               }
+
+               @Override
+               public void initialize(File storage, Map<String, String> configuration) {
+               }
+
+               @Override
+               public Optional<ConnectModule> connect(String location) throws BundleException {
+                       Bundle bundle = foreignBundleContext.getBundle(location);
+                       if (bundle != null && bundle.getBundleId() != 0) {
+                               System.out.println("Foreign Bundle: " + bundle.getSymbolicName() + " " + location);
+                               ConnectModule module = new ConnectModule() {
+
+                                       @Override
+                                       public ConnectContent getContent() throws IOException {
+                                               return new ForeignBundleConnectContent(localBundleContext, bundle);
+                                       }
+                               };
+                               return Optional.of(module);
+                       }
+                       return Optional.empty();
+               }
+       }
+
+       class ForeignBundleClassLoader extends ClassLoader implements BundleReference {
+               private BundleContext localBundleContext;
+               private Bundle foreignBundle;
+
+               public ForeignBundleClassLoader(BundleContext localBundleContext, Bundle foreignBundle) {
+                       super("Foreign bundle " + foreignBundle.toString(), Optional
+                                       .ofNullable(foreignBundle.adapt(BundleWiring.class)).map((bw) -> bw.getClassLoader()).orElse(null));
+                       this.localBundleContext = localBundleContext;
+                       this.foreignBundle = foreignBundle;
+               }
+
+               @Override
+               public Bundle getBundle() {
+                       return localBundleContext.getBundle(foreignBundle.getLocation());
+               }
+       }
+
+       class ForeignBundleConnectContent implements ConnectContent {
+               private final Bundle bundle;
+               private final ClassLoader classLoader;
+
+               public ForeignBundleConnectContent(BundleContext localBundleContext, Bundle bundle) {
+                       this.bundle = bundle;
+                       this.classLoader = new ForeignBundleClassLoader(localBundleContext, bundle);
+               }
+
+               @Override
+               public Optional<Map<String, String>> getHeaders() {
+                       Dictionary<String, String> dict = bundle.getHeaders();
+                       List<String> keys = Collections.list(dict.keys());
+                       Map<String, String> dictCopy = keys.stream().collect(Collectors.toMap(Function.identity(), dict::get));
+                       return Optional.of(dictCopy);
+               }
+
+               @Override
+               public Iterable<String> getEntries() throws IOException {
+                       List<String> lst = Collections.list(bundle.findEntries("", "*", true)).stream().map((u) -> u.getPath())
+                                       .toList();
+                       return lst;
+               }
+
+               @Override
+               public Optional<ConnectEntry> getEntry(String path) {
+                       URL u = bundle.getEntry(path);
+                       if (u == null) {
+                               u = bundle.getEntry("bin/" + path);
+                               // System.err.println(u2);
+                       }
+                       if (u == null) {
+                               if ("plugin.xml".equals(path))
+                                       return Optional.empty();
+                               System.err.println(bundle.getSymbolicName() + " " + path + " not found");
+                               return Optional.empty();
+                       }
+                       URL url = u;
+                       ConnectEntry urlConnectEntry = new ConnectEntry() {
+
+                               @Override
+                               public String getName() {
+                                       return path;
+                               }
+
+                               @Override
+                               public long getLastModified() {
+                                       // FIXME
+                                       return System.currentTimeMillis();
+                               }
+
+                               @Override
+                               public InputStream getInputStream() throws IOException {
+                                       return url.openStream();
+                               }
+
+                               @Override
+                               public long getContentLength() {
+                                       return -1;
+                               }
+                       };
+                       return Optional.of(urlConnectEntry);
+               }
+
+               @Override
+               public Optional<ClassLoader> getClassLoader() {
+                       ClassLoader cl;
+                       // cl = bundle.adapt(BundleWiring.class).getClassLoader();
+
+                       // cl = subFrameworkClassLoader;
+                       cl = classLoader;
+                       return Optional.of(cl);
+//                     return Optional.empty();
+               }
+
+               @Override
+               public void open() throws IOException {
+               }
+
+               @Override
+               public void close() throws IOException {
+               }
+
+       }
+}