You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by fm...@apache.org on 2009/09/03 08:44:20 UTC

svn commit: r810786 - in /sling/trunk/launchpad/base/src: main/java/org/apache/sling/launchpad/base/impl/ test/java/org/apache/sling/launchpad/base/ test/java/org/apache/sling/launchpad/base/impl/ test/resources/

Author: fmeschbe
Date: Thu Sep  3 06:44:19 2009
New Revision: 810786

URL: http://svn.apache.org/viewvc?rev=810786&view=rev
Log:
SLING-9222 (Finally) apply the patch to copy the bundles to the
filesystem for bootstrapping and easy extension of initial boot
time bundle installation

Added:
    sling/trunk/launchpad/base/src/test/java/org/apache/sling/launchpad/base/
    sling/trunk/launchpad/base/src/test/java/org/apache/sling/launchpad/base/impl/
    sling/trunk/launchpad/base/src/test/java/org/apache/sling/launchpad/base/impl/BootstrapInstallerTest.java   (with props)
    sling/trunk/launchpad/base/src/test/resources/
    sling/trunk/launchpad/base/src/test/resources/holaworld-invalid.jar   (with props)
    sling/trunk/launchpad/base/src/test/resources/holaworld-nomanifest.jar   (with props)
    sling/trunk/launchpad/base/src/test/resources/holaworld.jar   (with props)
Modified:
    sling/trunk/launchpad/base/src/main/java/org/apache/sling/launchpad/base/impl/BootstrapInstaller.java

Modified: sling/trunk/launchpad/base/src/main/java/org/apache/sling/launchpad/base/impl/BootstrapInstaller.java
URL: http://svn.apache.org/viewvc/sling/trunk/launchpad/base/src/main/java/org/apache/sling/launchpad/base/impl/BootstrapInstaller.java?rev=810786&r1=810785&r2=810786&view=diff
==============================================================================
--- sling/trunk/launchpad/base/src/main/java/org/apache/sling/launchpad/base/impl/BootstrapInstaller.java (original)
+++ sling/trunk/launchpad/base/src/main/java/org/apache/sling/launchpad/base/impl/BootstrapInstaller.java Thu Sep  3 06:44:19 2009
@@ -19,10 +19,13 @@
 package org.apache.sling.launchpad.base.impl;
 
 import java.io.File;
+import java.io.FileFilter;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.util.HashMap;
@@ -34,6 +37,7 @@
 import java.util.jar.Manifest;
 
 import org.apache.felix.framework.Logger;
+import org.apache.sling.launchpad.base.shared.SharedConstants;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleActivator;
 import org.osgi.framework.BundleContext;
@@ -69,6 +73,11 @@
     static final String PATH_RESOURCES = "resources/";
 
     /**
+     * The path of startup bundles in the sling home
+     */
+    static final String PATH_STARTUP = "startup/";
+
+    /**
      * The location of the core Bundles (value is "resources/corebundles").
      * These bundles are installed at startlevel
      * {@link #STARTLEVEL_CORE_BUNDLES}.
@@ -122,74 +131,99 @@
         this.resourceProvider = resourceProvider;
     }
 
+    //---------- BundleActivator interface
+
     /**
-     * Installs any Bundles missing in the current framework instance. The
-     * Bundles are verified by the Bundle location string. All missing Bundles
-     * are first installed and then started in the order of installation. Also
-     * install all deployment packages. This installation stuff is only
-     * performed during the first startup!
+     * https://issues.apache.org/jira/browse/SLING-922
+     * Handles the initial detection and installation of bundles into
+     * the Felix OSGi running in Sling
+     *
+     * Process:
+     * 1) Copy all bundles from enclosed resources (jar/war) to
+     *   ${sling.home}/startup. This gives something like
+     *   ${sling.home}/startup/0, /1, /10, /15, ...
+     *   Existing files are only replaced if the files
+     *   enclosed in the Sling launchpad jar/war file are newer.
+     * 2) Scan ${sling.home}/startup for bundles to install
+     *   in the same way as today the enclosed resources
+     *   are scanned directly.
+     *   So you could place your bundles in that structure and get them installed
+     *   at the requested start level (0 being "default bundle start level").
      */
     public void start(BundleContext context) throws Exception {
-        if (!isAlreadyInstalled(context)) {
+        // get the startup location in sling home
+        String slingHome = context.getProperty(SharedConstants.SLING_HOME);
+        File slingStartupDir = getSlingStartupDir(slingHome);
+
+        if (!isAlreadyInstalled(context, slingStartupDir)) {
+            // only run the deployment package stuff and war/jar copies when this war/jar is new/changed
+
             // register deployment package support
-            final DeploymentPackageInstaller dpi = new DeploymentPackageInstaller(
-                context, logger, resourceProvider);
-            context.addFrameworkListener(dpi);
-            context.addServiceListener(dpi, "(" + Constants.OBJECTCLASS + "="
-                + DeploymentPackageInstaller.DEPLOYMENT_ADMIN + ")");
+            try {
+                final DeploymentPackageInstaller dpi = new DeploymentPackageInstaller(
+                    context, logger, resourceProvider);
+                context.addFrameworkListener(dpi);
+                context.addServiceListener(dpi, "(" + Constants.OBJECTCLASS
+                    + "=" + DeploymentPackageInstaller.DEPLOYMENT_ADMIN + ")");
+            } catch (Throwable t) {
+                logger.log(
+                    Logger.LOG_WARNING,
+                    "Cannot register Deployment Admin support, continuing without",
+                    t);
+            }
+
+            // see if the loading of bundles from the package is disabled
+            String dpblString = context.getProperty(SharedConstants.DISABLE_PACKAGE_BUNDLE_LOADING);
+            Boolean disablePackageBundleLoading = Boolean.valueOf(dpblString);
+
+            if (disablePackageBundleLoading) {
+                logger.log(Logger.LOG_INFO, "Package bundle loading is disabled so no bundles will be installed from the resources location in the sling jar/war");
+            } else {
+                // get the bundles out of the jar/war and copy them to the startup location
+                Iterator<String> resources = resourceProvider.getChildren(PATH_BUNDLES);
+                while (resources.hasNext()) {
+                    String path = resources.next();
+                    // only consider folders
+                    if (path.endsWith("/")) {
+
+                        // cut off trailing slash
+                        path = path.substring(0, path.length() - 1);
+
+                        // calculate the startlevel of bundles contained
+                        int startLevel = getStartLevel(path);
+                        if (startLevel != STARTLEVEL_NONE) {
+                            copyBundles(slingStartupDir, path, startLevel);
+                        }
+                    }
+                }
+
+                // copy old-style core bundles
+                copyBundles(slingStartupDir, PATH_CORE_BUNDLES, STARTLEVEL_CORE_BUNDLES);
+
+                // copy old-style bundles
+                copyBundles(slingStartupDir, PATH_BUNDLES, STARTLEVEL_BUNDLES);
+
+                // done with copying at this point
+            }
 
-            // list all existing bundles
+            // get the set of all existing (installed) bundles by symbolic name
             Bundle[] bundles = context.getBundles();
             Map<String, Bundle> bySymbolicName = new HashMap<String, Bundle>();
             for (int i = 0; i < bundles.length; i++) {
                 bySymbolicName.put(bundles[i].getSymbolicName(), bundles[i]);
             }
 
-            // the start level service to set the initial start level
-            ServiceReference ref = context.getServiceReference(StartLevel.class.getName());
-            StartLevel startLevelService = (ref != null)
-                    ? (StartLevel) context.getService(ref)
-                    : null;
-
-            // install bundles
+            // holds the bundles we install during this processing
             List<Bundle> installed = new LinkedList<Bundle>();
 
-            Iterator<String> res = resourceProvider.getChildren(PATH_BUNDLES);
-            while (res.hasNext()) {
-                String path = res.next();
-                // only consider folders
-                if (path.endsWith("/")) {
-
-                    // cut off trailing slash
-                    path = path.substring(0, path.length() - 1);
-
-                    // calculate the startlevel of bundles contained
-                    int startLevel = getStartLevel(path);
-                    if (startLevel != STARTLEVEL_NONE) {
-                        installBundles(context, bySymbolicName, path,
-                            installed, startLevelService, startLevel);
-                    }
-                }
-            }
-
-            // install old-style core bundles
-            installBundles(context, bySymbolicName, PATH_CORE_BUNDLES,
-                installed, startLevelService, STARTLEVEL_CORE_BUNDLES);
-
-            // install old-style bundles
-            installBundles(context, bySymbolicName, PATH_BUNDLES, installed,
-                startLevelService, STARTLEVEL_BUNDLES);
-
-            // release the start level service
-            if (ref != null) {
-                context.ungetService(ref);
-            }
+            // get all bundles from the startup location and install them
+            installBundles(slingStartupDir, context, bySymbolicName, installed);
 
-            // set start levels on the bundles and start them
+            // start all the newly installed bundles (existing bundles are not started if they are stopped)
             startBundles(installed);
 
             // mark everything installed
-            markInstalled(context);
+            markInstalled(context, slingStartupDir);
         }
     }
 
@@ -197,100 +231,256 @@
     public void stop(BundleContext context) {
     }
 
+    //---------- Startup folder maintenance
+
     /**
-     * Install the Bundles from JAR files found in the given <code>parent</code>
-     * path.
-     * 
-     * @param context The <code>BundleContext</code> used to install the new
-     *            Bundles.
-     * @param currentBundles The currently installed Bundles indexed by their
-     *            Bundle location.
-     * @param parent The path to the location in which to look for JAR files to
-     *            install. Only resources whose name ends with <em>.jar</em> are
-     *            considered for installation.
-     * @param installed The list of Bundles installed by this method. Each
-     *            Bundle successfully installed is added to this list.
+     * Get the sling startup directory (or create it) in the sling home if possible
+     * @param slingHome the path to the sling home
+     * @return the sling startup directory
+     * @throws IllegalStateException if the sling home or startup directories cannot be created/accessed
+     */
+    private File getSlingStartupDir(String slingHome) {
+        if (isBlank(slingHome)) {
+            throw new IllegalStateException("Fatal error in bootstrap: Cannot get the "+SharedConstants.SLING_HOME+" value: " + slingHome);
+        }
+        File slingHomeDir = new File(slingHome).getAbsoluteFile();
+        if (! slingHomeDir.exists()
+                || ! slingHomeDir.canRead()
+                || ! slingHomeDir.canWrite()
+                || ! slingHomeDir.isDirectory()) {
+            throw new IllegalStateException("Fatal error in bootstrap: Cannot find accessible existing "
+                    +SharedConstants.SLING_HOME+" directory: " + slingHomeDir);
+        }
+        File slingHomeStartupDir = getOrCreateDirectory(slingHomeDir, PATH_STARTUP);
+        return slingHomeStartupDir;
+    }
+
+    /**
+     * Get or create a sub-directory from an existing parent
+     * @param parentDir the parent directory
+     * @param subDirName the name of the sub-directory
+     * @return the sub-directory
+     * @throws IllegalStateException if directory cannot be created/accessed
+     */
+    private File getOrCreateDirectory(File parentDir, String subDirName) {
+        File slingHomeStartupDir = new File(parentDir, subDirName).getAbsoluteFile();
+        if ( slingHomeStartupDir.exists() ) {
+            if (! slingHomeStartupDir.isDirectory()
+                    || ! parentDir.canRead()
+                    || ! parentDir.canWrite() ) {
+                throw new IllegalStateException("Fatal error in bootstrap: Cannot find accessible existing "
+                        +SharedConstants.SLING_HOME+PATH_STARTUP+" directory: " + slingHomeStartupDir);
+            }
+        } else if (! slingHomeStartupDir.mkdirs() ) {
+            throw new IllegalStateException("Sling Home " + slingHomeStartupDir + " cannot be created as a directory");
+        }
+        return slingHomeStartupDir;
+    }
+
+    /**
+     * Copies the bundles from the given parent location in the jar/war
+     * to the startup directory in the sling.home based on the startlevel
+     * e.g. {sling.home}/startup/{startLevel}
      */
-    private void installBundles(BundleContext context,
-            Map<String, Bundle> currentBundles, String parent,
-            List<Bundle> installed, StartLevel startLevelService, int startLevel) {
+    private void copyBundles(File slingStartupDir, String parent, int startLevel) {
+
+        // set default start level
+        if (startLevel < 0) {
+            startLevel = 0;
+        }
+        // this will be set and created on demand
+        File startUpLevelDir = null;
 
         Iterator<String> res = resourceProvider.getChildren(parent);
         while (res.hasNext()) {
-
+            // path to the next resource
             String path = res.next();
-
+            // we only deal with jars
             if (path.endsWith(".jar")) {
-
-                // get the manifest for the bundle information
-                Manifest manifest = getManifest(path);
-                if (manifest == null) {
-                    logger.log(Logger.LOG_ERROR, "Ignoring " + path
-                        + ": Cannot read manifest");
+                // try to access the JAR file, ignore if not possible
+                InputStream ins = resourceProvider.getResourceAsStream(path);
+                if (ins == null) {
                     continue;
                 }
 
-                // ensure a symbolic name in the jar file
-                String symbolicName = getBundleSymbolicName(manifest);
-                if (symbolicName == null) {
-                    logger.log(Logger.LOG_ERROR, "Ignoring " + path
-                        + ": Missing " + Constants.BUNDLE_SYMBOLICNAME
-                        + " in manifest");
-                    continue;
+                // ensure we have a directory for the startlevel only when
+                // needed
+                if (startUpLevelDir == null) {
+                    startUpLevelDir = getOrCreateDirectory(slingStartupDir,
+                        String.valueOf(startLevel));
                 }
 
-                // check for an nstalled Bundle with the symbolic name
-                Bundle installedBundle = currentBundles.get(symbolicName);
-                if (ignore(installedBundle, manifest)) {
-                    logger.log(Logger.LOG_INFO, "Ignoring " + path
-                        + ": More recent version already installed");
-                    continue;
+                // copy over the bundle based on the startlevel
+                String bundleFileName = extractFileName(path);
+                File bundleJar = new File(startUpLevelDir, bundleFileName);
+                try {
+                    copyStreamToFile(ins, bundleJar);
+                } catch (IOException e) {
+                    // should this fail here or just log a warning?
+                    throw new RuntimeException("Failure copying file from "
+                        + path + " to startup dir (" + startUpLevelDir
+                        + ") and name (" + bundleFileName + "): " + e, e);
                 }
+            }
+        }
+    }
 
-                // try to access the JAR file, ignore if not possible
-                InputStream ins = resourceProvider.getResourceAsStream(path);
-                if (ins == null) {
-                    continue;
+    /**
+     * Copies a stream from the resource (jar/war) to a file
+     * @param fromStream
+     * @param toFile
+     */
+    static void copyStreamToFile(InputStream fromStream, File toFile) throws IOException {
+        if (fromStream == null || toFile == null) {
+            throw new IllegalArgumentException("fromStream and toFile must not be null");
+        }
+        if (! toFile.exists()) {
+            toFile.createNewFile();
+        }
+        // overwrite
+        OutputStream out = new FileOutputStream(toFile);
+        try {
+            byte[] buf = new byte[1024];
+            int len;
+            while ((len = fromStream.read(buf)) > 0) {
+                out.write(buf, 0, len);
+            }
+        } finally {
+            out.close();
+        }
+    }
+
+    /**
+     * Install the Bundles from JAR files found in startup directory under the
+     * level directories, this will only install bundles which are new or updated
+     * and will skip over them otherwise
+     *
+     * @param context The <code>BundleContext</code> used to install the new Bundles.
+     * @param currentBundles The currently installed Bundles indexed by their
+     *            Bundle location.
+     * @param parent The path to the location in which to look for JAR files to
+     *            install. Only resources whose name ends with <em>.jar</em> are
+     *            considered for installation.
+     * @param installed The list of Bundles installed by this method. Each
+     *            Bundle successfully installed is added to this list.
+     */
+    private void installBundles(File slingStartupDir,
+            BundleContext context, Map<String, Bundle> currentBundles,
+            List<Bundle> installed) {
+
+        // get the start level service (if possible) so we can set the initial start level
+        ServiceReference ref = context.getServiceReference(StartLevel.class.getName());
+        StartLevel startLevelService = (ref != null)
+                ? (StartLevel) context.getService(ref)
+                : null;
+
+        try {
+            File[] directories = slingStartupDir.listFiles(DIRECTORY_FILTER);
+            for (File levelDir : directories) {
+                // get startlevel from dir name
+                String dirName = levelDir.getName();
+                int startLevel;
+                try {
+                    startLevel = Integer.decode(dirName);
+                } catch (NumberFormatException e) {
+                    startLevel = 0;
                 }
 
-                if (installedBundle != null) {
+                // iterate through all files in the startlevel dir
+                File[] jarFiles = levelDir.listFiles(JAR_FILE_FILTER);
+                for (File bundleJar : jarFiles) {
+                    installBundle(bundleJar, startLevel, context, currentBundles, installed, startLevelService);
+                }
+            }
 
-                    try {
-                        installedBundle.update(ins);
-                        logger.log(Logger.LOG_INFO, "Bundle "
-                            + installedBundle.getSymbolicName()
-                            + " updated from " + path);
-                    } catch (BundleException be) {
-                        logger.log(Logger.LOG_ERROR, "Bundle update from "
-                            + path + " failed", be);
-                    }
+        } finally {
+            // release the start level service
+            if (ref != null) {
+                context.ungetService(ref);
+            }
+        }
+    }
+
+    /**
+     * @param bundleJar the jar file for the bundle to install
+     * @param startLevel the start level to use for this bundle
+     * @param context The <code>BundleContext</code> used to install the new Bundles.
+     * @param currentBundles The currently installed Bundles indexed by their
+     *            Bundle location.
+     * @param installed The list of Bundles installed by this method. Each
+     *            Bundle successfully installed is added to this list.
+     * @param startLevelService the service which sets the start level
+     */
+    private void installBundle(File bundleJar, int startLevel,
+            BundleContext context, Map<String, Bundle> currentBundles,
+            List<Bundle> installed, StartLevel startLevelService) {
+        // get the manifest for the bundle information
+        Manifest manifest = getManifest(bundleJar);
+        if (manifest == null) {
+            logger.log(Logger.LOG_ERROR, "Ignoring " + bundleJar
+                + ": Cannot read manifest");
+            return; // SHORT CIRCUIT
+        }
 
-                } else {
+        // ensure a symbolic name in the jar file
+        String symbolicName = getBundleSymbolicName(manifest);
+        if (symbolicName == null) {
+            logger.log(Logger.LOG_ERROR, "Ignoring " + bundleJar
+                + ": Missing " + Constants.BUNDLE_SYMBOLICNAME
+                + " in manifest");
+            return; // SHORT CIRCUIT
+        }
 
-                    // install the JAR file as a bundle
-                    String location = SCHEME
-                        + path.substring(path.lastIndexOf('/') + 1);
-                    try {
-                        Bundle theBundle = context.installBundle(location, ins);
-                        logger.log(Logger.LOG_INFO, "Bundle "
-                            + theBundle.getSymbolicName() + " installed from "
-                            + location);
-
-                        // finally add the bundle to the list for later start
-                        installed.add(theBundle);
-
-                        // optionally set the start level
-                        if (startLevel > 0) {
-                            startLevelService.setBundleStartLevel(theBundle,
-                                startLevel);
-                        }
+        // check for an installed Bundle with the symbolic name
+        Bundle installedBundle = currentBundles.get(symbolicName);
+        if (ignore(installedBundle, manifest)) {
+            logger.log(Logger.LOG_INFO, "Ignoring " + bundleJar
+                + ": More recent version already installed");
+            return; // SHORT CIRCUIT
+        }
 
-                    } catch (BundleException be) {
-                        logger.log(Logger.LOG_ERROR,
-                            "Bundle installation from " + location + " failed",
-                            be);
-                    }
+        // try to access the JAR file, ignore if not possible
+        InputStream ins;
+        try {
+            ins = new FileInputStream(bundleJar);
+        } catch (FileNotFoundException e) {
+            return; // SHORT CIRCUIT
+        }
+
+        if (installedBundle != null) {
+            try {
+                installedBundle.update(ins);
+                logger.log(Logger.LOG_INFO, "Bundle "
+                    + installedBundle.getSymbolicName()
+                    + " updated from " + bundleJar);
+            } catch (BundleException be) {
+                logger.log(Logger.LOG_ERROR, "Bundle update from "
+                    + bundleJar + " failed", be);
+            }
+
+        } else {
+            // install the JAR file as a bundle
+            String path = bundleJar.getPath();
+            String location = SCHEME
+                + path.substring(path.lastIndexOf('/') + 1);
+            try {
+                Bundle theBundle = context.installBundle(location, ins);
+                logger.log(Logger.LOG_INFO, "Bundle "
+                    + theBundle.getSymbolicName() + " installed from "
+                    + location);
+
+                // finally add the bundle to the list for later start
+                installed.add(theBundle);
+
+                // optionally set the start level
+                if (startLevel > 0) {
+                    startLevelService.setBundleStartLevel(theBundle,
+                        startLevel);
                 }
+
+            } catch (BundleException be) {
+                logger.log(Logger.LOG_ERROR,
+                    "Bundle installation from " + location + " failed", be);
             }
         }
     }
@@ -336,30 +526,46 @@
     // ---------- Bundle JAR file information
 
     /**
-     * Returns the Manifrest from the JAR file in the given resource provided by
+     * Returns the Manifest from the JAR file in the given resource provided by
      * the resource provider or <code>null</code> if the resource does not
      * exists or is not a JAR file or has no Manifest.
-     * 
+     *
      * @param jarPath The path to the JAR file provided by the resource provider
      *            of this instance.
      */
-    private Manifest getManifest(String jarPath) {
-        InputStream ins = resourceProvider.getResourceAsStream(jarPath);
-        if (ins != null) {
-            try {
-                JarInputStream jar = new JarInputStream(ins);
-                return jar.getManifest();
-            } catch (IOException ioe) {
-                logger.log(Logger.LOG_ERROR, "Failed to read manifest from "
-                    + jarPath, ioe);
-            } finally {
-                try {
-                    ins.close();
-                } catch (IOException ignore) {
-                }
+    private Manifest getManifest(File jar) {
+        try {
+            InputStream ins = new FileInputStream(jar);
+            if (ins != null) {
+                return getManifest(ins);
             }
+        } catch (FileNotFoundException e) {
+            logger.log(Logger.LOG_WARNING, "Could not get inputstream from file ("+jar+"):"+e);
+            //throw new IllegalArgumentException("Could not get inputstream from file ("+jar+"):"+e, e);
         }
+        return null;
+    }
 
+    /**
+     * Return the manifest from a jar if it is possible to get it,
+     * this will also handle closing out the stream
+     *
+     * @param ins the inputstream for the jar
+     * @return the manifest OR null if it cannot be obtained
+     */
+    Manifest getManifest(InputStream ins) {
+        try {
+            JarInputStream jis = new JarInputStream(ins);
+            return jis.getManifest();
+        } catch (IOException ioe) {
+            logger.log(Logger.LOG_ERROR, "Failed to read manifest from stream: "
+                    + ins, ioe);
+        } finally {
+            try {
+                ins.close();
+            } catch (IOException ignore) {
+            }
+        }
         return null;
     }
 
@@ -370,10 +576,10 @@
      * Note that bundles are not allowed to have no symbolic name any more.
      * Therefore a bundle without a symbolic name header should not be
      * installed.
-     * 
+     *
      * @param manifest The Manifest from which to extract the header.
      */
-    private String getBundleSymbolicName(Manifest manifest) {
+    String getBundleSymbolicName(Manifest manifest) {
         return manifest.getMainAttributes().getValue(
             Constants.BUNDLE_SYMBOLICNAME);
     }
@@ -381,7 +587,7 @@
     /**
      * Checks whether the installed bundle is at the same version (or more
      * recent) than the bundle described by the given manifest.
-     * 
+     *
      * @param installedBundle The bundle currently installed in the framework
      * @param manifest The Manifest describing the bundle version potentially
      *            updating the installed bundle
@@ -408,14 +614,15 @@
 
     // ---------- Bundle Installation marker file
 
-    private boolean isAlreadyInstalled(BundleContext context) {
+    private boolean isAlreadyInstalled(BundleContext context,
+            File slingStartupDir) {
         final File dataFile = context.getDataFile(DATA_FILE);
         if (dataFile != null && dataFile.exists()) {
 
             FileInputStream fis = null;
             try {
 
-                long selfStamp = getSelfTimestamp();
+                long selfStamp = getSelfTimestamp(slingStartupDir);
                 if (selfStamp > 0) {
 
                     fis = new FileInputStream(dataFile);
@@ -450,12 +657,12 @@
         return false;
     }
 
-    private void markInstalled(BundleContext context) {
+    private void markInstalled(BundleContext context, File slingStartupDir) {
         final File dataFile = context.getDataFile(DATA_FILE);
         try {
             final FileOutputStream fos = new FileOutputStream(dataFile);
             try {
-                fos.write(String.valueOf(getSelfTimestamp()).getBytes());
+                fos.write(String.valueOf(getSelfTimestamp(slingStartupDir)).getBytes());
             } finally {
                 try {
                     fos.close();
@@ -476,7 +683,7 @@
      * URLClassLoader and that the first URL entry of this class loader is the
      * JAR providing this class. This is in fact true as the URLClassLoader has
      * been created by the launcher from the launcher JAR file.
-     * 
+     *
      * @return The last modification time stamp of the launcher JAR file or -1
      *         if the class loader of this class is not an URLClassLoader or the
      *         class loader has no URL entries. Both situations are not really
@@ -484,17 +691,91 @@
      * @throws IOException If an error occurrs reading accessing the last
      *             modification time stampe.
      */
-    private long getSelfTimestamp() throws IOException {
+    private long getSelfTimestamp(File slingStartupDir) throws IOException {
 
+        // the timestamp of the launcher jar
+        long selfStamp = -1;
         ClassLoader loader = getClass().getClassLoader();
         if (loader instanceof URLClassLoader) {
             URLClassLoader urlLoader = (URLClassLoader) loader;
             URL[] urls = urlLoader.getURLs();
             if (urls.length > 0) {
-                return urls[0].openConnection().getLastModified();
+                selfStamp = urls[0].openConnection().getLastModified();
+            }
+        }
+
+        // check whether any bundle is younger than the launcher jar
+        File[] directories = slingStartupDir.listFiles(DIRECTORY_FILTER);
+        for (File levelDir : directories) {
+
+            // iterate through all files in the startlevel dir
+            File[] jarFiles = levelDir.listFiles(JAR_FILE_FILTER);
+            for (File bundleJar : jarFiles) {
+                if (bundleJar.lastModified() > selfStamp) {
+                    selfStamp = bundleJar.lastModified();
+                }
             }
         }
 
-        return -1;
+        // return the final stamp (may be -1 if launcher jar cannot be checked
+        // and there are no bundle jar files)
+        return selfStamp;
     }
+
+    //---------- FileFilter implementations to scan startup folders
+
+    /**
+     * Simple directory filter
+     */
+    private static final FileFilter DIRECTORY_FILTER = new FileFilter() {
+        public boolean accept(File f) {
+            return f.isDirectory();
+        }
+    };
+
+    /**
+     * Simple jar file filter
+     */
+    private static final FileFilter JAR_FILE_FILTER = new FileFilter() {
+        public boolean accept(File f) {
+            return f.isFile() && f.getName().endsWith(".jar");
+        }
+    };
+
+    //---------- helper
+
+    /**
+     * Simple check to see if a string is blank since
+     * StringUtils is not available here, maybe fix this later
+     * @param str the string to check
+     * @return true if the string is null or empty OR false otherwise
+     */
+    static boolean isBlank(String str) {
+        return str == null || str.length() == 0 || str.trim().length() == 0;
+    }
+
+    /**
+     * @param path any path (cannot be blank)
+     * @return the filename from the end of the path
+     * @throws IllegalArgumentException if there is no filename available
+     */
+    static String extractFileName(String path) {
+        if (isBlank(path)) {
+            throw new IllegalArgumentException("Invalid blank path specified, cannot extract filename: " + path);
+        }
+        String name = "";
+        int slashPos = path.lastIndexOf(File.separatorChar);
+        if (slashPos == -1) {
+            // this is only a filename (no directory path included)
+            name = path;
+        } else if (path.length() > slashPos+1) {
+            // split off the ending of the path
+            name = path.substring(slashPos+1);
+        }
+        if (isBlank(name)) {
+            throw new IllegalArgumentException("Invalid path, no filename found: " + path);
+        }
+        return name;
+    }
+
 }

Added: sling/trunk/launchpad/base/src/test/java/org/apache/sling/launchpad/base/impl/BootstrapInstallerTest.java
URL: http://svn.apache.org/viewvc/sling/trunk/launchpad/base/src/test/java/org/apache/sling/launchpad/base/impl/BootstrapInstallerTest.java?rev=810786&view=auto
==============================================================================
--- sling/trunk/launchpad/base/src/test/java/org/apache/sling/launchpad/base/impl/BootstrapInstallerTest.java (added)
+++ sling/trunk/launchpad/base/src/test/java/org/apache/sling/launchpad/base/impl/BootstrapInstallerTest.java Thu Sep  3 06:44:19 2009
@@ -0,0 +1,191 @@
+/*
+ * 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.base.impl;
+
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.jar.Manifest;
+
+import org.junit.Test;
+
+/**
+ * Testing the bootstrap installer methods
+ */
+public class BootstrapInstallerTest {
+
+    /**
+     * Test method for
+     * {@link org.apache.sling.launchpad.base.impl.BootstrapInstaller#extractFileName(java.lang.String)}
+     * .
+     */
+    @Test
+    public void testExtractFileName() {
+        String filename = BootstrapInstaller.extractFileName("myfile.html");
+        assertEquals("myfile.html", filename);
+
+        filename = BootstrapInstaller.extractFileName("/things/myfile.html");
+        assertEquals("myfile.html", filename);
+
+        filename = BootstrapInstaller.extractFileName("LOTS/of/random/things/myfile.html");
+        assertEquals("myfile.html", filename);
+
+        try {
+            filename = BootstrapInstaller.extractFileName("LOTS/of/random/things/");
+            fail("should have thrown exception");
+        } catch (IllegalArgumentException e) {
+            assertNotNull(e.getMessage());
+        }
+
+        try {
+            filename = BootstrapInstaller.extractFileName("LOTS/of/random/things/");
+            fail("should have thrown exception");
+        } catch (IllegalArgumentException e) {
+            assertNotNull(e.getMessage());
+        }
+
+        try {
+            filename = BootstrapInstaller.extractFileName(null);
+            fail("should have thrown exception");
+        } catch (IllegalArgumentException e) {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    /**
+     * Test method for
+     * {@link org.apache.sling.launchpad.base.impl.BootstrapInstaller#copyStreamToFile(java.io.InputStream, java.io.File)}
+     * .
+     */
+    @Test
+    public void testCopyStreamToFile() {
+        InputStream stream = null;
+        File to = null;
+        File testDir = new File("testing");
+        testDir.deleteOnExit(); // cleanup
+        assertTrue(testDir.mkdir());
+
+        stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(
+            "holaworld.jar");
+        assertNotNull(stream); // cleanup
+        to = new File(testDir, "test.jar");
+        to.deleteOnExit();
+        try {
+            BootstrapInstaller.copyStreamToFile(stream, to);
+        } catch (IOException e) {
+            fail(e.getMessage());
+        }
+
+        File copy = new File(testDir, "test.jar");
+        try {
+            FileInputStream copyStream = new FileInputStream(copy);
+            byte[] copyData = new byte[copyStream.available()];
+            copyStream.read(copyData);
+            FileInputStream origStream = new FileInputStream(copy);
+            byte[] origData = new byte[origStream.available()];
+            origStream.read(origData);
+            assertArrayEquals(copyData, origData);
+        } catch (FileNotFoundException e) {
+            fail(e.getMessage());
+        } catch (IOException e) {
+            fail(e.getMessage());
+        }
+
+        try {
+            BootstrapInstaller.copyStreamToFile(null, to);
+            fail("should have thrown exception");
+        } catch (IOException e) {
+            fail(e.getMessage());
+        } catch (IllegalArgumentException e) {
+            assertNotNull(e.getMessage());
+        }
+
+        try {
+            BootstrapInstaller.copyStreamToFile(stream, null);
+            fail("should have thrown exception");
+        } catch (IOException e) {
+            fail(e.getMessage());
+        } catch (IllegalArgumentException e) {
+            assertNotNull(e.getMessage());
+        }
+    }
+
+    /**
+     * Test method for
+     * {@link org.apache.sling.launchpad.base.impl.BootstrapInstaller#isBlank(java.lang.String)}
+     * .
+     */
+    @Test
+    public void testIsBlank() {
+        assertTrue(BootstrapInstaller.isBlank(null));
+        assertTrue(BootstrapInstaller.isBlank(""));
+        assertTrue(BootstrapInstaller.isBlank(" "));
+
+        assertFalse(BootstrapInstaller.isBlank("Test"));
+        assertFalse(BootstrapInstaller.isBlank(" asdf "));
+    }
+
+    /**
+     * Test method for
+     * {@link org.apache.sling.launchpad.base.impl.BootstrapInstaller#getManifest(java.io.InputStream)}
+     * .
+     */
+    @Test
+    public void testGetManifestInputStream() {
+        BootstrapInstaller bsi = new BootstrapInstaller(null, null);
+        InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(
+            "holaworld.jar");
+        Manifest m = bsi.getManifest(is);
+        assertNotNull(m);
+
+        is = Thread.currentThread().getContextClassLoader().getResourceAsStream(
+            "holaworld-nomanifest.jar");
+        m = bsi.getManifest(is);
+        assertNull(m);
+    }
+
+    /**
+     * Test method for
+     * {@link org.apache.sling.launchpad.base.impl.BootstrapInstaller#getBundleSymbolicName(java.util.jar.Manifest)}
+     * .
+     */
+    @Test
+    public void testGetBundleSymbolicName() {
+        BootstrapInstaller bsi = new BootstrapInstaller(null, null);
+        InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(
+            "holaworld.jar");
+        Manifest m = bsi.getManifest(is);
+        String sname = bsi.getBundleSymbolicName(m);
+        assertNotNull(sname);
+
+        is = Thread.currentThread().getContextClassLoader().getResourceAsStream(
+            "holaworld-invalid.jar");
+        m = bsi.getManifest(is);
+        sname = bsi.getBundleSymbolicName(m);
+        assertNull(sname);
+    }
+
+    // TODO eventually add in tests that create a context so we can test more
+    // things in detail
+
+}

Propchange: sling/trunk/launchpad/base/src/test/java/org/apache/sling/launchpad/base/impl/BootstrapInstallerTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sling/trunk/launchpad/base/src/test/java/org/apache/sling/launchpad/base/impl/BootstrapInstallerTest.java
------------------------------------------------------------------------------
    svn:keywords = Author Date Id Revision Rev Url

Added: sling/trunk/launchpad/base/src/test/resources/holaworld-invalid.jar
URL: http://svn.apache.org/viewvc/sling/trunk/launchpad/base/src/test/resources/holaworld-invalid.jar?rev=810786&view=auto
==============================================================================
Binary file - no diff available.

Propchange: sling/trunk/launchpad/base/src/test/resources/holaworld-invalid.jar
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Added: sling/trunk/launchpad/base/src/test/resources/holaworld-nomanifest.jar
URL: http://svn.apache.org/viewvc/sling/trunk/launchpad/base/src/test/resources/holaworld-nomanifest.jar?rev=810786&view=auto
==============================================================================
Binary file - no diff available.

Propchange: sling/trunk/launchpad/base/src/test/resources/holaworld-nomanifest.jar
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Added: sling/trunk/launchpad/base/src/test/resources/holaworld.jar
URL: http://svn.apache.org/viewvc/sling/trunk/launchpad/base/src/test/resources/holaworld.jar?rev=810786&view=auto
==============================================================================
Binary file - no diff available.

Propchange: sling/trunk/launchpad/base/src/test/resources/holaworld.jar
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream