blob: 605b23c09395717b4fa6ba2fe423c1ba833721de [file] [log] [blame]
/*
* 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;
}
}