From: Mathieu Baudier Date: Thu, 12 Dec 2019 17:54:04 +0000 (+0100) Subject: Document A2 OSGi Boot. Add classpath provisioning source. X-Git-Tag: argeo-commons-2.1.84~14 X-Git-Url: http://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=25eec0a9cc31c67740ba4131ed40951a028a7669 Document A2 OSGi Boot. Add classpath provisioning source. --- diff --git a/org.argeo.osgi.boot/build.properties b/org.argeo.osgi.boot/build.properties index 406d7996c..d88c54b85 100644 --- a/org.argeo.osgi.boot/build.properties +++ b/org.argeo.osgi.boot/build.properties @@ -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 index 000000000..6f4b46733 --- /dev/null +++ b/org.argeo.osgi.boot/ext/test/org/argeo/osgi/boot/a2/ClasspathSourceTest.java @@ -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 + } + } +} diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootUtils.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootUtils.java index 455c96198..9593b0cde 100644 --- a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootUtils.java +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootUtils.java @@ -20,9 +20,13 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.StringTokenizer; import org.osgi.framework.Bundle; +import org.osgi.framework.BundleException; +import org.osgi.framework.launch.Framework; +import org.osgi.framework.launch.FrameworkFactory; /** Utilities, mostly related to logging. */ public class OsgiBootUtils { @@ -126,4 +130,16 @@ public class OsgiBootUtils { return comp; } + /** Launch an OSGi framework. */ + public static Framework launch(FrameworkFactory frameworkFactory, Map configuration) { + // start OSGi + Framework framework = frameworkFactory.newFramework(configuration); + try { + framework.start(); + } catch (BundleException e) { + throw new OsgiBootException("Cannot start OSGi framework", e); + } + return framework; + } + } diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Branch.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Branch.java index ae715ecea..a9e5e5f90 100644 --- a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Branch.java +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Branch.java @@ -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 { private final A2Component component; private final String id; diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Component.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Component.java index 5d8dc87cf..7c14c295e 100644 --- a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Component.java +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Component.java @@ -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 + * Bundle-SymbolicName. This is the equivalent of Maven's artifact + * id. + */ class A2Component implements Comparable { private final A2Contribution contribution; private final String id; @@ -71,7 +76,7 @@ class A2Component implements Comparable { if (prefix == null) prefix = ""; A2Branch lastBranch = last(); - SortedMap displayMap = new TreeMap<>(Collections.reverseOrder()); + SortedMap displayMap = new TreeMap<>(Collections.reverseOrder()); displayMap.putAll(branches); for (String branchId : displayMap.keySet()) { A2Branch branch = displayMap.get(branchId); diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Contribution.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Contribution.java index 9e6ca3084..847789186 100644 --- a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Contribution.java +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Contribution.java @@ -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 { final static String BOOT = "boot"; final static String RUNTIME = "runtime"; + final static String CLASSPATH = "classpath"; private final ProvisioningSource source; private final String id; diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Module.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Module.java index 1108cd72b..44f94e334 100644 --- a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Module.java +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Module.java @@ -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 + * Bundle-SymbolicName and Bundle-version. This is the + * equivalent of the full coordinates of a Maven artifact version. + */ class A2Module implements Comparable { private final A2Branch branch; private final Version version; @@ -54,5 +59,4 @@ class A2Module implements Comparable { 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 index 000000000..8fd202a99 --- /dev/null +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ClasspathSource.java @@ -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 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); + } + + } +} diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsA2Source.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsA2Source.java index c0e721941..a8e79d457 100644 --- a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsA2Source.java +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsA2Source.java @@ -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; diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsM2Source.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsM2Source.java index ecf8771a6..c2971759c 100644 --- a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsM2Source.java +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/FsM2Source.java @@ -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; diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/OsgiContext.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/OsgiContext.java index a3ddf55f3..948cbe26a 100644 --- a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/OsgiContext.java +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/OsgiContext.java @@ -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; diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningManager.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningManager.java index 8fb8b2f4d..ef9c04fd9 100644 --- a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningManager.java +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningManager.java @@ -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 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( diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningSource.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningSource.java index cb122e434..a9fab396d 100644 --- a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningSource.java +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ProvisioningSource.java @@ -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 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() { + 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 index 000000000..55cd067b9 --- /dev/null +++ b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/equinox/EquinoxUtils.java @@ -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 configuration) { + return OsgiBootUtils.launch(new EquinoxFactory(), configuration); + } + + /** Singleton. */ + private EquinoxUtils() { + + } +}