Improve A2 provisioning framework.
authorMathieu Baudier <mbaudier@argeo.org>
Tue, 17 Dec 2019 09:52:26 +0000 (10:52 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Tue, 17 Dec 2019 09:52:26 +0000 (10:52 +0100)
14 files changed:
org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBoot.java
org.argeo.osgi.boot/src/org/argeo/osgi/boot/OsgiBootException.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/A2Exception.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Source.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/AbstractProvisioningSource.java [new file with mode: 0644]
org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/ClasspathSource.java
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

index 4c3ee40f0c2095da9860af6f4ef4316a72ae5c59..6824ecaf549c9c2c3a335c011cdfb1d6940838a5 100644 (file)
@@ -19,6 +19,7 @@ import static org.argeo.osgi.boot.OsgiBootUtils.debug;
 import static org.argeo.osgi.boot.OsgiBootUtils.warn;
 
 import java.io.File;
+import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -31,6 +32,7 @@ import java.util.SortedMap;
 import java.util.StringTokenizer;
 import java.util.TreeMap;
 
+import org.argeo.osgi.boot.a2.A2Source;
 import org.argeo.osgi.boot.a2.ProvisioningManager;
 import org.argeo.osgi.boot.internal.springutil.AntPathMatcher;
 import org.argeo.osgi.boot.internal.springutil.PathMatcher;
@@ -101,7 +103,8 @@ public class OsgiBoot implements OsgiBootConstants {
        /** Constructor */
        public OsgiBoot(BundleContext bundleContext) {
                this.bundleContext = bundleContext;
-               String homeUri = Paths.get(System.getProperty("user.home")).toUri().toString();
+               Path homePath = Paths.get(System.getProperty("user.home")).toAbsolutePath();
+               String homeUri = homePath.toUri().toString();
                localCache = getProperty(PROP_ARGEO_OSGI_LOCAL_CACHE, homeUri + ".m2/repository/");
 
                provisioningManager = new ProvisioningManager(bundleContext);
@@ -110,7 +113,14 @@ public class OsgiBoot implements OsgiBootConstants {
                        provisioningManager.registerDefaultSource();
                } else {
                        for (String source : sources.split(",")) {
-                               provisioningManager.registerSource(source);
+                               if (source.trim().equals(A2Source.DEFAULT_A2_URI)) {
+                                       provisioningManager
+                                                       .registerSource(A2Source.SCHEME_A2 + "://" + homePath.toString() + "/.local/share/osgi");
+                                       provisioningManager.registerSource(A2Source.SCHEME_A2 + ":///usr/local/share/osgi");
+                                       provisioningManager.registerSource(A2Source.SCHEME_A2 + ":///usr/share/osgi");
+                               } else {
+                                       provisioningManager.registerSource(source);
+                               }
                        }
                }
        }
index 49c5e81b14355e13a06fcc3b183d1e13e15f3590..dd2b74d2dae21273aaf92e4f874ed6810bae9851 100644 (file)
@@ -16,7 +16,7 @@
 package org.argeo.osgi.boot;
 
 /** OsgiBoot specific exceptions */
-public class OsgiBootException extends RuntimeException {
+class OsgiBootException extends RuntimeException {
        private static final long serialVersionUID = 2414011711711425353L;
 
        public OsgiBootException() {
index a9e5e5f90df2a884071be2eda5f9510933aceb1b..9e3f81dec95cbfa5758d18c3ea34a55145cace05 100644 (file)
@@ -12,13 +12,13 @@ import org.osgi.framework.Version;
  * typically a combination of major and minor version, indicating backward
  * compatibility.
  */
-class A2Branch implements Comparable<A2Branch> {
+public class A2Branch implements Comparable<A2Branch> {
        private final A2Component component;
        private final String id;
 
        final SortedMap<Version, A2Module> modules = Collections.synchronizedSortedMap(new TreeMap<>());
 
-       A2Branch(A2Component component, String id) {
+       public A2Branch(A2Component component, String id) {
                this.component = component;
                this.id = id;
                component.branches.put(id, this);
index 7c14c295eb1a71807ed421e39208e3efa038d134..a2d5facf98b9cfc8c814cfbe1cd5de91b19486d8 100644 (file)
@@ -11,7 +11,7 @@ import org.osgi.framework.Version;
  * <code>Bundle-SymbolicName</code>. This is the equivalent of Maven's artifact
  * id.
  */
-class A2Component implements Comparable<A2Component> {
+public class A2Component implements Comparable<A2Component> {
        private final A2Contribution contribution;
        private final String id;
 
index 84778918641a0b2e8f6dc936dc13720a065a458b..e81ecadc8b9d661b5606a0195dd71f2b07c6ecfa 100644 (file)
@@ -8,7 +8,7 @@ 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> {
+public class A2Contribution implements Comparable<A2Contribution> {
        final static String BOOT = "boot";
        final static String RUNTIME = "runtime";
        final static String CLASSPATH = "classpath";
@@ -18,11 +18,16 @@ class A2Contribution implements Comparable<A2Contribution> {
 
        final Map<String, A2Component> components = Collections.synchronizedSortedMap(new TreeMap<>());
 
+       /**
+        * The contribution must be added to the source. Rather use
+        * {@link AbstractProvisioningSource#getOrAddContribution(String)} than this
+        * contructor directly.
+        */
        public A2Contribution(ProvisioningSource context, String id) {
                this.source = context;
                this.id = id;
-               if (context != null)
-                       context.contributions.put(id, this);
+//             if (context != null)
+//                     context.contributions.put(id, this);
        }
 
        A2Component getOrAddComponent(String componentId) {
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Exception.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Exception.java
new file mode 100644 (file)
index 0000000..129d38f
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.osgi.boot.a2;
+
+/** Unchecked A2 provisioning exception. */
+public class A2Exception extends RuntimeException {
+       private static final long serialVersionUID = 1927603558545397360L;
+
+       public A2Exception(String message, Throwable e) {
+               super(message, e);
+       }
+
+       public A2Exception(String message) {
+               super(message);
+       }
+
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Source.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/A2Source.java
new file mode 100644 (file)
index 0000000..eb23845
--- /dev/null
@@ -0,0 +1,7 @@
+package org.argeo.osgi.boot.a2;
+
+/** A provisioning source in A2 format. */
+public interface A2Source extends ProvisioningSource {
+       final static String SCHEME_A2 = "a2";
+       final static String DEFAULT_A2_URI = SCHEME_A2 + ":///";
+}
diff --git a/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/AbstractProvisioningSource.java b/org.argeo.osgi.boot/src/org/argeo/osgi/boot/a2/AbstractProvisioningSource.java
new file mode 100644 (file)
index 0000000..c8497d0
--- /dev/null
@@ -0,0 +1,212 @@
+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.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. */
+public abstract class AbstractProvisioningSource implements ProvisioningSource {
+       protected final Map<String, A2Contribution> contributions = Collections.synchronizedSortedMap(new TreeMap<>());
+
+       public Iterable<A2Contribution> listContributions(Object filter) {
+               return contributions.values();
+       }
+
+       @Override
+       public Bundle install(BundleContext bc, A2Module module) {
+               try {
+                       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;
+               } catch (BundleException | IOException e) {
+                       throw new A2Exception("Cannot install module " + module, e);
+               }
+       }
+
+       @Override
+       public void update(Bundle bundle, A2Module module) {
+               try {
+                       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);
+               } catch (BundleException | IOException e) {
+                       throw new A2Exception("Cannot update module " + module, e);
+               }
+       }
+
+       @Override
+       public A2Branch findBranch(String componentId, Version version) {
+               A2Component component = findComponent(componentId);
+               if (component == null)
+                       return null;
+               String branchId = version.getMajor() + "." + version.getMinor();
+               if (!component.branches.containsKey(branchId))
+                       return null;
+               return component.branches.get(branchId);
+       }
+
+       protected A2Contribution getOrAddContribution(String contributionId) {
+               if (contributions.containsKey(contributionId))
+                       return contributions.get(contributionId);
+               else {
+                       A2Contribution contribution = new A2Contribution(this, contributionId);
+                       contributions.put(contributionId, contribution);
+                       return contribution;
+               }
+       }
+
+       protected void asTree(String prefix, StringBuffer buf) {
+               if (prefix == null)
+                       prefix = "";
+               for (String contributionId : contributions.keySet()) {
+                       buf.append(prefix);
+                       buf.append(contributionId);
+                       buf.append('\n');
+                       A2Contribution contribution = contributions.get(contributionId);
+                       contribution.asTree(prefix + " ", buf);
+               }
+       }
+
+       protected void asTree() {
+               StringBuffer buf = new StringBuffer();
+               asTree("", buf);
+               System.out.println(buf);
+       }
+
+       protected A2Component findComponent(String componentId) {
+               SortedMap<A2Contribution, A2Component> res = new TreeMap<>();
+               for (A2Contribution contribution : contributions.values()) {
+                       components: for (String componentIdKey : contribution.components.keySet()) {
+                               if (componentId.equals(componentIdKey)) {
+                                       res.put(contribution, contribution.components.get(componentIdKey));
+                                       break components;
+                               }
+                       }
+               }
+               if (res.size() == 0)
+                       return null;
+               // TODO explicit contribution priorities
+               return res.get(res.lastKey());
+
+       }
+
+       protected String readVersionFromModule(Path modulePath) {
+               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 A2Exception("Cannot read manifest from " + modulePath, e);
+                       }
+               }
+               String versionStr = manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
+               return versionStr;
+       }
+
+       protected String readSymbolicNameFromModule(Path modulePath) {
+               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 A2Exception("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 A2Exception("Cannot read manifest from " + manifestPath, e);
+                       }
+               } else {
+                       Path parentPath = currentPath.getParent();
+                       if (parentPath == null)
+                               throw new A2Exception("MANIFEST.MF file not found.");
+                       return findManifest(currentPath.getParent());
+               }
+       }
+
+       private 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 A2Exception("Cannot install OSGi bundle from " + dir, e);
+               }
+
+       }
+
+       private InputStream newInputStream(Object locator) throws IOException {
+               if (locator instanceof Path) {
+                       return Files.newInputStream((Path) locator);
+               } else if (locator instanceof URL) {
+                       return ((URL) locator).openStream();
+               } else {
+                       throw new IllegalArgumentException("Unsupported module locator type " + locator.getClass());
+               }
+       }
+}
index 8fd202a99d78a1224e343abe78552026c98d8b50..330f66a999d52dc8aca0668eba439008cf45d8c2 100644 (file)
@@ -14,9 +14,9 @@ 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 {
+public class ClasspathSource extends AbstractProvisioningSource {
        void load() throws IOException {
-               A2Contribution classpathContribution = new A2Contribution(this, A2Contribution.CLASSPATH);
+               A2Contribution classpathContribution = getOrAddContribution( 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);
index a8e79d4574b1b82f327a6429841d23688a42aced..bea8db3eafd97791153d65c2f2fdf2aef8cb8234 100644 (file)
@@ -11,8 +11,8 @@ 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 {
+/** A file system {@link AbstractProvisioningSource} in A2 format. */
+public class FsA2Source extends AbstractProvisioningSource implements A2Source {
        private final Path base;
 
        public FsA2Source(Path base) {
@@ -28,7 +28,7 @@ public class FsA2Source extends ProvisioningSource {
                                String contributionId = contributionPath.getFileName().toString();
                                if (A2Contribution.BOOT.equals(contributionId))// skip boot
                                        continue contributions;
-                               A2Contribution contribution = new A2Contribution(this, contributionId);
+                               A2Contribution contribution = getOrAddContribution(contributionId);
                                contributions.add(contribution);
                        }
                }
index c2971759c1f36fe32b6b5d505de523a7e3019a5a..88145227975a98079cd7ea4096418187eb107b0c 100644 (file)
@@ -12,8 +12,8 @@ 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 {
+/** A file system {@link AbstractProvisioningSource} in Maven 2 format. */
+public class FsM2Source extends AbstractProvisioningSource {
        private final Path base;
 
        public FsM2Source(Path base) {
index 948cbe26a279b0819f06aa0b9ffb7596a0fe49d2..b4f101887d248075773f5f6cf8fa8dc67c449254 100644 (file)
@@ -6,8 +6,8 @@ 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 {
+/** A running OSGi bundle context seen as a {@link AbstractProvisioningSource}. */
+class OsgiContext extends AbstractProvisioningSource {
        private final BundleContext bc;
 
        public OsgiContext(BundleContext bc) {
@@ -24,7 +24,7 @@ class OsgiContext extends ProvisioningSource {
        }
 
        void load() {
-               A2Contribution runtimeContribution = new A2Contribution(this, A2Contribution.RUNTIME);
+               A2Contribution runtimeContribution = getOrAddContribution( A2Contribution.RUNTIME);
                for (Bundle bundle : bc.getBundles()) {
                        // OsgiBootUtils.debug(bundle.getDataFile("/"));
                        String componentId = bundle.getSymbolicName();
index ef9c04fd9f533a08b942a996649eef3607732ea1..c88af39d9d3bcb177fed1d8548f4ce3050e8b9d2 100644 (file)
@@ -13,7 +13,6 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import org.argeo.osgi.boot.OsgiBootException;
 import org.argeo.osgi.boot.OsgiBootUtils;
 import org.eclipse.osgi.launch.EquinoxFactory;
 import org.osgi.framework.Bundle;
@@ -35,13 +34,13 @@ public class ProvisioningManager {
                osgiContext.load();
        }
 
-       void addSource(ProvisioningSource context) {
-               sources.add(context);
+       protected void addSource(ProvisioningSource source) {
+               sources.add(source);
        }
 
-       void installWholeSource(ProvisioningSource context) {
+       void installWholeSource(ProvisioningSource source) {
                Set<Bundle> updatedBundles = new HashSet<>();
-               for (A2Contribution contribution : context.contributions.values()) {
+               for (A2Contribution contribution : source.listContributions(null)) {
                        for (A2Component component : contribution.components.values()) {
                                A2Module module = component.last().last();
                                Bundle bundle = installOrUpdate(module);
@@ -56,7 +55,7 @@ public class ProvisioningManager {
        public void registerSource(String uri) {
                try {
                        URI u = new URI(uri);
-                       if ("a2".equals(u.getScheme())) {
+                       if (A2Source.SCHEME_A2.equals(u.getScheme())) {
                                if (u.getHost() == null || "".equals(u.getHost())) {
                                        String baseStr = u.getPath();
                                        if (File.separatorChar == '\\') {// MS Windows
@@ -69,7 +68,7 @@ public class ProvisioningManager {
                                }
                        }
                } catch (Exception e) {
-                       throw new OsgiBootException("Cannot add source " + uri, e);
+                       throw new A2Exception("Cannot add source " + uri, e);
                }
        }
 
@@ -84,7 +83,7 @@ public class ProvisioningManager {
                                        String baseStr = base.toString();
                                        if (File.separatorChar == '\\')// MS Windows
                                                baseStr = '/' + baseStr.replace(File.separatorChar, '/');
-                                       URI baseUri = new URI("a2", null, null, 0, baseStr, null, null);
+                                       URI baseUri = new URI(A2Source.SCHEME_A2, null, null, 0, baseStr, null, null);
                                        registerSource(baseUri.toString());
                                        OsgiBootUtils.info("Registered " + baseUri + " as default source");
                                        return true;
@@ -104,8 +103,8 @@ public class ProvisioningManager {
                }
        }
 
-       /** @return the new/updated bundle, or null if nothign was done. */
-       Bundle installOrUpdate(A2Module module) {
+       /** @return the new/updated bundle, or null if nothing was done. */
+       protected Bundle installOrUpdate(A2Module module) {
                try {
                        ProvisioningSource moduleSource = module.getBranch().getComponent().getContribution().getSource();
                        Version moduleVersion = module.getVersion();
index a9fab396d6bc13974f70c20dce17a69506be593a..62d7042d69588588d7fb6ce0dd4b1cff33e119cc 100644 (file)
 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<>());
-
-       A2Contribution getOrAddContribution(String contributionId) {
-               if (contributions.containsKey(contributionId))
-                       return contributions.get(contributionId);
-               else
-                       return new A2Contribution(this, contributionId);
-       }
-
-       void asTree(String prefix, StringBuffer buf) {
-               if (prefix == null)
-                       prefix = "";
-               for (String contributionId : contributions.keySet()) {
-                       buf.append(prefix);
-                       buf.append(contributionId);
-                       buf.append('\n');
-                       A2Contribution contribution = contributions.get(contributionId);
-                       contribution.asTree(prefix + " ", buf);
-               }
-       }
-
-       void asTree() {
-               StringBuffer buf = new StringBuffer();
-               asTree("", buf);
-               System.out.println(buf);
-       }
-
-       A2Component findComponent(String componentId) {
-               SortedMap<A2Contribution, A2Component> res = new TreeMap<>();
-               for (A2Contribution contribution : contributions.values()) {
-                       components: for (String componentIdKey : contribution.components.keySet()) {
-                               if (componentId.equals(componentIdKey)) {
-                                       res.put(contribution, contribution.components.get(componentIdKey));
-                                       break components;
-                               }
-                       }
-               }
-               if (res.size() == 0)
-                       return null;
-               // TODO explicit contribution priorities
-               return res.get(res.lastKey());
-
-       }
-
-       A2Branch findBranch(String componentId, Version version) {
-               A2Component component = findComponent(componentId);
-               if (component == null)
-                       return null;
-               String branchId = version.getMajor() + "." + version.getMinor();
-               if (!component.branches.containsKey(branchId))
-                       return null;
-               return component.branches.get(branchId);
-       }
-
-       protected String readVersionFromModule(Path modulePath) {
-               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) {
-               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 interface ProvisioningSource {
+       /** List all contributions of this source. */
+       Iterable<A2Contribution> listContributions(Object filter);
 
-       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);
-       }
+       /** Install a module in the OSGi runtime. */
+       Bundle install(BundleContext bc, A2Module module);
 
-       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 install OSGi bundle from " + dir, e);
-               }
+       /** Update a module in the OSGi runtime. */
+       void update(Bundle bundle, A2Module module);
 
-       }
+       /** Finds the {@link A2Branch} related to this component and version. */
+       A2Branch findBranch(String componentId, Version version);
 
-       private InputStream newInputStream(Object locator) throws IOException {
-               if (locator instanceof Path) {
-                       return Files.newInputStream((Path) locator);
-               } else if (locator instanceof URL) {
-                       return ((URL) locator).openStream();
-               } else {
-                       throw new IllegalArgumentException("Unsupported module locator type " + locator.getClass());
-               }
-       }
 }