package de.skuzzle.polly.core.util;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLStreamHandler;
import java.security.SecureClassLoader;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import org.apache.log4j.Logger;
import de.skuzzle.polly.tools.io.FastByteArrayOutputStream;
/**
* This classloader implementation is adapted from the IRC Bot implementation JBot
* by Hani Suleiman (hani@formicary.net).
*
* Original sources can be found at
* <a href="http://java.net/projects/jbot/>JBot</a>
*
* @author Simon
* @version 20.02.2012
*/
public class PluginClassLoader extends SecureClassLoader implements Cloneable {
private class BytesURLStreamHandler extends URLStreamHandler {
private final byte[] content;
public BytesURLStreamHandler(byte[] content) {
this.content = content;
}
public URLConnection openConnection(URL url) {
return new BytesURLConnection(url, content);
}
}
private class BytesURLConnection extends URLConnection {
final protected byte[] content;
public BytesURLConnection(URL url, byte[] content) {
super(url);
this.content = content;
}
public void connect() {}
public InputStream getInputStream() {
return new ByteArrayInputStream(this.content);
}
}
private final static Logger logger = Logger
.getLogger(PluginClassLoader.class.getName());
/**
* Static object to synchronize on while loading classes and resources. This grants
* that only one plugin loader at a time may load a class.
*/
private final static Object MUTEX = new Object();
private final JarFile jar;
private final File file;
private long jarLastModified;
private final Map<String, byte[]> dependencyCache;
private final Map<String, Class<?>> classCache;
public PluginClassLoader(File file) throws IOException {
this(file, ClassLoader.getSystemClassLoader());
}
public PluginClassLoader(File file, ClassLoader parent) throws IOException {
super(parent);
if (!registerAsParallelCapable()) {
logger.error("Failed to register ClassLoader as parallel capable");
}
this.file = file;
this.dependencyCache = new WeakHashMap<String, byte[]>();
this.classCache = new HashMap<String, Class<?>>();
this.jar = new JarFile(this.file);
this.jarLastModified = file.lastModified();
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
if (this.jar == null) {
throw new ClassNotFoundException(name);
}
final String path = name.replace('.', '/').concat(".class");
synchronized (MUTEX) {
Class<?> cached = this.classCache.get(path);
if (cached != null) {
return cached;
}
final byte[] data = this.getFile(this.jar, path);
if (data == null) {
throw new ClassNotFoundException();
}
cached = this.defineClass(name, data, 0, data.length);
this.classCache.put(path, cached);
return cached;
}
}
@Override
protected URL findResource(String name) {
if (this.jar == null) {
return null;
}
final byte[] data = this.getFile(this.jar, name);
if (data == null) {
return null;
}
try {
return this.getDataURL(name, data);
} catch (MalformedURLException e) {
return null;
}
}
protected URL getDataURL(String name, byte[] data)
throws MalformedURLException {
return new URL(null, this.file.toURI().toURL().toExternalForm() + '!'
+ name, new BytesURLStreamHandler(data));
}
@Override
protected Enumeration<URL> findResources(String name) {
final URL url = this.findResource(name);
if (url == null)
return null;
return Collections.enumeration(Collections.singleton(url));
}
public boolean isStale() {
return new File(this.jar.getName()).lastModified() > this.jarLastModified;
}
private String[] readManifestClasspath(JarFile jar) throws IOException {
return getClasspath(jar.getManifest());
}
/*private String[] readMainfestClasspath(String jarName, JarInputStream in) {
String[] cp = this.cpCache.get(jarName);
if (cp != null) {
return cp;
}
cp = getClasspath(in.getManifest());
this.cpCache.put(jarName, cp);
return cp;
}*/
private String[] getClasspath(Manifest mf) {
if (mf == null) {
return null;
}
final Attributes attribs = mf.getMainAttributes();
final String classpath = attribs.getValue(Attributes.Name.CLASS_PATH);
if (classpath == null) {
return null;
}
return classpath.split(" ");
}
private byte[] readSimpleEntry(JarFile jar, ZipEntry entry) throws IOException {
final InputStream in = jar.getInputStream(entry);
int size = (int) entry.getSize();
return readStream(in, size);
}
private byte[] getFile(JarFile jar, String className) {
byte[] file = null;
try {
// try finding class in current jar
ZipEntry entry = jar.getEntry(className);
if (entry != null) {
file = this.readSimpleEntry(jar, entry);
return file;
}
// try finding class in dependency cache
synchronized (this.dependencyCache) {
file = this.dependencyCache.get(className);
}
if (file != null) {
return file;
}
final String[] classpath = this.readManifestClasspath(jar);
// now, try to find the path in our dependencies
for (int i = 0; classpath != null && i < classpath.length && file == null; ++i) {
String cpEntry = classpath[i];
entry = jar.getEntry(cpEntry);
if (entry != null) {
// dependency found in our jar
if (cpEntry.endsWith(".jar")) {
// dependencies in contained jars are not resolved recursively!
// now try to find requested class in the dependency.
// We read the whole referenced dependency into our cache so
// there is no need to open it again
try (final JarInputStream in = new JarInputStream(
jar.getInputStream(entry))) {
ZipEntry check = in.getNextEntry();
while (check != null) {
if (!check.isDirectory()) {
// cache the file so we do not need to read this jar
// file again
byte[] tmp = readStream(in);
synchronized (this.dependencyCache) {
this.dependencyCache.put(check.getName(), tmp);
}
if (check.getName().equals(className)) {
file = tmp;
}
}
check = in.getNextEntry();
}
}
} else if (cpEntry.endsWith(".class")) {
file = this.readSimpleEntry(jar, entry);
}
} else {
// try to find dependeny in directory
File cpEntryFile = new File(cpEntry);
if (!cpEntryFile.exists()) {
continue;
} else if (cpEntry.endsWith(".class")) {
// classpath references a classfile, so we can read it directly
file = readStream(new FileInputStream(cpEntryFile));
continue;
} else if (!cpEntry.endsWith("jar")) {
// we cannot process dependencies that are no jar files
continue;
}
// file exists, so try finding the requested class in the referenced
// jar file recursively
file = this.getFile(new JarFile(cpEntryFile), className);
}
}
} catch (IOException ignore) {
logger.error("", ignore);
/* returning null */
}
return file;
}
private static byte[] readStream(InputStream in) throws IOException {
final int BUFFER_SIZE = 4048;
byte[] buffer = new byte[BUFFER_SIZE];
try (final FastByteArrayOutputStream out = new FastByteArrayOutputStream(BUFFER_SIZE)) {
int len = 0;
do {
len = in.read(buffer);
out.write(buffer, 0, len);
} while (len > 0);
// shrink buffer to its actual size
out.shrink();
return out.getBuffer();
}
}
private static byte[] readStream(InputStream in, int size) throws IOException {
if (in == null) {
return null;
} else if (size == 0) {
return new byte[0];
}
int currentTotal = 0;
int bytesRead = 0;
byte[] data = new byte[size];
while (currentTotal < data.length
&& (bytesRead = in.read(data, currentTotal, data.length
- currentTotal)) >= 0)
currentTotal += bytesRead;
in.close();
return data;
}
public void dispose() {
this.classCache.clear();
this.dependencyCache.clear();
}
@Override
public String toString() {
return "PluginClassLoader:" + this.file;
}
}