You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by an...@apache.org on 2019/12/27 23:57:36 UTC

[sling-org-apache-sling-launchpad-base] 01/01: Added support for Feature Launcher and changed version to 3.0.0

This is an automated email from the ASF dual-hosted git repository.

andysch pushed a commit to branch feature/SLING-8955-Feature-Launcher-Support
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-launchpad-base.git

commit e4fd8cb2df4da025e297bf0cf73233bb6e0ffc5d
Author: Andreas Schaefer <sc...@iMac.local>
AuthorDate: Fri Dec 27 15:56:52 2019 -0800

    Added support for Feature Launcher and changed version to 3.0.0
---
 pom.xml                                            |  13 +-
 .../launchpad/app/{Main.java => AbstractMain.java} | 172 ++--
 .../sling/launchpad/app/ControlListener.java       |   4 +-
 .../apache/sling/launchpad/app/FeatureMain.java    | 221 +++++
 .../java/org/apache/sling/launchpad/app/Main.java  | 954 +--------------------
 .../org/apache/sling/launchpad/app/MainTest.java   |   2 +-
 6 files changed, 342 insertions(+), 1024 deletions(-)

diff --git a/pom.xml b/pom.xml
index bab110f..c342f60 100644
--- a/pom.xml
+++ b/pom.xml
@@ -32,7 +32,7 @@
        This version is a concatenation of the framework version and a version for the launchpad base itself.
        For a release only the qualifier (launchpad base version) is increased.
      -->
-    <version>6.0.2-2.6.37-SNAPSHOT</version>
+    <version>6.0.2-3.0.0-SNAPSHOT</version>
     <packaging>bundle</packaging>
 
     <name>Apache Sling Launchpad Base</name>
@@ -124,7 +124,7 @@
                             <goal>unpack-dependencies</goal>
                         </goals>
                         <configuration>
-                            <includeArtifactIds>org.apache.sling.commons.osgi</includeArtifactIds>
+                            <includeArtifactIds>commons-io,org.apache.sling.launchpad.base,org.apache.sling.feature.extension.content,org.apache.sling.feature.launcher,org.apache.sling.commons.osgi</includeArtifactIds>
                             <excludeTransitive>true</excludeTransitive>
                             <outputDirectory>
                                 ${project.build.directory}/classes
@@ -160,7 +160,7 @@
                             <archive>
                                 <manifest>
                                     <mainClass>
-                                        org.apache.sling.launchpad.app.Main
+                                        org.apache.sling.launchpad.app.FeatureMain
                                     </mainClass>
                                     <addDefaultImplementationEntries>
                                         true
@@ -174,6 +174,7 @@
                                 <include>org/apache/sling/commons/osgi/bundleversion/**</include>
                                 <include>org/osgi/framework/Version*</include>
                                 <include>META-INF/**</include>
+                                <include>org/apache/sling/feature/**</include>
                             </includes>
                         </configuration>
                     </execution>
@@ -268,6 +269,12 @@
             <scope>provided</scope>
         </dependency>
         <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.feature.launcher</artifactId>
+            <version>1.1.0</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
             <groupId>junit</groupId>
             <artifactId>junit</artifactId>
         </dependency>
diff --git a/src/main/java/org/apache/sling/launchpad/app/Main.java b/src/main/java/org/apache/sling/launchpad/app/AbstractMain.java
similarity index 91%
copy from src/main/java/org/apache/sling/launchpad/app/Main.java
copy to src/main/java/org/apache/sling/launchpad/app/AbstractMain.java
index 312ba8f..e03cc3f 100644
--- a/src/main/java/org/apache/sling/launchpad/app/Main.java
+++ b/src/main/java/org/apache/sling/launchpad/app/AbstractMain.java
@@ -16,9 +16,17 @@
  */
 package org.apache.sling.launchpad.app;
 
+import org.apache.sling.launchpad.base.shared.Launcher;
+import org.apache.sling.launchpad.base.shared.Loader;
+import org.apache.sling.launchpad.base.shared.Notifiable;
+import org.apache.sling.launchpad.base.shared.SharedConstants;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.text.DateFormat;
@@ -28,11 +36,6 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
 
-import org.apache.sling.launchpad.base.shared.Launcher;
-import org.apache.sling.launchpad.base.shared.Loader;
-import org.apache.sling.launchpad.base.shared.Notifiable;
-import org.apache.sling.launchpad.base.shared.SharedConstants;
-
 /**
  * The <code>Main</code> is the externally visible Standalone Java Application
  * launcher for Sling. Please refer to the full description <i>The Sling
@@ -47,7 +50,7 @@ import org.apache.sling.launchpad.base.shared.SharedConstants;
  * @see <a href="http://sling.apache.org/site/the-sling-launchpad.html">The
  *      Sling Launchpad</a>
  */
-public class Main {
+public class AbstractMain {
 
     // The name of the environment variable to consult to find out
     // about sling.home
@@ -110,11 +113,12 @@ public class Main {
      * @param args The command line arguments supplied when starting the Sling
      *            Launcher through the Java VM.
      */
-    public static void main(final String[] args) {
+    public static void main(Class mainClass, final String[] args) {
         final Map<String, String> rawArgs = parseCommandLine(args);
 
         // support usage first
-        if (doHelp(rawArgs)) {
+        boolean usedHelp = (boolean) callMethod(mainClass, "doHelp", Map.class, rawArgs);
+        if(usedHelp) {
             System.exit(0);
         }
 
@@ -123,8 +127,29 @@ public class Main {
             System.exit(1);
         }
 
-        final Main main = new Main(props);
+        final AbstractMain main = (AbstractMain) callMethod(mainClass, null, Map.class, props);
+
+        launchMain(main);
+    }
+
+    private static Object callMethod(Class clazz, String methodName, Class argumentType, Object argument) {
+        try {
+            Object answer;
+            if (methodName == null || methodName.isEmpty()) {
+                Constructor constructor = clazz.getConstructor(argumentType);
+                answer = constructor.newInstance(argument);
+            } else {
+                Method method = clazz.getMethod(methodName, argumentType);
+                System.out.println("Method found: '" + method + "'");
+                answer = method.invoke(null, argument);
+            }
+            return answer;
+        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) {
+            throw new RuntimeException("Unexpected issue with reflection", e);
+        }
+    }
 
+    static void launchMain(AbstractMain main) {
         // check for control commands
         int rc = main.doControlAction();
         if (rc >= 0) {
@@ -143,7 +168,7 @@ public class Main {
      * property names as known to the OSGi Framework and its installed
      * bundles.
      */
-    private final Map<String, String> commandLineArgs;
+    protected final Map<String, String> commandLineArgs;
 
     /**
      * Whether to install the shutdown hook.
@@ -152,7 +177,7 @@ public class Main {
      * @see #installShutdownHook(Map)
      * @see #addShutdownHook()
      */
-    private boolean installShutdownHook;
+    protected boolean installShutdownHook;
 
     /**
      * The shutdown hook installed into the Java VM after Sling has been
@@ -162,7 +187,7 @@ public class Main {
      * @see #addShutdownHook()
      * @see #removeShutdownHook()
      */
-    private Thread shutdownHook;
+    protected Thread shutdownHook;
 
     /**
      * The absolute path to the home directory of the launched Sling
@@ -171,24 +196,24 @@ public class Main {
      *
      * @see #getSlingHome(Map)
      */
-    private final String slingHome;
+    protected final String slingHome;
 
     /**
      * The {@link Loader} class used to create the Framework class loader and
      * to launch the framework.
      */
-    private Loader loader;
+    protected Loader loader;
 
     /**
      * The actual launcher accessed through the {@link #loader} to launch
      * the OSGi Framework.
      */
-    private Launcher sling;
+    protected Launcher sling;
 
     /**
      * Flag to indicate if Sling has already been started.
      */
-    private boolean started = false;
+    protected boolean started = false;
 
     /**
      * Creates an instance of this main loader class. The provided arguments are
@@ -201,7 +226,7 @@ public class Main {
      *            this parameter is <code>null</code> and empty map without
      *            configuration is assumed.
      */
-    protected Main(Map<String, String> args) {
+    protected AbstractMain(Map<String, String> args) {
         this.commandLineArgs = (args == null)
                 ? new HashMap<String, String>()
                 : args;
@@ -303,7 +328,7 @@ public class Main {
 
     /**
      * Terminates the VM which was started by calling the
-     * {@link #main(String[])} method of this class using the
+     * {@link #main(Class, String[])} method of this class using the
      * <code>status</code> value as the application exit status code.
      * <p>
      * This method does not return.
@@ -366,7 +391,7 @@ public class Main {
             loaderTmp = new Loader(launchpadHome) {
                 @Override
                 protected void info(String msg) {
-                    Main.info(msg, null);
+                    AbstractMain.info(msg, null);
                 }
             };
         } catch (IllegalArgumentException iae) {
@@ -457,14 +482,14 @@ public class Main {
         this.started = false;
     }
 
-    private void addShutdownHook() {
+    protected void addShutdownHook() {
         if (this.installShutdownHook && this.shutdownHook == null) {
             this.shutdownHook = new Thread(new ShutdownHook(), "Apache Sling Terminator");
             Runtime.getRuntime().addShutdownHook(this.shutdownHook);
         }
     }
 
-    private void removeShutdownHook() {
+    protected void removeShutdownHook() {
         // remove the shutdown hook, will fail if called from the
         // shutdown hook itself. Otherwise this prevents shutdown
         // from being called again
@@ -550,7 +575,7 @@ public class Main {
      *            <code>sling.launchpad</code> property.
      * @return The absolute <code>File</code> indicating the launchpad folder.
      */
-    private static File getLaunchpadHome(final String slingHome,
+    protected static File getLaunchpadHome(final String slingHome,
             final Map<String, String> commandLineArgs) {
         final String launchpadHomeParam = commandLineArgs.get(SharedConstants.SLING_LAUNCHPAD);
         if (launchpadHomeParam == null || launchpadHomeParam.length() == 0) {
@@ -684,37 +709,7 @@ public class Main {
         return commandLine;
     }
 
-    /** prints a simple usage plus optional error message */
-    private static boolean doHelp(Map<String, String> args) {
-        if (args.remove("h") != null) {
-            System.out.println("usage: "
-                + Main.class.getName()
-                + " [ start | stop | status ] [ -j adr ] [ -l loglevel ] [ -f logfile ] [ -c slinghome ] [ -i launchpadhome ] [ -a address ] [ -p port ] { -Dn=v } [ -h ]");
-
-            System.out.println("    start         listen for control connection (uses -j)");
-            System.out.println("    stop          terminate running Apache Sling (uses -j)");
-            System.out.println("    status        check whether Apache Sling is running (uses -j)");
-            System.out.println("    threads       request a thread dump from Apache Sling (uses -j)");
-            System.out.println("    -j adr        host and port to use for control connection in the format '[host:]port' (default 127.0.0.1:0)");
-            System.out.println("    -l loglevel   the initial loglevel (0..4, FATAL, ERROR, WARN, INFO, DEBUG)");
-            System.out.println("    -f logfile    the log file, \"-\" for stdout (default logs/error.log)");
-            System.out.println("    -c slinghome  the sling context directory (default sling)");
-            System.out.println("    -i launchpadhome  the launchpad directory (default slinghome)");
-            System.out.println("    -a address    the interfact to bind to (use 0.0.0.0 for any)");
-            System.out.println("    -p port       the port to listen to (default 8080)");
-            System.out.println("    -r path       the root servlet context path for the http service (default is /)");
-            System.out.println("    -n            don't install the shutdown hook");
-            System.out.println("    -Dn=v         sets property n to value v. Make sure to use this option *after* " +
-                                                  "the jar filename. The JVM also has a -D option which has a " +
-                                                  "different meaning");
-            System.out.println("    -h            prints this usage message");
-
-            return true;
-        }
-        return false;
-    }
-
-    private static boolean installShutdownHook(Map<String, String> props) {
+    protected static boolean installShutdownHook(Map<String, String> props) {
         String prop = props.remove(PROP_SHUTDOWN_HOOK);
         if (prop == null) {
             prop = System.getProperty(PROP_SHUTDOWN_HOOK);
@@ -904,15 +899,50 @@ public class Main {
         }
     }
 
+    /** prints a simple usage plus optional error message */
+    static boolean doHelp(Class mainClass, String[] additionalLines, Map<String, String> args) {
+        if (args.remove("h") != null) {
+            System.out.println("usage: "
+                + mainClass.getName()
+                + " [ start | stop | status ] [ -j adr ] [ -l loglevel ] [ -f logfile ] [ -c slinghome ] [ -i launchpadhome ] [ -a address ] [ -p port ] { -Dn=v } [ -h ]");
+
+            System.out.println("    start         listen for control connection (uses -j)");
+            System.out.println("    stop          terminate running Apache Sling (uses -j)");
+            System.out.println("    status        check whether Apache Sling is running (uses -j)");
+            System.out.println("    threads       request a thread dump from Apache Sling (uses -j)");
+            System.out.println("    -j adr        host and port to use for control connection in the format '[host:]port' (default 127.0.0.1:0)");
+            System.out.println("    -l loglevel   the initial loglevel (0..4, FATAL, ERROR, WARN, INFO, DEBUG)");
+            System.out.println("    -f logfile    the log file, \"-\" for stdout (default logs/error.log)");
+            System.out.println("    -c slinghome  the sling context directory (default sling)");
+            System.out.println("    -i launchpadhome  the launchpad directory (default slinghome)");
+            System.out.println("    -a address    the interface to bind to (use 0.0.0.0 for any)");
+            System.out.println("    -p port       the port to listen to (default 8080)");
+            System.out.println("    -r path       the root servlet context path for the http service (default is /)");
+            System.out.println("    -n            don't install the shutdown hook");
+            System.out.println("    -Dn=v         sets property n to value v. Make sure to use this option *after* " +
+                "the jar filename. The JVM also has a -D option which has a " +
+                "different meaning");
+            if(additionalLines != null && additionalLines.length > 0) {
+                for(String line: additionalLines) {
+                    System.out.println(line);
+                }
+            }
+            System.out.println("    -h            prints this usage message");
+
+            return true;
+        }
+        return false;
+    }
+
     private class ShutdownHook implements Runnable {
         @Override
         public void run() {
-            Main.info("Java VM is shutting down", null);
-            Main.this.doStop();
+            AbstractMain.info("Java VM is shutting down", null);
+            AbstractMain.this.doStop();
         }
     }
 
-    private class Notified implements Notifiable {
+    protected class Notified implements Notifiable {
 
         /**
          * The framework has been stopped by calling the
@@ -930,10 +960,10 @@ public class Main {
              * fails).
              */
 
-            Main.info("Apache Sling has been stopped", null);
+            AbstractMain.info("Apache Sling has been stopped", null);
 
-            Main.this.sling = null;
-            Main.this.doStop();
+            AbstractMain.this.sling = null;
+            AbstractMain.this.doStop();
         }
 
         /**
@@ -952,35 +982,35 @@ public class Main {
         @Override
         public void updated(File updateFile) {
 
-            Main.this.sling = null;
-            Main.this.doStop();
+            AbstractMain.this.sling = null;
+            AbstractMain.this.doStop();
 
-            Main.cleanupThreads();
+            AbstractMain.cleanupThreads();
 
             if (updateFile == null) {
 
-                Main.info("Restarting Framework and Apache Sling", null);
-                if (!Main.this.doStart(null)) {
-                    Main.error("Failed to restart Sling; terminating", null);
-                    Main.this.terminateVM(1);
+                AbstractMain.info("Restarting Framework and Apache Sling", null);
+                if (!AbstractMain.this.doStart(null)) {
+                    AbstractMain.error("Failed to restart Sling; terminating", null);
+                    AbstractMain.this.terminateVM(1);
                 }
 
             } else {
 
-                Main.info(
+                AbstractMain.info(
                     "Restarting Framework with update from " + updateFile, null);
                 boolean started = false;
                 try {
-                    started = Main.this.doStart(updateFile.toURI().toURL());
+                    started = AbstractMain.this.doStart(updateFile.toURI().toURL());
                 } catch (MalformedURLException mue) {
-                    Main.error("Cannot get URL for file " + updateFile, mue);
+                    AbstractMain.error("Cannot get URL for file " + updateFile, mue);
                 } finally {
                     updateFile.delete();
                 }
 
                 if (!started) {
-                    Main.error("Failed to restart Sling; terminating", null);
-                    Main.this.terminateVM(1);
+                    AbstractMain.error("Failed to restart Sling; terminating", null);
+                    AbstractMain.this.terminateVM(1);
                 }
             }
         }
diff --git a/src/main/java/org/apache/sling/launchpad/app/ControlListener.java b/src/main/java/org/apache/sling/launchpad/app/ControlListener.java
index 6e6284b..26689f8 100644
--- a/src/main/java/org/apache/sling/launchpad/app/ControlListener.java
+++ b/src/main/java/org/apache/sling/launchpad/app/ControlListener.java
@@ -93,7 +93,7 @@ class ControlListener implements Runnable {
     private static final int DEFAULT_LISTEN_PORT = 0;
 
     // The reference to the Main class to shutdown on request
-    private final Main slingMain;
+    private final AbstractMain slingMain;
 
     private final String listenSpec;
 
@@ -117,7 +117,7 @@ class ControlListener implements Runnable {
      * @param listenSpec The specification for the host and port for the socket
      *            connection. See above for the format of this parameter.
      */
-    ControlListener(final Main slingMain, final String listenSpec) {
+    ControlListener(final AbstractMain slingMain, final String listenSpec) {
         this.slingMain = slingMain;
         this.listenSpec = listenSpec; // socketAddress = this.getSocketAddress(listenSpec, selectNewPort);
     }
diff --git a/src/main/java/org/apache/sling/launchpad/app/FeatureMain.java b/src/main/java/org/apache/sling/launchpad/app/FeatureMain.java
new file mode 100644
index 0000000..ffa9301
--- /dev/null
+++ b/src/main/java/org/apache/sling/launchpad/app/FeatureMain.java
@@ -0,0 +1,221 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.launchpad.app;
+
+import org.apache.sling.launchpad.base.shared.Launcher;
+import org.apache.sling.launchpad.base.shared.Loader;
+import org.apache.sling.launchpad.base.shared.Notifiable;
+import org.apache.sling.launchpad.base.shared.SharedConstants;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * The <code>FeatureMain</code> is the externally visible Standalone Java Application
+ * launcher for Feature Model base Sling. Please refer to the full description <i>The Sling
+ * Launchpad</i> on the Sling Web Site for a full description of this class.
+ * <p>
+ * Logging goes to standard output for informational messages and to standard
+ * error for error messages.
+ * <p>
+ * This class goes into the secondary artifact with the classifier <i>app</i> to
+ * be used as the main class when starting the Java Application.
+ *
+ * @see <a href="http://sling.apache.org/site/the-sling-launchpad.html">The
+ *      Sling Launchpad</a>
+ */
+public class FeatureMain extends AbstractMain {
+
+    public static void main(final String[] args) {
+        AbstractMain.main(FeatureMain.class, args);
+    }
+
+    /** prints a simple usage plus optional error message */
+    public static boolean doHelp(Map<String, String> args) {
+        return AbstractMain.doHelp(Main.class,
+            new String[] {
+                "    -s             Main Feature File (will override the provided Sling Feature File)",
+                "    -af            Additional Feature Files added to the Main Feature File",
+            },
+            args
+        );
+    }
+
+    public FeatureMain(Map<String, String> args) {
+        super(args);
+    }
+
+    protected int doControlAction() {
+        //AS TODO: It would probably best to have the logic of handling Background Service to be inside the Feature Launcher
+//        final ControlAction action = getControlAction();
+//        if (action != null) {
+//            final ControlListener sl = new ControlListener(this,
+//                commandLineArgs.remove(PROP_CONTROL_SOCKET));
+//            switch (action) {
+//                case START:
+//                    if (!sl.listen()) {
+//                        // assume service already running
+//                        return 0;
+//                    }
+//                    break;
+//                case STATUS:
+//                    return sl.statusServer();
+//                case STOP:
+//                    return sl.shutdownServer();
+//                case THREADS:
+//                    return sl.dumpThreads();
+//            }
+//        }
+
+        return -1;
+    }
+
+    protected boolean doStart(final URL launcherJar) {
+
+        // prevent duplicate start
+        if ( this.started) {
+            info("Apache Sling has already been started", null);
+            return true;
+        }
+
+        info("Starting Apache Sling in " + slingHome, null);
+        this.started = true;
+
+        try {
+            java.net.URL url = getClass().getResource("/feature-sling12.json");
+            String[] arguments = new String[]{
+//                    "-f", tempFile.getAbsolutePath(),
+                "-f", url.toString(),
+                "-D", "org.osgi.service.http.port=8081",
+                "-v"
+            };
+            System.out.println("Before Launching Feature Launcher, arguments: " + Arrays.asList(arguments));
+            org.apache.sling.feature.launcher.impl.Main.main(arguments);
+        } catch(Throwable t) {
+            System.out.println("Caught an Exception: " + t.getLocalizedMessage());
+            t.printStackTrace();
+        }
+
+//        Loader loaderTmp = null;
+//        try {
+//            final File launchpadHome = getLaunchpadHome(slingHome,
+//                commandLineArgs);
+//            loaderTmp = new Loader(launchpadHome) {
+//                @Override
+//                protected void info(String msg) {
+//                    FeatureMain.info(msg, null);
+//                }
+//            };
+//        } catch (IllegalArgumentException iae) {
+//            error(
+//                "Cannot launch: Launchpad folder cannot be used: "
+//                    + iae.getMessage(), null);
+//            return false;
+//        }
+//        this.loader = loaderTmp;
+//
+//        if (launcherJar != null) {
+//            try {
+//                loader.installLauncherJar(launcherJar);
+//            } catch (IOException ioe) {
+//                error("Cannot launch: Cannot install " + launcherJar
+//                    + " for use", ioe);
+//                return false;
+//            }
+//        } else {
+//            info("No Launcher JAR to install", null);
+//        }
+//
+//        Object object = null;
+//        try {
+//            object = loader.loadLauncher(SharedConstants.DEFAULT_SLING_MAIN);
+//        } catch (IllegalArgumentException iae) {
+//            error("Cannot launch: Failed loading Sling class "
+//                + SharedConstants.DEFAULT_SLING_MAIN, iae);
+//            return false;
+//        }
+//
+//        if (object instanceof Launcher) {
+//
+//            // configure the launcher
+//            Launcher sling = (Launcher) object;
+//            sling.setNotifiable(new Notified());
+//            sling.setCommandLine(commandLineArgs);
+//            sling.setSlingHome(slingHome);
+//
+//            // launch it
+//            info("Starting launcher ...", null);
+//            if (sling.start()) {
+//                info("Startup completed", null);
+//                this.sling = sling;
+//                addShutdownHook();
+//                return true;
+//            }
+//
+//            error("Cannot launch: Launcher.start() returned false", null);
+//
+//        } else {
+//
+//            error("Cannot launch: Class " + SharedConstants.DEFAULT_SLING_MAIN + " is not a Launcher class", null);
+//
+//        }
+
+        return false;
+    }
+
+    /**
+     * Maybe called by the application to cause the Sling Application to
+     * properly terminate by stopping the OSGi Framework.
+     * <p>
+     * After calling this method the Sling Application can be started again
+     * by calling the {@link #doStart()} method.
+     * <p>
+     * Calling this method multiple times without calling the {@link #doStart()}
+     * method in between has no effect after the Sling Application has been
+     * terminated.
+     */
+    protected void doStop() {
+        removeShutdownHook();
+
+//        // now really shutdown sling
+//        if (this.sling != null) {
+//            info("Stopping Apache Sling", null);
+//            this.sling.stop();
+//            this.sling = null;
+//        }
+//
+//        // clean and VM caches
+//        if (this.loader != null) {
+//            this.loader.cleanupVM();
+//            this.loader = null;
+//        }
+//
+//        // further cleanup
+//        this.started = false;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/launchpad/app/Main.java b/src/main/java/org/apache/sling/launchpad/app/Main.java
index 312ba8f..38509d2 100644
--- a/src/main/java/org/apache/sling/launchpad/app/Main.java
+++ b/src/main/java/org/apache/sling/launchpad/app/Main.java
@@ -16,22 +16,7 @@
  */
 package org.apache.sling.launchpad.app;
 
-import java.io.File;
-import java.io.IOException;
-import java.io.PrintStream;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.HashMap;
 import java.util.Map;
-import java.util.Map.Entry;
-
-import org.apache.sling.launchpad.base.shared.Launcher;
-import org.apache.sling.launchpad.base.shared.Loader;
-import org.apache.sling.launchpad.base.shared.Notifiable;
-import org.apache.sling.launchpad.base.shared.SharedConstants;
 
 /**
  * The <code>Main</code> is the externally visible Standalone Java Application
@@ -47,943 +32,18 @@ import org.apache.sling.launchpad.base.shared.SharedConstants;
  * @see <a href="http://sling.apache.org/site/the-sling-launchpad.html">The
  *      Sling Launchpad</a>
  */
-public class Main {
-
-    // The name of the environment variable to consult to find out
-    // about sling.home
-    private static final String ENV_SLING_HOME = "SLING_HOME";
-
-    /**
-     * The name of the configuration property indicating the
-     * {@link ControlAction} to be taken in the {@link #doControlAction()}
-     * method.
-     */
-    protected static final String PROP_CONTROL_ACTION = "sling.control.action";
-
-    /**
-     * The name of the configuration property indicating the socket to use for
-     * the control connection. The value of this property is either just a port
-     * number (in which case the host is assumed to be <code>localhost</code>)
-     * or a host name (or IP address) and port number separated by a colon.
-     */
-    protected static final String PROP_CONTROL_SOCKET = "sling.control.socket";
-
-    /** The Sling configuration property name setting the initial log level */
-    private static final String PROP_LOG_LEVEL = "org.apache.sling.commons.log.level";
-
-    /** The Sling configuration property name setting the initial log file */
-    private static final String PROP_LOG_FILE = "org.apache.sling.commons.log.file";
-
-    /**
-     * The configuration property setting the port on which the HTTP service
-     * listens
-     */
-    private static final String PROP_PORT = "org.osgi.service.http.port";
-
-    /**
-     * The configuration property setting the context path where the HTTP service
-     * mounts itself.
-     */
-    private static final String PROP_CONTEXT_PATH = "org.apache.felix.http.context_path";
-
-    /**
-     * Host name or IP Address of the interface to listen on.
-     */
-    private static final String PROP_HOST = "org.apache.felix.http.host";
+public class Main extends AbstractMain {
 
-    /**
-     * Name of the configuration property (or system property) indicating
-     * whether the shutdown hook should be installed or not. If this property is
-     * not set or set to {@code true} (case insensitive), the shutdown hook
-     * properly shutting down the framework is installed on startup. Otherwise,
-     * if this property is set to any value other than {@code true} (case
-     * insensitive) the shutdown hook is not installed.
-     * <p>
-     * The respective command line option is {@code -n}.
-     */
-    private static final String PROP_SHUTDOWN_HOOK = "sling.shutdown.hook";
-
-    /**
-     * The main entry point to the Sling Launcher Standalone Java Application.
-     * This method is generally only called by the Java VM to launch Sling.
-     *
-     * @param args The command line arguments supplied when starting the Sling
-     *            Launcher through the Java VM.
-     */
     public static void main(final String[] args) {
-        final Map<String, String> rawArgs = parseCommandLine(args);
-
-        // support usage first
-        if (doHelp(rawArgs)) {
-            System.exit(0);
-        }
-
-        final Map<String, String> props = convertCommandLineArgs(rawArgs);
-        if (props == null) {
-            System.exit(1);
-        }
-
-        final Main main = new Main(props);
-
-        // check for control commands
-        int rc = main.doControlAction();
-        if (rc >= 0) {
-            main.terminateVM(rc);
-        }
-
-        // finally start Sling
-        if (!main.doStart()) {
-            error("Failed to start Sling; terminating", null);
-            main.terminateVM(1);
-        }
-    }
-
-    /**
-     * The map of command line arguments where the keys are the actual
-     * property names as known to the OSGi Framework and its installed
-     * bundles.
-     */
-    private final Map<String, String> commandLineArgs;
-
-    /**
-     * Whether to install the shutdown hook.
-     *
-     * @see #PROP_SHUTDOWN_HOOK
-     * @see #installShutdownHook(Map)
-     * @see #addShutdownHook()
-     */
-    private boolean installShutdownHook;
-
-    /**
-     * The shutdown hook installed into the Java VM after Sling has been
-     * started. The hook is removed again when Sling is being shut down
-     * or the {@link Notified notifier} is notified of the framework shutdown.
-     *
-     * @see #addShutdownHook()
-     * @see #removeShutdownHook()
-     */
-    private Thread shutdownHook;
-
-    /**
-     * The absolute path to the home directory of the launched Sling
-     * application. This corresponds to the value of the <code>sling.home</code>
-     * framework property.
-     *
-     * @see #getSlingHome(Map)
-     */
-    private final String slingHome;
-
-    /**
-     * The {@link Loader} class used to create the Framework class loader and
-     * to launch the framework.
-     */
-    private Loader loader;
-
-    /**
-     * The actual launcher accessed through the {@link #loader} to launch
-     * the OSGi Framework.
-     */
-    private Launcher sling;
-
-    /**
-     * Flag to indicate if Sling has already been started.
-     */
-    private boolean started = false;
-
-    /**
-     * Creates an instance of this main loader class. The provided arguments are
-     * used to configure the OSGi framework being launched with the
-     * {@link #doStart(URL)} method.
-     *
-     * @param args The map of configuration properties to be supplied to the
-     *            OSGi framework. The keys in this map are assumed to be usefull
-     *            without translation to the launcher and the OSGi Framework. If
-     *            this parameter is <code>null</code> and empty map without
-     *            configuration is assumed.
-     */
-    protected Main(Map<String, String> args) {
-        this.commandLineArgs = (args == null)
-                ? new HashMap<String, String>()
-                : args;
-
-        this.installShutdownHook = installShutdownHook(this.commandLineArgs);
-
-        // sling.home from the command line or system properties, else default
-        String home = getSlingHome(commandLineArgs);
-        final File slingHomeFile = new File(home);
-        if (!slingHomeFile.isAbsolute()) {
-            home = slingHomeFile.getAbsolutePath();
-        }
-        this.slingHome = home;
-    }
-
-    /**
-     * After instantiating this class, this method may be called to help with
-     * the communication with a running Sling instance. To setup this
-     * communication the configuration properties supplied to the constructor
-     * are evaluated as follows:
-     * <p>
-     * <table>
-     * <tr>
-     * <td><code>{@value #PROP_CONTROL_SOCKET}</code></td>
-     * <td>Specifies the socket to use for the control connection. This
-     * specification is of the form <i>host:port</i> where the host can be a
-     * host name or IP Address and may be omitted (along with the separating
-     * colon) and port is just the numeric port number at which to listen. The
-     * default is <i>localhost:63000</i>. It is suggested to not use an
-     * externally accessible interface for security reasons because there is no
-     * added security on this control channel for now.</td>
-     * </tr>
-     * <tr>
-     * <td><code>{@value #PROP_CONTROL_ACTION}</code></td>
-     * <td>The actual action to execute:
-     * <ul>
-     * <b>start</b> -- Start the listener on the configured socket and expect
-     * commands there. This action is useful only when launching the Sling
-     * application since this action helps manage a running system.
-     * </ul>
-     * <ul>
-     * <b>stop</b> -- Connects to the listener running on the configured socket
-     * and send the command to terminate the Sling Application. If this command
-     * is used, it is expected the Sling Application will not start.
-     * </ul>
-     * <ul>
-     * <b>status</b> -- Connects to the listener running on the configured
-     * socket and query about its status. If this command is used, it is
-     * expected the Sling Application will not start.
-     * </ul>
-     * </td>
-     * </tr>
-     * </table>
-     * <p>
-     * After this method has executed the <code>j</code> and
-     * {@link #PROP_CONTROL_ACTION} properties have been removed from the
-     * configuration properties.
-     * <p>
-     * While the {@link #doStart()} and {@link #doStop()} methods may be called
-     * multiple times this method should only be called once after creating this
-     * class's instance.
-     *
-     * @return An code indicating whether the Java VM is expected to be
-     *         terminated or not. If <code>-1</code> is returned, the VM should
-     *         continue as intended, maybe starting the Sling Application. This
-     *         code is returned if the start action (or no action at all) is
-     *         supplied. Otherwise the VM should terminate with the returned
-     *         code as its exit code. For the stop action, this will be zero.
-     *         For the status action, this will be a LSB compliant code for
-     *         daemon status check: 0 (application running), 1 (Program Dead),
-     *         3 (Program Not Running), 4 (Unknown Problem).
-     * @see <a
-     *      href="http://refspecs.linuxbase.org/LSB_3.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html">Init Script Actions</a>
-     *      for a definition of the LSB status codes
-     */
-    protected int doControlAction() {
-        final ControlAction action = getControlAction();
-        if (action != null) {
-            final ControlListener sl = new ControlListener(this,
-                commandLineArgs.remove(PROP_CONTROL_SOCKET));
-            switch (action) {
-                case START:
-                    if (!sl.listen()) {
-                        // assume service already running
-                        return 0;
-                    }
-                    break;
-                case STATUS:
-                    return sl.statusServer();
-                case STOP:
-                    return sl.shutdownServer();
-                case THREADS:
-                    return sl.dumpThreads();
-            }
-        }
-
-        return -1;
-    }
-
-    /**
-     * Terminates the VM which was started by calling the
-     * {@link #main(String[])} method of this class using the
-     * <code>status</code> value as the application exit status code.
-     * <p>
-     * This method does not return.
-     *
-     * @param status The application status exit code.
-     */
-    // default accessor to enable overwriting for unit tests
-    void terminateVM(final int status) {
-        System.exit(status);
-    }
-
-    private ControlAction getControlAction() {
-        Object action = this.commandLineArgs.remove(PROP_CONTROL_ACTION);
-        if (action != null) {
-            if (action instanceof ControlAction) {
-                return (ControlAction) action;
-            }
-
-            try {
-                return ControlAction.valueOf(action.toString().toUpperCase());
-            } catch (IllegalArgumentException iae) {
-                error("Illegal control action value: " + action, null);
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Starts the application with the configuration supplied with the
-     * configuration properties when this instance has been created.
-     * <p>
-     * Calling this method multiple times before calling {@link #doStop()} will
-     * cause a message to be printed and <code>true</code> being returned.
-     *
-     * @return <code>true</code> if startup was successful or the application
-     *         is considered to be started already. Otherwise an error message
-     *         has been logged and <code>false</code> is returned.
-     */
-    protected boolean doStart() {
-        // ensure up-to-date launcher jar
-        return doStart(getClass().getResource(
-            SharedConstants.DEFAULT_SLING_LAUNCHER_JAR));
-    }
-
-    protected boolean doStart(final URL launcherJar) {
-
-        // prevent duplicate start
-        if ( this.started) {
-            info("Apache Sling has already been started", null);
-            return true;
-        }
-
-        info("Starting Apache Sling in " + slingHome, null);
-        this.started = true;
-
-        Loader loaderTmp = null;
-        try {
-            final File launchpadHome = getLaunchpadHome(slingHome,
-                commandLineArgs);
-            loaderTmp = new Loader(launchpadHome) {
-                @Override
-                protected void info(String msg) {
-                    Main.info(msg, null);
-                }
-            };
-        } catch (IllegalArgumentException iae) {
-            error(
-                "Cannot launch: Launchpad folder cannot be used: "
-                    + iae.getMessage(), null);
-            return false;
-        }
-        this.loader = loaderTmp;
-
-        if (launcherJar != null) {
-            try {
-                loader.installLauncherJar(launcherJar);
-            } catch (IOException ioe) {
-                error("Cannot launch: Cannot install " + launcherJar
-                    + " for use", ioe);
-                return false;
-            }
-        } else {
-            info("No Launcher JAR to install", null);
-        }
-
-        Object object = null;
-        try {
-            object = loader.loadLauncher(SharedConstants.DEFAULT_SLING_MAIN);
-        } catch (IllegalArgumentException iae) {
-            error("Cannot launch: Failed loading Sling class "
-                + SharedConstants.DEFAULT_SLING_MAIN, iae);
-            return false;
-        }
-
-        if (object instanceof Launcher) {
-
-            // configure the launcher
-            Launcher sling = (Launcher) object;
-            sling.setNotifiable(new Notified());
-            sling.setCommandLine(commandLineArgs);
-            sling.setSlingHome(slingHome);
-
-            // launch it
-            info("Starting launcher ...", null);
-            if (sling.start()) {
-                info("Startup completed", null);
-                this.sling = sling;
-                addShutdownHook();
-                return true;
-            }
-
-            error("Cannot launch: Launcher.start() returned false", null);
-
-        } else {
-
-            error("Cannot launch: Class " + SharedConstants.DEFAULT_SLING_MAIN + " is not a Launcher class", null);
-
-        }
-
-        return false;
-    }
-
-    /**
-     * Maybe called by the application to cause the Sling Application to
-     * properly terminate by stopping the OSGi Framework.
-     * <p>
-     * After calling this method the Sling Application can be started again
-     * by calling the {@link #doStart()} method.
-     * <p>
-     * Calling this method multiple times without calling the {@link #doStart()}
-     * method in between has no effect after the Sling Application has been
-     * terminated.
-     */
-    protected void doStop() {
-        removeShutdownHook();
-
-        // now really shutdown sling
-        if (this.sling != null) {
-            info("Stopping Apache Sling", null);
-            this.sling.stop();
-            this.sling = null;
-        }
-
-        // clean and VM caches
-        if (this.loader != null) {
-            this.loader.cleanupVM();
-            this.loader = null;
-        }
-
-        // further cleanup
-        this.started = false;
-    }
-
-    private void addShutdownHook() {
-        if (this.installShutdownHook && this.shutdownHook == null) {
-            this.shutdownHook = new Thread(new ShutdownHook(), "Apache Sling Terminator");
-            Runtime.getRuntime().addShutdownHook(this.shutdownHook);
-        }
-    }
-
-    private void removeShutdownHook() {
-        // remove the shutdown hook, will fail if called from the
-        // shutdown hook itself. Otherwise this prevents shutdown
-        // from being called again
-        Thread shutdownHook = this.shutdownHook;
-        this.shutdownHook = null;
-
-        if (shutdownHook != null) {
-            try {
-                Runtime.getRuntime().removeShutdownHook(shutdownHook);
-            } catch (Throwable t) {
-                // don't care for problems removing the hook
-            }
-        }
-    }
-
-    /**
-     * Define the sling.home parameter implementing the algorithme defined on
-     * the wiki page to find the setting according to this algorithm:
-     * <ol>
-     * <li>Configuration property <code>sling.home</code></li>
-     * <li>System property <code>sling.home</code></li>
-     * <li>Environment variable <code>SLING_HOME</code></li>
-     * <li>Default value <code>sling</code></li>
-     * </ol>
-     *
-     * @param commandLine The command line arguments
-     * @return The value to use for sling.home
-     */
-    private static String getSlingHome(Map<String, String> commandLine) {
-        String source = null;
-
-        String slingHome = commandLine.get(SharedConstants.SLING_HOME);
-        if (slingHome != null) {
-
-            source = "command line";
-
-        } else {
-
-            slingHome = System.getProperty(SharedConstants.SLING_HOME);
-            if (slingHome != null) {
-
-                source = "system property sling.home";
-
-            } else {
-
-                slingHome = System.getenv(ENV_SLING_HOME);
-                if (slingHome != null) {
-
-                    source = "environment variable SLING_HOME";
-
-                } else {
-
-                    source = "default";
-                    slingHome = SharedConstants.SLING_HOME_DEFAULT;
-
-                }
-            }
-        }
-
-        info("Setting sling.home=" + slingHome + " (" + source + ")", null);
-        return slingHome;
-    }
-
-    /**
-     * Return the absolute path to sling home
-     */
-    public String getSlingHome() {
-        return this.slingHome;
-    }
-
-    /**
-     * Define the sling.launchpad parameter implementing the algorithm defined
-     * on the wiki page to find the setting according to this algorithm:
-     * <ol>
-     * <li>Configuration property <code>sling.launchpad</code>. This path is
-     * resolved against the <code>slingHome</code> folder if relative.</li>
-     * <li>Default to same as <code>sling.home</code></li>
-     * </ol>
-     *
-     * @param slingHome The absolute path to the Sling Home folder (aka the
-     *            <code>sling.home</code>.
-     * @param commandLineArgs The configuration properties from where to get the
-     *            <code>sling.launchpad</code> property.
-     * @return The absolute <code>File</code> indicating the launchpad folder.
-     */
-    private static File getLaunchpadHome(final String slingHome,
-            final Map<String, String> commandLineArgs) {
-        final String launchpadHomeParam = commandLineArgs.get(SharedConstants.SLING_LAUNCHPAD);
-        if (launchpadHomeParam == null || launchpadHomeParam.length() == 0) {
-            commandLineArgs.put(SharedConstants.SLING_LAUNCHPAD, slingHome);
-            return new File(slingHome);
-        }
-
-        File launchpadHome = new File(launchpadHomeParam);
-        if (!launchpadHome.isAbsolute()) {
-            launchpadHome = new File(slingHome, launchpadHomeParam);
-        }
-
-        commandLineArgs.put(SharedConstants.SLING_LAUNCHPAD,
-            launchpadHome.getAbsolutePath());
-        return launchpadHome;
-    }
-
-    // ---------- logging
-
-    // emit an informational message to standard out
-    protected static void info(String message, Throwable t) {
-        log(System.out, "*INFO *", message, t);
-    }
-
-    // emit an error message to standard err
-    protected static void error(String message, Throwable t) {
-        log(System.err, "*ERROR*", message, t);
-    }
-
-    private static final DateFormat fmt = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss.SSS ");
-
-    // helper method to format the message on the correct output channel
-    // the throwable if not-null is also prefixed line by line with the prefix
-    private static void log(PrintStream out, String prefix, String message,
-            Throwable t) {
-
-        final StringBuilder linePrefixBuilder = new StringBuilder();
-        synchronized (fmt) {
-            linePrefixBuilder.append(fmt.format(new Date()));
-        }
-        linePrefixBuilder.append(prefix);
-        linePrefixBuilder.append(" [");
-        linePrefixBuilder.append(Thread.currentThread().getName());
-        linePrefixBuilder.append("] ");
-        final String linePrefix = linePrefixBuilder.toString();
-
-        out.print(linePrefix);
-        out.println(message);
-        if (t != null) {
-            t.printStackTrace(new PrintStream(out) {
-                @Override
-                public void print(String x) {
-                    super.print(linePrefix);
-                    super.print(x);
-                }
-            });
-        }
-    }
-
-    /**
-     * Parses the command line arguments into a map of strings indexed by
-     * strings. This method supports single character option names only at the
-     * moment. Each pair of an option name and its value is stored into the
-     * map. If a single dash '-' character is encountered the rest of the command
-     * line are interpreted as option names and are stored in the map unmodified
-     * as entries with the same key and value.
-     * <table>
-     * <tr><th>Command Line</th><th>Mapping</th></tr>
-     * <tr><td>x</td><td>x -> x</td></tr>
-     * <tr><td>-y z</td><td>y -> z</td></tr>
-     * <tr><td>-yz</td><td>y -> z</td></tr>
-     * <tr><td>-y -z</td><td>y -> y, z -> z</td></tr>
-     * <tr><td>-y x - -z a</td><td>y -> x, -z -> -z, a -> a</td></tr>
-     * </table>
-     *
-     * @param args The command line to parse
-     *
-     * @return The map of command line options and their values
-     */
-    // default accessor to enable unit tests without requiring reflection
-    static Map<String, String> parseCommandLine(String... args) {
-        Map<String, String> commandLine = new HashMap<String, String>();
-        boolean readUnparsed = false;
-        for (int argc = 0; args != null && argc < args.length; argc++) {
-            String arg = args[argc];
-
-            if (readUnparsed) {
-                commandLine.put(arg, arg);
-            } else if (arg.startsWith("-")) {
-                if (arg.length() == 1) {
-                   readUnparsed = true;
-                } else {
-                    String key = String.valueOf(arg.charAt(1));
-                    if (arg.length() > 2) {
-                        final String val;
-                        final int indexOfEq = arg.indexOf('=');
-                        if (indexOfEq != -1) {
-                            //Handle case -Da=b
-                            key = arg.substring(1, indexOfEq);
-                            val = arg.substring(indexOfEq + 1);
-                        } else {
-                            val = arg.substring(2);
-                        }
-                        commandLine.put(key, val);
-                    } else {
-                        argc++;
-                        if (argc < args.length
-                            && (args[argc].equals("-") || !args[argc].startsWith("-"))) {
-                            String val = args[argc];
-
-                            //Special handling for -D a=b
-                            if(key.equals("D")){
-                                final int indexOfEq = val.indexOf('=');
-                                if (indexOfEq != -1) {
-                                    //Handle case -D a=b. Add key as Da
-                                    key = "D" + val.substring(0, indexOfEq);
-                                    val = val.substring(indexOfEq + 1);
-                                }
-                            }
-                            commandLine.put(key, val);
-                        } else {
-                            commandLine.put(key, key);
-                            argc--;
-                        }
-                    }
-                }
-            } else {
-                commandLine.put(arg, arg);
-            }
-        }
-        return commandLine;
-    }
-
-    /** prints a simple usage plus optional error message */
-    private static boolean doHelp(Map<String, String> args) {
-        if (args.remove("h") != null) {
-            System.out.println("usage: "
-                + Main.class.getName()
-                + " [ start | stop | status ] [ -j adr ] [ -l loglevel ] [ -f logfile ] [ -c slinghome ] [ -i launchpadhome ] [ -a address ] [ -p port ] { -Dn=v } [ -h ]");
-
-            System.out.println("    start         listen for control connection (uses -j)");
-            System.out.println("    stop          terminate running Apache Sling (uses -j)");
-            System.out.println("    status        check whether Apache Sling is running (uses -j)");
-            System.out.println("    threads       request a thread dump from Apache Sling (uses -j)");
-            System.out.println("    -j adr        host and port to use for control connection in the format '[host:]port' (default 127.0.0.1:0)");
-            System.out.println("    -l loglevel   the initial loglevel (0..4, FATAL, ERROR, WARN, INFO, DEBUG)");
-            System.out.println("    -f logfile    the log file, \"-\" for stdout (default logs/error.log)");
-            System.out.println("    -c slinghome  the sling context directory (default sling)");
-            System.out.println("    -i launchpadhome  the launchpad directory (default slinghome)");
-            System.out.println("    -a address    the interfact to bind to (use 0.0.0.0 for any)");
-            System.out.println("    -p port       the port to listen to (default 8080)");
-            System.out.println("    -r path       the root servlet context path for the http service (default is /)");
-            System.out.println("    -n            don't install the shutdown hook");
-            System.out.println("    -Dn=v         sets property n to value v. Make sure to use this option *after* " +
-                                                  "the jar filename. The JVM also has a -D option which has a " +
-                                                  "different meaning");
-            System.out.println("    -h            prints this usage message");
-
-            return true;
-        }
-        return false;
+        AbstractMain.main(Main.class, args);
     }
 
-    private static boolean installShutdownHook(Map<String, String> props) {
-        String prop = props.remove(PROP_SHUTDOWN_HOOK);
-        if (prop == null) {
-            prop = System.getProperty(PROP_SHUTDOWN_HOOK);
-        }
-
-        return (prop == null) ? true : Boolean.valueOf(prop);
+        /** prints a simple usage plus optional error message */
+    public static boolean doHelp(Map<String, String> args) {
+        return AbstractMain.doHelp(Main.class, null, args);
     }
 
-    // default accessor to enable unit tests without requiring reflection
-    static Map<String, String> convertCommandLineArgs(
-            Map<String, String> rawArgs) {
-        final HashMap<String, String> props = new HashMap<String, String>();
-        boolean errorArg = false;
-        for (Entry<String, String> arg : rawArgs.entrySet()) {
-            if (arg.getKey().length() == 1 || arg.getKey().startsWith("D")) {
-                String value = arg.getValue();
-                switch (arg.getKey().charAt(0)) {
-                    case 'j':
-                        if (value == arg.getKey()) {
-                            errorArg("-j", "Missing host:port value");
-                            errorArg = true;
-                            continue;
-                        }
-                        props.put(PROP_CONTROL_SOCKET, value);
-                        break;
-
-                    case 'l':
-                        if (value == arg.getKey()) {
-                            errorArg("-l", "Missing log level value");
-                            errorArg = true;
-                            continue;
-                        }
-                        props.put(PROP_LOG_LEVEL, value);
-                        break;
-
-                    case 'f':
-                        if (value == arg.getKey()) {
-                            errorArg("-f", "Missing log file value");
-                            errorArg = true;
-                            continue;
-                        } else if ("-".equals(value)) {
-                            value = "";
-                        }
-                        props.put(PROP_LOG_FILE, value);
-                        break;
-
-                    case 'c':
-                        if (value == arg.getKey()) {
-                            errorArg("-c", "Missing directory value");
-                            errorArg = true;
-                            continue;
-                        }
-                        props.put(SharedConstants.SLING_HOME, value);
-                        break;
-
-                    case 'i':
-                        if (value == arg.getKey()) {
-                            errorArg("-i", "Missing launchpad directory value");
-                            errorArg = true;
-                            continue;
-                        }
-                        props.put(SharedConstants.SLING_LAUNCHPAD, value);
-                        break;
-
-                    case 'a':
-                        if (value == arg.getKey()) {
-                            errorArg("-a", "Missing address value");
-                            errorArg = true;
-                            continue;
-                        }
-                        props.put(PROP_HOST, value);
-                        break;
-
-                    case 'p':
-                        if (value == arg.getKey()) {
-                            errorArg("-p", "Missing port value");
-                            errorArg = true;
-                            continue;
-                        }
-                        try {
-                            // just to verify it is a number
-                            Integer.parseInt(value);
-                            props.put(PROP_PORT, value);
-                        } catch (RuntimeException e) {
-                            errorArg("-p", "Bad port: " + value);
-                            errorArg = true;
-                        }
-                        break;
-
-                    case 'r':
-                        if (value == arg.getKey()) {
-                            errorArg("-r", "Missing root path value");
-                            errorArg = true;
-                            continue;
-                        }
-                        props.put(PROP_CONTEXT_PATH, value);
-                        break;
-
-                    case 'n':
-                        props.put(PROP_SHUTDOWN_HOOK, Boolean.FALSE.toString());
-                        break;
-
-                    case 'D':
-                        if (value == arg.getKey()) {
-                            errorArg("-D", "Missing property assignment");
-                            errorArg = true;
-                            continue;
-                        }
-                        if (arg.getKey().length() > 1) {
-                            //Dfoo=bar arg.key=Dfoo and arg.value=bar
-                            props.put(arg.getKey().substring(1), arg.getValue());
-                        } else {
-                            //D foo=bar arg.key=D and arg.value=foo=bar
-                            String[] parts = value.split("=");
-                            int valueIdx = (parts.length > 1) ? 1 : 0;
-                            props.put(parts[0], parts[valueIdx]);
-                        }
-                        break;
-
-                    default:
-                        errorArg("-" + arg.getKey(), "Unrecognized option");
-                        errorArg = true;
-                        break;
-                }
-            } else if ("start".equals(arg.getKey())
-                    || "stop".equals(arg.getKey())
-                    || "status".equals(arg.getKey())
-                    || "threads".equals(arg.getKey())) {
-                props.put(PROP_CONTROL_ACTION, arg.getValue());
-            } else {
-                errorArg(arg.getKey(), "Unrecognized option");
-                errorArg = true;
-            }
-        }
-        return errorArg ? null : props;
-    }
-
-    private static void errorArg(String option, String message) {
-        error(String.format("%s: %s (use -h for more information)", option,
-            message), null);
-    }
-
-    /**
-     * Removes well-known stray threads and thread groups and removes framework
-     * thread context class loaders. Well-known stray threads and thread groups
-     * are:
-     * <ul>
-     * <li>The FileCleaningTracker$Reaper thread of the commons-io library</li>
-     * <li>The QuartzScheduler:ApacheSling thread group. See <a
-     * href="https://issues.apache.org/jira/browse/SLING-2535">SLING-2535
-     * QuartzScheduler:ApacheSling thread group remaining after stopping the
-     * scheduler bundle</a></li>
-     * </ul>
-     */
-    @SuppressWarnings("deprecation")
-    static void cleanupThreads() {
-
-        // the current thread is the SlingNotifier thread part of
-        // the main thread group, whose parent is the system thread
-        // group. We only care for the main thread group here
-        ThreadGroup tg = Thread.currentThread().getThreadGroup();
-        Thread[] active = new Thread[tg.activeCount()];
-        tg.enumerate(active);
-        for (Thread thread : active) {
-            if (thread != null) {
-                if (thread.getName().equals("FileCleaningTracker$Reaper")) {
-                    // I know, but this thread is stray ...
-                    // Commons-IO bundle (or consumer of it) should
-                    // actually stop it
-                    thread.stop();
-                } else {
-                    ClassLoader loader = thread.getContextClassLoader();
-                    if (loader != null && loader.getClass().getName().startsWith("org.apache.felix.framework.")) {
-                        thread.setContextClassLoader(null);
-                    }
-                }
-            }
-        }
-
-        // SLING-2535 - Scheduler thread group
-        ThreadGroup[] groups = new ThreadGroup[tg.activeGroupCount()];
-        tg.enumerate(groups);
-        for (ThreadGroup group : groups) {
-            if (group != null && group.getName().equals("QuartzScheduler:ApacheSling")) {
-                group.destroy();
-            }
-        }
+    public Main(Map<String, String> args) {
+        super(args);
     }
-
-    private class ShutdownHook implements Runnable {
-        @Override
-        public void run() {
-            Main.info("Java VM is shutting down", null);
-            Main.this.doStop();
-        }
-    }
-
-    private class Notified implements Notifiable {
-
-        /**
-         * The framework has been stopped by calling the
-         * <code>Bundle.stop()</code> on the system bundle. This actually
-         * terminates the Sling Standalone application.
-         */
-        @Override
-        public void stopped() {
-            /**
-             * This method is called if the framework is stopped from within by
-             * calling stop on the system bundle or if the framework is stopped
-             * because the VM is going down and the shutdown hook has initiated
-             * the shutdown. In any case we ensure the reference to the framework
-             * is removed and remove the shutdown hook (but don't care if that
-             * fails).
-             */
-
-            Main.info("Apache Sling has been stopped", null);
-
-            Main.this.sling = null;
-            Main.this.doStop();
-        }
-
-        /**
-         * The framework has been stopped with the intent to be restarted by
-         * calling either of the <code>Bundle.update</code> methods on the
-         * system bundle.
-         * <p>
-         * If an <code>InputStream</code> was provided, this has been copied to
-         * a temporary file, which will be used in place of the existing
-         * launcher jar file.
-         *
-         * @param updateFile The temporary file to replace the existing launcher
-         *            jar file. If <code>null</code> the existing launcher jar
-         *            will be used again.
-         */
-        @Override
-        public void updated(File updateFile) {
-
-            Main.this.sling = null;
-            Main.this.doStop();
-
-            Main.cleanupThreads();
-
-            if (updateFile == null) {
-
-                Main.info("Restarting Framework and Apache Sling", null);
-                if (!Main.this.doStart(null)) {
-                    Main.error("Failed to restart Sling; terminating", null);
-                    Main.this.terminateVM(1);
-                }
-
-            } else {
-
-                Main.info(
-                    "Restarting Framework with update from " + updateFile, null);
-                boolean started = false;
-                try {
-                    started = Main.this.doStart(updateFile.toURI().toURL());
-                } catch (MalformedURLException mue) {
-                    Main.error("Cannot get URL for file " + updateFile, mue);
-                } finally {
-                    updateFile.delete();
-                }
-
-                if (!started) {
-                    Main.error("Failed to restart Sling; terminating", null);
-                    Main.this.terminateVM(1);
-                }
-            }
-        }
-    }
-
 }
diff --git a/src/test/java/org/apache/sling/launchpad/app/MainTest.java b/src/test/java/org/apache/sling/launchpad/app/MainTest.java
index 440ee89..58d778e 100644
--- a/src/test/java/org/apache/sling/launchpad/app/MainTest.java
+++ b/src/test/java/org/apache/sling/launchpad/app/MainTest.java
@@ -299,7 +299,7 @@ public class MainTest extends TestCase {
 
     public void test_installShutdownHook() throws SecurityException, NoSuchMethodException, IllegalArgumentException,
             IllegalAccessException, InvocationTargetException {
-        final Method m = Main.class.getDeclaredMethod("installShutdownHook", Map.class);
+        final Method m = AbstractMain.class.getDeclaredMethod("installShutdownHook", Map.class);
         m.setAccessible(true);
 
         final String key = "sling.shutdown.hook";