org.webical.plugin.classloading.PluginClassLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.webical.plugin.classloading.PluginClassLoader.java

Source

/*
 *    Webical - http://www.webical.org
 *    Copyright (C) 2007 Func. Internet Integration
 *
 *    This file is part of Webical.
 *
 *    $Id$
 *
 *    This program is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.webical.plugin.classloading;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.webical.plugin.PluginException;
import org.webical.plugin.file.FileUtils;

/**
 * ClassLoader that handles loading classes registered in a HashMap
 * @author ivo
 *
 */
public class PluginClassLoader extends ClassLoader {

    private static final String META_INF = "META-INF/";
    public static final String JAR_EXTRACTION_EXTENSION = "_EXTRACTED_JAR_RESOURCES";

    private static Log log = LogFactory.getLog(PluginClassLoader.class);

    /** Registered class files **/
    private Map<String, File> classNameToFileMap;

    /**
     * Resources registered on the classpath
     */
    private Map<String, File> resourceNameToFileMap;

    /** Loaded class bytes from jar files */
    private Hashtable<String, byte[]> classArrays;

    /**
     * This map holds all the class definitions so that the classloader always returns the same Class object
     * for a given classname (this is required, see the java language specs)
     */
    private HashMap<String, Class> definedClassesMap;

    /**
     * @param classLoader the parent ClassLoader
     */
    public PluginClassLoader(ClassLoader classLoader) {
        super(classLoader);
        classNameToFileMap = new HashMap<String, File>();
        classArrays = new Hashtable<String, byte[]>();
        resourceNameToFileMap = new HashMap<String, File>();
        definedClassesMap = new HashMap<String, Class>();
    }

    /**
     * Override of the default method. First tries the parent {@link ClassLoader}
     * (Standard practice) and then our implementation (first the jar registrations and then the class registrations)
     * @see java.lang.ClassLoader#findClass(java.lang.String)
     */
    @Override
    public Class<?> findClass(String className) throws ClassNotFoundException {
        if (StringUtils.isEmpty(className)) {
            throw new ClassNotFoundException("Cannot load a class without a name: " + className);
        }

        if (definedClassesMap.containsKey(className)) {
            return definedClassesMap.get(className);
        }

        //Try the default ClassLoader (standard practice)
        try {
            if (log.isDebugEnabled()) {
                log.debug("Trying to load class: " + className + " through the default ClassLoader");
            }
            return getParent().loadClass(className);
        } catch (ClassNotFoundException e) {
            if (log.isDebugEnabled()) {
                log.debug("Default ClassLoader could not load class: " + className);
            }
        }

        byte[] classBytes = null;

        //First try the loaded jar files
        if (log.isDebugEnabled()) {
            log.debug("Trying to load class: " + className + " from the registered jar files");
        }
        classBytes = (byte[]) classArrays.get(className);
        if (classBytes != null) {
            if (log.isDebugEnabled())
                log.debug("Class found in jar file");
            Class clazz = defineClass(className, classBytes, 0, classBytes.length, null);
            if (clazz != null) {
                definedClassesMap.put(className, clazz);
            }
            return clazz;
        }

        //Not found, use our implementation
        //Locate and read in the class file
        File classFile = classNameToFileMap.get(className);
        if (classFile == null) {
            log.error("Class does not seem to be registered: " + className);
            throw new ClassNotFoundException("Class does not seem to be registered: " + className);
        }
        if (!classFile.canRead()) {
            log.error("Class does not seem to be readable: " + className + " file: " + classFile.getAbsolutePath());
            throw new ClassNotFoundException(
                    "Class does not seem to be readable: " + className + " file: " + classFile.getAbsolutePath());
        }

        FileInputStream classInputStream = null;
        try {
            classInputStream = new FileInputStream(classFile);
            classBytes = getBytesFromFile(classInputStream, classFile.length());
        } catch (FileNotFoundException e) {
            log.error("Class: " + className + " could not be found at registered location: "
                    + classFile.getAbsolutePath());
            throw new ClassNotFoundException("Class: " + className + " could not be found at registered location: "
                    + classFile.getAbsolutePath());
        } catch (IOException e) {
            log.error("Class: " + className + " could not be read from file " + classFile.getAbsolutePath(), e);
            throw new ClassNotFoundException(
                    "Class: " + className + " could not be read from file " + classFile.getAbsolutePath(), e);
        } finally {
            if (classInputStream != null) {
                try {
                    classInputStream.close();
                } catch (IOException e) {
                    log.error("Could not close FileInputStream for file: " + classFile.getAbsolutePath());
                }
            }
        }

        if (log.isDebugEnabled()) {
            log.debug("Successfully loaded in the bytes for class: " + className);
        }

        //Define and return the class
        try {
            Class clazz = defineClass(className.substring(className.lastIndexOf("/") + 1), classBytes, 0,
                    classBytes.length);
            if (clazz != null) {
                definedClassesMap.put(className, clazz);
            }
            return clazz;
        } catch (ClassFormatError e) {
            log.error("Error parsing class file: " + classFile.getAbsolutePath(), e);
            throw new ClassNotFoundException("Error parsing class file: " + classFile.getAbsolutePath(), e);
        }
    }

    /**
     * Reads in a File
     * @param fileInputStream the fileinputstream
     * @param fileLength the length of the file
     * @return a byte array containing the file contents 
     * @throws IOException on IO error
     */
    public static byte[] getBytesFromFile(FileInputStream fileInputStream, long fileLength) throws IOException {

        if (fileLength > Integer.MAX_VALUE) {
            // File is too large
            throw new IOException("File is to large: " + fileLength + " exceeds to maximum: " + Integer.MAX_VALUE);
        }

        // Create the byte array to hold the data
        byte[] bytes = new byte[(int) fileLength];

        // Read in the bytes
        int offset = 0;
        int numRead = 0;
        while (offset < bytes.length
                && (numRead = fileInputStream.read(bytes, offset, bytes.length - offset)) >= 0) {
            offset += numRead;
        }

        // Ensure all the bytes have been read in
        if (offset < bytes.length) {
            throw new IOException("Could not completely read file");
        }

        // Close the input stream and return bytes
        fileInputStream.close();
        return bytes;
    }

    /* (non-Javadoc)
     * @see java.lang.ClassLoader#findResource(java.lang.String)
     */
    @Override
    public URL findResource(String name) {
        if (log.isDebugEnabled())
            log.debug("Trying to find resource: " + name);

        if (!StringUtils.isEmpty(name)) {
            File file = resourceNameToFileMap.get(name);
            if (file != null) {
                if (log.isDebugEnabled()) {
                    log.debug("Found resource: " + name + " in file: " + file.getAbsolutePath() + " readable: "
                            + file.canRead());
                }
                try {
                    URL url = file.toURI().toURL();
                    log.debug("Returning url:" + url.toString());
                    return url;
                } catch (MalformedURLException e) {
                    log.error("Could not create url from stored file for resource: " + name, e);
                }
            }
        }

        log.warn("Could not find resource: " + name + " trying default classloader");
        URL resource = getParent().getResource(name);
        if (resource == null) {
            log.warn("No luck with the default classloader aswell for resource: " + name);
        }
        return resource;
    }

    /* (non-Javadoc)
     * @see java.lang.ClassLoader#findResources(java.lang.String)
     */
    @Override
    protected Enumeration<URL> findResources(String name) throws IOException {
        Hashtable<String, URL> resources = new Hashtable<String, URL>();
        resources.put(name, findResource(name));
        return resources.elements();
    }

    /**
     * Reads in all classes in a jar file for later reference
     * @param name the full name and path of the jar file
     * @throws PluginException 
     */
    public void readJarFile(File jarFile) throws PluginException {
        //Firstly check input
        if (jarFile == null) {
            log.error("Cannot load jar without a reference...");
            throw new PluginException("Cannot load jar without a reference...");
        }

        JarInputStream jis;
        JarEntry je;

        if (!jarFile.exists()) {
            log.error("Jar does not exist: " + jarFile.getAbsolutePath());
            throw new PluginException("Jar does not exist: " + jarFile.getAbsolutePath());
        }
        if (!jarFile.canRead()) {
            log.error("Jar is not readable: " + jarFile.getAbsolutePath());
            throw new PluginException("Jar is not readable: " + jarFile.getAbsolutePath());
        }
        if (log.isDebugEnabled())
            log.debug("Loading jar file " + jarFile);

        FileInputStream fis = null;
        try {
            fis = new FileInputStream(jarFile);
            jis = new JarInputStream(fis);
        } catch (IOException ioe) {
            log.error("Can't open jar file " + jarFile, ioe);
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    log.error("Could not close filehandle to: " + jarFile.getAbsolutePath(), e);
                    throw new PluginException("Could not close filehandle to: " + jarFile.getAbsolutePath(), e);
                }
            }
            throw new PluginException("Can't open jar file " + jarFile, ioe);
        }

        //Loop over the jarfile entries
        try {
            while ((je = jis.getNextJarEntry()) != null) {
                String jarEntryName = je.getName();
                //Skip the META-INF dir
                if (jarEntryName.startsWith(META_INF)) {
                    continue;
                }

                if (jarEntryName.endsWith(FileUtils.CLASS_FILE_EXTENSION)) {
                    //Extract java class
                    loadClassBytes(jis, ClassUtils.fileToFullClassName(jarEntryName));
                } else if (je.isDirectory()) {
                    //Extract directory
                    unpackDirectory(jarEntryName, jarFile);
                } else {
                    //it could be an image or audio file so let's extract it and store it somewhere for later reference
                    try {
                        extractJarResource(jis, jarEntryName, jarFile);
                    } catch (Exception e) {
                        log.error("Cannot cache jar resource: " + jarEntryName + " from jar file: "
                                + jarFile.getAbsolutePath(), e);
                        throw new PluginException("Cannot cache jar resource: " + jarEntryName + " from jar file: "
                                + jarFile.getAbsolutePath(), e);
                    }
                }
                jis.closeEntry();
            }
        } catch (IOException ioe) {
            log.error("Badly formatted jar file: " + jarFile.getAbsolutePath(), ioe);
            throw new PluginException("Badly formatted jar file: " + jarFile.getAbsolutePath(), ioe);
        } finally {
            if (jis != null) {
                try {
                    jis.close();
                } catch (IOException e) {
                    log.error("Could not close connection to jar: " + jarFile.getAbsolutePath(), e);
                    throw new PluginException("Could not close connection to jar: " + jarFile.getAbsolutePath(), e);
                }
            }
        }
    }

    /**
     * loads the class' bytes into the {@link Hashtable} from a jar
     * @param jis the {@link JarInputStream} to read from
     * @param className the name of the jar
     * @throws IOException 
     */
    private void loadClassBytes(JarInputStream jis, String className) throws IOException {
        if (!classArrays.containsKey(className)) {
            try {
                log.debug("load " + className);
                classArrays.put(className, readBytesFromJarEntry(jis));
            } catch (IOException ioe) {
                log.error("Error reading entry " + className, ioe);
                throw ioe;
            }
        } else {
            //just not include this entry
            log.warn("Class: " + className + " was already registerd from a jar file");
        }
    }

    /**
     * Extracts a jar resources and adds it to the resourceNameToFileMap
     * @param jis the {@link JarInputStream} to read from
     * @param name the name of the jarResource
     * @param jarFile the jar {@link File}
     * @throws IOException
     */
    private void extractJarResource(JarInputStream jis, String name, File jarFile) throws IOException {
        File extactionDir = getExtractionDirectory(jarFile);
        File extractedFile = new File(extactionDir, name);
        String resourceIdentifier = ClassUtils.fileToClassPathResourceIdentifier(extactionDir, extractedFile);

        if (log.isDebugEnabled()) {
            log.debug("Extracting jar resource: " + resourceIdentifier + "  it to: "
                    + extractedFile.getAbsolutePath());
        }

        FileUtils.streamToFile(jis, extractedFile, false);
        resourceNameToFileMap.put(resourceIdentifier, extractedFile);
    }

    /**
     * @param jarFile
     * @return
     */
    private File getExtractionDirectory(File jarFile) {
        File extactionDir = new File(jarFile.getAbsolutePath() + JAR_EXTRACTION_EXTENSION);
        if (!extactionDir.exists()) {
            log.debug("Creating directory to extract resource files from jar: " + jarFile.getAbsolutePath() + " -> "
                    + extactionDir.getAbsolutePath());
            extactionDir.mkdir();
        }
        return extactionDir;
    }

    /**
     * @param name
     * @param jarFile
     */
    private void unpackDirectory(String name, File jarFile) {
        File extactionDir = getExtractionDirectory(jarFile);
        File extractedDirectory = new File(extactionDir, name);
        if (!extractedDirectory.exists()) {
            log.debug("Creating new directory: " + extractedDirectory.getAbsolutePath());
            extractedDirectory.mkdir();
        }
    }

    /**
     * @param jis the {@link JarInputStream}
     * @return the bytes
     * @throws IOException
     */
    private byte[] readBytesFromJarEntry(JarInputStream jis) throws IOException {
        BufferedInputStream jarBuf = new BufferedInputStream(jis);
        ByteArrayOutputStream jarOut = new ByteArrayOutputStream();
        int buffer;

        while ((buffer = jarBuf.read()) != -1) {
            jarOut.write(buffer);
        }
        return jarOut.toByteArray();
    }

    //////////////////////////////////////////////////
    // Getters for the Class/Resource registrations //
    //////////////////////////////////////////////////

    /**
     * Takes a fully qualified classname (eg some.package.SomeClass) and the associated file
     * @param fullyQualifiedClassName a fully qualified classname
     * @param classFile the file containing the class
     * @throws DuplicateClassPathEntryException if the class was already registered
     */
    public void addClassRegistration(String fullyQualifiedClassName, File classFile)
            throws DuplicateClassPathEntryException {
        if (!StringUtils.isEmpty(fullyQualifiedClassName) && classFile != null) {
            if (!classNameToFileMap.containsKey(fullyQualifiedClassName)) {
                log.debug("Registering class: " + fullyQualifiedClassName + " with associated file: "
                        + (classFile != null ? classFile.getAbsolutePath() : null));
                classNameToFileMap.put(fullyQualifiedClassName, classFile);
            } else {
                log.error("Class: " + fullyQualifiedClassName + " was already registerd from a plugin class file");
                throw new DuplicateClassPathEntryException(
                        "Class: " + fullyQualifiedClassName + " was already registerd from a plugin class file");
            }
        } else {
            log.error("Not registering class: " + fullyQualifiedClassName + " with file: "
                    + (classFile != null ? classFile.getAbsolutePath() : null));
        }
    }

    /**
     * Takes a resource identifier (eg. some/package/TheResource.extension) and the associated file to register
     * on the classpath 
     * @param resourceIdentifier a resource identifier (eg. some/package/TheResource.extension) 
     * @param file the associated file to register
     * @throws DuplicateClassPathEntryException if a resource with the same identifier was already registered.
     */
    public void addClassPathResourceRegistration(String resourceIdentifier, File file)
            throws DuplicateClassPathEntryException {
        if (!StringUtils.isEmpty(resourceIdentifier) && file != null) {
            if (!resourceNameToFileMap.containsKey(resourceIdentifier)) {
                log.debug("Registering resource on classpath: " + resourceIdentifier + " with associated file: "
                        + (file != null ? file.getAbsolutePath() : null));
                resourceNameToFileMap.put(resourceIdentifier, file);
            } else {
                log.error("Resource: " + resourceIdentifier + " was already registerd from a plugin class file");
                throw new DuplicateClassPathEntryException(
                        "Resource: " + resourceIdentifier + " was already registerd from a plugin class file");
            }
        } else {
            log.error("Not registering resource: " + resourceIdentifier + " with file: "
                    + (file != null ? file.getAbsolutePath() : null));
        }
    }
}