blob: 533c2ccd046c80e40bffe33bebb768ec8c419803 [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.myfaces.extensions.scripting.sandbox.loader;
import org.apache.myfaces.extensions.scripting.core.util.FileUtils;
import org.apache.myfaces.extensions.scripting.loaders.java.ThrowawayClassloader;
import org.apache.myfaces.extensions.scripting.sandbox.loader.support.ClassFileLoader;
import org.apache.myfaces.extensions.scripting.sandbox.loader.support.OverridingClassLoader;
import org.apache.myfaces.extensions.scripting.sandbox.loader.support.ThrowAwayClassLoader;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* <p>A class loader implementation that enables you to reload certain classes. It automatically
* reloads classes if there's a newer version of a .class file available in a specified compilation
* target path. However, it's also possible to explicitly reload other classes.</p>
* <p/>
* <p>This enables you to do both modify and reload various classes that you've used for Spring
* bean definitions, but it also enables you to reload for example classes depending on those
* dynamically compiled classes, like factory bean classes. By explicitly reloading a factory
* bean class the newly loaded factory bean will return updated bean instances as well!</p>
* <p/>
* <p>Note that even though this class extends the class URLClassLoader it doesn't use any
* of its functionalities. This class loader just works similar and provides a similar interface
* so it's useful to extend the class URLClassLoader as you can treat it like one (especially
* when it comes to resolving the classpath of a class loader).</p>
*/
public class ReloadingClassLoader extends URLClassLoader {
/**
* The system-dependent default name-separator character. Note that it's safe to
* use this version of the file separator in regex methods, like replaceAll().
*/
private static String FILE_SEPARATOR = FileUtils.getFileSeparatorForRegex();
/**
* The logger instance for this class.
*/
private static final Logger _logger = Logger.getLogger(ReloadingClassLoader.class.getName());
/**
* A table of class names and the according class loaders. It's basically like
* a list of classes that this class loader has already loaded. However, the
* thing is that this class loader isn't actually going to load them as we
* would loose the possibility to override them then, which is the reason why
* each class has got its own class loader.
*/
private Map<String, ThrowAwayClassLoader> _classLoaders =
new HashMap<String, ThrowAwayClassLoader>();
/**
* The target directory for the compiler, i.e. the directory that contains the
* dynamically compiled .class files.
*/
private File _compilationDirectory;
// ------------------------------------------ Constructors
/**
* <p>Constructs a new reloading classloader for the specified compilation
* directory using the default delegation parent classloader. Note that this
* classloader will only delegate to the parent classloader if there's no
* dynamically compiled class available.</p>
*
* @param compilationDirectory the compilation directory
*/
public ReloadingClassLoader(File compilationDirectory) {
super(new URL[0]);
this._compilationDirectory = compilationDirectory;
}
/**
* <p>Constructs a new reloading classloader for the specified compilation
* directory using the given delegation parent classloader. Note that this
* classloader will only delegate to the parent classloader if there's no
* dynamically compiled class available.</p>
*
* @param parentClassLoader the parent classloader
* @param compilationDirectory the compilation directory
*/
public ReloadingClassLoader(ClassLoader parentClassLoader, File compilationDirectory) {
super(new URL[0], parentClassLoader);
this._compilationDirectory = compilationDirectory;
}
// ------------------------------------------ URLClassLoader methods
/**
* <p>Loads the class with the specified binary name. This method searches for classes in the
* compilation directory that you've specified previously. Note that this class loader recognizes
* whether the class files have changed, that means, if you recompile and reload a class, you'll
* get a Class object that represents the recompiled class.</p>
*
* @param className the binary name of the class you want to load
* @param resolve <tt>true</tt>, if the class is to be resolved
* @return The resulting <tt>Class</tt> object
* @throws ClassNotFoundException if the class could not be found
*/
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
// First of all, check if there's a class file available in the compilation target path.
// It doesn't matter which class we're dealing with at the moment as there's always the
// possibility that the user is either trying to override a statically compiled class
// (i.e. a class that has been compiled before deploying the application) or he/she is
// trying to modify a dynamically compiled class, in which case we should compare
// timestamps, etc.
File classFile = resolveClassFile(className);
if (classFile != null && classFile.exists()) {
if (_classLoaders.containsKey(className)) {
// Check if the class loader is already outdated, i.e. there is a newer class file available
// for the class we want to load than the class file we've already loaded. If that's the case
// we're going to throw away this ClassLoader and create a new one for linkage reasons.
ThrowAwayClassLoader classLoader = _classLoaders.get(className);
if (classLoader.isOutdated(classFile.lastModified())) {
// If the class loader is outdated, create a new one. Otherwise the same class loader
// would have to load the same class twice or more often which would cause severe
// linkage errors. Actually the JVM wouldn't permit that anyway and throw some
// linkage errors / exceptions.
reloadClass(className);
}
} else {
if (_logger.isLoggable(Level.FINEST)) {
_logger.log(Level.FINEST, "A new dynamic class '"
+ className + "' has been found by this class loader '" + this + "'.");
}
// We haven't loaded this class so far, but there is a .class file available,
// so we have to reload the given class.
reloadClass(className);
}
ThrowAwayClassLoader classLoader = _classLoaders.get(className);
return classLoader.loadClass(className, resolve);
} else {
// Even though there is no class file available, there's still a chance that this
// class loader has forcefully reloaded a statically compiled class.
if (_classLoaders.containsKey(className)) {
ThrowAwayClassLoader classLoader = _classLoaders.get(className);
return classLoader.loadClass(className, resolve);
} else {
// However, if there's neither a .class file nor a reloadable class loader
// available, just delegate to the parent class loader.
return super.loadClass(className, resolve);
}
}
}
/**
* <p>Returns the search path of URLs for loading classes, i.e. the
* given compilation target directory, the directory that contains the
* dynamically compiled .class files.</p>
*
* @return the search path of URLs for loading classes
*/
public URL[] getURLs() {
try {
return new URL[]{_compilationDirectory.toURI().toURL()};
} catch (IOException ex) {
_logger.log(Level.SEVERE, "Couldn't resolve the URL to the compilation directory {0} {1} {2} ", new String[]{_compilationDirectory.getAbsolutePath(), ex.getMessage(), ex.toString()});
return new URL[0];
}
}
// ------------------------------------------ Public methods
/**
* <p>Determines whether the given class has been loaded by a class
* loader that is already outdated.</p>
*
* @param classObj the class you want to check
* @return <code>true</code, if there is a newer class file available for the given object
*/
public boolean isOutdated(Class classObj) {
// Is there even a dynamically compiled class file available for the given class?
File classFile = resolveClassFile(classObj.getName());
if (classFile.exists()) {
// If so, check if we the Class reference has been loaded by a ThrowAwayClassLoader.
// Otherwise it's definitely outdated and we don't have to compare timestamps.
if (classObj.getClassLoader() instanceof ThrowAwayClassLoader) {
// Compare the timestamps in order to determine whether the given Class
// reference is already outdated.
ThrowAwayClassLoader classLoader = (ThrowAwayClassLoader) classObj.getClassLoader();
return classLoader.isOutdated(classFile.lastModified());
} else {
return true;
}
}
return false;
}
/**
* <p>Reloads the given class internally explicitly. Note that this classloader usually
* reloads classes automatically, i.e. this classloader detects if there is a newer
* version of a class file available in the compilation directory. However, by using
* this method you tell this classloader to forcefully reload the given class. For
* example, if you've got a newer version of a dynamically recompiled class and a
* statically compiled class depending on this one, you can tell this classloader to
* reload the statically compiled class as well so that it references the correct
* version of the Class object.</p>
*
* @param className the class you want to reload
*/
public void reloadClass(final String className) {
ThrowAwayClassLoader classLoader;
final File classFile = resolveClassFile(className);
final ReloadingClassLoader _this = this;
try {
if (classFile != null && classFile.exists()) {
classLoader = AccessController.doPrivileged(new PrivilegedExceptionAction<ClassFileLoader>() {
public ClassFileLoader run() {
return new ClassFileLoader(className, classFile, _this);
}
});
} else {
classLoader = AccessController.doPrivileged(new PrivilegedExceptionAction<OverridingClassLoader>() {
public OverridingClassLoader run() {
return new OverridingClassLoader(className, _this);
}
});
}
} catch (PrivilegedActionException e) {
_logger.log(Level.SEVERE, "", e);
return;
}
ThrowAwayClassLoader oldClassLoader = _classLoaders.put(className, classLoader);
if (_logger.isLoggable(Level.INFO)) {
if (oldClassLoader != null) {
_logger.info("Replaced the class loader '" + oldClassLoader + "' with the class loader '"
+ classLoader + "' as this class loader is supposed to reload the class '" + className + "'.");
} else {
_logger.info("Installed a new class loader '" + classLoader + "' for the class '"
+ className + "' as this class loader is supposed to reload it.");
}
}
}
/**
* <p>Returns a copy of the current reloading class loader with the only difference
* being the parent class loader to use. Use this method if you just want to replace
* the parent class loader (obviously you can't do that after a ClassLoader has been
* created, hence a copy is created).</p>
*
* @param parentClassLoader the parent ClassLoader to use
* @return a copy of the current reloading class loader
*/
@SuppressWarnings("unused")
public ReloadingClassLoader cloneWithParentClassLoader(final ClassLoader parentClassLoader) {
ReloadingClassLoader classLoader = null;
try {
classLoader = AccessController.doPrivileged(new PrivilegedExceptionAction<ReloadingClassLoader>() {
public ReloadingClassLoader run() {
return new ReloadingClassLoader(parentClassLoader, _compilationDirectory);
}
});
} catch (PrivilegedActionException e) {
_logger.log(Level.SEVERE, "", e);
return null;
}
// Note that we don't have to create "deep copies" as the class loaders in the map
// are immutable anyway (they are only supposed to load a single class) and additionally
// this map doesn't contain any classes that have been loaded using the current parent
// class loader!
classLoader._classLoaders = new HashMap<String, ThrowAwayClassLoader>(_classLoaders);
return classLoader;
}
// ------------------------------------------ Utility methods
/**
* <p>Resolves and returns a File handle that represents the class file of
* the given class on the file system. However, note that this method only
* returns <code>null</code> if an error occured while resolving the class
* file. A non-null valuee doesn't necessarily mean that the class file
* actually exists. In oder to check the existence call the according
* method on the returned object.</p>
*
* @param className the name of the class that you want to resolve
* @return a File handle that represents the class file of the given class
* on the file system
* @see java.io.File#exists()
*/
protected File resolveClassFile(String className) {
// This method just has to look in the specified compilation directory. The
// relative class file path can be computed from the class name.
return new File(_compilationDirectory,
className.replaceAll("\\.", FILE_SEPARATOR).concat(".class"));
}
}