blob: 6ee65ae1170aeaba97f4631d409b75421e2f297b [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.jcr.classloader.internal;
import java.beans.Introspector;
import java.io.IOException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.security.SecureClassLoader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.ObservationManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>DynamicRepositoryClassLoader</code> class provides the
* functionality to load classes and resources from the JCR Repository.
* Additionally, this class supports the notion of getting 'dirty', which means,
* that if a resource loaded through this class loader has been modified in the
* Repository, this class loader marks itself dirty, which flag can get
* retrieved. This helps the user of this class loader to decide on whether to
* {@link #reinstantiate(Session, ClassLoader) reinstantiate} it or continue
* using this class loader.
* <p>
* When a user of the class loader recognizes an instance to be dirty, it can
* easily be reinstantiated with the {@link #reinstantiate} method. This
* reinstantiation will also rebuild the internal real class path from the same
* list of path patterns as was used to create the internal class path for the
* original class loader. The resulting internal class path need not be the
* same, though.
*/
public final class DynamicRepositoryClassLoader
extends SecureClassLoader implements EventListener {
/**
* The special resource representing a resource which could not be
* found in the class path.
*
* @see #cache
* @see #findClassLoaderResource(String)
*/
private static final ClassLoaderResource NOT_FOUND_RESOURCE =
new ClassLoaderResource(null, "[sentinel]", null) {
public boolean isExpired() {
return false;
}
};
/** default log category */
private final Logger log = LoggerFactory.getLogger(this.getClass().getName());
/**
* Cache of resources used to check class loader expiry. The map is indexed
* by the paths of the expiry properties of the cached resources. This map
* is not complete in terms of resources which have been loaded through this
* class loader. That is for resources loaded through an archive class path
* entry, only one of those resources (the last one loaded) is kept in this
* cache, while the others are ignored.
*
* @see #onEvent(EventIterator)
* @see #findClassLoaderResource(String)
*/
private final Map<String, ClassLoaderResource> modTimeCache = new HashMap<String, ClassLoaderResource>();
/**
* Flag indicating whether there are loaded classes which have later been
* expired (e.g. invalidated or modified)
*/
private boolean dirty = false;
/** The registered event listeners. */
private EventListener[] proxyListeners;
/**
* The classpath which this classloader searches for class definitions.
* Each element of the vector should be either a directory, a .zip
* file, or a .jar file.
* <p>
* It may be empty when only system classes are controlled.
*/
private ClassPathEntry[] repository;
/**
* The list of paths to use as a classpath.
*/
private String[] paths;
/**
* The <code>Session</code> grants access to the Repository to access the
* resources.
* <p>
* This field is not final such that it may be cleared when the class loader
* is destroyed.
*/
private Session session;
/**
* Cache of resources found or not found in the class path. The map is
* indexed by resource name and contains mappings to instances of the
* {@link ClassLoaderResource} class. If a resource has been tried to be
* loaded, which could not be found, the resource is cached with the
* special mapping to {@link #NOT_FOUND_RESOURCE}.
*
* @see #NOT_FOUND_RESOURCE
* @see #findClassLoaderResource(String)
*/
private final Map<String, ClassLoaderResource> cache = new HashMap<String, ClassLoaderResource>();
/**
* Flag indicating whether the {@link #destroy()} method has already been
* called (<code>true</code>) or not (<code>false</code>)
*/
private boolean destroyed = false;
/**
* Creates a <code>DynamicRepositoryClassLoader</code> from a list of item
* path strings containing globbing pattens for the paths defining the
* class path.
*
* @param session The <code>Session</code> to use to access the class items.
* @param classPath The list of path strings making up the (initial) class
* path of this class loader. The strings may contain globbing
* characters which will be resolved to build the actual class path.
* @param parent The parent <code>ClassLoader</code>, which may be
* <code>null</code>.
*
* @throws NullPointerException if either the session or the handles list
* is <code>null</code>.
*/
public DynamicRepositoryClassLoader(final Session session,
final String[] classPath,
final ClassLoader parent) {
// initialize the super class with an empty class path
super(parent);
// check session and handles
if (session == null) {
throw new NullPointerException("session");
}
if (classPath == null || classPath.length == 0) {
throw new NullPointerException("handles");
}
// set fields
this.session = session;
this.paths = classPath;
// build the class repositories list
buildRepository();
// register with observation service and path pattern list
registerListeners();
log.debug("DynamicRepositoryClassLoader: {} ready", this);
}
/**
* Creates a <code>DynamicRepositoryClassLoader</code> with the same
* configuration as the given <code>DynamicRepositoryClassLoader</code>.
* This constructor is used by the {@link #reinstantiate} method.
* <p>
* Before returning from this constructor the <code>old</code> class loader
* is destroyed and may not be used any more.
*
* @param session The session to associate with this class loader.
* @param old The <code>DynamicRepositoryClassLoader</code> to copy the
* cofiguration from.
* @param parent The parent <code>ClassLoader</code>, which may be
* <code>null</code>.
*/
private DynamicRepositoryClassLoader(final Session session,
final DynamicRepositoryClassLoader old,
final ClassLoader parent) {
// initialize the super class with an empty class path
super(parent);
// check session and handles
if (session == null) {
throw new NullPointerException("session");
}
// set fields
this.session = session;
this.paths = old.paths;
repository = old.repository;
buildRepository();
// register with observation service and path pattern list
registerListeners();
// finally finalize the old class loader
old.destroy();
log.debug(
"DynamicRepositoryClassLoader: Copied {}. Do not use that anymore",
old);
}
/**
* Destroys this class loader. This process encompasses all steps needed
* to remove as much references to this class loader as possible.
* <p>
* <em>NOTE</em>: This method just clears all internal fields and especially
* the class path to render this class loader unusable.
* <p>
* This implementation does not throw any exceptions.
*/
public void destroy() {
// we expect to be called only once, so we stop destroyal here
if (destroyed) {
log.debug("Instance is already destroyed");
return;
}
// remove ourselves as listeners from other places
unregisterListeners();
// set destroyal guard
destroyed = true;
// clear caches and references
repository = null;
paths = null;
session = null;
// clear the cache of loaded resources and flush cached class
// introspections of the JavaBean framework
final Iterator<ClassLoaderResource> ci = cache.values().iterator();
while ( ci.hasNext() ) {
final ClassLoaderResource res = ci.next();
if (res.getLoadedClass() != null) {
Introspector.flushFromCaches(res.getLoadedClass());
res.setLoadedClass(null);
}
}
cache.clear();
modTimeCache.clear();
}
//---------- URLClassLoader overwrites -------------------------------------
/**
* Finds and loads the class with the specified name from the class path.
*
* @param name the name of the class
* @return the resulting class
*
* @throws ClassNotFoundException If the named class could not be found or
* if this class loader has already been destroyed.
*/
protected Class<?> findClass(final String name) throws ClassNotFoundException {
if (destroyed) {
throw new ClassNotFoundException(name + " (Classloader destroyed)");
}
log.debug("findClass: Try to find class {}", name);
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
return findClassPrivileged(name);
}
});
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
}
/**
* Finds the resource with the specified name on the search path.
*
* @param name the name of the resource
*
* @return a <code>URL</code> for the resource, or <code>null</code>
* if the resource could not be found or if the class loader has
* already been destroyed.
*/
public URL findResource(String name) {
if (destroyed) {
log.warn("Destroyed class loader cannot find a resource");
return null;
}
log.debug("findResource: Try to find resource {}", name);
ClassLoaderResource res = findClassLoaderResource(name);
if (res != null) {
log.debug("findResource: Getting resource from {}, created {}",
res, new Date(res.getLastModificationTime()));
return res.getURL();
}
return null;
}
/**
* Returns an Enumeration of URLs representing all of the resources
* on the search path having the specified name.
*
* @param name the resource name
*
* @return an <code>Enumeration</code> of <code>URL</code>s. This is an
* empty enumeration if no resources are found by this class loader
* or if this class loader has already been destroyed.
*/
public Enumeration<URL> findResources(String name) {
if (destroyed) {
log.warn("Destroyed class loader cannot find resources");
return new Enumeration<URL>() {
public boolean hasMoreElements() {
return false;
}
public URL nextElement() {
throw new NoSuchElementException("No Entries");
}
};
}
log.debug("findResources: Try to find resources for {}", name);
List<URL> list = new LinkedList<URL>();
for (int i=0; i < repository.length; i++) {
final ClassPathEntry cp = repository[i];
log.debug("findResources: Trying {}", cp);
ClassLoaderResource res = cp.getResource(name);
if (res != null) {
log.debug("findResources: Adding resource from {}, created {}",
res, new Date(res.getLastModificationTime()));
URL url = res.getURL();
if (url != null) {
list.add(url);
}
}
}
// return the enumeration on the list
return Collections.enumeration(list);
}
//---------- Property access ----------------------------------------------
/**
* Removes all entries from the cache of loaded resources, which mark
* resources, which have not been found as of yet.
*
* @throws NullPointerException If this class loader has already been
* destroyed.
*/
private void cleanCache() {
final Iterator<ClassLoaderResource> ci = this.cache.values().iterator();
while (ci.hasNext()) {
if (ci.next() == NOT_FOUND_RESOURCE) {
ci.remove();
}
}
}
//---------- internal ------------------------------------------------------
/**
* Builds the repository list from the list of path patterns and appends
* the path entries from any added handles. This method may be used multiple
* times, each time replacing the currently defined repository list.
*
* @throws NullPointerException If this class loader has already been
* destroyed.
*/
private synchronized void buildRepository() {
List<ClassPathEntry> newRepository = new ArrayList<ClassPathEntry>(paths.length);
// build repository from path patterns
for (int i=0; i < paths.length; i++) {
final String entry = paths[i];
ClassPathEntry cp = null;
// try to find repository based on this path
if (repository != null) {
for (int j=0; j < repository.length; j++) {
final ClassPathEntry tmp = repository[i];
if (tmp.getPath().equals(entry)) {
cp = tmp;
break;
}
}
}
// not found, creating new one
if (cp == null) {
cp = ClassPathEntry.getInstance(session, entry);
}
if (cp != null) {
log.debug("Adding path {}", entry);
newRepository.add(cp);
} else {
log.debug("Cannot get a ClassPathEntry for {}", entry);
}
}
// replace old repository with new one
ClassPathEntry[] newClassPath = new ClassPathEntry[newRepository.size()];
newRepository.toArray(newClassPath);
repository = newClassPath;
// clear un-found resource cache
cleanCache();
}
/**
* Tries to find the class in the class path from within a
* <code>PrivilegedAction</code>. Throws <code>ClassNotFoundException</code>
* if no class can be found for the name.
*
* @param name the name of the class
*
* @return the resulting class
*
* @throws ClassNotFoundException if the class could not be found
* @throws NullPointerException If this class loader has already been
* destroyed.
*/
private Class<?> findClassPrivileged(String name) throws ClassNotFoundException {
// prepare the name of the class
final String path = name.replace('.', '/').concat(".class");
log.debug("findClassPrivileged: Try to find path {} for class {}",
path, name);
ClassLoaderResource res = findClassLoaderResource(path);
if (res != null) {
// try defining the class, error aborts
try {
log.debug(
"findClassPrivileged: Loading class from {}, created {}",
res, new Date(res.getLastModificationTime()));
Class<?> c = defineClass(name, res);
if (c == null) {
log.warn("defineClass returned null for class {}", name);
throw new ClassNotFoundException(name);
}
return c;
} catch (IOException ioe) {
log.debug("defineClass failed", ioe);
throw new ClassNotFoundException(name, ioe);
} catch (Throwable t) {
log.debug("defineClass failed", t);
throw new ClassNotFoundException(name, t);
}
}
throw new ClassNotFoundException(name);
}
/**
* Returns a {@link ClassLoaderResource} for the given <code>name</code> or
* <code>null</code> if not existing. If the resource has already been
* loaded earlier, the cached instance is returned. If the resource has
* not been found in an earlier call to this method, <code>null</code> is
* returned. Otherwise the resource is looked up in the class path. If
* found, the resource is cached and returned. If not found, the
* {@link #NOT_FOUND_RESOURCE} is cached for the name and <code>null</code>
* is returned.
*
* @param name The name of the resource to return.
*
* @return The named <code>ClassLoaderResource</code> if found or
* <code>null</code> if not found.
*
* @throws NullPointerException If this class loader has already been
* destroyed.
*/
private ClassLoaderResource findClassLoaderResource(String name) {
// check for cached resources first
ClassLoaderResource res = cache.get(name);
if (res == NOT_FOUND_RESOURCE) {
log.debug("Resource '{}' known to not exist in class path", name);
return null;
} else if (res == null) {
// walk the repository list and try to find the resource
for (int i = 0; i < repository.length; i++) {
final ClassPathEntry cp = repository[i];
log.debug("Checking {}", cp);
res = cp.getResource(name);
if (res != null) {
log.debug("Found resource in {}, created ", res, new Date(
res.getLastModificationTime()));
cache.put(name, res);
break;
}
}
if ( res == null ) {
log.debug("No classpath entry contains {}", name);
cache.put(name, NOT_FOUND_RESOURCE);
return null;
}
}
// if it could be found, we register it with the caches
// register the resource in the expiry map, if an appropriate
// property is available
Property prop = res.getExpiryProperty();
if (prop != null) {
try {
modTimeCache.put(prop.getPath(), res);
} catch (RepositoryException re) {
log.warn("Cannot register the resource " + res +
" for expiry", re);
}
}
// and finally return the resource
return res;
}
/**
* Defines a class getting the bytes for the class from the resource
*
* @param name The fully qualified class name
* @param res The resource to obtain the class bytes from
*
* @throws RepositoryException If a problem occurrs getting at the data.
* @throws IOException If a problem occurrs reading the class bytes from
* the resource.
* @throws ClassFormatError If the class bytes read from the resource are
* not a valid class.
*/
private Class<?> defineClass(String name, ClassLoaderResource res)
throws IOException, RepositoryException {
log.debug("defineClass({}, {})", name, res);
Class<?> clazz = res.getLoadedClass();
if (clazz == null) {
final byte[] data = res.getBytes();
clazz = defineClass(name, data, 0, data.length);
res.setLoadedClass(clazz);
}
return clazz;
}
//---------- reload support ------------------------------------------------
/**
* Returns whether the class loader is dirty. This can be the case if any
* of the loaded class has been expired through the observation.
* <p>
* This method may also return <code>true</code> if the <code>Session</code>
* associated with this class loader is not valid anymore.
* <p>
* Finally the method always returns <code>true</code> if the class loader
* has already been destroyed. Note, however, that a destroyed class loader
* cannot be reinstantiated. See {@link #reinstantiate(Session, ClassLoader)}.
* <p>
* If the class loader is dirty, it should be reinstantiated through the
* {@link #reinstantiate} method.
*
* @return <code>true</code> if the class loader is dirty and needs
* reinstantiation.
*/
public boolean isDirty() {
return destroyed || dirty || !session.isLive();
}
/**
* Reinstantiates this class loader. That is, a new ClassLoader with no
* loaded class is created with the same configuration as this class loader.
* <p>
* When the new class loader is returned, this class loader has been
* destroyed and may not be used any more.
*
* @param parent The parent <code>ClassLoader</code> for the reinstantiated
* <code>DynamicRepositoryClassLoader</code>, which may be
* <code>null</code>.
*
* @return a new instance with the same configuration as this class loader.
*
* @throws IllegalStateException if <code>this</code>
* {@link DynamicRepositoryClassLoader} has already been destroyed
* through the {@link #destroy()} method.
*/
public DynamicRepositoryClassLoader reinstantiate(Session session, ClassLoader parent) {
log.debug("reinstantiate: Copying {} with parent {}", this, parent);
if (destroyed) {
throw new IllegalStateException("Destroyed class loader cannot be recreated");
}
// create the new loader
DynamicRepositoryClassLoader newLoader =
new DynamicRepositoryClassLoader(session, this, parent);
// return the new loader
return newLoader;
}
//---------- EventListener interface -------------------------------
/**
* Handles a repository item modifcation events checking whether a class
* needs to be expired. As a side effect, this method sets the class loader
* dirty if a loaded class has been modified in the repository.
*
* @param events The iterator of repository events to be handled.
*/
public void onEvent(EventIterator events) {
while (events.hasNext()) {
Event event = events.nextEvent();
String path;
try {
path = event.getPath();
} catch (RepositoryException re) {
log.warn("onEvent: Cannot get path of event, ignoring", re);
continue;
}
log.debug(
"onEvent: Item {} has been modified, checking with cache", path);
ClassLoaderResource resource = modTimeCache.get(path);
if (resource != null) {
log.debug("pageModified: Expiring cache entry {}", resource);
expireResource(resource);
} else {
// might be in not-found cache - remove from there
if (event.getType() == Event.NODE_ADDED
|| event.getType() == Event.PROPERTY_ADDED) {
log.debug("pageModified: Clearing not-found cache for possible new class");
cleanCache();
}
}
}
}
//----------- Object overwrite ---------------------------------------------
/**
* Returns a string representation of this class loader.
*/
public String toString() {
StringBuilder buf = new StringBuilder(getClass().getName());
if (destroyed) {
buf.append(" - destroyed");
} else {
buf.append(": parent: { ");
buf.append(getParent());
buf.append(" }, user: ");
buf.append(session.getUserID());
buf.append(", dirty: ");
buf.append(isDirty());
}
return buf.toString();
}
//---------- internal ------------------------------------------------------
/**
* Registers this class loader with the observation service to get
* information on page updates in the class path and to the path
* pattern list to get class path updates.
*
* @throws NullPointerException if this class loader has already been
* destroyed.
*/
private final void registerListeners() {
log.debug("registerListeners: Registering to the observation service");
this.proxyListeners = new EventListener[this.paths.length];
for(int i=0; i < paths.length; i++ ) {
final String path = paths[i];
try {
final EventListener listener = new ProxyEventListener(this);
final ObservationManager om = session.getWorkspace().getObservationManager();
om.addEventListener(listener, 255, path, true, null, null, false);
proxyListeners[i] = listener;
} catch (RepositoryException re) {
log.error("registerModificationListener: Cannot register " +
this + " with observation manager", re);
}
}
}
/**
* Removes this instances registrations from the observation service and
* the path pattern list.
*
* @throws NullPointerException if this class loader has already been
* destroyed.
*/
private final void unregisterListeners() {
log.debug("unregisterListeners: Deregistering from the observation service");
if ( this.proxyListeners != null ) {
// check session first!
if ( session.isLive() ) {
for(final EventListener listener : this.proxyListeners) {
if ( listener != null ) {
try {
final ObservationManager om = session.getWorkspace().getObservationManager();
om.removeEventListener(listener);
} catch (RepositoryException re) {
log.error("unregisterListener: Cannot unregister " +
this + " from observation manager", re);
}
}
}
}
this.proxyListeners = null;
}
}
/**
* Checks whether the page backing the resource has been updated with a
* version, such that this new version would be used to access the resource.
* In this case the resource has expired and the class loader needs to be
* set dirty.
*
* @param resource The <code>ClassLoaderResource</code> to check for
* expiry.
*/
private boolean expireResource(ClassLoaderResource resource) {
// check whether the resource is expired (only if a class has been loaded)
boolean exp = resource.getLoadedClass() != null && resource.isExpired();
// update dirty flag accordingly
dirty |= exp;
log.debug("expireResource: Loader dirty: {}", isDirty());
// return the expiry status
return exp;
}
protected final static class ProxyEventListener implements EventListener {
private final EventListener delegatee;
public ProxyEventListener(final EventListener delegatee) {
this.delegatee = delegatee;
}
/**
* @see javax.jcr.observation.EventListener#onEvent(javax.jcr.observation.EventIterator)
*/
public void onEvent(EventIterator events) {
this.delegatee.onEvent(events);
}
}
}