| /* |
| * 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(); |
| } |
| } |