Return-Path: Delivered-To: apmail-sling-commits-archive@www.apache.org Received: (qmail 65303 invoked from network); 3 Sep 2009 06:44:47 -0000 Received: from hermes.apache.org (HELO mail.apache.org) (140.211.11.3) by minotaur.apache.org with SMTP; 3 Sep 2009 06:44:47 -0000 Received: (qmail 98671 invoked by uid 500); 3 Sep 2009 06:44:47 -0000 Delivered-To: apmail-sling-commits-archive@sling.apache.org Received: (qmail 98615 invoked by uid 500); 3 Sep 2009 06:44:46 -0000 Mailing-List: contact commits-help@sling.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@sling.apache.org Delivered-To: mailing list commits@sling.apache.org Received: (qmail 98606 invoked by uid 99); 3 Sep 2009 06:44:46 -0000 Received: from nike.apache.org (HELO nike.apache.org) (192.87.106.230) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 03 Sep 2009 06:44:46 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=10.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Thu, 03 Sep 2009 06:44:42 +0000 Received: by eris.apache.org (Postfix, from userid 65534) id 20FD223888E7; Thu, 3 Sep 2009 06:44:21 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: 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/ Date: Thu, 03 Sep 2009 06:44:20 -0000 To: commits@sling.apache.org From: fmeschbe@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20090903064421.20FD223888E7@eris.apache.org> X-Virus-Checked: Checked by ClamAV on apache.org 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 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 bySymbolicName = new HashMap(); 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 installed = new LinkedList(); - Iterator 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 parent - * path. - * - * @param context The BundleContext 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 .jar 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 currentBundles, String parent, - List 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 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 BundleContext 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 .jar 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 currentBundles, + List 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 BundleContext 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 currentBundles, + List 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 null 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