/* * 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.shared; import java.beans.Introspector; import java.io.File; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.jar.JarFile; import org.apache.sling.commons.osgi.bundleversion.BundleVersionInfo; import org.apache.sling.commons.osgi.bundleversion.FileBundleVersionInfo; /** * The <code>Loader</code> class provides utility methods for the actual * launchers to help launching the framework. */ public class Loader { /** * The launchpad home folder set by the constructor */ private final File launchpadHome; private final File extLibHome; /** * Default External Library Home */ private static final String EXTENSION_LIB_PATH="ext"; /** * Creates a loader instance to load from the given launchpad home folder. * Besides ensuring the existence of the launchpad home folder, the * constructor also removes all but the most recent launcher JAR files from * the Sling home folder (thus cleaning up from previous upgrades). * * @param launchpadHome The launchpad home folder. This must not be * <code>null</code> or an empty string. * @throws IllegalArgumentException If the <code>launchpadHome</code> * argument is <code>null</code> or an empty string or if the * launchpad home folder exists but is not a directory or if the * Sling home folder cannot be created. */ public Loader(final File launchpadHome) { if (launchpadHome == null) { throw new IllegalArgumentException( "Launchpad Home folder must not be null or empty"); } this.launchpadHome = getLaunchpadHomeFile(launchpadHome); extLibHome = getExtensionLibHome(); removeOldLauncherJars(); } /** * Creates an URLClassLoader from a _launcher JAR_ file in the given * launchpadHome directory and loads and returns the launcher class * identified by the launcherClassName. * * @param launcherClassName The fully qualified name of a class implementing * the Launcher interface. This class must have a public * constructor taking no arguments. * @return the Launcher instance loaded from the newly created classloader * @throws NullPointerException if launcherClassName is null * @throws IllegalArgumentException if the launcherClassName cannot be * instantiated. The cause of the failure is contained as the * cause of the exception. */ @SuppressWarnings("resource") public Object loadLauncher(String launcherClassName) { final File launcherJarFile = getLauncherJarFile(); info("Loading launcher class " + launcherClassName + " from " + launcherJarFile.getName()); if (!launcherJarFile.canRead()) { throw new IllegalArgumentException("Sling Launcher JAR " + launcherJarFile + " is not accessible"); } final ClassLoader loader; try { loader = new LauncherClassLoader(launcherJarFile, getExtLibs()); } catch (MalformedURLException e) { throw new IllegalArgumentException( "Cannot create an URL from the JAR path name", e); } try { final Class<?> launcherClass = loader.loadClass(launcherClassName); return launcherClass.newInstance(); } catch (ClassNotFoundException cnfe) { throw new IllegalArgumentException("Cannot find class " + launcherClassName + " in " + launcherJarFile, cnfe); } catch (InstantiationException e) { throw new IllegalArgumentException( "Cannot instantiate launcher class " + launcherClassName, e); } catch (IllegalAccessException e) { throw new IllegalArgumentException( "Cannot access constructor of class " + launcherClassName, e); } } /** * Tries to remove as many traces of class loaded by the framework from the * Java VM as possible. Most notably the following traces are removed: * <ul> * <li>JavaBeans property caches * <li>Close the Launcher Jar File (if opened by the platform) * </ul> * <p> * This method must be called when the notifier is called. */ public void cleanupVM() { // ensure the JavaBeans introspector lets go of any classes it // may haved cached after introspection Introspector.flushCaches(); // if sling home is set, check whether we have to close the // launcher JAR JarFile, which might be cached in the platform closeLauncherJarFile(getLauncherJarFile()); } /** * Copies the contents of the launcher JAR as indicated by the URL to the * sling home directory. If the existing file is is a more recent bundle * version than the supplied launcher JAR file, it is is not replaced. * * @return <code>true</code> if the launcher JAR file has been installed or * updated, <code>false</code> otherwise. * @throws IOException If an error occurrs transferring the contents */ public boolean installLauncherJar(URL launcherJar) throws IOException { info("Checking launcher JAR in folder " + launchpadHome); final File currentLauncherJarFile = getLauncherJarFile(); // Copy the new launcher jar to a temporary file, and // extract bundle version info final URLConnection launcherJarConn = launcherJar.openConnection(); launcherJarConn.setUseCaches(false); final File tmp = new File(launchpadHome, "Loader_tmp_" + System.currentTimeMillis() + SharedConstants.LAUNCHER_JAR_REL_PATH); spool(launcherJarConn.getInputStream(), tmp); final FileBundleVersionInfo newVi = new FileBundleVersionInfo(tmp); boolean installNewLauncher = true; try { if(!newVi.isBundle()) { throw new IOException("New launcher jar is not a bundle, cannot get version info:" + launcherJar); } // Compare versions to decide whether to use the existing or new launcher jar if (currentLauncherJarFile.exists()) { final FileBundleVersionInfo currentVi = new FileBundleVersionInfo(currentLauncherJarFile); if(!currentVi.isBundle()) { throw new IOException("Existing launcher jar is not a bundle, cannot get version info:" + currentLauncherJarFile.getAbsolutePath()); } String info = null; if(currentVi.compareTo(newVi) == 0) { info = "up to date"; installNewLauncher = false; } else if(currentVi.compareTo(newVi) > 0) { info = "more recent than ours"; installNewLauncher = false; } if(info != null) { info("Existing launcher is " + info + ", using it: " + getBundleInfo(currentVi) + " (" + currentLauncherJarFile.getName() + ")"); } } if(installNewLauncher) { final File f = new File(tmp.getParentFile(), SharedConstants.LAUNCHER_JAR_REL_PATH + "." + System.currentTimeMillis()); if(!tmp.renameTo(f)) { throw new IOException("Failed to rename " + tmp.getName() + " to " + f.getName()); } info("Installing new launcher: " + launcherJar + ", " + getBundleInfo(newVi) + " (" + f.getName() + ")"); } } finally { if(tmp.exists()) { tmp.delete(); } } return installNewLauncher; } /** Return relevant bundle version info for logging */ static String getBundleInfo(BundleVersionInfo<?> v) { final StringBuilder sb = new StringBuilder(); sb.append(v.getVersion()); if(v.isSnapshot()) { sb.append(", Last-Modified:"); sb.append(new Date(v.getBundleLastModified())); } return sb.toString(); } /** * Removes old candidate launcher JAR files leaving the most recent one as * the launcher JAR file to use on next Sling startup. */ private void removeOldLauncherJars() { final File[] launcherJars = getLauncherJarFiles(); if (launcherJars != null && launcherJars.length > 0) { // Remove all files except current one final File current = getLauncherJarFile(); for(File f : launcherJars) { if(f.getAbsolutePath().equals(current.getAbsolutePath())) { continue; } String versionInfo = null; try { FileBundleVersionInfo vi = new FileBundleVersionInfo(f); versionInfo = getBundleInfo(vi); } catch(IOException ignored) { } info("Deleting obsolete launcher jar: " + f.getName() + ", " + versionInfo); f.delete(); } // And ensure the current file has the standard launcher name if (!SharedConstants.LAUNCHER_JAR_REL_PATH.equals(current.getName())) { info("Renaming current launcher jar " + current.getName() + " to " + SharedConstants.LAUNCHER_JAR_REL_PATH); File launcherFileName = new File( current.getParentFile(), SharedConstants.LAUNCHER_JAR_REL_PATH); current.renameTo(launcherFileName); } } } /** * Spools the contents of the input stream to the given file replacing the * contents of the file with the contents of the input stream. When this * method returns, the input stream is guaranteed to be closed. * * @throws IOException If an error occurrs reading or writing the input * stream contents. */ public static void spool(InputStream ins, File destFile) throws IOException { OutputStream out = null; try { out = new FileOutputStream(destFile); byte[] buf = new byte[8192]; int rd; while ((rd = ins.read(buf)) >= 0) { out.write(buf, 0, rd); } } finally { if (ins != null) { try { ins.close(); } catch (IOException ignore) { } } if (out != null) { try { out.close(); } catch (IOException ignore) { } } } } // ---------- internal helper /** * Returns a <code>File</code> object representing the Launcher JAR file * found in the sling home folder. */ private File getLauncherJarFile() { File result = null; final File[] launcherJars = getLauncherJarFiles(); if (launcherJars == null || launcherJars.length == 0) { // return a non-existing file naming the desired primary name result = new File(launchpadHome, SharedConstants.LAUNCHER_JAR_REL_PATH); } else { // last file is the most recent one, use it result = launcherJars[launcherJars.length - 1]; } return result; } /** * Returns all files in the <code>launchpadHome</code> directory which may * be considered as launcher JAR files, sorted based on their bundle version * information, most recent last. These files all start with the * {@link SharedConstants#LAUNCHER_JAR_REL_PATH}. This list may be empty if * the launcher JAR file has not been installed yet. * * @return The list of candidate launcher JAR files, which may be empty. * <code>null</code> is returned if an IO error occurs trying to * list the files. */ private File[] getLauncherJarFiles() { // Get list of files with names starting with our prefix final File[] rawList = launchpadHome.listFiles(new FileFilter() { @Override public boolean accept(File pathname) { return pathname.isFile() && pathname.getName().startsWith( SharedConstants.LAUNCHER_JAR_REL_PATH); } }); // Keep only those which have valid Bundle headers, and // sort them according to the bundle version numbers final List<FileBundleVersionInfo> list = new ArrayList<FileBundleVersionInfo>(); for(File f : rawList) { FileBundleVersionInfo fvi = null; try { fvi = new FileBundleVersionInfo(f); } catch(IOException ioe) { // Cannot read bundle info from jar file - should never happen?? throw new IllegalStateException("Cannot read bundle information from loader file " + f.getAbsolutePath()); } if(fvi.isBundle()) { list.add(fvi); } } Collections.sort(list); final File [] result = new File[list.size()]; int i = 0; for(FileBundleVersionInfo fvi : list) { result[i++] = fvi.getSource(); } return result; } /** * Returns the <code>launchpadHome</code> path as a directory. If the * directory does not exist it is created. If creation fails or if * <code>launchpadHome</code> exists but is not a directory a * <code>IllegalArgumentException</code> is thrown. * * @param launchpadHome The sling home directory where the launcher JAR * files are stored * @return The Sling home directory * @throws IllegalArgumentException if <code>launchpadHome</code> exists and * is not a directory or cannot be created as a directory. */ private static File getLaunchpadHomeFile(File launchpadHome) { if (launchpadHome.exists()) { if (!launchpadHome.isDirectory()) { throw new IllegalArgumentException("Sling Home " + launchpadHome + " exists but is not a directory"); } } else if (!launchpadHome.mkdirs()) { throw new IllegalArgumentException("Sling Home " + launchpadHome + " cannot be created as a directory"); } return launchpadHome; } private static void closeLauncherJarFile(final File launcherJar) { try { final URI launcherJarUri = launcherJar.toURI(); final URL launcherJarRoot = new URL("jar:" + launcherJarUri + "!/"); final URLConnection conn = launcherJarRoot.openConnection(); if (conn instanceof JarURLConnection) { final JarFile jarFile = ((JarURLConnection) conn).getJarFile(); jarFile.close(); } } catch (Exception e) { // better logging here } } /** Meant to be overridden to display or log info */ protected void info(String msg) { } private File getExtensionLibHome(){ //check if sling home is initialized if(launchpadHome == null || !launchpadHome.exists()){ throw new IllegalArgumentException("Sling Home has not been initialized" ); } //assumes launchpadHome is initialized File extLibFile=new File(launchpadHome, EXTENSION_LIB_PATH); if (extLibFile.exists()) { if (!extLibFile.isDirectory()) { throw new IllegalArgumentException("Sling Extension Lib Home " + extLibFile + " exists but is not a directory"); } } info("Sling Extension Lib Home : " + extLibFile); return extLibFile; } private File[] getExtLibs(){ if (extLibHome == null || !extLibHome.exists()) { info("External Libs Home (ext) is null or does not exists."); return new File[]{}; } File[] libs = extLibHome.listFiles(new FilenameFilter() { @Override public boolean accept(File dir, String name) { return (name.endsWith(".jar")); } }); if (libs == null) { libs = new File[]{}; } StringBuilder logStringBldr = new StringBuilder("Sling Extension jars found = [ "); for(File lib:libs){ logStringBldr.append(lib); logStringBldr.append(","); } logStringBldr.append(" ] "); info(logStringBldr.toString()); return libs; } }