Author: fmeschbe
Date: Tue Jan 27 10:13:44 2009
New Revision: 738046

URL: http://svn.apache.org/viewvc?rev=738046&view=rev
Log:
SLING-751 Expect bundles by startlevel below resources/bundles/* instead of 
resources/*
SLING-842 Allow for bundle update with new launcher JAR

Modified:
    
incubator/sling/trunk/launchpad/base/src/main/java/org/apache/sling/launchpad/base/impl/BootstrapInstaller.java

Modified: 
incubator/sling/trunk/launchpad/base/src/main/java/org/apache/sling/launchpad/base/impl/BootstrapInstaller.java
URL: 
http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/base/src/main/java/org/apache/sling/launchpad/base/impl/BootstrapInstaller.java?rev=738046&r1=738045&r2=738046&view=diff
==============================================================================
--- 
incubator/sling/trunk/launchpad/base/src/main/java/org/apache/sling/launchpad/base/impl/BootstrapInstaller.java
 (original)
+++ 
incubator/sling/trunk/launchpad/base/src/main/java/org/apache/sling/launchpad/base/impl/BootstrapInstaller.java
 Tue Jan 27 10:13:44 2009
@@ -23,13 +23,15 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
+import java.net.URL;
+import java.net.URLClassLoader;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.jar.JarInputStream;
+import java.util.jar.Manifest;
 
 import org.apache.felix.framework.Logger;
 import org.osgi.framework.Bundle;
@@ -38,6 +40,7 @@
 import org.osgi.framework.BundleException;
 import org.osgi.framework.Constants;
 import org.osgi.framework.ServiceReference;
+import org.osgi.framework.Version;
 import org.osgi.service.startlevel.StartLevel;
 
 /**
@@ -57,25 +60,47 @@
      * location of Bundles installed by this class is the name (without the
      * path) of the resource from which the Bundle was installed.
      */
-    public static final String SCHEME = "slinginstall:";
+    static final String SCHEME = "slinginstall:";
 
     /**
-     * The location the core Bundles (value is "resources"). These
-     * bundles are installed first.
+     * The root location in which the bundles are looked up for installation
+     * (value is "resources/").
      */
-    public static final String PATH_BUNDLE_ROOT = "resources";
-    
+    static final String PATH_RESOURCES = "resources/";
+
     /**
-     * The location the core Bundles (value is "resources/corebundles"). These
-     * bundles are installed first.
+     * The location of the core Bundles (value is "resources/corebundles").
+     * These bundles are installed at startlevel
+     * {...@link #STARTLEVEL_CORE_BUNDLES}.
+     * <p>
+     * This location is deprecated, instead these core bundles should be 
located
+     * in <code>resources/bundles/1</code>.
      */
-    public static final String PATH_CORE_BUNDLES = "corebundles";
+    static final String PATH_CORE_BUNDLES = PATH_RESOURCES + "corebundles";
 
     /**
      * The location the additional Bundles (value is "resources/bundles"). 
These
      * Bundles are installed after the {...@link #PATH_CORE_BUNDLES core 
Bundles}.
      */
-    public static final String PATH_BUNDLES = "bundles";
+    static final String PATH_BUNDLES = PATH_RESOURCES + "bundles";
+
+    /**
+     * The start level to be assigned to bundles found in the (old style)
+     * {...@link #PATH_CORE_BUNDLES resources/corebundles} location (value is 
1).
+     */
+    static final int STARTLEVEL_CORE_BUNDLES = 1;
+
+    /**
+     * The start level to be assigned to bundles found in the (old style)
+     * {...@link #PATH_BUNDLES resources/bundles} location (value is 0).
+     */
+    static final int STARTLEVEL_BUNDLES = 0;
+
+    /**
+     * The marker start level indicating the location of the bundle cannot be
+     * resolved to a valid start level (value is -1).
+     */
+    static final int STARTLEVEL_NONE = -1;
 
     /**
      * The {...@link Logger} use for logging messages during installation and
@@ -100,25 +125,24 @@
     /**
      * 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!
+     * 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!
      */
     public void start(BundleContext context) throws Exception {
         if (!isAlreadyInstalled(context)) {
             // register deployment package support
-            final DeploymentPackageInstaller dpi =
-                new DeploymentPackageInstaller(context, logger, 
resourceProvider);
+            final DeploymentPackageInstaller dpi = new 
DeploymentPackageInstaller(
+                context, logger, resourceProvider);
             context.addFrameworkListener(dpi);
-            context.addServiceListener(dpi, "("
-                    + Constants.OBJECTCLASS + "=" + 
DeploymentPackageInstaller.DEPLOYMENT_ADMIN + ")");
+            context.addServiceListener(dpi, "(" + Constants.OBJECTCLASS + "="
+                + DeploymentPackageInstaller.DEPLOYMENT_ADMIN + ")");
 
             // list all existing bundles
             Bundle[] bundles = context.getBundles();
-            Map<String, Bundle> byLocation = new HashMap<String, Bundle>();
+            Map<String, Bundle> bySymbolicName = new HashMap<String, Bundle>();
             for (int i = 0; i < bundles.length; i++) {
-                byLocation.put(bundles[i].getLocation(), bundles[i]);
+                bySymbolicName.put(bundles[i].getSymbolicName(), bundles[i]);
             }
 
             // the start level service to set the initial start level
@@ -130,24 +154,32 @@
             // install bundles
             List<Bundle> installed = new LinkedList<Bundle>();
 
-            Iterator<String> res = 
resourceProvider.getChildren(PATH_BUNDLE_ROOT);
+            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);
+                    path = path.substring(0, path.length() - 1);
 
                     // calculate the startlevel of bundles contained
                     int startLevel = getStartLevel(path);
-                    if (startLevel >= 0) {
-                        installBundles(context, byLocation, path, installed,
-                            startLevelService, startLevel);
+                    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);
@@ -155,7 +187,7 @@
 
             // set start levels on the bundles and start them
             startBundles(installed);
-            
+
             // mark everything installed
             markInstalled(context);
         }
@@ -168,14 +200,14 @@
     /**
      * 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.
+     *            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.
      */
@@ -190,39 +222,75 @@
 
             if (path.endsWith(".jar")) {
 
-                // check for an already installed Bundle with the given 
location
-                String location = SCHEME
-                    + path.substring(path.lastIndexOf('/') + 1);
-                if (currentBundles.containsKey(location)) {
+                // get the manifest for the bundle information
+                Manifest manifest = getManifest(path);
+                if (manifest == null) {
+                    logger.log(Logger.LOG_ERROR, "Ignoring " + path
+                        + ": Cannot read manifest");
                     continue;
                 }
 
-                // try to access the JAR file, ignore if not possible
-                InputStream ins = resourceProvider.getResourceAsStream(path);
-                if (ins == null) {
+                // 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;
                 }
 
-                // install the JAR file as a bundle
-                Bundle newBundle;
-                try {
-                    newBundle = context.installBundle(location, ins);
-                    logger.log(Logger.LOG_INFO, "Bundle "
-                        + newBundle.getSymbolicName() + " installed from "
-                        + location);
-                } catch (BundleException be) {
-                    logger.log(Logger.LOG_ERROR, "Bundle installation from "
-                        + location + " failed", be);
+                // 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;
                 }
-                
-                // optionally set the start level
-                if (startLevel > 0) {
-                    startLevelService.setBundleStartLevel(newBundle, 
startLevel);
+
+                // try to access the JAR file, ignore if not possible
+                InputStream ins = resourceProvider.getResourceAsStream(path);
+                if (ins == null) {
+                    continue;
                 }
 
-                // finally add the bundle to the list for later start
-                installed.add(newBundle);
+                if (installedBundle != null) {
+
+                    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);
+                    }
+
+                } else {
+
+                    // 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);
+                        }
+
+                    } catch (BundleException be) {
+                        logger.log(Logger.LOG_ERROR,
+                            "Bundle installation from " + location + " failed",
+                            be);
+                    }
+                }
             }
         }
     }
@@ -248,73 +316,185 @@
 
     private int getStartLevel(String path) {
         String name = path.substring(path.lastIndexOf('/') + 1);
-        
-        // core bundles are installed at start level 1
-        if (PATH_CORE_BUNDLES.equals(name)) {
-            return 1;
-        }
-        
-        // bundles in the default location are started at the defualt
-        // framework start level
-        if (PATH_BUNDLES.equals(name)) {
-            return 0;
-        }
-        
-        // otherwise the name is the start level
         try {
             int level = Integer.parseInt(name);
             if (level >= 0) {
                 return level;
             }
-            
+
             logger.log(Logger.LOG_ERROR, "Illegal Runlevel for " + path
                 + ", ignoring");
         } catch (NumberFormatException nfe) {
             logger.log(Logger.LOG_INFO, "Folder " + path
                 + " does not denote start level, ignoring");
         }
-        
+
         // no valid start level, ignore this location
-        return -1;
+        return STARTLEVEL_NONE;
+    }
+
+    // ---------- Bundle JAR file information
+
+    /**
+     * Returns the Manifrest 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) {
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Returns the <i>Bundle-SymbolicName</i> header from the given manifest or
+     * <code>null</code> if no such header exists.
+     * <p>
+     * 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) {
+        return manifest.getMainAttributes().getValue(
+            Constants.BUNDLE_SYMBOLICNAME);
+    }
+
+    /**
+     * 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
+     * @return <code>true</code> if the manifest does not describe a bundle 
with
+     *         a higher version number.
+     */
+    private boolean ignore(Bundle installedBundle, Manifest manifest) {
+
+        // the bundle is not installed yet, so we have to install it
+        if (installedBundle == null) {
+            return false;
+        }
+
+        String versionProp = manifest.getMainAttributes().getValue(
+            Constants.BUNDLE_VERSION);
+        Version newVersion = Version.parseVersion(versionProp);
+
+        String installedVersionProp = (String) 
installedBundle.getHeaders().get(
+            Constants.BUNDLE_VERSION);
+        Version installedVersion = Version.parseVersion(installedVersionProp);
+
+        return newVersion.compareTo(installedVersion) <= 0;
     }
-    
+
+    // ---------- Bundle Installation marker file
+
     private boolean isAlreadyInstalled(BundleContext context) {
         final File dataFile = context.getDataFile(DATA_FILE);
-        if ( dataFile != null && dataFile.exists() ) {
+        if (dataFile != null && dataFile.exists()) {
+
+            FileInputStream fis = null;
             try {
-                final FileInputStream fis = new FileInputStream(dataFile);
-                try {
-                    // only care for the first few bytes
-                    byte[] bytes = new byte[10];
+
+                long selfStamp = getSelfTimestamp();
+                if (selfStamp > 0) {
+
+                    fis = new FileInputStream(dataFile);
+                    byte[] bytes = new byte[20];
                     int len = fis.read(bytes);
-                    return Boolean.parseBoolean(new String(bytes, 0, len));
-                } finally {
+                    String value = new String(bytes, 0, len);
+
+                    long storedStamp = Long.parseLong(value);
+
+                    return storedStamp >= selfStamp;
+                }
+
+            } catch (NumberFormatException nfe) {
+                // probably still the old value, fallback to assume not
+                // installed
+
+            } catch (IOException ioe) {
+                logger.log(Logger.LOG_ERROR,
+                    "IOException during reading of installed flag.", ioe);
+
+            } finally {
+                if (fis != null) {
                     try {
                         fis.close();
-                    } catch (IOException ignore) {}
+                    } catch (IOException ignore) {
+                    }
                 }
-            } catch (IOException ioe) {
-                logger.log(Logger.LOG_ERROR, "IOException during reading of 
installed flag.", ioe);
             }
         }
-        
+
         // fallback assuming not installed yet
         return false;
     }
-    
+
     private void markInstalled(BundleContext context) {
         final File dataFile = context.getDataFile(DATA_FILE);
         try {
             final FileOutputStream fos = new FileOutputStream(dataFile);
             try {
-                fos.write("true".getBytes());
+                fos.write(String.valueOf(getSelfTimestamp()).getBytes());
             } finally {
                 try {
                     fos.close();
-                } catch (IOException ignore) {}
+                } catch (IOException ignore) {
+                }
             }
         } catch (IOException ioe) {
-            logger.log(Logger.LOG_ERROR, "IOException during writing of 
installed flag.", ioe);
+            logger.log(Logger.LOG_ERROR,
+                "IOException during writing of installed flag.", ioe);
         }
     }
+
+    /**
+     * Returns the time stamp of JAR file from which this class has been loaded
+     * or -1 if the timestamp cannot be resolved.
+     * <p>
+     * This method assumes that the ClassLoader of this class is an
+     * 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
+     *         expected.
+     * @throws IOException If an error occurrs reading accessing the last
+     *             modification time stampe.
+     */
+    private long getSelfTimestamp() throws IOException {
+
+        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();
+            }
+        }
+
+        return -1;
+    }
 }


Reply via email to