org.squidy.common.dynamiccode.DynamicCodeClassLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.squidy.common.dynamiccode.DynamicCodeClassLoader.java

Source

/**
 * Squidy Interaction Library is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of the License,
 * or (at your option) any later version.
 * 
 * Squidy Interaction Library 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Squidy Interaction Library. If not, see
 * <http://www.gnu.org/licenses/>.
 * 
 * 2009 Human-Computer Interaction Group, University of Konstanz.
 * <http://hci.uni-konstanz.de>
 * 
 * Please contact info@squidy-lib.de or visit our website
 * <http://www.squidy-lib.de> for further information.
 */
package org.squidy.common.dynamiccode;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.squidy.SquidyException;

/**
 * <code>DynamicCode</code>.
 * 
 * <pre>
 * Date: Mar 30, 2009
 * Time: 7:19:03 PM
 * </pre>
 * 
 * @author Roman Rdle <a href="mailto:Roman.Raedle@uni-konstanz.de">Roman.Raedle@uni-konstanz.de</a>
 *         Human-Computer Interaction Group University of Konstanz
 * @version $Id: DynamicCodeClassLoader.java 772 2011-09-16 15:39:44Z raedle $
 * @since 1.0.0
 */
public class DynamicCodeClassLoader extends ClassLoader {

    // Logger to log info, error, debug,... messages.
    private static final Log LOG = LogFactory.getLog(DynamicCodeClassLoader.class);

    private String compileClasspath;

    private File compilerOutput = DYNAMIC_CODE_REPOSITORY;

    private ClassLoader parentClassLoader;

    private ArrayList<SourceDir> sourceDirs = new ArrayList<SourceDir>();

    // class name => LoadedClass
    private HashMap<String, LoadedClass> loadedClasses = new HashMap<String, LoadedClass>();

    // public static final String DYNAMIC_CODE_REPOSITORY_NAME =
    // "DynamicCodeRepository";
    public static final File DYNAMIC_CODE_REPOSITORY;
    static {
        DYNAMIC_CODE_REPOSITORY = new File("target/classes");
    }

    public static final DynamicCodeClassLoader DYNAMIC_CODE;
    static {

        if (LOG.isInfoEnabled()) {
            LOG.info("Using context class loader: " + Thread.currentThread().getContextClassLoader());
        }

        DYNAMIC_CODE = new DynamicCodeClassLoader(Thread.currentThread().getContextClassLoader());

        /*
         * // TODO: JFileChooser crashes with exception: ClassCircularityError JFileChooser sourceDirChooser = new
         * JFileChooser(); sourceDirChooser.setDialogTitle("Choose Source Directory");
         * sourceDirChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); int option =
         * sourceDirChooser.showOpenDialog(null); if (option == JFileChooser.APPROVE_OPTION) { File sourceDirectory =
         * sourceDirChooser.getSelectedFile(); if (sourceDirectory.isDirectory()) {
         * DYNAMIC_CODE.addSourceDir(sourceDirectory); } else { DYNAMIC_CODE.addSourceDir(new File("src/main/java")); }
         * } else { DYNAMIC_CODE.addSourceDir(new File("src/main/java")); }
         */

        Thread.currentThread().setContextClassLoader(DYNAMIC_CODE);
        // DYNAMIC_CODE.addSourceDir(new File("extension"));
        // DYNAMIC_CODE.addSourceDir(new
        // File("../squidy-extension-basic-1.1.0-SNAPSHOT/src/main/java"));
    }

    public DynamicCodeClassLoader() {
        this(Thread.currentThread().getContextClassLoader());
    }

    public DynamicCodeClassLoader(ClassLoader parentClassLoader) {
        this(extractClasspath(parentClassLoader), parentClassLoader);
    }

    /**
     * @param compileClasspath used to compile dynamic classes
     * @param parentClassLoader the parent of the class loader that loads all the dynamic classes
     */
    public DynamicCodeClassLoader(String compileClasspath, ClassLoader parentClassLoader) {
        // super(new URL[] { DYNAMIC_CODE_REPOSITORY_URL });

        if (LOG.isDebugEnabled()) {
            LOG.debug("Classpath: " + compileClasspath);
        }

        this.compileClasspath = compileClasspath;
        this.parentClassLoader = parentClassLoader;
    }

    /**
     * Add a directory that contains the source of dynamic java code.
     * 
     * @param srcDir
     * @return true if the add is successful
     */
    public boolean addSourceDir(File srcDir) {

        try {
            srcDir = srcDir.getCanonicalFile();
        } catch (IOException e) {
            // ignore
        }

        synchronized (sourceDirs) {

            // check existence
            for (int i = 0; i < sourceDirs.size(); i++) {
                SourceDir src = (SourceDir) sourceDirs.get(i);
                if (src.srcDir.equals(srcDir)) {
                    return false;
                }
            }

            // add new
            SourceDir src = new SourceDir(srcDir);
            sourceDirs.add(src);

            if (LOG.isInfoEnabled()) {
                LOG.info("Added source directory: " + srcDir);
            }
        }

        return true;
    }

    /**
     * Returns the up-to-date dynamic class by name.
     * 
     * @param className
     * @return
     * @throws ClassNotFoundException if source file not found or compilation error
     */
    public Class<?> loadClass(String className) throws ClassNotFoundException {

        LoadedClass loadedClass = null;
        synchronized (loadedClasses) {
            loadedClass = (LoadedClass) loadedClasses.get(className);
        }

        // first access of a class
        if (loadedClass == null) {

            String resource = className.replace('.', '/') + ".java";
            SourceDir src = locateResource(resource);
            if (src == null) {
                try {
                    return parentClassLoader.loadClass(className);
                } catch (Exception e) {
                    throw new ClassNotFoundException("DynamicCode class not found " + className);
                }
            }

            synchronized (this) {

                // compile and load class
                loadedClass = new LoadedClass(className, src);

                synchronized (loadedClasses) {
                    loadedClasses.put(className, loadedClass);
                }
            }

            return loadedClass.loadedClass;
        }

        // subsequent access
        if (loadedClass.isChanged()) {
            // unload and load again
            unload(loadedClass.srcDir);
            return loadClass(className);
        }

        return loadedClass.loadedClass;
    }

    private SourceDir locateResource(String resource) {
        for (int i = 0; i < sourceDirs.size(); i++) {
            SourceDir src = (SourceDir) sourceDirs.get(i);
            if (new File(src.srcDir, resource).exists()) {
                return src;
            }
        }
        return null;
    }

    private void unload(SourceDir src) {
        // clear loaded classes
        synchronized (loadedClasses) {
            for (Iterator iter = loadedClasses.values().iterator(); iter.hasNext();) {
                LoadedClass loadedClass = (LoadedClass) iter.next();
                if (loadedClass.srcDir == src) {
                    iter.remove();
                }
            }
        }

        // create new class loader
        src.recreateClassLoader();
    }

    /**
     * Get a resource from added source directories.
     * 
     * @param resource
     * @return the resource URL, or null if resource not found
     */
    public URL getResource(String resource) {
        try {

            SourceDir src = locateResource(resource);
            return src == null ? null : new File(src.srcDir, resource).toURL();

        } catch (MalformedURLException e) {
            // should not happen
            return null;
        }
    }

    /**
     * Get a resource stream from added source directories.
     * 
     * @param resource
     * @return the resource stream, or null if resource not found
     */
    public InputStream getResourceAsStream(String resource) {
        try {

            SourceDir src = locateResource(resource);
            return src == null ? null : new FileInputStream(new File(src.srcDir, resource));

        } catch (FileNotFoundException e) {
            // should not happen
            return null;
        }
    }

    /**
     * <code>SourceDir</code>.
     * 
     * <pre>
     * Date: Aug 20, 2009
     * Time: 9:36:53 PM
     * </pre>
     * 
     * @author Roman Rdle <a href="mailto:Roman.Raedle@uni-konstanz.de">Roman.Raedle
     * @uni-konstanz.de</a> Human-Computer Interaction Group University of Konstanz
     * @version $Id: DynamicCodeClassLoader.java 772 2011-09-16 15:39:44Z raedle $
     * @since 1.0.0
     */
    private class SourceDir {
        File srcDir;

        File binDir;

        Javac javac;

        URLClassLoader classLoader;

        SourceDir(File srcDir) {
            this.srcDir = srcDir;

            // Make directories for hot deployment compiler output
            if (!compilerOutput.mkdirs()) {
                throw new SquidyException("Could not make directories for hot deployment compiler output");
            }

            this.binDir = compilerOutput;// new

            // Make directories for hot deployment binaries
            if (!binDir.mkdirs()) {
                throw new SquidyException("Could not make directories for hot deployment binaries");
            }

            // prepare compiler
            this.javac = new Javac(compileClasspath, binDir.getAbsolutePath());

            // class loader
            recreateClassLoader();
        }

        /**
         * Recreate class loader.
         */
        void recreateClassLoader() {
            classLoader = (URLClassLoader) AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
                public ClassLoader run() {
                    try {
                        return new URLClassLoader(new URL[] { binDir.toURL() }, parentClassLoader);
                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                    } catch (SecurityException e) {
                        e.printStackTrace();
                    }
                    return null;
                }
            });
        }
    }

    /**
     * <code>LoadedClass</code>.
     * 
     * <pre>
     * Date: Aug 20, 2009
     * Time: 9:37:28 PM
     * </pre>
     * 
     * @author Roman Rdle <a href="mailto:Roman.Raedle@uni-konstanz.de">Roman.Raedle
     * @uni-konstanz.de</a> Human-Computer Interaction Group University of Konstanz
     * @version $Id: DynamicCodeClassLoader.java 772 2011-09-16 15:39:44Z raedle $
     * @since 1.0.0
     */
    private static class LoadedClass {
        String className;

        SourceDir srcDir;

        File srcFile;

        File binFile;

        Class loadedClass;

        long lastModified;

        LoadedClass(String className, SourceDir src) {
            this.className = className;
            this.srcDir = src;

            String path = className.replace('.', '/');
            this.srcFile = new File(src.srcDir, path + ".java");
            this.binFile = new File(src.binDir, path + ".class");

            compileAndLoadClass();
        }

        boolean isChanged() {
            return srcFile.lastModified() != lastModified;
        }

        void compileAndLoadClass() {

            if (loadedClass != null) {
                return; // class already loaded
            }

            // System.out.println("SRC: " + srcFile);

            // compile, if required
            String error = null;
            if (binFile.lastModified() < srcFile.lastModified()) {
                error = srcDir.javac.compile(new File[] { srcFile });
            }

            if (error != null) {
                throw new RuntimeException("Failed to compile " + srcFile.getAbsolutePath() + ". Error: " + error);
            }

            try {
                // load class
                loadedClass = srcDir.classLoader.loadClass(className);

                // load class success, remember timestamp
                lastModified = srcFile.lastModified();

            } catch (ClassNotFoundException e) {
                throw new RuntimeException("Failed to load class " + srcFile.getAbsolutePath());
            }

            if (LOG.isInfoEnabled()) {
                LOG.info("Initialized " + loadedClass);
            }
        }
    }

    /**
     * Extracts a classpath string from a given class loader. Recognizes only URLClassLoader.
     */
    private static String extractClasspath(ClassLoader cl) {
        StringBuffer buf = new StringBuffer();

        while (cl != null) {

            if (LOG.isDebugEnabled()) {
                LOG.debug("Trying to extract classpath of class loader: " + cl.getClass().getName());
            }

            if (cl instanceof URLClassLoader) {
                URL urls[] = ((URLClassLoader) cl).getURLs();
                for (int i = 0; i < urls.length; i++) {
                    if (buf.length() > 0) {
                        buf.append(File.pathSeparatorChar);
                    }

                    String path = urls[i].getFile();
                    if (path.startsWith("/C:/") || path.startsWith("/c:/")) {
                        path = path.substring(1, path.length());
                        path = path.replace('/', '\\');
                        path = path.replace("%20", " ");
                    }
                    buf.append(path);
                }
            }
            cl = cl.getParent();
        }

        return buf.toString();
    }
}