Refactor Argeo init
authorMathieu Baudier <mbaudier@argeo.org>
Tue, 5 Mar 2024 09:14:20 +0000 (10:14 +0100)
committerMathieu Baudier <mbaudier@argeo.org>
Tue, 5 Mar 2024 09:14:20 +0000 (10:14 +0100)
48 files changed:
org.argeo.cms.lib.sshd/src/org/argeo/cms/ssh/CmsSshServer.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsContextImpl.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java
org.argeo.init/bnd.bnd
org.argeo.init/src/org/argeo/api/a2/A2Branch.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/A2Component.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/A2Contribution.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/A2Exception.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/A2Module.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/A2Source.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/AbstractProvisioningSource.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/ClasspathSource.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/FsA2Source.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/FsM2Source.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/OsgiContext.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/ProvisioningManager.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/ProvisioningSource.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/a2/package-info.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/init/InitConstants.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/init/RuntimeContext.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/api/init/RuntimeManager.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/init/RuntimeContext.java [deleted file]
org.argeo.init/src/org/argeo/init/RuntimeManager.java [deleted file]
org.argeo.init/src/org/argeo/init/RuntimeManagerMain.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/init/Service.java [deleted file]
org.argeo.init/src/org/argeo/init/ServiceMain.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/init/StaticRuntimeContext.java
org.argeo.init/src/org/argeo/init/SysInitMain.java [new file with mode: 0644]
org.argeo.init/src/org/argeo/init/a2/A2Branch.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/A2Component.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/A2Contribution.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/A2Exception.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/A2Module.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/A2Source.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/AbstractProvisioningSource.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/ClasspathSource.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/FsA2Source.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/FsM2Source.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/OsgiContext.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/ProvisioningManager.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/ProvisioningSource.java [deleted file]
org.argeo.init/src/org/argeo/init/a2/package-info.java [deleted file]
org.argeo.init/src/org/argeo/init/logging/ThinLogging.java
org.argeo.init/src/org/argeo/init/osgi/Activator.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/internal/init/InternalState.java [new file with mode: 0644]

index 98bb045441b51a306988f95346127998ba08e50c..2480c3df450e1195fce6f7c672a2cc293ef94173 100644 (file)
@@ -207,9 +207,9 @@ public class CmsSshServer implements CmsSshd {
                } catch (IOException | KeyStoreException | NoSuchProviderException | NoSuchAlgorithmException
                                | CertificateException | IllegalArgumentException | UnrecoverableKeyException e) {
                        if (log.isTraceEnabled())
-                               log.error("Cannot add node public key to SSH authorized keys", e);
+                               log.warn("Cannot add node public key to SSH authorized keys", e);
                        else
-                               log.error("Cannot add node public key to SSH authorized keys: " + e.getMessage());
+                               log.warn("Cannot add node public key to SSH authorized keys: " + e);
                        return null;
                }
 
index 01d285b8cfd2767b8d4252b5cb93cb30861950a9..25f8dad6a784f1c496b6ff3a2b2efaa941e2512d 100644 (file)
@@ -96,7 +96,8 @@ public class CmsContextImpl implements CmsContext {
                        availableSince = System.currentTimeMillis();
                        long jvmUptime = ManagementFactory.getRuntimeMXBean().getUptime();
                        String jvmUptimeStr = " in " + (jvmUptime / 1000) + "." + (jvmUptime % 1000) + "s";
-                       log.info("## ARGEO CMS AVAILABLE" + (log.isDebugEnabled() ? jvmUptimeStr : "") + " ##");
+                       log.info("## ARGEO CMS " + cmsState.getUuid() + " AVAILABLE" + (log.isDebugEnabled() ? jvmUptimeStr : "")
+                                       + " ##");
                        if (log.isDebugEnabled()) {
                                log.debug("## state: " + state);
                                if (data != null)
index 60a51b44f7f8a8f913f8008e11460b2a956114f1..6fe8dcb7b2b5a175eec5141ec16674b8dbff34c9 100644 (file)
@@ -259,7 +259,8 @@ public class CmsStateImpl implements CmsState {
                        log.debug("CMS stopping...  (" + this.uuid + ")");
 
                long duration = ((System.currentTimeMillis() - availableSince) / 1000) / 60;
-               log.info("## ARGEO CMS STOPPED after " + (duration / 60) + "h " + (duration % 60) + "min uptime ##");
+               log.info("## ARGEO CMS " + uuid + " STOPPED after " + (duration / 60) + "h " + (duration % 60)
+                               + "min uptime ##");
        }
 
        private void firstInit() throws IOException {
index 3dbe6831bd05d53d5172b931bc3d8d6fe79534a2..34023cc7d023e9793de433c6cb74bd1ce0b8cc7c 100644 (file)
@@ -6,3 +6,5 @@ Bundle-Activator: org.argeo.init.osgi.Activator
 Import-Package: \
 org.osgi.*;version=0.0.0,\
 java.util.logging;resolution:=optional
+
+Export-Package: org.argeo.api.*
\ No newline at end of file
diff --git a/org.argeo.init/src/org/argeo/api/a2/A2Branch.java b/org.argeo.init/src/org/argeo/api/a2/A2Branch.java
new file mode 100644 (file)
index 0000000..1db82a7
--- /dev/null
@@ -0,0 +1,89 @@
+package org.argeo.api.a2;
+
+import java.util.Collections;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import org.argeo.init.osgi.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.
+ */
+public class A2Branch implements Comparable<A2Branch> {
+       private final A2Component component;
+       private final String id;
+
+       final SortedMap<Version, A2Module> modules = Collections.synchronizedSortedMap(new TreeMap<>());
+
+       public A2Branch(A2Component component, String id) {
+               this.component = component;
+               this.id = id;
+               component.branches.put(id, this);
+       }
+
+       public Iterable<A2Module> listModules(Object filter) {
+               return modules.values();
+       }
+
+       A2Module getOrAddModule(Version version, Object locator) {
+               if (modules.containsKey(version)) {
+                       A2Module res = modules.get(version);
+                       if (OsgiBootUtils.isDebug() && !res.getLocator().equals(locator)) {
+                               OsgiBootUtils.debug("Inconsistent locator " + locator + " (registered: " + res.getLocator() + ")");
+                       }
+                       return res;
+               } else
+                       return new A2Module(this, version, locator);
+       }
+
+       public A2Module last() {
+               return modules.get(modules.lastKey());
+       }
+
+       public A2Module first() {
+               return modules.get(modules.firstKey());
+       }
+
+       public A2Component getComponent() {
+               return component;
+       }
+
+       public String getId() {
+               return id;
+       }
+
+       @Override
+       public int compareTo(A2Branch o) {
+               return id.compareTo(id);
+       }
+
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof A2Branch) {
+                       A2Branch o = (A2Branch) obj;
+                       return component.equals(o.component) && id.equals(o.id);
+               } else
+                       return false;
+       }
+
+       @Override
+       public String toString() {
+               return getCoordinates();
+       }
+
+       public String getCoordinates() {
+               return component + ":" + id;
+       }
+
+       static String versionToBranchId(Version version) {
+               return version.getMajor() + "." + version.getMinor();
+       }
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/A2Component.java b/org.argeo.init/src/org/argeo/api/a2/A2Component.java
new file mode 100644 (file)
index 0000000..d8b7512
--- /dev/null
@@ -0,0 +1,105 @@
+package org.argeo.api.a2;
+
+import java.util.Collections;
+import java.util.SortedMap;
+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.
+ */
+public class A2Component implements Comparable<A2Component> {
+       private final A2Contribution contribution;
+       private final String id;
+
+       final SortedMap<String, A2Branch> branches = Collections.synchronizedSortedMap(new TreeMap<>());
+
+       public A2Component(A2Contribution contribution, String id) {
+               this.contribution = contribution;
+               this.id = id;
+               contribution.components.put(id, this);
+       }
+
+       public Iterable<A2Branch> listBranches(Object filter) {
+               return branches.values();
+       }
+
+       A2Branch getOrAddBranch(String branchId) {
+               if (!branches.containsKey(branchId)) {
+                       A2Branch a2Branch = new A2Branch(this, branchId);
+                       branches.put(branchId, a2Branch);
+               }
+               return branches.get(branchId);
+       }
+
+       A2Module getOrAddModule(Version version, Object locator) {
+               A2Branch branch = getOrAddBranch(A2Branch.versionToBranchId(version));
+               A2Module module = branch.getOrAddModule(version, locator);
+               return module;
+       }
+
+       public A2Branch last() {
+               return branches.get(branches.lastKey());
+       }
+
+       public A2Contribution getContribution() {
+               return contribution;
+       }
+
+       public String getId() {
+               return id;
+       }
+
+       @Override
+       public int compareTo(A2Component o) {
+               return id.compareTo(o.id);
+       }
+
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof A2Component) {
+                       A2Component o = (A2Component) obj;
+                       return contribution.equals(o.contribution) && id.equals(o.id);
+               } else
+                       return false;
+       }
+
+       @Override
+       public String toString() {
+               return contribution.getId() + ":" + id;
+       }
+
+       void asTree(String prefix, StringBuffer buf) {
+               if (prefix == null)
+                       prefix = "";
+               A2Branch lastBranch = last();
+               SortedMap<String, A2Branch> displayMap = new TreeMap<>(Collections.reverseOrder());
+               displayMap.putAll(branches);
+               for (String branchId : displayMap.keySet()) {
+                       A2Branch branch = displayMap.get(branchId);
+                       if (!lastBranch.equals(branch)) {
+                               buf.append('\n');
+                               buf.append(prefix);
+                       } else {
+                               buf.append(" -");
+                       }
+                       buf.append(prefix);
+                       buf.append(branchId);
+                       A2Module first = branch.first();
+                       A2Module last = branch.last();
+                       buf.append(" (").append(last.getVersion());
+                       if (!first.equals(last))
+                               buf.append(" ... ").append(first.getVersion());
+                       buf.append(')');
+               }
+       }
+
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/A2Contribution.java b/org.argeo.init/src/org/argeo/api/a2/A2Contribution.java
new file mode 100644 (file)
index 0000000..f099ceb
--- /dev/null
@@ -0,0 +1,151 @@
+package org.argeo.api.a2;
+
+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.
+ */
+public class A2Contribution implements Comparable<A2Contribution> {
+       final static String BOOT = "boot";
+       final static String RUNTIME = "runtime";
+       final static String CLASSPATH = "classpath";
+
+       final static String DEFAULT = "default";
+       final static String LIB = "lib";
+
+       private final ProvisioningSource source;
+       private final String id;
+
+       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);
+       }
+
+       public Iterable<A2Component> listComponents(Object filter) {
+               return components.values();
+       }
+
+       A2Component getOrAddComponent(String componentId) {
+               if (components.containsKey(componentId))
+                       return components.get(componentId);
+               else
+                       return new A2Component(this, componentId);
+       }
+
+       public ProvisioningSource getSource() {
+               return source;
+       }
+
+       public String getId() {
+               return id;
+       }
+
+       @Override
+       public int compareTo(A2Contribution o) {
+               return id.compareTo(o.id);
+       }
+
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof A2Contribution) {
+                       A2Contribution o = (A2Contribution) obj;
+                       return id.equals(o.id);
+               } else
+                       return false;
+       }
+
+       @Override
+       public String toString() {
+               return id;
+       }
+
+       void asTree(String prefix, StringBuffer buf) {
+               if (prefix == null)
+                       prefix = "";
+               for (String componentId : components.keySet()) {
+                       buf.append(prefix);
+                       buf.append(componentId);
+                       A2Component component = components.get(componentId);
+                       component.asTree(prefix, buf);
+                       buf.append('\n');
+               }
+       }
+
+       static String localOsArchRelativePath() {
+               return Os.local().toString() + "/" + Arch.local().toString();
+       }
+
+       static enum Os {
+               LINUX, WIN32, MACOSX, UNKOWN;
+
+               @Override
+               public String toString() {
+                       return name().toLowerCase();
+               }
+
+               public static Os local() {
+                       String osStr = System.getProperty("os.name").toLowerCase();
+                       if (osStr.startsWith("linux"))
+                               return LINUX;
+                       if (osStr.startsWith("win"))
+                               return WIN32;
+                       if (osStr.startsWith("mac"))
+                               return MACOSX;
+                       return UNKOWN;
+               }
+
+       }
+
+       static enum Arch {
+               X86_64, AARCH64, X86, POWERPC, UNKOWN;
+
+               @Override
+               public String toString() {
+                       return name().toLowerCase();
+               }
+
+               public static Arch local() {
+                       String archStr = System.getProperty("os.arch").toLowerCase();
+                       return switch (archStr) {
+                       case "x86_64":
+                       case "amd64":
+                       case "x86-64": {
+                               yield X86_64;
+                       }
+                       case "aarch64":
+                       case "arm64": {
+                               yield AARCH64;
+                       }
+                       case "x86":
+                       case "i386":
+                       case "i686": {
+                               yield X86;
+                       }
+                       case "powerpc":
+                       case "ppc": {
+                               yield POWERPC;
+                       }
+                       default:
+                               yield UNKOWN;
+                       };
+               }
+       }
+
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/A2Exception.java b/org.argeo.init/src/org/argeo/api/a2/A2Exception.java
new file mode 100644 (file)
index 0000000..2e609f0
--- /dev/null
@@ -0,0 +1,15 @@
+package org.argeo.api.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.init/src/org/argeo/api/a2/A2Module.java b/org.argeo.init/src/org/argeo/api/a2/A2Module.java
new file mode 100644 (file)
index 0000000..4ad4362
--- /dev/null
@@ -0,0 +1,62 @@
+package org.argeo.api.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.
+ */
+public class A2Module implements Comparable<A2Module> {
+       private final A2Branch branch;
+       private final Version version;
+       private final Object locator;
+
+       public A2Module(A2Branch branch, Version version, Object locator) {
+               this.branch = branch;
+               this.version = version;
+               this.locator = locator;
+               branch.modules.put(version, this);
+       }
+
+       public A2Branch getBranch() {
+               return branch;
+       }
+
+       public Version getVersion() {
+               return version;
+       }
+
+       Object getLocator() {
+               return locator;
+       }
+
+       @Override
+       public int compareTo(A2Module o) {
+               return version.compareTo(o.version);
+       }
+
+       @Override
+       public int hashCode() {
+               return version.hashCode();
+       }
+
+       @Override
+       public boolean equals(Object obj) {
+               if (obj instanceof A2Module) {
+                       A2Module o = (A2Module) obj;
+                       return branch.equals(o.branch) && version.equals(o.version);
+               } else
+                       return false;
+       }
+
+       @Override
+       public String toString() {
+               return getCoordinates();
+       }
+
+       public String getCoordinates() {
+               return branch.getComponent() + ":" + version;
+       }
+
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/A2Source.java b/org.argeo.init/src/org/argeo/api/a2/A2Source.java
new file mode 100644 (file)
index 0000000..5883e9d
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.api.a2;
+
+import java.net.URI;
+
+/** A provisioning source in A2 format. */
+public interface A2Source extends ProvisioningSource {
+       /** Use standard a2 protocol, installing from source URL. */
+       final static String SCHEME_A2 = "a2";
+       /**
+        * Use equinox-specific reference: installation, which does not copy the bundle
+        * content.
+        */
+       final static String SCHEME_A2_REFERENCE = "a2+reference";
+       final static String DEFAULT_A2_URI = SCHEME_A2 + ":///";
+       final static String DEFAULT_A2_REFERENCE_URI = SCHEME_A2_REFERENCE + ":///";
+
+       final static String INCLUDE = "include";
+       final static String EXCLUDE = "exclude";
+
+       URI getUri();
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/AbstractProvisioningSource.java b/org.argeo.init/src/org/argeo/api/a2/AbstractProvisioningSource.java
new file mode 100644 (file)
index 0000000..62416fe
--- /dev/null
@@ -0,0 +1,269 @@
+package org.argeo.api.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<>());
+
+       private final boolean usingReference;
+
+       public AbstractProvisioningSource(boolean usingReference) {
+               this.usingReference = usingReference;
+       }
+
+       public Iterable<A2Contribution> listContributions(Object filter) {
+               return contributions.values();
+       }
+
+       @Override
+       public Bundle install(BundleContext bc, A2Module module) {
+               try {
+                       Object locator = module.getLocator();
+                       if (usingReference && locator instanceof Path locatorPath) {
+                               String referenceUrl = "reference:file:" + locatorPath.toString();
+                               Bundle bundle = bc.installBundle(referenceUrl);
+                               return bundle;
+                       } else {
+                               Path locatorPath = (Path) locator;
+                               Path pathToUse;
+                               boolean isTemp = false;
+                               if (locator instanceof Path && Files.isDirectory(locatorPath)) {
+                                       pathToUse = toTempJar(locatorPath);
+                                       isTemp = true;
+                               } else {
+                                       pathToUse = locatorPath;
+                               }
+                               Bundle bundle;
+                               try (InputStream in = newInputStream(pathToUse)) {
+                                       bundle = bc.installBundle(locatorPath.toAbsolutePath().toString(), in);
+                               }
+
+                               if (isTemp && pathToUse != null)
+                                       Files.deleteIfExists(pathToUse);
+                               return bundle;
+                       }
+               } catch (BundleException | IOException e) {
+                       throw new A2Exception("Cannot install module " + module, e);
+               }
+       }
+
+       @Override
+       public void update(Bundle bundle, A2Module module) {
+               try {
+                       Object locator = module.getLocator();
+                       if (usingReference && locator instanceof Path) {
+                               try (InputStream in = newInputStream(locator)) {
+                                       bundle.update(in);
+                               }
+                       } else {
+                               Path locatorPath = (Path) locator;
+                               Path pathToUse;
+                               boolean isTemp = false;
+                               if (locator instanceof Path && Files.isDirectory(locatorPath)) {
+                                       pathToUse = toTempJar(locatorPath);
+                                       isTemp = true;
+                               } else {
+                                       pathToUse = locatorPath;
+                               }
+                               try (InputStream in = newInputStream(pathToUse)) {
+                                       bundle.update(in);
+                               }
+                               if (isTemp && pathToUse != null)
+                                       Files.deleteIfExists(pathToUse);
+                       }
+               } 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[] readNameVersionFromModule(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);
+               String symbolicName = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
+               int semiColIndex = symbolicName.indexOf(';');
+               if (semiColIndex >= 0)
+                       symbolicName = symbolicName.substring(0, semiColIndex);
+               return new String[] { symbolicName, versionStr };
+       }
+
+       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;
+       }
+
+       protected boolean isUsingReference() {
+               return usingReference;
+       }
+
+       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());
+               }
+       }
+
+       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);
+               }
+
+       }
+
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/ClasspathSource.java b/org.argeo.init/src/org/argeo/api/a2/ClasspathSource.java
new file mode 100644 (file)
index 0000000..2f1cf95
--- /dev/null
@@ -0,0 +1,43 @@
+package org.argeo.api.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.init.osgi.OsgiBootUtils;
+import org.osgi.framework.Version;
+
+/**
+ * A provisioning source based on the linear classpath with which the JVM has
+ * been started.
+ */
+public class ClasspathSource extends AbstractProvisioningSource {
+       
+       public ClasspathSource() {
+               super(true);
+       }
+
+       void load() throws IOException {
+               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);
+                       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.init/src/org/argeo/api/a2/FsA2Source.java b/org.argeo.init/src/org/argeo/api/a2/FsA2Source.java
new file mode 100644 (file)
index 0000000..ebb6020
--- /dev/null
@@ -0,0 +1,171 @@
+package org.argeo.api.a2;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.StringJoiner;
+import java.util.TreeMap;
+
+import org.argeo.init.osgi.OsgiBootUtils;
+import org.osgi.framework.Version;
+
+/** A file system {@link AbstractProvisioningSource} in A2 format. */
+public class FsA2Source extends AbstractProvisioningSource implements A2Source {
+       private final Path base;
+       private final Map<String, String> variantsXOr;
+
+       private final List<String> includes;
+       private final List<String> excludes;
+
+       public FsA2Source(Path base, Map<String, String> variantsXOr, boolean usingReference, List<String> includes,
+                       List<String> excludes) {
+               super(usingReference);
+               this.base = base;
+               this.variantsXOr = new HashMap<>(variantsXOr);
+               this.includes = includes;
+               this.excludes = excludes;
+       }
+
+       void load() throws IOException {
+               SortedMap<Path, A2Contribution> contributions = new TreeMap<>();
+
+               DirectoryStream<Path> contributionPaths = Files.newDirectoryStream(base);
+               contributions: for (Path contributionPath : contributionPaths) {
+                       if (Files.isDirectory(contributionPath)) {
+                               String contributionId = contributionPath.getFileName().toString();
+                               if (A2Contribution.BOOT.equals(contributionId))// skip boot
+                                       continue contributions;
+                               if (contributionId.contains(".")) {
+                                       A2Contribution contribution = getOrAddContribution(contributionId);
+                                       contributions.put(contributionPath, contribution);
+                               } else {// variants
+                                       Path variantPath = null;
+                                       // is it an explicit variant?
+                                       String variant = variantsXOr.get(contributionPath.getFileName().toString());
+                                       if (variant != null) {
+                                               variantPath = contributionPath.resolve(variant);
+                                       }
+
+                                       // is there a default variant?
+                                       if (variantPath == null) {
+                                               Path defaultPath = contributionPath.resolve(A2Contribution.DEFAULT);
+                                               if (Files.exists(defaultPath)) {
+                                                       variantPath = defaultPath;
+                                               }
+                                       }
+
+                                       if (variantPath == null)
+                                               continue contributions;
+
+                                       // a variant was found, let's collect its contributions (also common ones in its
+                                       // parent)
+                                       if (Files.exists(variantPath.getParent())) {
+                                               for (Path variantContributionPath : Files.newDirectoryStream(variantPath.getParent())) {
+                                                       String variantContributionId = variantContributionPath.getFileName().toString();
+                                                       if (variantContributionId.contains(".")) {
+                                                               A2Contribution contribution = getOrAddContribution(variantContributionId);
+                                                               contributions.put(variantContributionPath, contribution);
+                                                       }
+                                               }
+                                       }
+                                       if (Files.exists(variantPath)) {
+                                               for (Path variantContributionPath : Files.newDirectoryStream(variantPath)) {
+                                                       String variantContributionId = variantContributionPath.getFileName().toString();
+                                                       if (variantContributionId.contains(".")) {
+                                                               A2Contribution contribution = getOrAddContribution(variantContributionId);
+                                                               contributions.put(variantContributionPath, contribution);
+                                                       }
+                                               }
+                                       }
+                               }
+                       }
+               }
+
+               contributions: for (Path contributionPath : contributions.keySet()) {
+                       String contributionId = contributionPath.getFileName().toString();
+                       if (includes != null && !includes.contains(contributionId))
+                               continue contributions;
+                       if (excludes != null && excludes.contains(contributionId))
+                               continue contributions;
+                       A2Contribution contribution = getOrAddContribution(contributionId);
+                       DirectoryStream<Path> modulePaths = Files.newDirectoryStream(contributionPath);
+                       modules: for (Path modulePath : modulePaths) {
+                               if (!Files.isDirectory(modulePath)) {
+                                       // OsgiBootUtils.debug("Registering " + modulePath);
+                                       String moduleFileName = modulePath.getFileName().toString();
+                                       int lastDot = moduleFileName.lastIndexOf('.');
+                                       String ext = moduleFileName.substring(lastDot + 1);
+                                       if (!"jar".equals(ext))
+                                               continue modules;
+                                       Version version;
+                                       // TODO optimise? check attributes?
+                                       String[] nameVersion = readNameVersionFromModule(modulePath);
+                                       String componentName = nameVersion[0];
+                                       String versionStr = nameVersion[1];
+                                       if (versionStr != null) {
+                                               version = new Version(versionStr);
+                                       } else {
+                                               OsgiBootUtils.debug("Ignore " + modulePath + " since version cannot be found");
+                                               continue modules;
+                                       }
+//                                     }
+                                       A2Component component = contribution.getOrAddComponent(componentName);
+                                       A2Module module = component.getOrAddModule(version, modulePath);
+                                       if (OsgiBootUtils.isDebug())
+                                               OsgiBootUtils.debug("Registered " + module);
+                               }
+                       }
+               }
+
+       }
+
+       @Override
+       public URI getUri() {
+               URI baseUri = base.toUri();
+               try {
+                       if (baseUri.getScheme().equals("file")) {
+                               String queryPart = "";
+                               if (!getVariantsXOr().isEmpty()) {
+                                       StringJoiner sj = new StringJoiner("&");
+                                       for (String key : getVariantsXOr().keySet()) {
+                                               sj.add(key + "=" + getVariantsXOr().get(key));
+                                       }
+                                       queryPart = sj.toString();
+                               }
+                               return new URI(isUsingReference() ? SCHEME_A2_REFERENCE : SCHEME_A2, null, base.toString(), queryPart,
+                                               null);
+                       } else {
+                               throw new UnsupportedOperationException("Unsupported scheme " + baseUri.getScheme());
+                       }
+               } catch (URISyntaxException e) {
+                       throw new IllegalStateException("Cannot build URI from " + baseUri, e);
+               }
+       }
+
+       protected Map<String, String> getVariantsXOr() {
+               return variantsXOr;
+       }
+
+//     public static void main(String[] args) {
+//             if (args.length == 0)
+//                     throw new IllegalArgumentException("Usage: <path to A2 base>");
+//             try {
+//                     Map<String, String> xOr = new HashMap<>();
+//                     xOr.put("osgi", "equinox");
+//                     xOr.put("swt", "rap");
+//                     FsA2Source context = new FsA2Source(Paths.get(args[0]), xOr);
+//                     context.load();
+//                     context.asTree();
+//             } catch (Exception e) {
+//                     e.printStackTrace();
+//             }
+//     }
+
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/FsM2Source.java b/org.argeo.init/src/org/argeo/api/a2/FsM2Source.java
new file mode 100644 (file)
index 0000000..f63a904
--- /dev/null
@@ -0,0 +1,66 @@
+package org.argeo.api.a2;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+
+import org.argeo.init.osgi.OsgiBootUtils;
+import org.osgi.framework.Version;
+
+/** A file system {@link AbstractProvisioningSource} in Maven 2 format. */
+public class FsM2Source extends AbstractProvisioningSource {
+       private final Path base;
+
+       public FsM2Source(Path base) {
+               super(false);
+               this.base = base;
+       }
+
+       void load() throws IOException {
+               Files.walkFileTree(base, new ArtifactFileVisitor());
+       }
+
+       class ArtifactFileVisitor extends SimpleFileVisitor<Path> {
+
+               @Override
+               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                       // OsgiBootUtils.debug("Processing " + file);
+                       if (file.toString().endsWith(".jar")) {
+                               Version version;
+                               try {
+                                       version = new Version(readVersionFromModule(file));
+                               } catch (Exception e) {
+                                       // ignore non OSGi
+                                       return FileVisitResult.CONTINUE;
+                               }
+                               String moduleName = readSymbolicNameFromModule(file);
+                               Path groupPath = file.getParent().getParent().getParent();
+                               Path relGroupPath = base.relativize(groupPath);
+                               String contributionName = relGroupPath.toString().replace(File.separatorChar, '.');
+                               A2Contribution contribution = getOrAddContribution(contributionName);
+                               A2Component component = contribution.getOrAddComponent(moduleName);
+                               A2Module module = component.getOrAddModule(version, file);
+                               if (OsgiBootUtils.isDebug())
+                                       OsgiBootUtils.debug("Registered " + module);
+                       }
+                       return super.visitFile(file, attrs);
+               }
+
+       }
+
+       public static void main(String[] args) {
+               try {
+                       FsM2Source context = new FsM2Source(Paths.get("/home/mbaudier/.m2/repository"));
+                       context.load();
+                       context.asTree();
+               } catch (Exception e) {
+                       e.printStackTrace();
+               }
+       }
+
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/OsgiContext.java b/org.argeo.init/src/org/argeo/api/a2/OsgiContext.java
new file mode 100644 (file)
index 0000000..dfed170
--- /dev/null
@@ -0,0 +1,48 @@
+package org.argeo.api.a2;
+
+import org.argeo.init.osgi.OsgiBootUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.Version;
+
+/**
+ * A running OSGi bundle context seen as a {@link AbstractProvisioningSource}.
+ */
+class OsgiContext extends AbstractProvisioningSource {
+       private final BundleContext bc;
+
+       private A2Contribution runtimeContribution;
+
+       public OsgiContext(BundleContext bc) {
+               super(false);
+               this.bc = bc;
+               runtimeContribution = getOrAddContribution(A2Contribution.RUNTIME);
+       }
+
+       public OsgiContext() {
+               super(false);
+               Bundle bundle = FrameworkUtil.getBundle(OsgiContext.class);
+               if (bundle == null)
+                       throw new IllegalArgumentException(
+                                       "OSGi Boot bundle must be started or a bundle context must be specified");
+               this.bc = bundle.getBundleContext();
+       }
+
+       void load() {
+               for (Bundle bundle : bc.getBundles()) {
+                       registerBundle(bundle);
+               }
+
+       }
+
+       void registerBundle(Bundle bundle) {
+               String componentId = bundle.getSymbolicName();
+               Version version = bundle.getVersion();
+               A2Component component = runtimeContribution.getOrAddComponent(componentId);
+               A2Module module = component.getOrAddModule(version, bundle);
+               if (OsgiBootUtils.isDebug())
+                       OsgiBootUtils.debug("Registered bundle module " + module + " (location id: " + bundle.getLocation() + ")");
+
+       }
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/ProvisioningManager.java b/org.argeo.init/src/org/argeo/api/a2/ProvisioningManager.java
new file mode 100644 (file)
index 0000000..104f08a
--- /dev/null
@@ -0,0 +1,303 @@
+package org.argeo.api.a2;
+
+import static org.argeo.api.a2.A2Source.SCHEME_A2;
+import static org.argeo.api.a2.A2Source.SCHEME_A2_REFERENCE;
+
+import java.io.File;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.argeo.init.osgi.OsgiBootUtils;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.Version;
+import org.osgi.framework.wiring.FrameworkWiring;
+
+/** Loads provisioning sources into an OSGi context. */
+public class ProvisioningManager {
+       BundleContext bc;
+       OsgiContext osgiContext;
+       List<ProvisioningSource> sources = Collections.synchronizedList(new ArrayList<>());
+
+       public ProvisioningManager(BundleContext bc) {
+               this.bc = bc;
+               osgiContext = new OsgiContext(bc);
+               osgiContext.load();
+       }
+
+       protected void addSource(ProvisioningSource source) {
+               sources.add(source);
+       }
+
+       void installWholeSource(ProvisioningSource source) {
+               Set<Bundle> updatedBundles = new HashSet<>();
+               for (A2Contribution contribution : source.listContributions(null)) {
+                       for (A2Component component : contribution.components.values()) {
+                               A2Module module = component.last().last();
+                               Bundle bundle = installOrUpdate(module);
+                               if (bundle != null)
+                                       updatedBundles.add(bundle);
+                       }
+               }
+//             FrameworkWiring frameworkWiring = bc.getBundle(0).adapt(FrameworkWiring.class);
+//             frameworkWiring.refreshBundles(updatedBundles);
+       }
+
+       public void registerSource(String uri) {
+               try {
+                       URI u = new URI(uri);
+
+                       // XOR
+                       Map<String, List<String>> properties = queryToMap(u);
+                       Map<String, String> xOr = new HashMap<>();
+                       List<String> includes = null;
+                       List<String> excludes = null;
+                       for (String key : properties.keySet()) {
+                               List<String> lst = properties.get(key);
+                               if (A2Source.INCLUDE.equals(key)) {
+                                       includes = new ArrayList<>(lst);
+                               } else if (A2Source.EXCLUDE.equals(key)) {
+                                       excludes = new ArrayList<>(lst);
+                               } else {
+                                       if (lst.size() != 1)
+                                               throw new IllegalArgumentException("Invalid XOR definitions in " + uri);
+                                       xOr.put(key, lst.get(0));
+                               }
+                       }
+
+                       if (SCHEME_A2.equals(u.getScheme()) || SCHEME_A2_REFERENCE.equals(u.getScheme())) {
+                               if (u.getHost() == null || "".equals(u.getHost())) {
+                                       String baseStr = u.getPath();
+                                       if (File.separatorChar == '\\') {// MS Windows
+                                               baseStr = baseStr.substring(1).replace('/', File.separatorChar);
+                                       }
+                                       Path base = Paths.get(baseStr);
+                                       if (Files.exists(base)) {
+                                               FsA2Source source = new FsA2Source(base, xOr, SCHEME_A2_REFERENCE.equals(u.getScheme()),
+                                                               includes, excludes);
+                                               source.load();
+                                               addSource(source);
+                                               OsgiBootUtils.info("Registered " + uri + " as source");
+
+                                               // OS specific / native
+                                               String localRelPath = A2Contribution.localOsArchRelativePath();
+                                               Path localLibBase = base.resolve(A2Contribution.LIB).resolve(localRelPath);
+                                               if (Files.exists(localLibBase)) {
+                                                       FsA2Source libSource = new FsA2Source(localLibBase, xOr,
+                                                                       SCHEME_A2_REFERENCE.equals(u.getScheme()), includes, excludes);
+                                                       libSource.load();
+                                                       addSource(libSource);
+                                                       OsgiBootUtils.info("Registered OS-specific " + uri + " as source (" + localRelPath + ")");
+                                               }
+                                       } else {
+                                               OsgiBootUtils.debug("Source " + base + " does not exist, ignoring.");
+                                       }
+                               } else {
+                                       throw new UnsupportedOperationException(
+                                                       "Remote installation is not yet supported, cannot add source " + u);
+                               }
+                       } else {
+                               throw new IllegalArgumentException("Unkown scheme: for source " + u);
+                       }
+               } catch (Exception e) {
+                       throw new A2Exception("Cannot add source " + uri, e);
+               }
+       }
+
+       public boolean registerDefaultSource() {
+               String frameworkLocation = bc.getProperty("osgi.framework");
+               try {
+                       URI frameworkLocationUri = new URI(frameworkLocation);
+                       if ("file".equals(frameworkLocationUri.getScheme())) {
+                               Path frameworkPath = Paths.get(frameworkLocationUri);
+                               if (frameworkPath.getParent().getFileName().toString().equals(A2Contribution.BOOT)) {
+                                       Path base = frameworkPath.getParent().getParent();
+                                       String baseStr = base.toString();
+                                       if (File.separatorChar == '\\')// MS Windows
+                                               baseStr = '/' + baseStr.replace(File.separatorChar, '/');
+                                       URI baseUri = new URI(A2Source.SCHEME_A2, null, null, 0, baseStr, null, null);
+                                       registerSource(baseUri.toString());
+                                       OsgiBootUtils.debug("Default source from framework location " + frameworkLocation);
+                                       return true;
+                               }
+                       }
+               } catch (Exception e) {
+                       OsgiBootUtils.error("Cannot register default source based on framework location " + frameworkLocation, e);
+               }
+               return false;
+       }
+
+       public void install(String spec) {
+               if (spec == null) {
+                       for (ProvisioningSource source : sources) {
+                               installWholeSource(source);
+                       }
+               }
+       }
+
+       /** @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();
+                       A2Branch osgiBranch = osgiContext.findBranch(module.getBranch().getComponent().getId(), moduleVersion);
+                       if (osgiBranch == null) {
+                               Bundle bundle = moduleSource.install(bc, module);
+                               // TODO make it more dynamic, based on OSGi APIs
+                               osgiContext.registerBundle(bundle);
+//                             if (OsgiBootUtils.isDebug())
+//                                     OsgiBootUtils.debug("Installed bundle " + bundle.getLocation() + " with version " + moduleVersion);
+                               return bundle;
+                       } else {
+                               A2Module lastOsgiModule = osgiBranch.last();
+                               int compare = moduleVersion.compareTo(lastOsgiModule.getVersion());
+                               if (compare >= 0) {// update (also if same version)
+                                       Bundle bundle = (Bundle) lastOsgiModule.getLocator();
+                                       if (bundle.getBundleId() == 0)// ignore framework bundle
+                                               return null;
+                                       moduleSource.update(bundle, module);
+                                       // TODO make it more dynamic, based on OSGi APIs
+                                       // TODO remove old module? Or keep update history?
+                                       osgiContext.registerBundle(bundle);
+                                       if (compare == 0)
+                                               OsgiBootUtils
+                                                               .debug("Updated bundle " + bundle.getLocation() + " to same version " + moduleVersion);
+                                       else
+                                               OsgiBootUtils.info("Updated bundle " + bundle.getLocation() + " to version " + moduleVersion);
+                                       return bundle;
+                               } else {
+                                       if (OsgiBootUtils.isDebug())
+                                               OsgiBootUtils.debug("Did not install as bundle module " + module
+                                                               + " since a module with higher version " + lastOsgiModule.getVersion()
+                                                               + " is already installed for branch " + osgiBranch);
+                               }
+                       }
+               } catch (Exception e) {
+                       OsgiBootUtils.error("Could not install module " + module + ": " + e.getMessage(), e);
+               }
+               return null;
+       }
+
+       public Collection<Bundle> update() {
+               boolean fragmentsUpdated = false;
+               Set<Bundle> updatedBundles = new HashSet<>();
+               bundles: for (Bundle bundle : bc.getBundles()) {
+                       for (ProvisioningSource source : sources) {
+                               String componentId = bundle.getSymbolicName();
+                               Version version = bundle.getVersion();
+                               A2Branch branch = source.findBranch(componentId, version);
+                               if (branch == null)
+                                       continue bundles;
+                               A2Module module = branch.last();
+                               Version moduleVersion = module.getVersion();
+                               int compare = moduleVersion.compareTo(version);
+                               if (compare > 0) {// update
+                                       try {
+                                               source.update(bundle, module);
+//                                             bundle.update(in);
+                                               String fragmentHost = bundle.getHeaders().get(Constants.FRAGMENT_HOST);
+                                               if (fragmentHost != null)
+                                                       fragmentsUpdated = true;
+                                               OsgiBootUtils.info("Updated bundle " + bundle.getLocation() + " to version " + moduleVersion);
+                                               updatedBundles.add(bundle);
+                                       } catch (Exception e) {
+                                               OsgiBootUtils.error("Cannot update with module " + module, e);
+                                       }
+                               }
+                       }
+               }
+               FrameworkWiring frameworkWiring = bc.getBundle(0).adapt(FrameworkWiring.class);
+               if (fragmentsUpdated)// refresh all
+                       frameworkWiring.refreshBundles(null);
+               else
+                       frameworkWiring.refreshBundles(updatedBundles);
+               return updatedBundles;
+       }
+
+       private static Map<String, List<String>> queryToMap(URI uri) {
+               return queryToMap(uri.getQuery());
+       }
+
+       private static Map<String, List<String>> queryToMap(String queryPart) {
+               try {
+                       final Map<String, List<String>> query_pairs = new LinkedHashMap<String, List<String>>();
+                       if (queryPart == null)
+                               return query_pairs;
+                       final String[] pairs = queryPart.split("&");
+                       for (String pair : pairs) {
+                               final int idx = pair.indexOf("=");
+                               final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name())
+                                               : pair;
+                               if (!query_pairs.containsKey(key)) {
+                                       query_pairs.put(key, new LinkedList<String>());
+                               }
+                               final String value = idx > 0 && pair.length() > idx + 1
+                                               ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name())
+                                               : null;
+                               query_pairs.get(key).add(value);
+                       }
+                       return query_pairs;
+               } catch (UnsupportedEncodingException e) {
+                       throw new IllegalArgumentException("Cannot convert " + queryPart + " to map", e);
+               }
+       }
+
+//     public static void main(String[] args) {
+//             if (args.length == 0)
+//                     throw new IllegalArgumentException("Usage: <path to A2 base>");
+//             Map<String, String> configuration = new HashMap<>();
+//             configuration.put("osgi.console", "2323");
+//             configuration.put("org.osgi.framework.bootdelegation",
+//                             "com.sun.jndi.ldap,com.sun.jndi.ldap.sasl,com.sun.security.jgss,com.sun.jndi.dns,com.sun.nio.file,com.sun.nio.sctp,sun.nio.cs");
+//             Framework framework = OsgiBootUtils.launch(configuration);
+//             try {
+//                     ProvisioningManager pm = new ProvisioningManager(framework.getBundleContext());
+//                     Map<String, String> xOr = new HashMap<>();
+//                     xOr.put("osgi", "equinox");
+//                     xOr.put("swt", "rap");
+//                     FsA2Source context = new FsA2Source(Paths.get(args[0]), xOr);
+//                     context.load();
+//                     pm.addSource(context);
+//                     if (framework.getBundleContext().getBundles().length == 1) {// initial
+//                             pm.install(null);
+//                     } else {
+//                             pm.update();
+//                     }
+//
+//                     Thread.sleep(2000);
+//
+//                     Bundle[] bundles = framework.getBundleContext().getBundles();
+//                     Arrays.sort(bundles, (b1, b2) -> b1.getSymbolicName().compareTo(b2.getSymbolicName()));
+//                     for (Bundle b : bundles)
+//                             if (b.getState() == Bundle.RESOLVED || b.getState() == Bundle.STARTING || b.getState() == Bundle.ACTIVE)
+//                                     System.out.println(b.getSymbolicName() + " " + b.getVersion());
+//                             else
+//                                     System.err.println(b.getSymbolicName() + " " + b.getVersion() + " (" + b.getState() + ")");
+//             } catch (Exception e) {
+//                     e.printStackTrace();
+//             } finally {
+//                     try {
+//                             framework.stop();
+//                     } catch (Exception e) {
+//                             e.printStackTrace();
+//                     }
+//             }
+//     }
+
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/ProvisioningSource.java b/org.argeo.init/src/org/argeo/api/a2/ProvisioningSource.java
new file mode 100644 (file)
index 0000000..ddba2a9
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.api.a2;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Version;
+
+/** Where components are retrieved from. */
+public interface ProvisioningSource {
+       /** List all contributions of this source. */
+       Iterable<A2Contribution> listContributions(Object filter);
+
+       /** Install a module in the OSGi runtime. */
+       Bundle install(BundleContext bc, A2Module module);
+
+       /** 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);
+
+}
diff --git a/org.argeo.init/src/org/argeo/api/a2/package-info.java b/org.argeo.init/src/org/argeo/api/a2/package-info.java
new file mode 100644 (file)
index 0000000..6a8bf71
--- /dev/null
@@ -0,0 +1,2 @@
+/** A2 OSGi repository format. */
+package org.argeo.api.a2;
\ No newline at end of file
diff --git a/org.argeo.init/src/org/argeo/api/init/InitConstants.java b/org.argeo.init/src/org/argeo/api/init/InitConstants.java
new file mode 100644 (file)
index 0000000..998a0a4
--- /dev/null
@@ -0,0 +1,22 @@
+package org.argeo.api.init;
+
+/** Supported init constants. */
+public interface InitConstants {
+
+       String PROP_ARGEO_OSGI_SOURCES = "argeo.osgi.sources";
+       String PROP_ARGEO_OSGI_START = "argeo.osgi.start";
+       String PROP_OSGI_INSTANCE_AREA = "osgi.instance.area";
+       String PROP_OSGI_CONFIGURATION_AREA = "osgi.configuration.area";
+       String PROP_OSGI_SHARED_CONFIGURATION_AREA = "osgi.sharedConfiguration.area";
+       String PROP_ARGEO_OSGI_MAX_START_LEVEL = "argeo.osgi.maxStartLevel";
+
+       // OSGi standard properties
+       String PROP_OSGI_BUNDLES_DEFAULTSTARTLEVEL = "osgi.bundles.defaultStartLevel";
+       String PROP_OSGI_STARTLEVEL = "osgi.startLevel";
+       String PROP_OSGI_USE_SYSTEM_PROPERTIES = "osgi.framework.useSystemProperties";
+
+       // Symbolic names
+       String SYMBOLIC_NAME_INIT = "org.argeo.init";
+       String SYMBOLIC_NAME_EQUINOX = "org.eclipse.osgi";
+
+}
diff --git a/org.argeo.init/src/org/argeo/api/init/RuntimeContext.java b/org.argeo.init/src/org/argeo/api/init/RuntimeContext.java
new file mode 100644 (file)
index 0000000..9f78d13
--- /dev/null
@@ -0,0 +1,10 @@
+package org.argeo.api.init;
+
+/** A runtime context with a life cycle. */
+public interface RuntimeContext extends Runnable {
+       /** Wait until this runtime context has closed. */
+       void waitForStop(long timeout) throws InterruptedException;
+
+       /** Close this runtime context. */
+       void close() throws Exception;
+}
diff --git a/org.argeo.init/src/org/argeo/api/init/RuntimeManager.java b/org.argeo.init/src/org/argeo/api/init/RuntimeManager.java
new file mode 100644 (file)
index 0000000..1f4a3ec
--- /dev/null
@@ -0,0 +1,11 @@
+package org.argeo.api.init;
+
+import java.util.Map;
+import java.util.function.Consumer;
+
+/** Dynamically manages multiple runtimes within a single JVM. */
+public interface RuntimeManager {
+       public void startRuntime(String relPath, Consumer<Map<String, String>> configCallback);
+
+       public void stopRuntime(String relPath, boolean async);
+}
diff --git a/org.argeo.init/src/org/argeo/init/RuntimeContext.java b/org.argeo.init/src/org/argeo/init/RuntimeContext.java
deleted file mode 100644 (file)
index d83f2ca..0000000
+++ /dev/null
@@ -1,10 +0,0 @@
-package org.argeo.init;
-
-/** A runtime context with a life cycle. */
-public interface RuntimeContext extends Runnable {
-       /** Wait until this runtime context has closed. */
-       void waitForStop(long timeout) throws InterruptedException;
-
-       /** Close this runtime context. */
-       void close() throws Exception;
-}
diff --git a/org.argeo.init/src/org/argeo/init/RuntimeManager.java b/org.argeo.init/src/org/argeo/init/RuntimeManager.java
deleted file mode 100644 (file)
index a5f48d4..0000000
+++ /dev/null
@@ -1,150 +0,0 @@
-package org.argeo.init;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
-import java.lang.System.Logger;
-import java.lang.System.Logger.Level;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.HashMap;
-import java.util.Hashtable;
-import java.util.Map;
-import java.util.Properties;
-import java.util.TreeMap;
-import java.util.function.Consumer;
-
-import org.argeo.init.logging.ThinLoggerFinder;
-import org.argeo.init.osgi.OsgiBoot;
-import org.argeo.init.osgi.OsgiRuntimeContext;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-
-public class RuntimeManager {
-       private final static Logger logger = System.getLogger(RuntimeManager.class.getName());
-
-       private final static String ENV_STATE_DIRECTORY = "STATE_DIRECTORY";
-//     private final static String ENV_CONFIGURATION_DIRECTORY = "CONFIGURATION_DIRECTORY";
-//     private final static String ENV_CACHE_DIRECTORY = "CACHE_DIRECTORY";
-
-       private final static String JVM_ARGS = "jvm.args";
-
-       private Path baseConfigArea;
-       private Path baseStateArea;
-       private Map<String, String> configuration = new HashMap<>();
-
-       private Map<String, OsgiRuntimeContext> runtimeContexts = new TreeMap<>();
-
-       RuntimeManager(Path configArea, Path stateArea) {
-               loadConfig(configArea, configuration);
-               configuration.put(OsgiBoot.PROP_OSGI_CONFIGURATION_AREA, stateArea.toUri().toString());
-               this.baseConfigArea = configArea.getParent();
-               this.baseStateArea = stateArea.getParent();
-
-               logger.log(Level.TRACE, () -> "Runtime manager configuration: " + configuration);
-
-       }
-
-       public void run() {
-               logger.log(Level.DEBUG, "Start OSGi");
-               try (OsgiRuntimeContext runtimeContext = new OsgiRuntimeContext(configuration)) {
-                       runtimeContext.run();
-
-                       BundleContext bc = runtimeContext.getFramework().getBundleContext();
-                       // uninstall init as a bundle since it will be available via OSGi system
-                       for (Bundle b : bc.getBundles()) {
-                               if (b.getSymbolicName().equals("org.argeo.init")) {
-                                       b.uninstall();
-                               }
-                       }
-                       bc.registerService(RuntimeManager.class, this, new Hashtable<>(configuration));
-                       logger.log(Level.DEBUG, "Registered runtime manager");
-
-                       runtimeContext.waitForStop(0);
-               } catch (Exception e) {
-                       e.printStackTrace();
-                       System.exit(1);
-               }
-
-       }
-
-       public static void loadConfig(Path dir, Map<String, String> config) {
-               try {
-                       System.out.println("Load from "+dir);
-                       Path jvmArgsPath = dir.resolve(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()));
-                                       }
-                               }
-               } catch (IOException e) {
-                       throw new UncheckedIOException("Cannot load configuration from " + dir, e);
-               }
-       }
-
-       OsgiRuntimeContext loadRuntime(String relPath, Consumer<Map<String, String>> configCallback) {
-               stopRuntime(relPath);
-               Path stateArea = baseStateArea.resolve(relPath);
-               Path configArea = baseConfigArea.resolve(relPath);
-               Map<String, String> config = new HashMap<>();
-               loadConfig(configArea, config);
-               config.put(OsgiBoot.PROP_OSGI_CONFIGURATION_AREA, stateArea.toUri().toString());
-
-               if (configCallback != null)
-                       configCallback.accept(config);
-               OsgiRuntimeContext runtimeContext = new OsgiRuntimeContext(config);
-               runtimeContexts.put(relPath, runtimeContext);
-               return runtimeContext;
-       }
-
-       public void startRuntime(String relPath, Consumer<Map<String, String>> configCallback) {
-               OsgiRuntimeContext runtimeContext = loadRuntime(relPath, configCallback);
-               runtimeContext.run();
-       }
-
-       public void stopRuntime(String relPath) {
-               if (!runtimeContexts.containsKey(relPath))
-                       return;
-               RuntimeContext runtimeContext = runtimeContexts.get(relPath);
-               try {
-                       runtimeContext.close();
-                       runtimeContext.waitForStop(60 * 1000);
-               } catch (Exception e) {
-                       logger.log(Level.ERROR, "Cannot close runtime context " + relPath, e);
-               } finally {
-                       runtimeContexts.remove(relPath);
-               }
-
-       }
-
-       public static void main(String[] args) {
-               ThinLoggerFinder.reloadConfiguration();
-               Map<String, String> env = System.getenv();
-//             for (String envName : new TreeSet<>(env.keySet())) {
-//                     System.out.format("%s=%s%n", envName, env.get(envName));
-//             }
-               if (args.length < 1)
-                       throw new IllegalArgumentException("A relative configuration directory must be specified");
-               Path configArea = Paths.get(System.getProperty("user.dir"), args[0]);
-
-               // System.out.println("## Start with PID " + ProcessHandle.current().pid());
-               // System.out.println("user.dir=" + System.getProperty("user.dir"));
-
-               Path stateArea = Paths.get(env.get(ENV_STATE_DIRECTORY));
-
-               RuntimeManager runtimeManager = new RuntimeManager(configArea, stateArea);
-               runtimeManager.run();
-       }
-
-}
diff --git a/org.argeo.init/src/org/argeo/init/RuntimeManagerMain.java b/org.argeo.init/src/org/argeo/init/RuntimeManagerMain.java
new file mode 100644 (file)
index 0000000..72b673b
--- /dev/null
@@ -0,0 +1,203 @@
+package org.argeo.init;
+
+import static org.argeo.api.init.InitConstants.SYMBOLIC_NAME_INIT;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Properties;
+import java.util.TreeMap;
+import java.util.function.Consumer;
+
+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.OsgiRuntimeContext;
+import org.argeo.internal.init.InternalState;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+
+/**
+ * Dynamically configures and launches multiple runtimes, coordinated by a main
+ * one.
+ */
+public class RuntimeManagerMain implements RuntimeManager {
+       private final static Logger logger = System.getLogger(RuntimeManagerMain.class.getName());
+
+       private final static String ENV_STATE_DIRECTORY = "STATE_DIRECTORY";
+//     private final static String ENV_CONFIGURATION_DIRECTORY = "CONFIGURATION_DIRECTORY";
+//     private final static String ENV_CACHE_DIRECTORY = "CACHE_DIRECTORY";
+
+       private final static long RUNTIME_SHUTDOWN_TIMEOUT = 60 * 1000;
+
+       private final static String JVM_ARGS = "jvm.args";
+
+       private Path baseConfigArea;
+       private Path baseStateArea;
+       private Map<String, String> configuration = new HashMap<>();
+
+       private Map<String, OsgiRuntimeContext> runtimeContexts = new TreeMap<>();
+
+       RuntimeManagerMain(Path configArea, Path stateArea) {
+               loadConfig(configArea, configuration);
+               configuration.put(InitConstants.PROP_OSGI_CONFIGURATION_AREA, stateArea.toUri().toString());
+               this.baseConfigArea = configArea.getParent();
+               this.baseStateArea = stateArea.getParent();
+
+               logger.log(Level.TRACE, () -> "Runtime manager configuration: " + configuration);
+
+       }
+
+       public void run() {
+               OsgiRuntimeContext managerRuntimeContext = new OsgiRuntimeContext(configuration);
+               try {
+                       managerRuntimeContext.run();
+                       InternalState.setMainRuntimeContext(managerRuntimeContext);
+
+                       // shutdown on exit
+                       Runtime.getRuntime().addShutdownHook(new Thread(() -> shutdown(), "Runtime shutdown"));
+
+                       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();
+                               }
+                       }
+                       bc.registerService(RuntimeManager.class, this, new Hashtable<>(configuration));
+                       logger.log(Level.DEBUG, "Registered runtime manager");
+
+                       managerRuntimeContext.waitForStop(0);
+               } catch (InterruptedException | BundleException e) {
+                       e.printStackTrace();
+                       System.exit(1);
+               }
+
+       }
+
+       protected void shutdown() {
+               // shutdowm runtimes
+               Map<String, RuntimeContext> shutdowning = new HashMap<>(runtimeContexts);
+               for (String id : new HashSet<>(runtimeContexts.keySet())) {
+                       logger.log(Logger.Level.DEBUG, "Shutting down runtime " + id + " ...");
+                       stopRuntime(id, true);
+               }
+               for (String id : shutdowning.keySet())
+                       try {
+                               RuntimeContext runtimeContext = shutdowning.get(id);
+                               runtimeContext.waitForStop(RUNTIME_SHUTDOWN_TIMEOUT);
+                       } catch (InterruptedException e) {
+                               // silent
+                       }
+
+               // shutdown manager runtime
+               try {
+                       InternalState.getMainRuntimeContext().close();
+                       InternalState.getMainRuntimeContext().waitForStop(RUNTIME_SHUTDOWN_TIMEOUT);
+//                     logger.log(Logger.Level.INFO, "Argeo Init stopped with PID " + ProcessHandle.current().pid());
+                       System.out.flush();
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       Runtime.getRuntime().halt(1);
+               }
+       }
+
+       public static void loadConfig(Path dir, Map<String, String> config) {
+               try {
+                       System.out.println("Load from " + dir);
+                       Path jvmArgsPath = dir.resolve(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()));
+                                       }
+                               }
+               } catch (IOException e) {
+                       throw new UncheckedIOException("Cannot load configuration from " + dir, e);
+               }
+       }
+
+       OsgiRuntimeContext loadRuntime(String relPath, Consumer<Map<String, String>> configCallback) {
+               stopRuntime(relPath, false);
+               Path stateArea = baseStateArea.resolve(relPath);
+               Path configArea = baseConfigArea.resolve(relPath);
+               Map<String, String> config = new HashMap<>();
+               loadConfig(configArea, config);
+               config.put(InitConstants.PROP_OSGI_CONFIGURATION_AREA, stateArea.toUri().toString());
+
+               if (configCallback != null)
+                       configCallback.accept(config);
+
+               // use config area if instance area is not set
+               if (!config.containsKey(InitConstants.PROP_OSGI_INSTANCE_AREA))
+                       config.put(InitConstants.PROP_OSGI_INSTANCE_AREA, config.get(InitConstants.PROP_OSGI_CONFIGURATION_AREA));
+
+               OsgiRuntimeContext runtimeContext = new OsgiRuntimeContext(config);
+               runtimeContexts.put(relPath, runtimeContext);
+               return runtimeContext;
+       }
+
+       public void startRuntime(String relPath, Consumer<Map<String, String>> configCallback) {
+               OsgiRuntimeContext runtimeContext = loadRuntime(relPath, configCallback);
+               runtimeContext.run();
+       }
+
+       public void stopRuntime(String relPath, boolean async) {
+               if (!runtimeContexts.containsKey(relPath))
+                       return;
+               RuntimeContext runtimeContext = runtimeContexts.get(relPath);
+               try {
+                       runtimeContext.close();
+                       if (!async) {
+                               runtimeContext.waitForStop(RUNTIME_SHUTDOWN_TIMEOUT);
+                               System.gc();
+                       }
+               } catch (Exception e) {
+                       logger.log(Level.ERROR, "Cannot close runtime context " + relPath, e);
+               } finally {
+                       runtimeContexts.remove(relPath);
+               }
+
+       }
+
+       public static void main(String[] args) {
+               ThinLoggerFinder.reloadConfiguration();
+               logger.log(Logger.Level.INFO, () -> "Argeo Init starting with PID " + ProcessHandle.current().pid());
+               Map<String, String> env = System.getenv();
+//             for (String envName : new TreeSet<>(env.keySet())) {
+//                     System.out.format("%s=%s%n", envName, env.get(envName));
+//             }
+               if (args.length < 1)
+                       throw new IllegalArgumentException("A relative configuration directory must be specified");
+               Path configArea = Paths.get(System.getProperty("user.dir"), args[0]);
+
+               // System.out.println("## Start with PID " + ProcessHandle.current().pid());
+               // System.out.println("user.dir=" + System.getProperty("user.dir"));
+
+               Path stateArea = Paths.get(env.get(ENV_STATE_DIRECTORY));
+
+               RuntimeManagerMain runtimeManager = new RuntimeManagerMain(configArea, stateArea);
+               runtimeManager.run();
+       }
+
+}
diff --git a/org.argeo.init/src/org/argeo/init/Service.java b/org.argeo.init/src/org/argeo/init/Service.java
deleted file mode 100644 (file)
index b080a75..0000000
+++ /dev/null
@@ -1,164 +0,0 @@
-package org.argeo.init;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.System.Logger;
-import java.lang.System.Logger.Level;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Properties;
-import java.util.TreeMap;
-
-import org.argeo.init.logging.ThinLoggerFinder;
-import org.argeo.init.osgi.OsgiBoot;
-import org.argeo.init.osgi.OsgiRuntimeContext;
-
-/** Configure and launch an Argeo service. */
-public class Service {
-       private final static Logger logger = System.getLogger(Service.class.getName());
-
-       final static String FILE_SYSTEM_PROPERTIES = "system.properties";
-
-       public final static String PROP_ARGEO_INIT_MAIN = "argeo.init.main";
-
-       private static RuntimeContext runtimeContext = null;
-
-       private static List<Runnable> postStart = Collections.synchronizedList(new ArrayList<>());
-
-       protected Service(String[] args) {
-       }
-
-       public static void main(String[] args) {
-               final long pid = ProcessHandle.current().pid();
-               logger.log(Logger.Level.DEBUG, () -> "Argeo Init starting with PID " + pid);
-
-               // shutdown on exit
-               Runtime.getRuntime().addShutdownHook(new Thread(() -> {
-                       try {
-                               if (Service.runtimeContext != null) {
-//                                     System.out.println("Argeo Init stopping with PID " + pid);
-                                       Service.runtimeContext.close();
-                                       Service.runtimeContext.waitForStop(0);
-                               }
-                       } catch (Exception e) {
-                               e.printStackTrace();
-                               Runtime.getRuntime().halt(1);
-                       }
-               }, "Runtime shutdown"));
-
-               // TODO use args as well
-               String dataArea = System.getProperty(OsgiBoot.PROP_OSGI_INSTANCE_AREA);
-               String stateArea = System.getProperty(OsgiBoot.PROP_OSGI_CONFIGURATION_AREA);
-               String configArea = System.getProperty(OsgiBoot.PROP_OSGI_SHARED_CONFIGURATION_AREA);
-
-               if (configArea != null) {
-                       Path configAreaPath = Paths.get(configArea);
-                       Path additionalSystemPropertiesPath = configAreaPath.resolve(FILE_SYSTEM_PROPERTIES);
-                       if (Files.exists(additionalSystemPropertiesPath)) {
-                               Properties properties = new Properties();
-                               try (InputStream in = Files.newInputStream(additionalSystemPropertiesPath)) {
-                                       properties.load(in);
-                               } catch (IOException e) {
-                                       logger.log(Logger.Level.ERROR,
-                                                       "Cannot load additional system properties " + additionalSystemPropertiesPath, e);
-                               }
-
-                               for (Object key : properties.keySet()) {
-                                       String currentValue = System.getProperty(key.toString());
-                                       String value = properties.getProperty(key.toString());
-                                       if (currentValue != null) {
-                                               if (!Objects.equals(value, currentValue))
-                                                       logger.log(Logger.Level.WARNING, "System property " + key + " already set with value "
-                                                                       + currentValue + " instead of " + value + ". Ignoring new value.");
-                                       } else {
-                                               System.setProperty(key.toString(), value);
-                                               logger.log(Logger.Level.TRACE, () -> "Added " + key + "=" + value
-                                                               + " to system properties, from " + additionalSystemPropertiesPath.getFileName());
-                                       }
-                               }
-                               ThinLoggerFinder.reloadConfiguration();
-                       }
-               }
-
-               Map<String, String> config = new HashMap<>();
-               config.put(PROP_ARGEO_INIT_MAIN, "true");
-
-               // add OSGi system properties to the configuration
-               sysprops: for (Object key : new TreeMap<>(System.getProperties()).keySet()) {
-                       String keyStr = key.toString();
-                       switch (keyStr) {
-                       case OsgiBoot.PROP_OSGI_CONFIGURATION_AREA:
-                       case OsgiBoot.PROP_OSGI_SHARED_CONFIGURATION_AREA:
-                       case OsgiBoot.PROP_OSGI_INSTANCE_AREA:
-                               // we should already have dealt with those
-                               continue sysprops;
-                       default:
-                       }
-
-                       if (keyStr.startsWith("osgi.") || keyStr.startsWith("org.osgi.") || keyStr.startsWith("eclipse.")
-                                       || keyStr.startsWith("org.eclipse.equinox.") || keyStr.startsWith("felix.")) {
-                               String value = System.getProperty(keyStr);
-                               config.put(keyStr, value);
-                               logger.log(Logger.Level.TRACE,
-                                               () -> "Added " + key + "=" + value + " to configuration, from system properties");
-                       }
-               }
-
-               try {
-                       try {
-                               if (stateArea != null)
-                                       config.put(OsgiBoot.PROP_OSGI_CONFIGURATION_AREA, stateArea);
-                               if (configArea != null)
-                                       config.put(OsgiBoot.PROP_OSGI_SHARED_CONFIGURATION_AREA, configArea);
-                               if (dataArea != null)
-                                       config.put(OsgiBoot.PROP_OSGI_INSTANCE_AREA, dataArea);
-                               // config.put(OsgiBoot.PROP_OSGI_USE_SYSTEM_PROPERTIES, "true");
-
-                               OsgiRuntimeContext osgiRuntimeContext = new OsgiRuntimeContext(config);
-                               osgiRuntimeContext.run();
-                               Service.runtimeContext = osgiRuntimeContext;
-                               for (Runnable run : postStart) {
-                                       try {
-                                               run.run();
-                                       } catch (Exception e) {
-                                               logger.log(Level.ERROR, "Cannot run post start callback " + run, e);
-                                       }
-                               }
-                               Service.runtimeContext.waitForStop(0);
-                       } catch (NoClassDefFoundError noClassDefFoundE) {
-                               StaticRuntimeContext staticRuntimeContext = new StaticRuntimeContext((Map<String, String>) config);
-                               staticRuntimeContext.run();
-                               Service.runtimeContext = staticRuntimeContext;
-                               for (Runnable run : postStart) {
-                                       try {
-                                               run.run();
-                                       } catch (Exception e) {
-                                               logger.log(Level.ERROR, "Cannot run post start callback " + run, e);
-                                       }
-                               }
-                               Service.runtimeContext.waitForStop(0);
-                       }
-               } catch (Exception e) {
-                       e.printStackTrace();
-                       System.exit(1);
-               }
-               logger.log(Logger.Level.DEBUG, "Argeo Init stopped with PID " + pid);
-       }
-
-       /** The root runtime context in this JVM. */
-       public static RuntimeContext getRuntimeContext() {
-               return runtimeContext;
-       }
-
-       /** Add a post-start call back to be run after the runtime has been started. */
-       public static void addPostStart(Runnable runnable) {
-               postStart.add(runnable);
-       }
-}
diff --git a/org.argeo.init/src/org/argeo/init/ServiceMain.java b/org.argeo.init/src/org/argeo/init/ServiceMain.java
new file mode 100644 (file)
index 0000000..ca8ba3a
--- /dev/null
@@ -0,0 +1,159 @@
+package org.argeo.init;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.System.Logger;
+import java.lang.System.Logger.Level;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.TreeMap;
+
+import org.argeo.api.init.InitConstants;
+import org.argeo.init.logging.ThinLoggerFinder;
+import org.argeo.init.osgi.OsgiRuntimeContext;
+import org.argeo.internal.init.InternalState;
+
+/** Configures and launches a single runtime, typically as a systemd service. */
+public class ServiceMain {
+       private final static Logger logger = System.getLogger(ServiceMain.class.getName());
+
+       final static String FILE_SYSTEM_PROPERTIES = "system.properties";
+
+       public final static String PROP_ARGEO_INIT_MAIN = "argeo.init.main";
+
+//     private static RuntimeContext runtimeContext = null;
+
+       private static List<Runnable> postStart = Collections.synchronizedList(new ArrayList<>());
+
+       protected ServiceMain(String[] args) {
+       }
+
+       public static void main(String[] args) {
+               final long pid = ProcessHandle.current().pid();
+               logger.log(Logger.Level.DEBUG, () -> "Argeo Init starting with PID " + pid);
+
+               // shutdown on exit
+               Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+                       try {
+                               if (InternalState.getMainRuntimeContext() != null) {
+                                       InternalState.getMainRuntimeContext().close();
+                                       InternalState.getMainRuntimeContext().waitForStop(0);
+                               }
+                       } catch (Exception e) {
+                               e.printStackTrace();
+                               Runtime.getRuntime().halt(1);
+                       }
+               }, "Runtime shutdown"));
+
+               // TODO use args as well
+               String dataArea = System.getProperty(InitConstants.PROP_OSGI_INSTANCE_AREA);
+               String stateArea = System.getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA);
+               String configArea = System.getProperty(InitConstants.PROP_OSGI_SHARED_CONFIGURATION_AREA);
+
+               if (configArea != null) {
+                       Path configAreaPath = Paths.get(configArea);
+                       Path additionalSystemPropertiesPath = configAreaPath.resolve(FILE_SYSTEM_PROPERTIES);
+                       if (Files.exists(additionalSystemPropertiesPath)) {
+                               Properties properties = new Properties();
+                               try (InputStream in = Files.newInputStream(additionalSystemPropertiesPath)) {
+                                       properties.load(in);
+                               } catch (IOException e) {
+                                       logger.log(Logger.Level.ERROR,
+                                                       "Cannot load additional system properties " + additionalSystemPropertiesPath, e);
+                               }
+
+                               for (Object key : properties.keySet()) {
+                                       String currentValue = System.getProperty(key.toString());
+                                       String value = properties.getProperty(key.toString());
+                                       if (currentValue != null) {
+                                               if (!Objects.equals(value, currentValue))
+                                                       logger.log(Logger.Level.WARNING, "System property " + key + " already set with value "
+                                                                       + currentValue + " instead of " + value + ". Ignoring new value.");
+                                       } else {
+                                               System.setProperty(key.toString(), value);
+                                               logger.log(Logger.Level.TRACE, () -> "Added " + key + "=" + value
+                                                               + " to system properties, from " + additionalSystemPropertiesPath.getFileName());
+                                       }
+                               }
+                               ThinLoggerFinder.reloadConfiguration();
+                       }
+               }
+
+               Map<String, String> config = new HashMap<>();
+               config.put(PROP_ARGEO_INIT_MAIN, "true");
+
+               // add OSGi system properties to the configuration
+               sysprops: for (Object key : new TreeMap<>(System.getProperties()).keySet()) {
+                       String keyStr = key.toString();
+                       switch (keyStr) {
+                       case InitConstants.PROP_OSGI_CONFIGURATION_AREA:
+                       case InitConstants.PROP_OSGI_SHARED_CONFIGURATION_AREA:
+                       case InitConstants.PROP_OSGI_INSTANCE_AREA:
+                               // we should already have dealt with those
+                               continue sysprops;
+                       default:
+                       }
+
+                       if (keyStr.startsWith("osgi.") || keyStr.startsWith("org.osgi.") || keyStr.startsWith("eclipse.")
+                                       || keyStr.startsWith("org.eclipse.equinox.") || keyStr.startsWith("felix.")) {
+                               String value = System.getProperty(keyStr);
+                               config.put(keyStr, value);
+                               logger.log(Logger.Level.TRACE,
+                                               () -> "Added " + key + "=" + value + " to configuration, from system properties");
+                       }
+               }
+
+               try {
+                       try {
+                               if (stateArea != null)
+                                       config.put(InitConstants.PROP_OSGI_CONFIGURATION_AREA, stateArea);
+                               if (configArea != null)
+                                       config.put(InitConstants.PROP_OSGI_SHARED_CONFIGURATION_AREA, configArea);
+                               if (dataArea != null)
+                                       config.put(InitConstants.PROP_OSGI_INSTANCE_AREA, dataArea);
+                               // config.put(OsgiBoot.PROP_OSGI_USE_SYSTEM_PROPERTIES, "true");
+
+                               OsgiRuntimeContext osgiRuntimeContext = new OsgiRuntimeContext(config);
+                               osgiRuntimeContext.run();
+                               InternalState.setMainRuntimeContext(osgiRuntimeContext);
+                               for (Runnable run : postStart) {
+                                       try {
+                                               run.run();
+                                       } catch (Exception e) {
+                                               logger.log(Level.ERROR, "Cannot run post start callback " + run, e);
+                                       }
+                               }
+                               InternalState.getMainRuntimeContext().waitForStop(0);
+                       } catch (NoClassDefFoundError noClassDefFoundE) {
+                               StaticRuntimeContext staticRuntimeContext = new StaticRuntimeContext((Map<String, String>) config);
+                               staticRuntimeContext.run();
+                               InternalState.setMainRuntimeContext(staticRuntimeContext);
+                               for (Runnable run : postStart) {
+                                       try {
+                                               run.run();
+                                       } catch (Exception e) {
+                                               logger.log(Level.ERROR, "Cannot run post start callback " + run, e);
+                                       }
+                               }
+                               InternalState.getMainRuntimeContext().waitForStop(0);
+                       }
+               } catch (Exception e) {
+                       e.printStackTrace();
+                       System.exit(1);
+               }
+               logger.log(Logger.Level.DEBUG, "Argeo Init stopped with PID " + pid);
+       }
+
+       /** Add a post-start call back to be run after the runtime has been started. */
+       public static void addPostStart(Runnable runnable) {
+               postStart.add(runnable);
+       }
+}
index e01e6194dd6916a377b3b8ad1afa3e9bf5fa2a02..51a968804308fe616064c2f498f0fa8972816db5 100644 (file)
@@ -2,6 +2,8 @@ package org.argeo.init;
 
 import java.util.Map;
 
+import org.argeo.api.init.RuntimeContext;
+
 public class StaticRuntimeContext implements RuntimeContext {
        private Map<String, String> config;
 
diff --git a/org.argeo.init/src/org/argeo/init/SysInitMain.java b/org.argeo.init/src/org/argeo/init/SysInitMain.java
new file mode 100644 (file)
index 0000000..8e59c24
--- /dev/null
@@ -0,0 +1,308 @@
+package org.argeo.init;
+//#! /usr/bin/java --source 17 @/usr/local/etc/freed/pid1/jvm.args
+
+import static java.lang.System.Logger.Level.DEBUG;
+import static java.lang.System.Logger.Level.ERROR;
+import static java.lang.System.Logger.Level.INFO;
+import static java.lang.System.Logger.Level.WARNING;
+
+import java.io.Console;
+import java.io.IOException;
+import java.lang.System.Logger;
+import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import sun.misc.Signal;
+
+/** A minimalistic Linux init process. */
+class SysInitMain {
+       final static AtomicInteger runLevel = new AtomicInteger(-1);
+
+       private final static Logger logger = System.getLogger(SysInitMain.class.getName());
+
+       private final static List<String> initDServices = Collections.synchronizedList(new ArrayList<>());
+
+       public static void main(String... args) {
+               try {
+                       final long pid = ProcessHandle.current().pid();
+                       Signal.handle(new Signal("TERM"), (signal) -> {
+                               System.out.println("SIGTERM caught");
+                               System.exit(0);
+                       });
+                       Signal.handle(new Signal("INT"), (signal) -> {
+                               System.out.println("SIGINT caught");
+                               System.exit(0);
+                       });
+                       Signal.handle(new Signal("HUP"), (signal) -> {
+                               System.out.println("SIGHUP caught");
+                               System.exit(0);
+                       });
+
+                       boolean isSystemInit = pid == 1 || pid == 2;
+
+                       if (isSystemInit && args.length > 0 && ("1".equals(args[0]) //
+                                       || "single".equals(args[0]) //
+                                       || "emergency".equals(args[0]))) {
+                               runLevel.set(1);
+                               for (Object key : new TreeMap<>(System.getProperties()).keySet()) {
+                                       System.out.println(key + "=" + System.getProperty(key.toString()));
+                               }
+                               System.out.println("Single user mode");
+                               System.out.flush();
+                               ProcessBuilder pb = new ProcessBuilder("/bin/bash");
+                               pb.redirectError(ProcessBuilder.Redirect.INHERIT);
+                               pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
+                               pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
+                               Process singleUserShell = pb.start();
+                               singleUserShell.waitFor();
+                       } else {
+                               if (args.length == 0)
+                                       runLevel.set(5);
+                               else
+                                       runLevel.set(Integer.parseInt(args[0]));
+
+                               if (runLevel.get() == 0) {// shutting down the whole system
+                                       if (!isSystemInit) {
+                                               logger.log(INFO, "Shutting down system...");
+                                               shutdown(false);
+                                               System.exit(0);
+                                       } else {
+                                               logger.log(ERROR, "Cannot start at run level " + runLevel.get());
+                                               System.exit(1);
+                                       }
+                               } else if (runLevel.get() == 6) {// reboot the whole system
+                                       if (!isSystemInit) {
+                                               logger.log(INFO, "Rebooting the system...");
+                                               shutdown(true);
+                                       } else {
+                                               logger.log(ERROR, "Cannot start at run level " + runLevel.get());
+                                               System.exit(1);
+                                       }
+                               }
+
+                               logger.log(INFO, "FREEd Init daemon starting with pid " + pid + " after "
+                                               + ManagementFactory.getRuntimeMXBean().getUptime() + " ms");
+                               // hostname
+                               String hostname = Files.readString(Paths.get("/etc/hostname"));
+                               new ProcessBuilder("/usr/bin/hostname", hostname).start();
+                               logger.log(DEBUG, "Set hostname to " + hostname);
+                               // networking
+                               initSysctl();
+                               startInitDService("networking", true);
+//                             Thread.sleep(3000);// leave some time for network to start up
+                               if (!waitForNetwork(10 * 1000))
+                                       logger.log(ERROR, "No network available");
+
+                               // OpenSSH
+                               // TODO make it coherent with Java sshd
+                               startInitDService("ssh", true);
+
+                               // NSS services
+                               startInitDService("nslcd", false);// Note: nslcd fails to stop
+
+                               // login prompt
+                               ServiceMain.addPostStart(() -> new LoginThread().start());
+
+                               // init Argeo CMS
+                               logger.log(INFO, "FREEd Init daemon starting Argeo Init after "
+                                               + ManagementFactory.getRuntimeMXBean().getUptime() + " ms");
+                               ServiceMain.main(args);
+                       }
+               } catch (Throwable e) {
+                       logger.log(ERROR, "Unexpected exception in free-pid1 init, shutting down... ", e);
+                       System.exit(1);
+               } finally {
+                       stopInitDServices();
+               }
+       }
+
+       static void initSysctl() {
+               try {
+                       Path sysctlD = Paths.get("/etc/sysctl.d/");
+                       for (Path conf : Files.newDirectoryStream(sysctlD, "*.conf")) {
+                               try {
+                                       new ProcessBuilder("/usr/sbin/sysctl", "-p", conf.toString()).start();
+                               } catch (IOException e) {
+                                       e.printStackTrace();
+                               }
+                       }
+               } catch (IOException e) {
+                       e.printStackTrace();
+               }
+       }
+
+       static void startInitDService(String serviceName, boolean stopOnShutdown) {
+               Path serviceInit = Paths.get("/etc/init.d/", serviceName);
+               if (Files.exists(serviceInit))
+                       try {
+                               int exitCode = new ProcessBuilder(serviceInit.toString(), "start").start().waitFor();
+                               if (exitCode != 0)
+                                       logger.log(ERROR, "Service " + serviceName + " dit not stop properly");
+                               else
+                                       logger.log(DEBUG, "Service " + serviceName + " started");
+                               if (stopOnShutdown)
+                                       initDServices.add(serviceName);
+//                                     Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+//                                             try {
+//                                                     new ProcessBuilder(serviceInit.toString(), "stop").start().waitFor();
+//                                             } catch (IOException | InterruptedException e) {
+//                                                     e.printStackTrace();
+//                                             }
+//                                     }, "FREEd stop service " + serviceName));
+                       } catch (IOException | InterruptedException e) {
+                               e.printStackTrace();
+                       }
+               else
+                       logger.log(WARNING, "Service " + serviceName + " not found and therefore not started");
+       }
+
+       static boolean waitForNetwork(long timeout) {
+               long begin = System.currentTimeMillis();
+               long duration = 0;
+               boolean networkAvailable = false;
+               try {
+                       networkAvailable: while (!networkAvailable) {
+                               duration = System.currentTimeMillis() - begin;
+                               if (duration > timeout)
+                                       break networkAvailable;
+                               Enumeration<NetworkInterface> netInterfaces = null;
+                               try {
+                                       netInterfaces = NetworkInterface.getNetworkInterfaces();
+                               } catch (SocketException e) {
+                                       throw new IllegalStateException("Cannot list network interfaces", e);
+                               }
+                               if (netInterfaces != null) {
+                                       while (netInterfaces.hasMoreElements()) {
+                                               NetworkInterface netInterface = netInterfaces.nextElement();
+                                               logger.log(DEBUG, "Interface:" + netInterface);
+                                               for (InterfaceAddress addr : netInterface.getInterfaceAddresses()) {
+                                                       InetAddress inetAddr = addr.getAddress();
+                                                       logger.log(DEBUG, "  addr: " + inetAddr);
+                                                       if (!inetAddr.isLoopbackAddress() && !inetAddr.isLinkLocalAddress()) {
+                                                               try {
+                                                                       if (inetAddr.isReachable((int) timeout)) {
+                                                                               networkAvailable = true;
+                                                                               duration = System.currentTimeMillis() - begin;
+                                                                               logger.log(DEBUG,
+                                                                                               "Network available after " + duration + " ms. IP: " + inetAddr);
+                                                                               break networkAvailable;
+                                                                       }
+                                                               } catch (IOException e) {
+                                                                       logger.log(ERROR, "Cannot check whether " + inetAddr + " is reachable", e);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               } else {
+                                       throw new IllegalStateException("No network interface has been found");
+                               }
+                               try {
+                                       Thread.sleep(1000);
+                               } catch (InterruptedException e) {
+                                       // silent
+                               }
+                       }
+               } catch (Exception e) {
+                       logger.log(ERROR, "Cannot check whether network is available", e);
+               }
+               return networkAvailable;
+       }
+
+       static void shutdown(boolean reboot) {
+               try {
+                       stopInitDServices();
+                       Path sysrqP = Paths.get("/proc/sys/kernel/sysrq");
+                       Files.writeString(sysrqP, "1");
+                       Path sysrqTriggerP = Paths.get("/proc/sysrq-trigger");
+                       Files.writeString(sysrqTriggerP, "e");// send SIGTERM to all processes
+                       // Files.writeString(sysrqTriggerP, "i");// send SIGKILL to all processes
+                       Files.writeString(sysrqTriggerP, "e");// flush data to disk
+                       Files.writeString(sysrqTriggerP, "u");// unmount
+                       if (reboot)
+                               Files.writeString(sysrqTriggerP, "b");
+                       else
+                               Files.writeString(sysrqTriggerP, "o");
+               } catch (IOException e) {
+                       logger.log(ERROR, "Cannot shut down system", e);
+               }
+       }
+
+       static void stopInitDServices() {
+               for (int i = initDServices.size() - 1; i >= 0; i--) {
+                       String serviceName = initDServices.get(i);
+                       Path serviceInit = Paths.get("/etc/init.d/", serviceName);
+                       try {
+                               int exitCode = new ProcessBuilder(serviceInit.toString(), "stop").start().waitFor();
+                               if (exitCode != 0)
+                                       logger.log(ERROR, "Service " + serviceName + " dit not stop properly");
+                       } catch (InterruptedException | IOException e) {
+                               logger.log(ERROR, "Cannot stop service " + serviceName, e);
+                       }
+               }
+       }
+
+       /** A thread watching the login prompt. */
+       static class LoginThread extends Thread {
+               private boolean systemShuttingDown = false;
+               private Process process = null;
+
+               public LoginThread() {
+                       super("FREEd login prompt");
+                       setDaemon(true);
+                       Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+                               systemShuttingDown = true;
+                               if (process != null)
+                                       process.destroy();
+                       }));
+               }
+
+               @Override
+               public void run() {
+                       boolean getty = true;
+                       prompt: while (!systemShuttingDown) {
+                               try {
+                                       if (getty) {
+                                               ProcessBuilder pb = new ProcessBuilder("/usr/sbin/getty", "38400", "tty2");
+                                               process = pb.start();
+                                       } else {
+                                               Console console = System.console();
+                                               console.readLine(); // type return once to activate login prompt
+                                               console.printf("login: ");
+                                               String username = console.readLine();
+                                               username = username.trim();
+                                               if ("".equals(username))
+                                                       continue prompt;
+                                               ProcessBuilder pb = new ProcessBuilder("su", "--login", username);
+                                               pb.redirectError(ProcessBuilder.Redirect.INHERIT);
+                                               pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
+                                               pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
+                                               process = pb.start();
+                                       }
+                                       Runtime.getRuntime().addShutdownHook(new Thread(() -> process.destroy()));
+                                       try {
+                                               process.waitFor();
+                                       } catch (InterruptedException e) {
+                                               process.destroy();
+                                       }
+                               } catch (Exception e) {
+                                       e.printStackTrace();
+                               } finally {
+                                       process = null;
+                               }
+                       }
+               }
+
+       }
+}
diff --git a/org.argeo.init/src/org/argeo/init/a2/A2Branch.java b/org.argeo.init/src/org/argeo/init/a2/A2Branch.java
deleted file mode 100644 (file)
index cd8d895..0000000
+++ /dev/null
@@ -1,89 +0,0 @@
-package org.argeo.init.a2;
-
-import java.util.Collections;
-import java.util.SortedMap;
-import java.util.TreeMap;
-
-import org.argeo.init.osgi.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.
- */
-public class A2Branch implements Comparable<A2Branch> {
-       private final A2Component component;
-       private final String id;
-
-       final SortedMap<Version, A2Module> modules = Collections.synchronizedSortedMap(new TreeMap<>());
-
-       public A2Branch(A2Component component, String id) {
-               this.component = component;
-               this.id = id;
-               component.branches.put(id, this);
-       }
-
-       public Iterable<A2Module> listModules(Object filter) {
-               return modules.values();
-       }
-
-       A2Module getOrAddModule(Version version, Object locator) {
-               if (modules.containsKey(version)) {
-                       A2Module res = modules.get(version);
-                       if (OsgiBootUtils.isDebug() && !res.getLocator().equals(locator)) {
-                               OsgiBootUtils.debug("Inconsistent locator " + locator + " (registered: " + res.getLocator() + ")");
-                       }
-                       return res;
-               } else
-                       return new A2Module(this, version, locator);
-       }
-
-       public A2Module last() {
-               return modules.get(modules.lastKey());
-       }
-
-       public A2Module first() {
-               return modules.get(modules.firstKey());
-       }
-
-       public A2Component getComponent() {
-               return component;
-       }
-
-       public String getId() {
-               return id;
-       }
-
-       @Override
-       public int compareTo(A2Branch o) {
-               return id.compareTo(id);
-       }
-
-       @Override
-       public int hashCode() {
-               return id.hashCode();
-       }
-
-       @Override
-       public boolean equals(Object obj) {
-               if (obj instanceof A2Branch) {
-                       A2Branch o = (A2Branch) obj;
-                       return component.equals(o.component) && id.equals(o.id);
-               } else
-                       return false;
-       }
-
-       @Override
-       public String toString() {
-               return getCoordinates();
-       }
-
-       public String getCoordinates() {
-               return component + ":" + id;
-       }
-
-       static String versionToBranchId(Version version) {
-               return version.getMajor() + "." + version.getMinor();
-       }
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/A2Component.java b/org.argeo.init/src/org/argeo/init/a2/A2Component.java
deleted file mode 100644 (file)
index 8942706..0000000
+++ /dev/null
@@ -1,105 +0,0 @@
-package org.argeo.init.a2;
-
-import java.util.Collections;
-import java.util.SortedMap;
-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.
- */
-public class A2Component implements Comparable<A2Component> {
-       private final A2Contribution contribution;
-       private final String id;
-
-       final SortedMap<String, A2Branch> branches = Collections.synchronizedSortedMap(new TreeMap<>());
-
-       public A2Component(A2Contribution contribution, String id) {
-               this.contribution = contribution;
-               this.id = id;
-               contribution.components.put(id, this);
-       }
-
-       public Iterable<A2Branch> listBranches(Object filter) {
-               return branches.values();
-       }
-
-       A2Branch getOrAddBranch(String branchId) {
-               if (!branches.containsKey(branchId)) {
-                       A2Branch a2Branch = new A2Branch(this, branchId);
-                       branches.put(branchId, a2Branch);
-               }
-               return branches.get(branchId);
-       }
-
-       A2Module getOrAddModule(Version version, Object locator) {
-               A2Branch branch = getOrAddBranch(A2Branch.versionToBranchId(version));
-               A2Module module = branch.getOrAddModule(version, locator);
-               return module;
-       }
-
-       public A2Branch last() {
-               return branches.get(branches.lastKey());
-       }
-
-       public A2Contribution getContribution() {
-               return contribution;
-       }
-
-       public String getId() {
-               return id;
-       }
-
-       @Override
-       public int compareTo(A2Component o) {
-               return id.compareTo(o.id);
-       }
-
-       @Override
-       public int hashCode() {
-               return id.hashCode();
-       }
-
-       @Override
-       public boolean equals(Object obj) {
-               if (obj instanceof A2Component) {
-                       A2Component o = (A2Component) obj;
-                       return contribution.equals(o.contribution) && id.equals(o.id);
-               } else
-                       return false;
-       }
-
-       @Override
-       public String toString() {
-               return contribution.getId() + ":" + id;
-       }
-
-       void asTree(String prefix, StringBuffer buf) {
-               if (prefix == null)
-                       prefix = "";
-               A2Branch lastBranch = last();
-               SortedMap<String, A2Branch> displayMap = new TreeMap<>(Collections.reverseOrder());
-               displayMap.putAll(branches);
-               for (String branchId : displayMap.keySet()) {
-                       A2Branch branch = displayMap.get(branchId);
-                       if (!lastBranch.equals(branch)) {
-                               buf.append('\n');
-                               buf.append(prefix);
-                       } else {
-                               buf.append(" -");
-                       }
-                       buf.append(prefix);
-                       buf.append(branchId);
-                       A2Module first = branch.first();
-                       A2Module last = branch.last();
-                       buf.append(" (").append(last.getVersion());
-                       if (!first.equals(last))
-                               buf.append(" ... ").append(first.getVersion());
-                       buf.append(')');
-               }
-       }
-
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/A2Contribution.java b/org.argeo.init/src/org/argeo/init/a2/A2Contribution.java
deleted file mode 100644 (file)
index 9de09ce..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-package org.argeo.init.a2;
-
-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.
- */
-public class A2Contribution implements Comparable<A2Contribution> {
-       final static String BOOT = "boot";
-       final static String RUNTIME = "runtime";
-       final static String CLASSPATH = "classpath";
-
-       final static String DEFAULT = "default";
-       final static String LIB = "lib";
-
-       private final ProvisioningSource source;
-       private final String id;
-
-       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);
-       }
-
-       public Iterable<A2Component> listComponents(Object filter) {
-               return components.values();
-       }
-
-       A2Component getOrAddComponent(String componentId) {
-               if (components.containsKey(componentId))
-                       return components.get(componentId);
-               else
-                       return new A2Component(this, componentId);
-       }
-
-       public ProvisioningSource getSource() {
-               return source;
-       }
-
-       public String getId() {
-               return id;
-       }
-
-       @Override
-       public int compareTo(A2Contribution o) {
-               return id.compareTo(o.id);
-       }
-
-       @Override
-       public int hashCode() {
-               return id.hashCode();
-       }
-
-       @Override
-       public boolean equals(Object obj) {
-               if (obj instanceof A2Contribution) {
-                       A2Contribution o = (A2Contribution) obj;
-                       return id.equals(o.id);
-               } else
-                       return false;
-       }
-
-       @Override
-       public String toString() {
-               return id;
-       }
-
-       void asTree(String prefix, StringBuffer buf) {
-               if (prefix == null)
-                       prefix = "";
-               for (String componentId : components.keySet()) {
-                       buf.append(prefix);
-                       buf.append(componentId);
-                       A2Component component = components.get(componentId);
-                       component.asTree(prefix, buf);
-                       buf.append('\n');
-               }
-       }
-
-       static String localOsArchRelativePath() {
-               return Os.local().toString() + "/" + Arch.local().toString();
-       }
-
-       static enum Os {
-               LINUX, WIN32, MACOSX, UNKOWN;
-
-               @Override
-               public String toString() {
-                       return name().toLowerCase();
-               }
-
-               public static Os local() {
-                       String osStr = System.getProperty("os.name").toLowerCase();
-                       if (osStr.startsWith("linux"))
-                               return LINUX;
-                       if (osStr.startsWith("win"))
-                               return WIN32;
-                       if (osStr.startsWith("mac"))
-                               return MACOSX;
-                       return UNKOWN;
-               }
-
-       }
-
-       static enum Arch {
-               X86_64, AARCH64, X86, POWERPC, UNKOWN;
-
-               @Override
-               public String toString() {
-                       return name().toLowerCase();
-               }
-
-               public static Arch local() {
-                       String archStr = System.getProperty("os.arch").toLowerCase();
-                       return switch (archStr) {
-                       case "x86_64":
-                       case "amd64":
-                       case "x86-64": {
-                               yield X86_64;
-                       }
-                       case "aarch64":
-                       case "arm64": {
-                               yield AARCH64;
-                       }
-                       case "x86":
-                       case "i386":
-                       case "i686": {
-                               yield X86;
-                       }
-                       case "powerpc":
-                       case "ppc": {
-                               yield POWERPC;
-                       }
-                       default:
-                               yield UNKOWN;
-                       };
-               }
-       }
-
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/A2Exception.java b/org.argeo.init/src/org/argeo/init/a2/A2Exception.java
deleted file mode 100644 (file)
index 6ba87a7..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.argeo.init.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.init/src/org/argeo/init/a2/A2Module.java b/org.argeo.init/src/org/argeo/init/a2/A2Module.java
deleted file mode 100644 (file)
index 0b6d3a9..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.argeo.init.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.
- */
-public class A2Module implements Comparable<A2Module> {
-       private final A2Branch branch;
-       private final Version version;
-       private final Object locator;
-
-       public A2Module(A2Branch branch, Version version, Object locator) {
-               this.branch = branch;
-               this.version = version;
-               this.locator = locator;
-               branch.modules.put(version, this);
-       }
-
-       public A2Branch getBranch() {
-               return branch;
-       }
-
-       public Version getVersion() {
-               return version;
-       }
-
-       Object getLocator() {
-               return locator;
-       }
-
-       @Override
-       public int compareTo(A2Module o) {
-               return version.compareTo(o.version);
-       }
-
-       @Override
-       public int hashCode() {
-               return version.hashCode();
-       }
-
-       @Override
-       public boolean equals(Object obj) {
-               if (obj instanceof A2Module) {
-                       A2Module o = (A2Module) obj;
-                       return branch.equals(o.branch) && version.equals(o.version);
-               } else
-                       return false;
-       }
-
-       @Override
-       public String toString() {
-               return getCoordinates();
-       }
-
-       public String getCoordinates() {
-               return branch.getComponent() + ":" + version;
-       }
-
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/A2Source.java b/org.argeo.init/src/org/argeo/init/a2/A2Source.java
deleted file mode 100644 (file)
index 19134b7..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.argeo.init.a2;
-
-import java.net.URI;
-
-/** A provisioning source in A2 format. */
-public interface A2Source extends ProvisioningSource {
-       /** Use standard a2 protocol, installing from source URL. */
-       final static String SCHEME_A2 = "a2";
-       /**
-        * Use equinox-specific reference: installation, which does not copy the bundle
-        * content.
-        */
-       final static String SCHEME_A2_REFERENCE = "a2+reference";
-       final static String DEFAULT_A2_URI = SCHEME_A2 + ":///";
-       final static String DEFAULT_A2_REFERENCE_URI = SCHEME_A2_REFERENCE + ":///";
-
-       final static String INCLUDE = "include";
-       final static String EXCLUDE = "exclude";
-
-       URI getUri();
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/AbstractProvisioningSource.java b/org.argeo.init/src/org/argeo/init/a2/AbstractProvisioningSource.java
deleted file mode 100644 (file)
index f946add..0000000
+++ /dev/null
@@ -1,269 +0,0 @@
-package org.argeo.init.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<>());
-
-       private final boolean usingReference;
-
-       public AbstractProvisioningSource(boolean usingReference) {
-               this.usingReference = usingReference;
-       }
-
-       public Iterable<A2Contribution> listContributions(Object filter) {
-               return contributions.values();
-       }
-
-       @Override
-       public Bundle install(BundleContext bc, A2Module module) {
-               try {
-                       Object locator = module.getLocator();
-                       if (usingReference && locator instanceof Path locatorPath) {
-                               String referenceUrl = "reference:file:" + locatorPath.toString();
-                               Bundle bundle = bc.installBundle(referenceUrl);
-                               return bundle;
-                       } else {
-                               Path locatorPath = (Path) locator;
-                               Path pathToUse;
-                               boolean isTemp = false;
-                               if (locator instanceof Path && Files.isDirectory(locatorPath)) {
-                                       pathToUse = toTempJar(locatorPath);
-                                       isTemp = true;
-                               } else {
-                                       pathToUse = locatorPath;
-                               }
-                               Bundle bundle;
-                               try (InputStream in = newInputStream(pathToUse)) {
-                                       bundle = bc.installBundle(locatorPath.toAbsolutePath().toString(), in);
-                               }
-
-                               if (isTemp && pathToUse != null)
-                                       Files.deleteIfExists(pathToUse);
-                               return bundle;
-                       }
-               } catch (BundleException | IOException e) {
-                       throw new A2Exception("Cannot install module " + module, e);
-               }
-       }
-
-       @Override
-       public void update(Bundle bundle, A2Module module) {
-               try {
-                       Object locator = module.getLocator();
-                       if (usingReference && locator instanceof Path) {
-                               try (InputStream in = newInputStream(locator)) {
-                                       bundle.update(in);
-                               }
-                       } else {
-                               Path locatorPath = (Path) locator;
-                               Path pathToUse;
-                               boolean isTemp = false;
-                               if (locator instanceof Path && Files.isDirectory(locatorPath)) {
-                                       pathToUse = toTempJar(locatorPath);
-                                       isTemp = true;
-                               } else {
-                                       pathToUse = locatorPath;
-                               }
-                               try (InputStream in = newInputStream(pathToUse)) {
-                                       bundle.update(in);
-                               }
-                               if (isTemp && pathToUse != null)
-                                       Files.deleteIfExists(pathToUse);
-                       }
-               } 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[] readNameVersionFromModule(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);
-               String symbolicName = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
-               int semiColIndex = symbolicName.indexOf(';');
-               if (semiColIndex >= 0)
-                       symbolicName = symbolicName.substring(0, semiColIndex);
-               return new String[] { symbolicName, versionStr };
-       }
-
-       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;
-       }
-
-       protected boolean isUsingReference() {
-               return usingReference;
-       }
-
-       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());
-               }
-       }
-
-       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);
-               }
-
-       }
-
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/ClasspathSource.java b/org.argeo.init/src/org/argeo/init/a2/ClasspathSource.java
deleted file mode 100644 (file)
index 12de422..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.argeo.init.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.init.osgi.OsgiBootUtils;
-import org.osgi.framework.Version;
-
-/**
- * A provisioning source based on the linear classpath with which the JVM has
- * been started.
- */
-public class ClasspathSource extends AbstractProvisioningSource {
-       
-       public ClasspathSource() {
-               super(true);
-       }
-
-       void load() throws IOException {
-               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);
-                       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.init/src/org/argeo/init/a2/FsA2Source.java b/org.argeo.init/src/org/argeo/init/a2/FsA2Source.java
deleted file mode 100644 (file)
index acc553d..0000000
+++ /dev/null
@@ -1,171 +0,0 @@
-package org.argeo.init.a2;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.nio.file.DirectoryStream;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.SortedMap;
-import java.util.StringJoiner;
-import java.util.TreeMap;
-
-import org.argeo.init.osgi.OsgiBootUtils;
-import org.osgi.framework.Version;
-
-/** A file system {@link AbstractProvisioningSource} in A2 format. */
-public class FsA2Source extends AbstractProvisioningSource implements A2Source {
-       private final Path base;
-       private final Map<String, String> variantsXOr;
-
-       private final List<String> includes;
-       private final List<String> excludes;
-
-       public FsA2Source(Path base, Map<String, String> variantsXOr, boolean usingReference, List<String> includes,
-                       List<String> excludes) {
-               super(usingReference);
-               this.base = base;
-               this.variantsXOr = new HashMap<>(variantsXOr);
-               this.includes = includes;
-               this.excludes = excludes;
-       }
-
-       void load() throws IOException {
-               SortedMap<Path, A2Contribution> contributions = new TreeMap<>();
-
-               DirectoryStream<Path> contributionPaths = Files.newDirectoryStream(base);
-               contributions: for (Path contributionPath : contributionPaths) {
-                       if (Files.isDirectory(contributionPath)) {
-                               String contributionId = contributionPath.getFileName().toString();
-                               if (A2Contribution.BOOT.equals(contributionId))// skip boot
-                                       continue contributions;
-                               if (contributionId.contains(".")) {
-                                       A2Contribution contribution = getOrAddContribution(contributionId);
-                                       contributions.put(contributionPath, contribution);
-                               } else {// variants
-                                       Path variantPath = null;
-                                       // is it an explicit variant?
-                                       String variant = variantsXOr.get(contributionPath.getFileName().toString());
-                                       if (variant != null) {
-                                               variantPath = contributionPath.resolve(variant);
-                                       }
-
-                                       // is there a default variant?
-                                       if (variantPath == null) {
-                                               Path defaultPath = contributionPath.resolve(A2Contribution.DEFAULT);
-                                               if (Files.exists(defaultPath)) {
-                                                       variantPath = defaultPath;
-                                               }
-                                       }
-
-                                       if (variantPath == null)
-                                               continue contributions;
-
-                                       // a variant was found, let's collect its contributions (also common ones in its
-                                       // parent)
-                                       if (Files.exists(variantPath.getParent())) {
-                                               for (Path variantContributionPath : Files.newDirectoryStream(variantPath.getParent())) {
-                                                       String variantContributionId = variantContributionPath.getFileName().toString();
-                                                       if (variantContributionId.contains(".")) {
-                                                               A2Contribution contribution = getOrAddContribution(variantContributionId);
-                                                               contributions.put(variantContributionPath, contribution);
-                                                       }
-                                               }
-                                       }
-                                       if (Files.exists(variantPath)) {
-                                               for (Path variantContributionPath : Files.newDirectoryStream(variantPath)) {
-                                                       String variantContributionId = variantContributionPath.getFileName().toString();
-                                                       if (variantContributionId.contains(".")) {
-                                                               A2Contribution contribution = getOrAddContribution(variantContributionId);
-                                                               contributions.put(variantContributionPath, contribution);
-                                                       }
-                                               }
-                                       }
-                               }
-                       }
-               }
-
-               contributions: for (Path contributionPath : contributions.keySet()) {
-                       String contributionId = contributionPath.getFileName().toString();
-                       if (includes != null && !includes.contains(contributionId))
-                               continue contributions;
-                       if (excludes != null && excludes.contains(contributionId))
-                               continue contributions;
-                       A2Contribution contribution = getOrAddContribution(contributionId);
-                       DirectoryStream<Path> modulePaths = Files.newDirectoryStream(contributionPath);
-                       modules: for (Path modulePath : modulePaths) {
-                               if (!Files.isDirectory(modulePath)) {
-                                       // OsgiBootUtils.debug("Registering " + modulePath);
-                                       String moduleFileName = modulePath.getFileName().toString();
-                                       int lastDot = moduleFileName.lastIndexOf('.');
-                                       String ext = moduleFileName.substring(lastDot + 1);
-                                       if (!"jar".equals(ext))
-                                               continue modules;
-                                       Version version;
-                                       // TODO optimise? check attributes?
-                                       String[] nameVersion = readNameVersionFromModule(modulePath);
-                                       String componentName = nameVersion[0];
-                                       String versionStr = nameVersion[1];
-                                       if (versionStr != null) {
-                                               version = new Version(versionStr);
-                                       } else {
-                                               OsgiBootUtils.debug("Ignore " + modulePath + " since version cannot be found");
-                                               continue modules;
-                                       }
-//                                     }
-                                       A2Component component = contribution.getOrAddComponent(componentName);
-                                       A2Module module = component.getOrAddModule(version, modulePath);
-                                       if (OsgiBootUtils.isDebug())
-                                               OsgiBootUtils.debug("Registered " + module);
-                               }
-                       }
-               }
-
-       }
-
-       @Override
-       public URI getUri() {
-               URI baseUri = base.toUri();
-               try {
-                       if (baseUri.getScheme().equals("file")) {
-                               String queryPart = "";
-                               if (!getVariantsXOr().isEmpty()) {
-                                       StringJoiner sj = new StringJoiner("&");
-                                       for (String key : getVariantsXOr().keySet()) {
-                                               sj.add(key + "=" + getVariantsXOr().get(key));
-                                       }
-                                       queryPart = sj.toString();
-                               }
-                               return new URI(isUsingReference() ? SCHEME_A2_REFERENCE : SCHEME_A2, null, base.toString(), queryPart,
-                                               null);
-                       } else {
-                               throw new UnsupportedOperationException("Unsupported scheme " + baseUri.getScheme());
-                       }
-               } catch (URISyntaxException e) {
-                       throw new IllegalStateException("Cannot build URI from " + baseUri, e);
-               }
-       }
-
-       protected Map<String, String> getVariantsXOr() {
-               return variantsXOr;
-       }
-
-//     public static void main(String[] args) {
-//             if (args.length == 0)
-//                     throw new IllegalArgumentException("Usage: <path to A2 base>");
-//             try {
-//                     Map<String, String> xOr = new HashMap<>();
-//                     xOr.put("osgi", "equinox");
-//                     xOr.put("swt", "rap");
-//                     FsA2Source context = new FsA2Source(Paths.get(args[0]), xOr);
-//                     context.load();
-//                     context.asTree();
-//             } catch (Exception e) {
-//                     e.printStackTrace();
-//             }
-//     }
-
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/FsM2Source.java b/org.argeo.init/src/org/argeo/init/a2/FsM2Source.java
deleted file mode 100644 (file)
index 0313d20..0000000
+++ /dev/null
@@ -1,66 +0,0 @@
-package org.argeo.init.a2;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-
-import org.argeo.init.osgi.OsgiBootUtils;
-import org.osgi.framework.Version;
-
-/** A file system {@link AbstractProvisioningSource} in Maven 2 format. */
-public class FsM2Source extends AbstractProvisioningSource {
-       private final Path base;
-
-       public FsM2Source(Path base) {
-               super(false);
-               this.base = base;
-       }
-
-       void load() throws IOException {
-               Files.walkFileTree(base, new ArtifactFileVisitor());
-       }
-
-       class ArtifactFileVisitor extends SimpleFileVisitor<Path> {
-
-               @Override
-               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                       // OsgiBootUtils.debug("Processing " + file);
-                       if (file.toString().endsWith(".jar")) {
-                               Version version;
-                               try {
-                                       version = new Version(readVersionFromModule(file));
-                               } catch (Exception e) {
-                                       // ignore non OSGi
-                                       return FileVisitResult.CONTINUE;
-                               }
-                               String moduleName = readSymbolicNameFromModule(file);
-                               Path groupPath = file.getParent().getParent().getParent();
-                               Path relGroupPath = base.relativize(groupPath);
-                               String contributionName = relGroupPath.toString().replace(File.separatorChar, '.');
-                               A2Contribution contribution = getOrAddContribution(contributionName);
-                               A2Component component = contribution.getOrAddComponent(moduleName);
-                               A2Module module = component.getOrAddModule(version, file);
-                               if (OsgiBootUtils.isDebug())
-                                       OsgiBootUtils.debug("Registered " + module);
-                       }
-                       return super.visitFile(file, attrs);
-               }
-
-       }
-
-       public static void main(String[] args) {
-               try {
-                       FsM2Source context = new FsM2Source(Paths.get("/home/mbaudier/.m2/repository"));
-                       context.load();
-                       context.asTree();
-               } catch (Exception e) {
-                       e.printStackTrace();
-               }
-       }
-
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/OsgiContext.java b/org.argeo.init/src/org/argeo/init/a2/OsgiContext.java
deleted file mode 100644 (file)
index 7f1133f..0000000
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.argeo.init.a2;
-
-import org.argeo.init.osgi.OsgiBootUtils;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.FrameworkUtil;
-import org.osgi.framework.Version;
-
-/**
- * A running OSGi bundle context seen as a {@link AbstractProvisioningSource}.
- */
-class OsgiContext extends AbstractProvisioningSource {
-       private final BundleContext bc;
-
-       private A2Contribution runtimeContribution;
-
-       public OsgiContext(BundleContext bc) {
-               super(false);
-               this.bc = bc;
-               runtimeContribution = getOrAddContribution(A2Contribution.RUNTIME);
-       }
-
-       public OsgiContext() {
-               super(false);
-               Bundle bundle = FrameworkUtil.getBundle(OsgiContext.class);
-               if (bundle == null)
-                       throw new IllegalArgumentException(
-                                       "OSGi Boot bundle must be started or a bundle context must be specified");
-               this.bc = bundle.getBundleContext();
-       }
-
-       void load() {
-               for (Bundle bundle : bc.getBundles()) {
-                       registerBundle(bundle);
-               }
-
-       }
-
-       void registerBundle(Bundle bundle) {
-               String componentId = bundle.getSymbolicName();
-               Version version = bundle.getVersion();
-               A2Component component = runtimeContribution.getOrAddComponent(componentId);
-               A2Module module = component.getOrAddModule(version, bundle);
-               if (OsgiBootUtils.isDebug())
-                       OsgiBootUtils.debug("Registered bundle module " + module + " (location id: " + bundle.getLocation() + ")");
-
-       }
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/ProvisioningManager.java b/org.argeo.init/src/org/argeo/init/a2/ProvisioningManager.java
deleted file mode 100644 (file)
index 8000626..0000000
+++ /dev/null
@@ -1,303 +0,0 @@
-package org.argeo.init.a2;
-
-import static org.argeo.init.a2.A2Source.SCHEME_A2;
-import static org.argeo.init.a2.A2Source.SCHEME_A2_REFERENCE;
-
-import java.io.File;
-import java.io.UnsupportedEncodingException;
-import java.net.URI;
-import java.net.URLDecoder;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import org.argeo.init.osgi.OsgiBootUtils;
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.Constants;
-import org.osgi.framework.Version;
-import org.osgi.framework.wiring.FrameworkWiring;
-
-/** Loads provisioning sources into an OSGi context. */
-public class ProvisioningManager {
-       BundleContext bc;
-       OsgiContext osgiContext;
-       List<ProvisioningSource> sources = Collections.synchronizedList(new ArrayList<>());
-
-       public ProvisioningManager(BundleContext bc) {
-               this.bc = bc;
-               osgiContext = new OsgiContext(bc);
-               osgiContext.load();
-       }
-
-       protected void addSource(ProvisioningSource source) {
-               sources.add(source);
-       }
-
-       void installWholeSource(ProvisioningSource source) {
-               Set<Bundle> updatedBundles = new HashSet<>();
-               for (A2Contribution contribution : source.listContributions(null)) {
-                       for (A2Component component : contribution.components.values()) {
-                               A2Module module = component.last().last();
-                               Bundle bundle = installOrUpdate(module);
-                               if (bundle != null)
-                                       updatedBundles.add(bundle);
-                       }
-               }
-//             FrameworkWiring frameworkWiring = bc.getBundle(0).adapt(FrameworkWiring.class);
-//             frameworkWiring.refreshBundles(updatedBundles);
-       }
-
-       public void registerSource(String uri) {
-               try {
-                       URI u = new URI(uri);
-
-                       // XOR
-                       Map<String, List<String>> properties = queryToMap(u);
-                       Map<String, String> xOr = new HashMap<>();
-                       List<String> includes = null;
-                       List<String> excludes = null;
-                       for (String key : properties.keySet()) {
-                               List<String> lst = properties.get(key);
-                               if (A2Source.INCLUDE.equals(key)) {
-                                       includes = new ArrayList<>(lst);
-                               } else if (A2Source.EXCLUDE.equals(key)) {
-                                       excludes = new ArrayList<>(lst);
-                               } else {
-                                       if (lst.size() != 1)
-                                               throw new IllegalArgumentException("Invalid XOR definitions in " + uri);
-                                       xOr.put(key, lst.get(0));
-                               }
-                       }
-
-                       if (SCHEME_A2.equals(u.getScheme()) || SCHEME_A2_REFERENCE.equals(u.getScheme())) {
-                               if (u.getHost() == null || "".equals(u.getHost())) {
-                                       String baseStr = u.getPath();
-                                       if (File.separatorChar == '\\') {// MS Windows
-                                               baseStr = baseStr.substring(1).replace('/', File.separatorChar);
-                                       }
-                                       Path base = Paths.get(baseStr);
-                                       if (Files.exists(base)) {
-                                               FsA2Source source = new FsA2Source(base, xOr, SCHEME_A2_REFERENCE.equals(u.getScheme()),
-                                                               includes, excludes);
-                                               source.load();
-                                               addSource(source);
-                                               OsgiBootUtils.info("Registered " + uri + " as source");
-
-                                               // OS specific / native
-                                               String localRelPath = A2Contribution.localOsArchRelativePath();
-                                               Path localLibBase = base.resolve(A2Contribution.LIB).resolve(localRelPath);
-                                               if (Files.exists(localLibBase)) {
-                                                       FsA2Source libSource = new FsA2Source(localLibBase, xOr,
-                                                                       SCHEME_A2_REFERENCE.equals(u.getScheme()), includes, excludes);
-                                                       libSource.load();
-                                                       addSource(libSource);
-                                                       OsgiBootUtils.info("Registered OS-specific " + uri + " as source (" + localRelPath + ")");
-                                               }
-                                       } else {
-                                               OsgiBootUtils.debug("Source " + base + " does not exist, ignoring.");
-                                       }
-                               } else {
-                                       throw new UnsupportedOperationException(
-                                                       "Remote installation is not yet supported, cannot add source " + u);
-                               }
-                       } else {
-                               throw new IllegalArgumentException("Unkown scheme: for source " + u);
-                       }
-               } catch (Exception e) {
-                       throw new A2Exception("Cannot add source " + uri, e);
-               }
-       }
-
-       public boolean registerDefaultSource() {
-               String frameworkLocation = bc.getProperty("osgi.framework");
-               try {
-                       URI frameworkLocationUri = new URI(frameworkLocation);
-                       if ("file".equals(frameworkLocationUri.getScheme())) {
-                               Path frameworkPath = Paths.get(frameworkLocationUri);
-                               if (frameworkPath.getParent().getFileName().toString().equals(A2Contribution.BOOT)) {
-                                       Path base = frameworkPath.getParent().getParent();
-                                       String baseStr = base.toString();
-                                       if (File.separatorChar == '\\')// MS Windows
-                                               baseStr = '/' + baseStr.replace(File.separatorChar, '/');
-                                       URI baseUri = new URI(A2Source.SCHEME_A2, null, null, 0, baseStr, null, null);
-                                       registerSource(baseUri.toString());
-                                       OsgiBootUtils.debug("Default source from framework location " + frameworkLocation);
-                                       return true;
-                               }
-                       }
-               } catch (Exception e) {
-                       OsgiBootUtils.error("Cannot register default source based on framework location " + frameworkLocation, e);
-               }
-               return false;
-       }
-
-       public void install(String spec) {
-               if (spec == null) {
-                       for (ProvisioningSource source : sources) {
-                               installWholeSource(source);
-                       }
-               }
-       }
-
-       /** @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();
-                       A2Branch osgiBranch = osgiContext.findBranch(module.getBranch().getComponent().getId(), moduleVersion);
-                       if (osgiBranch == null) {
-                               Bundle bundle = moduleSource.install(bc, module);
-                               // TODO make it more dynamic, based on OSGi APIs
-                               osgiContext.registerBundle(bundle);
-//                             if (OsgiBootUtils.isDebug())
-//                                     OsgiBootUtils.debug("Installed bundle " + bundle.getLocation() + " with version " + moduleVersion);
-                               return bundle;
-                       } else {
-                               A2Module lastOsgiModule = osgiBranch.last();
-                               int compare = moduleVersion.compareTo(lastOsgiModule.getVersion());
-                               if (compare >= 0) {// update (also if same version)
-                                       Bundle bundle = (Bundle) lastOsgiModule.getLocator();
-                                       if (bundle.getBundleId() == 0)// ignore framework bundle
-                                               return null;
-                                       moduleSource.update(bundle, module);
-                                       // TODO make it more dynamic, based on OSGi APIs
-                                       // TODO remove old module? Or keep update history?
-                                       osgiContext.registerBundle(bundle);
-                                       if (compare == 0)
-                                               OsgiBootUtils
-                                                               .debug("Updated bundle " + bundle.getLocation() + " to same version " + moduleVersion);
-                                       else
-                                               OsgiBootUtils.info("Updated bundle " + bundle.getLocation() + " to version " + moduleVersion);
-                                       return bundle;
-                               } else {
-                                       if (OsgiBootUtils.isDebug())
-                                               OsgiBootUtils.debug("Did not install as bundle module " + module
-                                                               + " since a module with higher version " + lastOsgiModule.getVersion()
-                                                               + " is already installed for branch " + osgiBranch);
-                               }
-                       }
-               } catch (Exception e) {
-                       OsgiBootUtils.error("Could not install module " + module + ": " + e.getMessage(), e);
-               }
-               return null;
-       }
-
-       public Collection<Bundle> update() {
-               boolean fragmentsUpdated = false;
-               Set<Bundle> updatedBundles = new HashSet<>();
-               bundles: for (Bundle bundle : bc.getBundles()) {
-                       for (ProvisioningSource source : sources) {
-                               String componentId = bundle.getSymbolicName();
-                               Version version = bundle.getVersion();
-                               A2Branch branch = source.findBranch(componentId, version);
-                               if (branch == null)
-                                       continue bundles;
-                               A2Module module = branch.last();
-                               Version moduleVersion = module.getVersion();
-                               int compare = moduleVersion.compareTo(version);
-                               if (compare > 0) {// update
-                                       try {
-                                               source.update(bundle, module);
-//                                             bundle.update(in);
-                                               String fragmentHost = bundle.getHeaders().get(Constants.FRAGMENT_HOST);
-                                               if (fragmentHost != null)
-                                                       fragmentsUpdated = true;
-                                               OsgiBootUtils.info("Updated bundle " + bundle.getLocation() + " to version " + moduleVersion);
-                                               updatedBundles.add(bundle);
-                                       } catch (Exception e) {
-                                               OsgiBootUtils.error("Cannot update with module " + module, e);
-                                       }
-                               }
-                       }
-               }
-               FrameworkWiring frameworkWiring = bc.getBundle(0).adapt(FrameworkWiring.class);
-               if (fragmentsUpdated)// refresh all
-                       frameworkWiring.refreshBundles(null);
-               else
-                       frameworkWiring.refreshBundles(updatedBundles);
-               return updatedBundles;
-       }
-
-       private static Map<String, List<String>> queryToMap(URI uri) {
-               return queryToMap(uri.getQuery());
-       }
-
-       private static Map<String, List<String>> queryToMap(String queryPart) {
-               try {
-                       final Map<String, List<String>> query_pairs = new LinkedHashMap<String, List<String>>();
-                       if (queryPart == null)
-                               return query_pairs;
-                       final String[] pairs = queryPart.split("&");
-                       for (String pair : pairs) {
-                               final int idx = pair.indexOf("=");
-                               final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name())
-                                               : pair;
-                               if (!query_pairs.containsKey(key)) {
-                                       query_pairs.put(key, new LinkedList<String>());
-                               }
-                               final String value = idx > 0 && pair.length() > idx + 1
-                                               ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name())
-                                               : null;
-                               query_pairs.get(key).add(value);
-                       }
-                       return query_pairs;
-               } catch (UnsupportedEncodingException e) {
-                       throw new IllegalArgumentException("Cannot convert " + queryPart + " to map", e);
-               }
-       }
-
-//     public static void main(String[] args) {
-//             if (args.length == 0)
-//                     throw new IllegalArgumentException("Usage: <path to A2 base>");
-//             Map<String, String> configuration = new HashMap<>();
-//             configuration.put("osgi.console", "2323");
-//             configuration.put("org.osgi.framework.bootdelegation",
-//                             "com.sun.jndi.ldap,com.sun.jndi.ldap.sasl,com.sun.security.jgss,com.sun.jndi.dns,com.sun.nio.file,com.sun.nio.sctp,sun.nio.cs");
-//             Framework framework = OsgiBootUtils.launch(configuration);
-//             try {
-//                     ProvisioningManager pm = new ProvisioningManager(framework.getBundleContext());
-//                     Map<String, String> xOr = new HashMap<>();
-//                     xOr.put("osgi", "equinox");
-//                     xOr.put("swt", "rap");
-//                     FsA2Source context = new FsA2Source(Paths.get(args[0]), xOr);
-//                     context.load();
-//                     pm.addSource(context);
-//                     if (framework.getBundleContext().getBundles().length == 1) {// initial
-//                             pm.install(null);
-//                     } else {
-//                             pm.update();
-//                     }
-//
-//                     Thread.sleep(2000);
-//
-//                     Bundle[] bundles = framework.getBundleContext().getBundles();
-//                     Arrays.sort(bundles, (b1, b2) -> b1.getSymbolicName().compareTo(b2.getSymbolicName()));
-//                     for (Bundle b : bundles)
-//                             if (b.getState() == Bundle.RESOLVED || b.getState() == Bundle.STARTING || b.getState() == Bundle.ACTIVE)
-//                                     System.out.println(b.getSymbolicName() + " " + b.getVersion());
-//                             else
-//                                     System.err.println(b.getSymbolicName() + " " + b.getVersion() + " (" + b.getState() + ")");
-//             } catch (Exception e) {
-//                     e.printStackTrace();
-//             } finally {
-//                     try {
-//                             framework.stop();
-//                     } catch (Exception e) {
-//                             e.printStackTrace();
-//                     }
-//             }
-//     }
-
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/ProvisioningSource.java b/org.argeo.init/src/org/argeo/init/a2/ProvisioningSource.java
deleted file mode 100644 (file)
index 9935630..0000000
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.argeo.init.a2;
-
-import org.osgi.framework.Bundle;
-import org.osgi.framework.BundleContext;
-import org.osgi.framework.Version;
-
-/** Where components are retrieved from. */
-public interface ProvisioningSource {
-       /** List all contributions of this source. */
-       Iterable<A2Contribution> listContributions(Object filter);
-
-       /** Install a module in the OSGi runtime. */
-       Bundle install(BundleContext bc, A2Module module);
-
-       /** 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);
-
-}
diff --git a/org.argeo.init/src/org/argeo/init/a2/package-info.java b/org.argeo.init/src/org/argeo/init/a2/package-info.java
deleted file mode 100644 (file)
index bb8fa6e..0000000
+++ /dev/null
@@ -1,2 +0,0 @@
-/** A2 OSGi repository format. */
-package org.argeo.init.a2;
\ No newline at end of file
index ff602ad51a7c06abe532fc5af3111f9c18870a93..30a149bc99811487af51d1cc90a07c254ab50821 100644 (file)
@@ -29,8 +29,8 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.function.Consumer;
 
-import org.argeo.init.RuntimeContext;
-import org.argeo.init.Service;
+import org.argeo.api.init.RuntimeContext;
+import org.argeo.internal.init.InternalState;
 
 /**
  * A thin logging system based on the {@link Logger} framework. It is a
@@ -150,7 +150,7 @@ class ThinLogging implements Consumer<Map<String, Object>> {
        }
 
        private void close() {
-               RuntimeContext runtimeContext = Service.getRuntimeContext();
+               RuntimeContext runtimeContext = InternalState.getMainRuntimeContext();
                if (runtimeContext != null) {
                        try {
                                runtimeContext.waitForStop(0);
@@ -609,24 +609,24 @@ class ThinLogging implements Consumer<Map<String, Object>> {
                }
        }
 
-       public static void main(String args[]) {
-               Logger logger = System.getLogger(ThinLogging.class.getName());
-               logger.log(Logger.Level.ALL, "Log all");
-               logger.log(Logger.Level.TRACE, "Multi\nline\ntrace");
-               logger.log(Logger.Level.DEBUG, "Log debug");
-               logger.log(Logger.Level.INFO, "Log info");
-               logger.log(Logger.Level.WARNING, "Log warning");
-               logger.log(Logger.Level.ERROR, "Log exception", new Throwable());
-
-               try {
-                       // we ait a bit in order to make sure all messages are flushed
-                       // TODO synchronize more efficiently
-                       // executor.awaitTermination(300, TimeUnit.MILLISECONDS);
-                       ForkJoinPool.commonPool().awaitTermination(300, TimeUnit.MILLISECONDS);
-               } catch (InterruptedException e) {
-                       // silent
-               }
-
-       }
+//     public static void main(String args[]) {
+//             Logger logger = System.getLogger(ThinLogging.class.getName());
+//             logger.log(Logger.Level.ALL, "Log all");
+//             logger.log(Logger.Level.TRACE, "Multi\nline\ntrace");
+//             logger.log(Logger.Level.DEBUG, "Log debug");
+//             logger.log(Logger.Level.INFO, "Log info");
+//             logger.log(Logger.Level.WARNING, "Log warning");
+//             logger.log(Logger.Level.ERROR, "Log exception", new Throwable());
+//
+//             try {
+//                     // we wait a bit in order to make sure all messages are flushed
+//                     // TODO synchronize more efficiently
+//                     // executor.awaitTermination(300, TimeUnit.MILLISECONDS);
+//                     ForkJoinPool.commonPool().awaitTermination(300, TimeUnit.MILLISECONDS);
+//             } catch (InterruptedException e) {
+//                     // silent
+//             }
+//
+//     }
 
 }
index b85b248b9e17c94f01be20070cb86e69f3a79dd4..057c2178663f7a11ec5288eff28013133469e8e0 100644 (file)
@@ -4,7 +4,7 @@ import java.lang.System.Logger;
 import java.lang.System.Logger.Level;
 import java.util.Objects;
 
-import org.argeo.init.Service;
+import org.argeo.init.ServiceMain;
 import org.argeo.init.logging.ThinLoggerFinder;
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
@@ -29,7 +29,7 @@ public class Activator implements BundleActivator {
 
        public void start(final BundleContext bundleContext) throws Exception {
                // The OSGi runtime was created by us, and therefore already initialized
-               argeoInit = Boolean.parseBoolean(bundleContext.getProperty(Service.PROP_ARGEO_INIT_MAIN));
+               argeoInit = Boolean.parseBoolean(bundleContext.getProperty(ServiceMain.PROP_ARGEO_INIT_MAIN));
                if (!argeoInit) {
                        if (runtimeContext == null) {
                                runtimeContext = new OsgiRuntimeContext(bundleContext);
index f5b260caba404735f9f296830bab0e6e29c369bf..67ee7b6dffd32189e316f365afbbef6ecc38f7a8 100644 (file)
@@ -20,8 +20,9 @@ import java.util.SortedMap;
 import java.util.StringTokenizer;
 import java.util.TreeMap;
 
-import org.argeo.init.a2.A2Source;
-import org.argeo.init.a2.ProvisioningManager;
+import org.argeo.api.a2.A2Source;
+import org.argeo.api.a2.ProvisioningManager;
+import org.argeo.api.init.InitConstants;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleException;
@@ -37,10 +38,6 @@ import org.osgi.framework.wiring.FrameworkWiring;
  * methods, configured via properties.
  */
 public class OsgiBoot implements OsgiBootConstants {
-       public final static String PROP_ARGEO_OSGI_START = "argeo.osgi.start";
-       public final static String PROP_ARGEO_OSGI_MAX_START_LEVEL = "argeo.osgi.maxStartLevel";
-       public final static String PROP_ARGEO_OSGI_SOURCES = "argeo.osgi.sources";
-
        @Deprecated
        final static String PROP_ARGEO_OSGI_BUNDLES = "argeo.osgi.bundles";
        final static String PROP_ARGEO_OSGI_BASE_URL = "argeo.osgi.baseUrl";
@@ -59,18 +56,6 @@ public class OsgiBoot implements OsgiBootConstants {
        public final static String DEFAULT_BASE_URL = "reference:file:";
        final static String DEFAULT_MAX_START_LEVEL = "32";
 
-       // OSGi standard properties
-       final static String PROP_OSGI_BUNDLES_DEFAULTSTARTLEVEL = "osgi.bundles.defaultStartLevel";
-       final static String PROP_OSGI_STARTLEVEL = "osgi.startLevel";
-       public final static String PROP_OSGI_INSTANCE_AREA = "osgi.instance.area";
-       public final static String PROP_OSGI_CONFIGURATION_AREA = "osgi.configuration.area";
-       public final static String PROP_OSGI_SHARED_CONFIGURATION_AREA = "osgi.sharedConfiguration.area";
-       public final static String PROP_OSGI_USE_SYSTEM_PROPERTIES = "osgi.framework.useSystemProperties";
-
-       // Symbolic names
-       final static String SYMBOLIC_NAME_OSGI_BOOT = "org.argeo.init";
-       final static String SYMBOLIC_NAME_EQUINOX = "org.eclipse.osgi";
-
        private final BundleContext bundleContext;
        private final String localCache;
        private final ProvisioningManager provisioningManager;
@@ -86,7 +71,7 @@ public class OsgiBoot implements OsgiBootConstants {
                localCache = getProperty(PROP_ARGEO_OSGI_LOCAL_CACHE, homeUri + ".m2/repository/");
 
                provisioningManager = new ProvisioningManager(bundleContext);
-               String sources = getProperty(PROP_ARGEO_OSGI_SOURCES);
+               String sources = getProperty(InitConstants.PROP_ARGEO_OSGI_SOURCES);
                if (sources == null) {
                        provisioningManager.registerDefaultSource();
                } else {
@@ -141,9 +126,9 @@ public class OsgiBoot implements OsgiBootConstants {
                        long begin = System.currentTimeMillis();
 
                        // notify start
-                       String osgiInstancePath = getProperty(PROP_OSGI_INSTANCE_AREA);
-                       String osgiConfigurationPath = getProperty(PROP_OSGI_CONFIGURATION_AREA);
-                       String osgiSharedConfigurationPath = getProperty(PROP_OSGI_CONFIGURATION_AREA);
+                       String osgiInstancePath = getProperty(InitConstants.PROP_OSGI_INSTANCE_AREA);
+                       String osgiConfigurationPath = getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA);
+                       String osgiSharedConfigurationPath = getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA);
                        OsgiBootUtils.info("OSGi bootstrap starting" //
                                        + (osgiInstancePath != null ? " data: " + osgiInstancePath + "" : "") //
                                        + (osgiConfigurationPath != null ? " state: " + osgiConfigurationPath + "" : "") //
@@ -238,8 +223,8 @@ public class OsgiBoot implements OsgiBootConstants {
                                Bundle bundle = (Bundle) installedBundles.get(url);
                                if (OsgiBootUtils.isDebug())
                                        debug("Bundle " + bundle.getSymbolicName() + " already installed from " + url);
-                       } else if (url.contains("/" + SYMBOLIC_NAME_EQUINOX + "/")
-                                       || url.contains("/" + SYMBOLIC_NAME_OSGI_BOOT + "/")) {
+                       } else if (url.contains("/" + InitConstants.SYMBOLIC_NAME_EQUINOX + "/")
+                                       || url.contains("/" + InitConstants.SYMBOLIC_NAME_INIT + "/")) {
                                if (OsgiBootUtils.isDebug())
                                        warn("Skip " + url);
                                return;
@@ -285,8 +270,8 @@ public class OsgiBoot implements OsgiBootConstants {
                } catch (BundleException e) {
                        final String ALREADY_INSTALLED = "is already installed";
                        String message = e.getMessage();
-                       if ((message.contains("Bundle \"" + SYMBOLIC_NAME_OSGI_BOOT + "\"")
-                                       || message.contains("Bundle \"" + SYMBOLIC_NAME_EQUINOX + "\""))
+                       if ((message.contains("Bundle \"" + InitConstants.SYMBOLIC_NAME_INIT + "\"")
+                                       || message.contains("Bundle \"" + InitConstants.SYMBOLIC_NAME_EQUINOX + "\""))
                                        && message.contains(ALREADY_INSTALLED)) {
                                // silent, in order to avoid warnings: we know that both
                                // have already been installed...
@@ -317,15 +302,15 @@ public class OsgiBoot implements OsgiBootConstants {
                if (properties != null) {
                        for (String key : properties.keySet()) {
                                String property = key;
-                               if (property.startsWith(PROP_ARGEO_OSGI_START)) {
+                               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(PROP_ARGEO_OSGI_MAX_START_LEVEL, DEFAULT_MAX_START_LEVEL));
+               int maxStartLevel = Integer.parseInt(getProperty(InitConstants.PROP_ARGEO_OSGI_MAX_START_LEVEL, DEFAULT_MAX_START_LEVEL));
                for (int i = 1; i <= maxStartLevel; i++) {
-                       String key = PROP_ARGEO_OSGI_START + "." + i;
+                       String key = InitConstants.PROP_ARGEO_OSGI_START + "." + i;
                        String value = getProperty(key);
                        if (value != null)
                                map.put(key, value);
@@ -333,7 +318,7 @@ public class OsgiBoot implements OsgiBootConstants {
                }
                // finally, override with system properties
                for (Object key : System.getProperties().keySet()) {
-                       if (key.toString().startsWith(PROP_ARGEO_OSGI_START)) {
+                       if (key.toString().startsWith(InitConstants.PROP_ARGEO_OSGI_START)) {
                                map.put(key.toString(), System.getProperty(key.toString()));
                        }
                }
@@ -348,7 +333,7 @@ public class OsgiBoot implements OsgiBootConstants {
                if (properties != null) {
                        for (Object key : properties.keySet()) {
                                String property = key.toString();
-                               if (property.startsWith(PROP_ARGEO_OSGI_START)) {
+                               if (property.startsWith(InitConstants.PROP_ARGEO_OSGI_START)) {
                                        map.put(property, properties.get(property).toString());
                                }
                        }
@@ -356,18 +341,18 @@ public class OsgiBoot implements OsgiBootConstants {
                startBundles(map);
        }
 
-       /** Start bundle based on keys starting with {@link #PROP_ARGEO_OSGI_START}. */
+       /** Start bundle based on keys starting with {@link InitConstants#PROP_ARGEO_OSGI_START}. */
        protected void doStartBundles(Map<String, String> properties) {
                FrameworkStartLevel frameworkStartLevel = bundleContext.getBundle(0).adapt(FrameworkStartLevel.class);
 
                // default and active start levels from System properties
                int initialStartLevel = frameworkStartLevel.getInitialBundleStartLevel();
-               int defaultStartLevel = Integer.parseInt(getProperty(PROP_OSGI_BUNDLES_DEFAULTSTARTLEVEL, "4"));
-               int activeStartLevel = Integer.parseInt(getProperty(PROP_OSGI_STARTLEVEL, "6"));
+               int defaultStartLevel = Integer.parseInt(getProperty(InitConstants.PROP_OSGI_BUNDLES_DEFAULTSTARTLEVEL, "4"));
+               int activeStartLevel = Integer.parseInt(getProperty(InitConstants.PROP_OSGI_STARTLEVEL, "6"));
                if (OsgiBootUtils.isDebug()) {
                        OsgiBootUtils.debug("OSGi default start level: "
-                                       + getProperty(PROP_OSGI_BUNDLES_DEFAULTSTARTLEVEL, "<not set>") + ", using " + defaultStartLevel);
-                       OsgiBootUtils.debug("OSGi active start level: " + getProperty(PROP_OSGI_STARTLEVEL, "<not set>")
+                                       + getProperty(InitConstants.PROP_OSGI_BUNDLES_DEFAULTSTARTLEVEL, "<not set>") + ", using " + defaultStartLevel);
+                       OsgiBootUtils.debug("OSGi active start level: " + getProperty(InitConstants.PROP_OSGI_STARTLEVEL, "<not set>")
                                        + ", using " + activeStartLevel);
                        OsgiBootUtils.debug("Framework start level: " + frameworkStartLevel.getStartLevel() + " (initial: "
                                        + initialStartLevel + ")");
@@ -454,11 +439,11 @@ public class OsgiBoot implements OsgiBootConstants {
                        Integer defaultStartLevel) {
 
                // default (and previously, only behaviour)
-               appendToStartLevels(startLevels, defaultStartLevel, properties.getOrDefault(PROP_ARGEO_OSGI_START, ""));
+               appendToStartLevels(startLevels, defaultStartLevel, properties.getOrDefault(InitConstants.PROP_ARGEO_OSGI_START, ""));
 
                // list argeo.osgi.start.* system properties
                Iterator<String> keys = properties.keySet().iterator();
-               final String prefix = PROP_ARGEO_OSGI_START + ".";
+               final String prefix = InitConstants.PROP_ARGEO_OSGI_START + ".";
                while (keys.hasNext()) {
                        String key = keys.next();
                        if (key.startsWith(prefix)) {
index cd2c80acb556792558ba6f316362932e3898b08b..6eed6db533ab659e71861ae601bbbf672fc45b66 100644 (file)
@@ -12,6 +12,7 @@ import java.util.Properties;
 import java.util.Set;
 import java.util.TreeMap;
 
+import org.argeo.api.init.InitConstants;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleEvent;
@@ -38,8 +39,8 @@ public class OsgiBuilder {
 
        public OsgiBuilder() {
                // configuration.put("osgi.clean", "true");
-               configuration.put(OsgiBoot.PROP_OSGI_CONFIGURATION_AREA, System.getProperty(OsgiBoot.PROP_OSGI_CONFIGURATION_AREA));
-               configuration.put(OsgiBoot.PROP_OSGI_INSTANCE_AREA, System.getProperty(OsgiBoot.PROP_OSGI_INSTANCE_AREA));
+               configuration.put(InitConstants.PROP_OSGI_CONFIGURATION_AREA, System.getProperty(InitConstants.PROP_OSGI_CONFIGURATION_AREA));
+               configuration.put(InitConstants.PROP_OSGI_INSTANCE_AREA, System.getProperty(InitConstants.PROP_OSGI_INSTANCE_AREA));
                configuration.put(PROP_OSGI_CLEAN, System.getProperty(PROP_OSGI_CLEAN));
        }
 
@@ -48,7 +49,7 @@ public class OsgiBuilder {
                framework = OsgiBootUtils.launch(configuration);
 
                BundleContext bc = framework.getBundleContext();
-               String osgiData = bc.getProperty(OsgiBoot.PROP_OSGI_INSTANCE_AREA);
+               String osgiData = bc.getProperty(InitConstants.PROP_OSGI_INSTANCE_AREA);
                // String osgiConf = bc.getProperty(OsgiBoot.CONFIGURATION_AREA_PROP);
                String osgiConf = framework.getDataFile("").getAbsolutePath();
                if (OsgiBootUtils.isDebug())
@@ -278,7 +279,7 @@ public class OsgiBuilder {
        private Properties startLevelsToProperties() {
                Properties properties = new Properties();
                for (Integer startLevel : startLevels.keySet()) {
-                       String property = OsgiBoot.PROP_ARGEO_OSGI_START + "." + startLevel;
+                       String property = InitConstants.PROP_ARGEO_OSGI_START + "." + startLevel;
                        StringBuilder value = new StringBuilder();
                        for (String bundle : startLevels.get(startLevel).getBundles()) {
                                value.append(bundle);
index 8afe05922ef5d57686be46b37480aebd65514e6b..c35046b20aec4f0576f272a6e1b4bd11a013418f 100644 (file)
@@ -12,7 +12,7 @@ import java.util.concurrent.Flow;
 import java.util.function.Consumer;
 import java.util.function.Supplier;
 
-import org.argeo.init.RuntimeContext;
+import org.argeo.api.init.RuntimeContext;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
 import org.osgi.framework.BundleException;
diff --git a/org.argeo.init/src/org/argeo/internal/init/InternalState.java b/org.argeo.init/src/org/argeo/internal/init/InternalState.java
new file mode 100644 (file)
index 0000000..03ab750
--- /dev/null
@@ -0,0 +1,21 @@
+package org.argeo.internal.init;
+
+import org.argeo.api.init.RuntimeContext;
+
+/**
+ * Keep track of the internal state mostly with static variables, typically in
+ * order to synchronise shutdown.
+ */
+public class InternalState {
+       private static RuntimeContext mainRuntimeContext;
+
+       /** The root runtime context in this JVM. */
+       public static RuntimeContext getMainRuntimeContext() {
+               return mainRuntimeContext;
+       }
+
+       public static void setMainRuntimeContext(RuntimeContext mainRuntimeContext) {
+               InternalState.mainRuntimeContext = mainRuntimeContext;
+       }
+
+}