Document A2 OSGi Boot. Add classpath provisioning source.
authorMathieu Baudier <mbaudier@argeo.org>
Thu, 12 Dec 2019 17:54:04 +0000 (18:54 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Thu, 12 Dec 2019 17:54:04 +0000 (18:54 +0100)
14 files changed:
org.argeo.osgi.boot/build.properties
org.argeo.osgi.boot/ext/test/org/argeo/osgi/boot/a2/ClasspathSourceTest.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootUtils.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Branch.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Component.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Contribution.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Module.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ClasspathSource.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsA2Source.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsM2Source.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/OsgiContext.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningManager.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningSource.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/equinox/EquinoxUtils.java [new file with mode: 0644]

index 406d7996c66478c08ed03f31a3eb8b58f6bc77ef..d88c54b8500e783d5777ab62af4990cbd5c09d48 100644 (file)
@@ -1,3 +1,4 @@
 source.. = src/,\
            ext/test/
-additional.bundles = org.junit
+additional.bundles = org.junit,\
+                     org.hamcrest
diff --git a/org.argeo.osgi.boot/ext/test/org/argeo/osgi/boot/a2/ClasspathSourceTest.java b/org.argeo.osgi.boot/ext/test/org/argeo/osgi/boot/a2/ClasspathSourceTest.java
new file mode 100644 (file)
index 0000000..6f4b467
--- /dev/null
@@ -0,0 +1,29 @@
+package org.argeo.osgi.boot.a2;
+
+import java.io.IOException;
+
+import org.argeo.osgi.boot.equinox.EquinoxUtils;
+import org.junit.Test;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.launch.Framework;
+
+public class ClasspathSourceTest {
+       @Test
+       public void testProvisioning() throws IOException {
+               Framework framework = EquinoxUtils.launch(null);
+               ProvisioningManager provisioningManager = new ProvisioningManager(framework.getBundleContext());
+               ClasspathSource classpathSource = new ClasspathSource();
+               classpathSource.load();
+               provisioningManager.addSource(classpathSource);
+               provisioningManager.install(null);
+               for (Bundle bundle : framework.getBundleContext().getBundles()) {
+                       System.out.println(bundle.getSymbolicName() + ":" + bundle.getVersion());
+               }
+               try {
+                       framework.stop();
+               } catch (BundleException e) {
+                       // silent
+               }
+       }
+}
index 455c96198b3d76f1e8c344608db3b1f08a0a6d54..9593b0cde8bf8babdec9a79fe959aba90c0a679c 100644 (file)
@@ -20,9 +20,13 @@ import java.text.SimpleDateFormat;
 import java.util.ArrayList;\r
 import java.util.Date;\r
 import java.util.List;\r
+import java.util.Map;\r
 import java.util.StringTokenizer;\r
 \r
 import org.osgi.framework.Bundle;\r
+import org.osgi.framework.BundleException;\r
+import org.osgi.framework.launch.Framework;\r
+import org.osgi.framework.launch.FrameworkFactory;\r
 \r
 /** Utilities, mostly related to logging. */\r
 public class OsgiBootUtils {\r
@@ -126,4 +130,16 @@ public class OsgiBootUtils {
                return comp;\r
        }\r
 \r
+       /** Launch an OSGi framework. */\r
+       public static Framework launch(FrameworkFactory frameworkFactory, Map<String, String> configuration) {\r
+               // start OSGi\r
+               Framework framework = frameworkFactory.newFramework(configuration);\r
+               try {\r
+                       framework.start();\r
+               } catch (BundleException e) {\r
+                       throw new OsgiBootException("Cannot start OSGi framework", e);\r
+               }\r
+               return framework;\r
+       }\r
+\r
 }\r
index ae715eceae5420c9cc2d42b0c65639a3e51f9d26..a9e5e5f90df2a884071be2eda5f9510933aceb1b 100644 (file)
@@ -7,6 +7,11 @@ import java.util.TreeMap;
 import org.argeo.osgi.boot.OsgiBootUtils;
 import org.osgi.framework.Version;
 
+/**
+ * A logical linear sequence of versions of a given {@link A2Component}. This is
+ * typically a combination of major and minor version, indicating backward
+ * compatibility.
+ */
 class A2Branch implements Comparable<A2Branch> {
        private final A2Component component;
        private final String id;
index 5d8dc87cfa6103c89cf7e5a6afa8811ba8d4cc12..7c14c295eb1a71807ed421e39208e3efa038d134 100644 (file)
@@ -6,6 +6,11 @@ import java.util.TreeMap;
 
 import org.osgi.framework.Version;
 
+/**
+ * The logical name of a software package. In OSGi's case this is
+ * <code>Bundle-SymbolicName</code>. This is the equivalent of Maven's artifact
+ * id.
+ */
 class A2Component implements Comparable<A2Component> {
        private final A2Contribution contribution;
        private final String id;
@@ -71,7 +76,7 @@ class A2Component implements Comparable<A2Component> {
                if (prefix == null)
                        prefix = "";
                A2Branch lastBranch = last();
-               SortedMap<String, A2Branch> displayMap =  new TreeMap<>(Collections.reverseOrder());
+               SortedMap<String, A2Branch> displayMap = new TreeMap<>(Collections.reverseOrder());
                displayMap.putAll(branches);
                for (String branchId : displayMap.keySet()) {
                        A2Branch branch = displayMap.get(branchId);
index 9e6ca3084b7b75751f1501999dc8d008c160708e..84778918641a0b2e8f6dc936dc13720a065a458b 100644 (file)
@@ -4,9 +4,14 @@ import java.util.Collections;
 import java.util.Map;
 import java.util.TreeMap;
 
+/**
+ * A category grouping a set of {@link A2Component}, typically based on the
+ * provider of these components. This is the equivalent of Maven's group Id.
+ */
 class A2Contribution implements Comparable<A2Contribution> {
        final static String BOOT = "boot";
        final static String RUNTIME = "runtime";
+       final static String CLASSPATH = "classpath";
 
        private final ProvisioningSource source;
        private final String id;
index 1108cd72bbc9fa369e5a5929298dc27b6154dab4..44f94e33453de4acbbcf8af012d62b81833106ac 100644 (file)
@@ -2,6 +2,11 @@ package org.argeo.osgi.boot.a2;
 
 import org.osgi.framework.Version;
 
+/**
+ * An identified software package. In OSGi's case this is the combination of
+ * <code>Bundle-SymbolicName</code> and <code>Bundle-version</code>. This is the
+ * equivalent of the full coordinates of a Maven artifact version.
+ */
 class A2Module implements Comparable<A2Module> {
        private final A2Branch branch;
        private final Version version;
@@ -54,5 +59,4 @@ class A2Module implements Comparable<A2Module> {
                return branch.getComponent() + ":" + version;
        }
 
-       
 }
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ClasspathSource.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ClasspathSource.java
new file mode 100644 (file)
index 0000000..8fd202a
--- /dev/null
@@ -0,0 +1,38 @@
+package org.argeo.osgi.boot.a2;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.List;
+
+import org.argeo.osgi.boot.OsgiBootUtils;
+import org.osgi.framework.Version;
+
+/**
+ * A provisioning source based on the linear classpath with which the JCM has
+ * been started.
+ */
+public class ClasspathSource extends ProvisioningSource {
+       void load() throws IOException {
+               A2Contribution classpathContribution = new A2Contribution(this, A2Contribution.CLASSPATH);
+               List<String> classpath = Arrays.asList(System.getProperty("java.class.path").split(File.pathSeparator));
+               parts: for (String part : classpath) {
+                       Path file = Paths.get(part);
+                       Version version;
+                       try {
+                               version = new Version(readVersionFromModule(file));
+                       } catch (Exception e) {
+                               // ignore non OSGi
+                               continue parts;
+                       }
+                       String moduleName = readSymbolicNameFromModule(file);
+                       A2Component component = classpathContribution.getOrAddComponent(moduleName);
+                       A2Module module = component.getOrAddModule(version, file);
+                       if (OsgiBootUtils.isDebug())
+                               OsgiBootUtils.debug("Registered " + module);
+               }
+
+       }
+}
index c0e721941b1a82c8385d43c7dba170b1e5b5490e..a8e79d4574b1b82f327a6429841d23688a42aced 100644 (file)
@@ -11,6 +11,7 @@ import java.util.TreeSet;
 import org.argeo.osgi.boot.OsgiBootUtils;
 import org.osgi.framework.Version;
 
+/** A file system {@link ProvisioningSource} in A2 format. */
 public class FsA2Source extends ProvisioningSource {
        private final Path base;
 
index ecf8771a6f3f6d83e647e587d5473f2fd8c9160b..c2971759c1f36fe32b6b5d505de523a7e3019a5a 100644 (file)
@@ -12,6 +12,7 @@ import java.nio.file.attribute.BasicFileAttributes;
 import org.argeo.osgi.boot.OsgiBootUtils;
 import org.osgi.framework.Version;
 
+/** A file system {@link ProvisioningSource} in Maven 2 format. */
 public class FsM2Source extends ProvisioningSource {
        private final Path base;
 
index a3ddf55f307d371de7988eb3041bb2a12d7c3784..948cbe26a279b0819f06aa0b9ffb7596a0fe49d2 100644 (file)
@@ -6,6 +6,7 @@ import org.osgi.framework.BundleContext;
 import org.osgi.framework.FrameworkUtil;
 import org.osgi.framework.Version;
 
+/** A running OSGi bundle context seen as a {@link ProvisioningSource}. */
 class OsgiContext extends ProvisioningSource {
        private final BundleContext bc;
 
index 8fb8b2f4df18325786f38214709d442d3be82407..ef9c04fd9f533a08b942a996649eef3607732ea1 100644 (file)
@@ -1,7 +1,6 @@
 package org.argeo.osgi.boot.a2;
 
 import java.io.File;
-import java.io.InputStream;
 import java.net.URI;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -19,13 +18,12 @@ import org.argeo.osgi.boot.OsgiBootUtils;
 import org.eclipse.osgi.launch.EquinoxFactory;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
-import org.osgi.framework.BundleException;
 import org.osgi.framework.Constants;
 import org.osgi.framework.Version;
 import org.osgi.framework.launch.Framework;
-import org.osgi.framework.launch.FrameworkFactory;
 import org.osgi.framework.wiring.FrameworkWiring;
 
+/** Loads provisioning sources into an OSGi context. */
 public class ProvisioningManager {
        BundleContext bc;
        OsgiContext osgiContext;
@@ -113,8 +111,9 @@ public class ProvisioningManager {
                        Version moduleVersion = module.getVersion();
                        A2Branch osgiBranch = osgiContext.findBranch(module.getBranch().getComponent().getId(), moduleVersion);
                        if (osgiBranch == null) {
-                               Bundle bundle = bc.installBundle(module.getBranch().getCoordinates(),
-                                               moduleSource.newInputStream(module.getLocator()));
+//                             Bundle bundle = bc.installBundle(module.getBranch().getCoordinates(),
+//                                             moduleSource.newInputStream(module.getLocator()));
+                               Bundle bundle = moduleSource.install(bc, module);
                                if (OsgiBootUtils.isDebug())
                                        OsgiBootUtils.debug("Installed bundle " + bundle.getLocation() + " with version " + moduleVersion);
                                return bundle;
@@ -123,13 +122,14 @@ public class ProvisioningManager {
                                int compare = moduleVersion.compareTo(lastOsgiModule.getVersion());
                                if (compare > 0) {// update
                                        Bundle bundle = (Bundle) lastOsgiModule.getLocator();
-                                       bundle.update(moduleSource.newInputStream(module.getLocator()));
+//                                     bundle.update(moduleSource.newInputStream(module.getLocator()));
+                                       moduleSource.update(bundle, module);
                                        OsgiBootUtils.info("Updated bundle " + bundle.getLocation() + " to version " + moduleVersion);
                                        return bundle;
                                }
                        }
                } catch (Exception e) {
-                       OsgiBootUtils.error("Could not install module " + module + ": " + e.getMessage(), null);
+                       OsgiBootUtils.error("Could not install module " + module + ": " + e.getMessage(), e);
                }
                return null;
        }
@@ -148,8 +148,9 @@ public class ProvisioningManager {
                                Version moduleVersion = module.getVersion();
                                int compare = moduleVersion.compareTo(version);
                                if (compare > 0) {// update
-                                       try (InputStream in = source.newInputStream(module.getLocator())) {
-                                               bundle.update(in);
+                                       try {
+                                               source.update(bundle, module);
+//                                             bundle.update(in);
                                                String fragmentHost = bundle.getHeaders().get(Constants.FRAGMENT_HOST);
                                                if (fragmentHost != null)
                                                        fragmentsUpdated = true;
@@ -169,22 +170,10 @@ public class ProvisioningManager {
                return updatedBundles;
        }
 
-       private static Framework launch() {
-               // start OSGi
-               FrameworkFactory frameworkFactory = new EquinoxFactory();
+       public static void main(String[] args) {
                Map<String, String> configuration = new HashMap<>();
                configuration.put("osgi.console", "2323");
-               Framework framework = frameworkFactory.newFramework(configuration);
-               try {
-                       framework.start();
-               } catch (BundleException e) {
-                       throw new OsgiBootException("Cannot start OSGi framework", e);
-               }
-               return framework;
-       }
-
-       public static void main(String[] args) {
-               Framework framework = launch();
+               Framework framework = OsgiBootUtils.launch(new EquinoxFactory(), configuration);
                try {
                        ProvisioningManager pm = new ProvisioningManager(framework.getBundleContext());
                        FsA2Source context = new FsA2Source(Paths.get(
index cb122e434441dde4a82dde1e9fbadb5c9c1930b2..a9fab396d6bc13974f70c20dce17a69506be593a 100644 (file)
@@ -1,21 +1,31 @@
 package org.argeo.osgi.boot.a2;
 
+import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URL;
+import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
 import java.util.Collections;
 import java.util.Map;
 import java.util.SortedMap;
 import java.util.TreeMap;
 import java.util.jar.JarInputStream;
+import java.util.jar.JarOutputStream;
 import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
 
 import org.argeo.osgi.boot.OsgiBootException;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
 import org.osgi.framework.Constants;
 import org.osgi.framework.Version;
 
+/** Where components are retrieved from. */
 abstract class ProvisioningSource {
        final Map<String, A2Contribution> contributions = Collections.synchronizedSortedMap(new TreeMap<>());
 
@@ -72,29 +82,108 @@ abstract class ProvisioningSource {
        }
 
        protected String readVersionFromModule(Path modulePath) {
-               try (JarInputStream in = new JarInputStream(newInputStream(modulePath))) {
-                       Manifest manifest = in.getManifest();
-                       String versionStr = manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
-                       return versionStr;
-               } catch (IOException e) {
-                       throw new OsgiBootException("Cannot read manifest from " + modulePath, e);
+               Manifest manifest;
+               if (Files.isDirectory(modulePath)) {
+                       manifest = findManifest(modulePath);
+               } else {
+                       try (JarInputStream in = new JarInputStream(newInputStream(modulePath))) {
+                               manifest = in.getManifest();
+                       } catch (IOException e) {
+                               throw new OsgiBootException("Cannot read manifest from " + modulePath, e);
+                       }
                }
+               String versionStr = manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+               return versionStr;
        }
 
        protected String readSymbolicNameFromModule(Path modulePath) {
-               try (JarInputStream in = new JarInputStream(newInputStream(modulePath))) {
-                       Manifest manifest = in.getManifest();
-                       String symbolicName = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
-                       int semiColIndex = symbolicName.indexOf(';');
-                       if (semiColIndex >= 0)
-                               symbolicName = symbolicName.substring(0, semiColIndex);
-                       return symbolicName;
+               Manifest manifest;
+               if (Files.isDirectory(modulePath)) {
+                       manifest = findManifest(modulePath);
+               } else {
+                       try (JarInputStream in = new JarInputStream(newInputStream(modulePath))) {
+                               manifest = in.getManifest();
+                       } catch (IOException e) {
+                               throw new OsgiBootException("Cannot read manifest from " + modulePath, e);
+                       }
+               }
+               String symbolicName = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+               int semiColIndex = symbolicName.indexOf(';');
+               if (semiColIndex >= 0)
+                       symbolicName = symbolicName.substring(0, semiColIndex);
+               return symbolicName;
+       }
+
+       private static Manifest findManifest(Path currentPath) {
+               Path metaInfPath = currentPath.resolve("META-INF");
+               if (Files.exists(metaInfPath) && Files.isDirectory(metaInfPath)) {
+                       Path manifestPath = metaInfPath.resolve("MANIFEST.MF");
+                       try {
+                               try (InputStream in = Files.newInputStream(manifestPath)) {
+                                       Manifest manifest = new Manifest(in);
+                                       return manifest;
+                               }
+                       } catch (IOException e) {
+                               throw new OsgiBootException("Cannot read manifest from " + manifestPath, e);
+                       }
+               } else {
+                       Path parentPath = currentPath.getParent();
+                       if (parentPath == null)
+                               throw new OsgiBootException("MANIFEST.MF file not found.");
+                       return findManifest(currentPath.getParent());
+               }
+       }
+
+       public Bundle install(BundleContext bc, A2Module module) throws IOException, BundleException {
+               Path tempJar = null;
+               if (module.getLocator() instanceof Path && Files.isDirectory((Path) module.getLocator()))
+                       tempJar = toTempJar((Path) module.getLocator());
+               Bundle bundle;
+               try (InputStream in = newInputStream(tempJar != null ? tempJar : module.getLocator())) {
+                       bundle = bc.installBundle(module.getBranch().getCoordinates(), in);
+               }
+               if (tempJar != null)
+                       Files.deleteIfExists(tempJar);
+               return bundle;
+       }
+
+       public void update(Bundle bundle, A2Module module) throws IOException, BundleException {
+               Path tempJar = null;
+               if (module.getLocator() instanceof Path && Files.isDirectory((Path) module.getLocator()))
+                       tempJar = toTempJar((Path) module.getLocator());
+               try (InputStream in = newInputStream(tempJar != null ? tempJar : module.getLocator())) {
+                       bundle.update(in);
+               }
+               if (tempJar != null)
+                       Files.deleteIfExists(tempJar);
+       }
+
+       static Path toTempJar(Path dir) {
+               try {
+                       Manifest manifest = findManifest(dir);
+                       Path jarPath = Files.createTempFile("a2Source", ".jar");
+                       try (JarOutputStream zos = new JarOutputStream(new FileOutputStream(jarPath.toFile()), manifest)) {
+                               Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
+                                       public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                               Path relPath = dir.relativize(file);
+                                               // skip MANIFEST from folder
+                                               if (relPath.toString().contentEquals("META-INF/MANIFEST.MF"))
+                                                       return FileVisitResult.CONTINUE;
+                                               zos.putNextEntry(new ZipEntry(relPath.toString()));
+                                               Files.copy(file, zos);
+                                               zos.closeEntry();
+                                               return FileVisitResult.CONTINUE;
+                                       }
+                               });
+                       }
+                       return jarPath;
                } catch (IOException e) {
-                       throw new OsgiBootException("Cannot read manifest from " + modulePath, e);
+                       throw new OsgiBootException("Cannot install OSGi bundle from " + dir, e);
                }
+
        }
 
-       InputStream newInputStream(Object locator) throws IOException {
+       private InputStream newInputStream(Object locator) throws IOException {
                if (locator instanceof Path) {
                        return Files.newInputStream((Path) locator);
                } else if (locator instanceof URL) {
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/equinox/EquinoxUtils.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/equinox/EquinoxUtils.java
new file mode 100644 (file)
index 0000000..55cd067
--- /dev/null
@@ -0,0 +1,22 @@
+package org.argeo.osgi.boot.equinox;
+
+import java.util.Map;
+
+import org.argeo.osgi.boot.OsgiBootUtils;
+import org.eclipse.osgi.launch.EquinoxFactory;
+import org.osgi.framework.launch.Framework;
+
+/**
+ * Utilities with a dependency to the Equinox OSGi runtime or its configuration.
+ */
+public class EquinoxUtils {
+
+       public static Framework launch(Map<String, String> configuration) {
+               return OsgiBootUtils.launch(new EquinoxFactory(), configuration);
+       }
+
+       /** Singleton. */
+       private EquinoxUtils() {
+
+       }
+}