blob: d69aed52a018647ca8975a8b79794bf9b2ea647c [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.io.IOException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.security.SecureClassLoader;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import org.apache.sling.commons.classloader.DynamicClassLoader;
import org.apache.sling.jcr.classloader.internal.net.JCRURLHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>RepositoryClassLoader</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.
*/
public final class RepositoryClassLoader
extends SecureClassLoader
implements DynamicClassLoader {
/** Logger */
private final Logger logger = LoggerFactory.getLogger(this.getClass().getName());
/** Set of loaded resources and classes. */
private final Set<String> usedResources = new HashSet<String>();
/**
* Flag indicating whether there are loaded classes which have later been
* expired (e.g. invalidated or modified)
*/
private boolean dirty = false;
/**
* The path to use as a classpath.
*/
private String repositoryPath;
/**
* The <code>ClassLoaderWriterImpl</code> grants access to the repository
* <p>
* This field is not final such that it may be cleared when the class loader
* is destroyed.
*/
private ClassLoaderWriterImpl writer;
/**
* 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>RepositoryClassLoader</code> for a given
* repository path.
*
* @param classPath The path making up the class path of this class
* loder
* @param writer The class loader write to get a jcr session.
* @param parent The parent <code>ClassLoader</code>, which may be
* <code>null</code>.
*
* @throws NullPointerException if either the session or the classPath
* is <code>null</code>.
*/
public RepositoryClassLoader(final String classPath,
final ClassLoaderWriterImpl writer,
final ClassLoader parent) {
// initialize the super class with an empty class path
super(parent);
// check writer and classPath
if (writer == null) {
throw new NullPointerException("writer");
}
if (classPath == null) {
throw new NullPointerException("classPath");
}
// set fields
this.writer = writer;
this.repositoryPath = classPath;
logger.debug("RepositoryClassLoader: {} ready", this);
}
/**
* 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) {
logger.debug("Instance is already destroyed");
return;
}
// set destroyal guard
destroyed = true;
this.writer = null;
this.repositoryPath = null;
synchronized ( this.usedResources ) {
this.usedResources.clear();
}
}
/**
* 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)");
}
logger.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(final String name) {
if (destroyed) {
logger.warn("Destroyed class loader cannot find a resource");
return null;
}
logger.debug("findResource: Try to find resource {}", name);
final String path = this.repositoryPath + '/' + name;
try {
if ( findClassLoaderResource(path) != null ) {
logger.debug("findResource: Getting resource from {}", path);
return JCRURLHandler.createURL(this.writer, path);
}
} catch (final Exception e) {
logger.warn("findResource: Cannot getURL for " + name, e);
}
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(final String name) {
if (destroyed) {
logger.warn("Destroyed class loader cannot find resources");
return new Enumeration<URL>() {
public boolean hasMoreElements() {
return false;
}
public URL nextElement() {
throw new NoSuchElementException("No Entries");
}
};
}
logger.debug("findResources: Try to find resources for {}", name);
final URL url = this.findResource(name);
final List<URL> list = new LinkedList<URL>();
if (url != null) {
list.add(url);
}
// return the enumeration on the list
return Collections.enumeration(list);
}
/**
* 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(final String name) throws ClassNotFoundException {
// prepare the name of the class
logger.debug("findClassPrivileged: Try to find path {class {}",
name);
final String path = this.repositoryPath + '/' + name.replace('.', '/') + (".class");
// try defining the class, error aborts
try {
final byte[] data = this.findClassLoaderResource(path);
if (data != null) {
logger.debug(
"findClassPrivileged: Loading class from {}", data);
final Class<?> c = defineClass(name, data);
if (c == null) {
logger.warn("defineClass returned null for class {}", name);
throw new ClassNotFoundException(name);
}
return c;
}
} catch (final IOException ioe) {
logger.debug("defineClass failed", ioe);
throw new ClassNotFoundException(name, ioe);
} catch (final Throwable t) {
logger.debug("defineClass failed", t);
throw new ClassNotFoundException(name, t);
}
throw new ClassNotFoundException(name);
}
/**
* Returns the contents for the given <code>path</code> or
* <code>null</code> if not existing.
*
* @param path The repository path of the resource to return.
*
* @return The contents if found or <code>null</code> if not found.
*
* @throws NullPointerException If this class loader has already been
* destroyed.
*/
private byte[] findClassLoaderResource(final String path) throws IOException {
Session session = null;
byte[] res = null;
try {
session = this.writer.createSession();
if ( session.itemExists(path) ) {
final Node node = (Node)session.getItem(path);
logger.debug("Found resource at {}", path);
res = Util.getBytes(node);
} else {
logger.debug("No classpath entry contains {}", path);
}
} catch (final RepositoryException re) {
logger.debug("Error while trying to get node at " + path, re);
} finally {
if ( session != null ) {
session.logout();
}
}
synchronized ( this.usedResources ) {
this.usedResources.add(path);
}
return res;
}
/**
* Defines a class using the bytes
*
* @param name The fully qualified class name
* @param contents The class in bytes
*
* @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(final String name, final byte[] contents) {
logger.debug("defineClass({}, {})", name, contents.length);
final Class<?> clazz = defineClass(name, contents, 0, contents.length);
return clazz;
}
/**
* @see org.apache.sling.commons.classloader.DynamicClassLoader#isLive()
*/
public boolean isLive() {
return !destroyed && !dirty && this.writer != null && this.writer.isActivate();
}
/**
* Handle a modification event.
*/
public void handleEvent(final String path) {
synchronized ( this.usedResources ) {
if ( this.usedResources.contains(path) ) {
logger.debug("handleEvent: Item {} has been modified - marking class loader as dirty {}", this);
this.dirty = true;
}
}
}
//----------- 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(" }, live: ");
buf.append(isLive());
}
return buf.toString();
}
}