/*
* RHQ Management Platform
* Copyright (C) 2005-2008 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.core.pc.plugin;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.clientapi.agent.PluginContainerException;
import org.rhq.core.pluginapi.util.FileUtils;
/**
* Classloader for the plugin jar itself and any embedded lib/* jars.
*/
public class PluginClassLoader extends URLClassLoader {
private final Log log = LogFactory.getLog(this.getClass());
private File embeddedJarsDirectory;
private String stringValue;
protected PluginClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// this method is here simply to log success or failure so it is logging along side RootPluginClassLoader.
// Both these logs and RootPluginClassLoader logs helps determine where a class is being loaded from.
try {
Class<?> clazz = super.loadClass(name, resolve);
if (log.isTraceEnabled()) {
log.trace("Plugin class loaded: " + name);
}
return clazz;
} catch (ClassNotFoundException cnfe) {
if (log.isTraceEnabled()) {
log.trace("Plugin class not found: " + name);
}
throw cnfe;
}
}
public void destroy() {
try {
FileUtils.purge(embeddedJarsDirectory, true);
} catch (IOException e) {
log.warn("Failed to purge embedded jars directory. Cause: " + e);
}
// help GC
LogFactory.release(this);
}
/**
* Creates a classloader for the given named plugin whose plugin jar is found at the given URL.
*
* @param pluginJarName the logical name of the plugin
* @param pluginUrl the location where the plugin jar can be found
* @param unpackNestedJars if <code>true</code>, any lib/*.jar files found in the plugin jar
* are unpacked and put in the classloader
* @param parent the parent classloader for the new classloader being created
* @param tmpDirectory the directory where the unpacked nested jars are placed
*
* @return the new plugin classloader
*
* @throws PluginContainerException
*/
public static PluginClassLoader create(String pluginJarName, URL pluginUrl, boolean unpackNestedJars,
ClassLoader parent, File tmpDirectory) throws PluginContainerException {
return create(pluginJarName, new URL[] { pluginUrl }, unpackNestedJars, parent, tmpDirectory);
}
/**
* Creates a classloader for the given named plugin whose plugin jar is found at the URL found
* in the first index of the given URL array. The rest of the URLs in the array are to be added
* to the classloader as additional jars.
*
* @param pluginJarName the logical name of the plugin
* @param pluginUrls the first element is the location where the plugin jar can be found, the remaining
* are additional URLs to jars that will be added to the new classloader
* @param unpackNestedJars if <code>true</code>, any lib/*.jar files found in the plugin jar
* are unpacked and put in the classloader. The additional jars are NEVER unpacked.
* @param parent the parent classloader for the new classloader being created
* @param tmpDirectory the directory where the unpacked nested jars are placed
*
* @return the new plugin classloader
*
* @throws PluginContainerException
*/
public static PluginClassLoader create(String pluginJarName, URL[] pluginUrls, boolean unpackNestedJars,
ClassLoader parent, File tmpDirectory) throws PluginContainerException {
List<URL> classpathUrlList = new ArrayList<URL>();
File unpackedDirectory = null;
boolean processedPluginJar = false; // after the first URL is processed (which is the plugin jar) this will be true
for (URL pluginUrl : pluginUrls) {
classpathUrlList.add(pluginUrl);
// note that we only ever unpacked the plugin jar itself
if (!processedPluginJar && unpackNestedJars) {
try {
unpackedDirectory = unpackEmbeddedJars(pluginJarName, pluginUrl, classpathUrlList, tmpDirectory);
} catch (Exception e) {
throw new PluginContainerException("Failed to unpack embedded JARs within: " + pluginUrl, e);
}
}
processedPluginJar = true;
}
URL[] classpath = classpathUrlList.toArray(new URL[classpathUrlList.size()]);
PluginClassLoader newLoader = new PluginClassLoader(classpath, parent);
newLoader.embeddedJarsDirectory = unpackedDirectory;
return newLoader;
}
/**
* Unpacks all lib/* resources into a temporary directory, adds URLs to those newly extracted resources and returns
* the directory where the jars were extracted. This will actually create a unique subdirectory under the given
* <code>tmpDirectory</code>) which is where the extracted resources will be placed. If the give <code>
* tmpDirectory</code> is <code>null</code>, the standard platform's tmp directory will be used.
*
* @param pluginJarName name of the main plugin jar, used as part of the name to the tmp directory
* @param pluginUrl the URL to the main plugin jar we are unpacking
* @param urls the URLs to the tmp directory resources that were unpacked
* @param tmpDirectory the parent directory that will contain the child directory which will contain all extracted
* resources
*
* @return the location where all the extract files are now located
*
* @throws IOException If any IO goes wrong
*/
private static File unpackEmbeddedJars(String pluginJarName, URL pluginUrl, List<URL> urls, File tmpDirectory)
throws IOException {
InputStream pluginStream = pluginUrl.openStream();
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(pluginStream));
ZipEntry entry;
File extractionDirectory = null; // this is where we will actually store the files we extract
try {
while ((entry = zis.getNextEntry()) != null) {
String entryName = entry.getName();
// Only care about entries in the lib directory
if (entryName.startsWith("lib") && (entryName.length() > 4)) {
if (extractionDirectory == null) {
extractionDirectory = createTempDirectory(tmpDirectory, pluginJarName);
}
int i = entryName.lastIndexOf('/');
if (i < 0) {
i = entryName.lastIndexOf('\\');
}
String s = entryName.substring(i + 1);
File file = null;
try {
if (s.endsWith(".jar")) {
file = File.createTempFile(s, null, extractionDirectory);
urls.add(file.toURI().toURL());
} else {
// All non-jar files are extracted as-is with the
// same filename.
file = new File(extractionDirectory, s);
// since we have a regular file, we need to make sure the tmp dir is in classpath so it can be found
URL tmpUrl = extractionDirectory.toURI().toURL();
if (!urls.contains(tmpUrl)) {
urls.add(tmpUrl);
}
}
FileOutputStream fileOutputStream;
try {
fileOutputStream = new FileOutputStream(file);
} catch (FileNotFoundException ex) {
if (file.exists() && (file.length() > 0)) {
// e.g. on win32, agent running w/ dll loaded PluginDumper cannot overwrite file inuse.
continue;
}
throw ex;
}
try {
BufferedOutputStream outputStream = new BufferedOutputStream(fileOutputStream);
try {
file.deleteOnExit();
// do NOT close this inputStream since it is buffering the ZipInputStream
// and we are going to still process that input stream later. We close
// this ZipInputStream down below in the outer most try-finally block.
BufferedInputStream inputStream = new BufferedInputStream(zis);
int count;
byte[] b = new byte[8192];
while ((count = inputStream.read(b)) > -1) {
outputStream.write(b, 0, count);
}
} finally {
outputStream.close(); // this also closes the fileOutputStream
}
} finally {
fileOutputStream.close();
}
} catch (IOException ioe) {
if (file != null) {
file.delete();
}
throw ioe;
}
}
}
} finally {
try {
zis.close();
} catch (Exception ignored) {
}
}
return extractionDirectory;
}
private static File createTempDirectory(File tmpDirectory, String pluginName) throws IOException {
// Let's reuse the algorithm the JDK uses to determine a unique name:
// 1) create a temp file to get a unique name using JDK createTempFile
// 2) then quickly delete the file and...
// 3) convert it to a directory
File tmpDir = File.createTempFile(pluginName, ".classloader", tmpDirectory); // create file with unique name
boolean deleteOk = tmpDir.delete(); // delete the tmp file and...
boolean mkdirsOk = tmpDir.mkdirs(); // ...convert it to a directory
if (!deleteOk || !mkdirsOk) {
throw new IOException("Failed to create temp classloader directory named [" + tmpDir + "]");
}
tmpDir.deleteOnExit();
return tmpDir;
}
@Override
public String toString() {
if (this.stringValue == null) {
URL[] urls = getURLs();
String dir = "<>";
if (this.embeddedJarsDirectory != null) {
dir = this.embeddedJarsDirectory.toURI().toString();
}
StringBuilder stringBuilder = new StringBuilder(this.getClass().getSimpleName());
stringBuilder.append('@').append(Integer.toHexString(this.hashCode())).append("[");
stringBuilder.append("parent=").append(getParent()).append(",");
stringBuilder.append("embedded-dir=[").append(dir).append("],");
stringBuilder.append("urls=[");
if (urls != null) {
for (int i = 0; i < urls.length; i++) {
if (i != 0) {
stringBuilder.append(',');
}
if (urls[i].toString().startsWith(dir)) {
stringBuilder.append(new File(urls[i].getPath()).getName()); // convert to file just so we parse out only the filename
} else {
stringBuilder.append(urls[i]); // must be the plugin jar itself or an additional jar that is somewhere else
}
}
}
stringBuilder.append("]]");
this.stringValue = stringBuilder.toString();
}
return this.stringValue;
}
}