org.openmrs.util.OpenmrsClassLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.util.OpenmrsClassLoader.java

Source

/**
 * This Source Code Form is subject to the terms of the Mozilla Public License,
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
 *
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
 * graphic logo is a trademark of OpenMRS Inc.
 */
package org.openmrs.util;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;

import net.sf.ehcache.CacheManager;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.api.APIException;
import org.openmrs.api.context.Context;
import org.openmrs.module.ModuleClassLoader;
import org.openmrs.module.ModuleFactory;
import org.openmrs.module.ModuleUtil;
import org.openmrs.scheduler.SchedulerException;
import org.openmrs.scheduler.SchedulerService;

/**
 * This classloader knows about the current ModuleClassLoaders and will attempt to load classes from
 * them if needed
 */
public class OpenmrsClassLoader extends URLClassLoader {

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

    private static File libCacheFolder;

    private static boolean libCacheFolderInitialized = false;

    // placeholder to hold mementos to restore
    private static Map<String, OpenmrsMemento> mementos = new WeakHashMap<String, OpenmrsMemento>();

    /**
     * Holds all classes that has been requested from this class loader. We use weak references so that
     * module classes can be garbage collected when modules are unloaded.
     */
    private Map<String, WeakReference<Class<?>>> cachedClasses = new ConcurrentHashMap<String, WeakReference<Class<?>>>();

    // suffix of the OpenMRS required library cache folder
    private static final String LIBCACHESUFFIX = ".openmrs-lib-cache";

    /**
     * Creates the instance for the OpenmrsClassLoader
     */
    public OpenmrsClassLoader(ClassLoader parent) {
        super(new URL[0], parent);

        if (parent instanceof OpenmrsClassLoader) {
            throw new IllegalArgumentException("Parent must not be OpenmrsClassLoader nor null");
        } else if (parent instanceof ModuleClassLoader) {
            throw new IllegalArgumentException("Parent must not be ModuleClassLoader");
        }

        OpenmrsClassLoaderHolder.INSTANCE = this;

        if (log.isDebugEnabled()) {
            log.debug("Creating new OpenmrsClassLoader instance with parent: " + parent);
        }

        //disable caching so the jars aren't locked
        //if performance is effected, this can be disabled in favor of
        //copying all opened jars to a temp location
        //(ala org.apache.catalina.loader.WebappClassLoader antijarlocking)
        URLConnection urlConnection = new OpenmrsURLConnection();
        urlConnection.setDefaultUseCaches(false);
    }

    /**
     * Normal constructor. Sets this class as the parent classloader
     */
    public OpenmrsClassLoader() {
        this(OpenmrsClassLoader.class.getClassLoader());
    }

    /**
     * Private class to hold the one classloader used throughout openmrs. This is an alternative to
     * storing the instance object on {@link OpenmrsClassLoader} itself so that garbage collection
     * can happen correctly.
     */
    private static class OpenmrsClassLoaderHolder {

        private static OpenmrsClassLoader INSTANCE = null;

    }

    /**
     * Get the static/singular instance of the module class loader
     *
     * @return OpenmrsClassLoader
     */
    public static OpenmrsClassLoader getInstance() {
        if (OpenmrsClassLoaderHolder.INSTANCE == null) {
            OpenmrsClassLoaderHolder.INSTANCE = new OpenmrsClassLoader();
        }

        return OpenmrsClassLoaderHolder.INSTANCE;
    }

    /**
     * It loads classes from the web container class loader first (parent class loader) and then
     * tries module class loaders.
     * 
     * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean)
     * @should load class from cache second time
     * @should not load class from cache if class loader has been disposed
     * @should load class from parent first
     * @should load class if two module class loaders have same packages
     */
    @Override
    public synchronized Class<?> loadClass(String name, final boolean resolve) throws ClassNotFoundException {
        // Check if the class has already been requested from this class loader
        Class<?> c = getCachedClass(name);
        if (c == null) {
            // We do not try to load classes using this.findClass on purpose.
            // All classes are loaded by web container or by module class loaders.

            // First try loading from modules such that we allow modules to load
            // different versions of the same libraries that may already be used
            // by core or the web container. An example is the chartsearch module
            // which uses different versions of lucene and solr from core
            String packageName = StringUtils.substringBeforeLast(name, ".");
            Set<ModuleClassLoader> moduleClassLoaders = ModuleFactory.getModuleClassLoadersForPackage(packageName);
            for (ModuleClassLoader moduleClassLoader : moduleClassLoaders) {
                try {
                    c = moduleClassLoader.loadClass(name);
                    break;
                } catch (ClassNotFoundException e) {
                    // Continue trying...
                }
            }

            if (c == null) {
                // Finally try loading from web container
                c = getParent().loadClass(name);
            }

            cacheClass(name, c);
        }

        if (resolve) {
            resolveClass(c);
        }

        return c;
    }

    private Class<?> getCachedClass(String name) {
        WeakReference<Class<?>> ref = cachedClasses.get(name);
        if (ref != null) {
            Class<?> loadedClass = ref.get();
            if (loadedClass == null || loadedClass.getClassLoader() == null) {
                // Class has been garbage collected
                cachedClasses.remove(name);
                loadedClass = null;
            } else if (loadedClass.getClassLoader() instanceof ModuleClassLoader) {
                ModuleClassLoader moduleClassLoader = (ModuleClassLoader) loadedClass.getClassLoader();
                if (moduleClassLoader.isDisposed()) {
                    // Class has been unloaded
                    cachedClasses.remove(name);
                    loadedClass = null;
                }
            }

            return loadedClass;
        }
        return null;
    }

    private void cacheClass(String name, Class<?> clazz) {
        cachedClasses.put(name, new WeakReference<Class<?>>(clazz));
    }

    /**
     * @see java.net.URLClassLoader#findResource(java.lang.String)
     */
    @Override
    public URL findResource(final String name) {
        if (log.isTraceEnabled()) {
            log.trace("finding resource: " + name);
        }

        URL result;
        for (ModuleClassLoader classLoader : ModuleFactory.getModuleClassLoaders()) {
            result = classLoader.findResource(name);
            if (result != null) {
                return result;
            }
        }

        // look for the resource in the parent
        result = super.findResource(name);

        // expand the jar url if necessary
        if (result != null && "jar".equals(result.getProtocol()) && name.contains("openmrs")) {
            result = expandURL(result, getLibCacheFolder());
        }

        return result;
    }

    /**
     * @see java.net.URLClassLoader#findResources(java.lang.String)
     */
    @Override
    public Enumeration<URL> findResources(final String name) throws IOException {
        Set<URI> results = new HashSet<URI>();
        for (ModuleClassLoader classLoader : ModuleFactory.getModuleClassLoaders()) {
            Enumeration<URL> urls = classLoader.findResources(name);
            while (urls.hasMoreElements()) {
                URL result = urls.nextElement();
                if (result != null) {
                    try {
                        results.add(result.toURI());
                    } catch (URISyntaxException e) {
                        throwInvalidURI(result, e);
                    }
                }
            }
        }

        for (Enumeration<URL> en = super.findResources(name); en.hasMoreElements();) {
            URL url = en.nextElement();
            try {
                results.add(url.toURI());
            } catch (URISyntaxException e) {
                throwInvalidURI(url, e);
            }
        }

        List<URL> resources = new ArrayList<URL>(results.size());
        for (URI result : results) {
            resources.add(result.toURL());
        }

        return Collections.enumeration(resources);
    }

    private void throwInvalidURI(URL url, Exception e) throws IOException {
        throw new IOException(url.getPath() + " is not a valid URI", e);
    }

    /**
     * Searches all known module classloaders first, then parent classloaders
     *
     * @see java.lang.ClassLoader#getResourceAsStream(java.lang.String)
     */
    @Override
    public InputStream getResourceAsStream(String file) {
        for (ModuleClassLoader classLoader : ModuleFactory.getModuleClassLoaders()) {
            InputStream result = classLoader.getResourceAsStream(file);
            if (result != null) {
                return result;
            }
        }

        return super.getResourceAsStream(file);
    }

    /**
     * Searches all known module classloaders first, then parent classloaders
     *
     * @see java.lang.ClassLoader#getResources(java.lang.String)
     */
    @Override
    public Enumeration<URL> getResources(String packageName) throws IOException {
        Set<URI> results = new HashSet<URI>();
        for (ModuleClassLoader classLoader : ModuleFactory.getModuleClassLoaders()) {
            Enumeration<URL> urls = classLoader.getResources(packageName);
            while (urls.hasMoreElements()) {
                URL result = urls.nextElement();
                if (result != null) {
                    try {
                        results.add(result.toURI());
                    } catch (URISyntaxException e) {
                        throwInvalidURI(result, e);
                    }
                }
            }
        }

        for (Enumeration<URL> en = super.getResources(packageName); en.hasMoreElements();) {
            URL url = en.nextElement();
            try {
                results.add(url.toURI());
            } catch (URISyntaxException e) {
                throwInvalidURI(url, e);
            }
        }

        List<URL> resources = new ArrayList<URL>(results.size());
        for (URI result : results) {
            resources.add(result.toURL());
        }

        return Collections.enumeration(resources);
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "Openmrs" + super.toString();
    }

    /**
     * Destroy the current instance of the classloader. Note**: After calling this and after the new
     * service is set up, All classes using this instance should be flushed. This would allow all
     * java classes that were loaded by the old instance variable to be gc'd and modules to load in
     * new java classes
     *
     * @see #flushInstance()
     */
    public static void destroyInstance() {

        // remove all thread references to this class
        // Walk up all the way to the root thread group
        ThreadGroup rootGroup = Thread.currentThread().getThreadGroup();
        ThreadGroup parent;
        while ((parent = rootGroup.getParent()) != null) {
            rootGroup = parent;
        }

        log.info("this classloader hashcode: " + OpenmrsClassLoaderHolder.INSTANCE.hashCode());

        //      List<Thread> threads = listThreads(rootGroup, "");
        //      for (Thread thread : threads) {
        //         if (thread.getContextClassLoader() != null) {
        //            log.debug("context classloader on thread: " + thread.getName() + " is: "
        //                    + thread.getContextClassLoader().getClass().getName() + ":"
        //                    + thread.getContextClassLoader().hashCode());
        //            if (thread.getContextClassLoader() == OpenmrsClassLoaderHolder.INSTANCE) {
        //               thread.setContextClassLoader(OpenmrsClassLoaderHolder.INSTANCE.getParent());
        //               log.error("Cleared context classloader to save the world from memory leaks. thread: " + thread.getName()
        //                       + " ");
        //            }
        //         }
        //      }

        //Shut down and remove all cache managers.
        List<CacheManager> knownCacheManagers = CacheManager.ALL_CACHE_MANAGERS;
        while (!knownCacheManagers.isEmpty()) {
            CacheManager cacheManager = CacheManager.ALL_CACHE_MANAGERS.get(0);
            try {
                //This shuts down and removes the cache manager.
                cacheManager.shutdown();

                //Just in case the the timer does not stop, set the cacheManager 
                //timer to null because it references this class loader.
                Field field = cacheManager.getClass().getDeclaredField("cacheManagerTimer");
                field.setAccessible(true);
                field.set(cacheManager, null);
            } catch (Exception ex) {
                log.error(ex.getMessage(), ex);
            }
        }

        OpenmrsClassScanner.destroyInstance();

        OpenmrsClassLoaderHolder.INSTANCE = null;
    }

    /**
     * Sets the class loader, for all threads referencing a destroyed openmrs class loader, 
     * to the current one.
     */
    public static void setThreadsToNewClassLoader() {
        //Give ownership of all threads loaded by the old class loader to the new one.
        //Examples of such threads are: Keep-Alive-Timer, MySQL Statement Cancellation Timer, etc
        //That way they will no longer hold onto the destroyed OpenmrsClassLoader and hence
        //allow it to be garbage collected after a spring application context refresh, when a new one is created.
        Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
        Thread[] threadArray = threadSet.toArray(new Thread[threadSet.size()]);
        for (Thread thread : threadArray) {

            ClassLoader classLoader = thread.getContextClassLoader();

            //Some threads have a null class loader reference. e.g Finalizer, Reference Handler, etc
            if (classLoader == null) {
                continue;
            }

            //Threads referencing the current class loader are good.
            if (classLoader == getInstance()) {
                continue;
            }

            //For threads referencing any destroyed class loader, point them to the new one.
            if (classLoader instanceof OpenmrsClassLoader) {
                thread.setContextClassLoader(getInstance());
            }
        }
    }

    // List all threads and recursively list all subgroup
    private static List<Thread> listThreads(ThreadGroup group, String indent) {
        List<Thread> threadToReturn = new ArrayList<Thread>();

        log.error(indent + "Group[" + group.getName() + ":" + group.getClass() + "]");
        int nt = group.activeCount();
        Thread[] threads = new Thread[nt * 2 + 10]; //nt is not accurate
        nt = group.enumerate(threads, false);

        // List every thread in the group
        for (int i = 0; i < nt; i++) {
            Thread t = threads[i];
            log.error(indent + "  Thread[" + t.getName() + ":" + t.getClass() + ":"
                    + (t.getContextClassLoader() == null ? "null cl"
                            : t.getContextClassLoader().getClass().getName() + " "
                                    + t.getContextClassLoader().hashCode())
                    + "]");
            threadToReturn.add(t);
        }

        // Recursively list all subgroups
        int ng = group.activeGroupCount();
        ThreadGroup[] groups = new ThreadGroup[ng * 2 + 10];
        ng = group.enumerate(groups, false);

        for (int i = 0; i < ng; i++) {
            threadToReturn.addAll(listThreads(groups[i], indent + "  "));
        }

        return threadToReturn;
    }

    public static void onShutdown() {

        //Since we are shutting down, stop all threads that reference the openmrs class loader.
        Set<Thread> threadSet = Thread.getAllStackTraces().keySet();
        Thread[] threadArray = threadSet.toArray(new Thread[threadSet.size()]);
        for (Thread thread : threadArray) {

            ClassLoader classLoader = thread.getContextClassLoader();

            //Threads like Finalizer, Reference Handler, etc have null class loader reference.
            if (classLoader == null) {
                continue;
            }

            if (classLoader instanceof OpenmrsClassLoader) {
                try {
                    //Set to WebappClassLoader just in case stopping fails.
                    thread.setContextClassLoader(classLoader.getParent());

                    //Stopping the current thread will halt all current cleanup.
                    //So do not ever ever even attempt stopping it. :)
                    if (thread == Thread.currentThread()) {
                        continue;
                    }

                    log.info("onShutdown Stopping thread: " + thread.getName());
                    thread.stop();
                } catch (Exception ex) {
                    log.error(ex.getMessage(), ex);
                }
            }
        }

        clearReferences();
    }

    /**
     * This clears any references this classloader might have that will prevent garbage collection. <br>
     * <br>
     * Borrowed from Tomcat's WebappClassLoader#clearReferences() (not javadoc linked intentionally) <br>
     * The only difference between this and Tomcat's implementation is that this one only acts on
     * openmrs objects and also clears out static java.* packages. Tomcat acts on all objects and
     * does not clear our static java.* objects.
     *
     * @since 1.5
     */
    protected static void clearReferences() {

        // Unregister any JDBC drivers loaded by this classloader
        Enumeration<Driver> drivers = DriverManager.getDrivers();
        while (drivers.hasMoreElements()) {
            Driver driver = drivers.nextElement();
            if (driver.getClass().getClassLoader() == getInstance()) {
                try {
                    DriverManager.deregisterDriver(driver);
                } catch (SQLException e) {
                    log.warn("SQL driver deregistration failed", e);
                }
            }
        }

        // Null out any static or final fields from loaded classes,
        // as a workaround for apparent garbage collection bugs
        for (WeakReference<Class<?>> refClazz : getInstance().cachedClasses.values()) {
            if (refClazz == null) {
                continue;
            }
            Class<?> clazz = refClazz.get();
            if (clazz != null && clazz.getName().contains("openmrs")) { // only clean up openmrs classes
                try {
                    Field[] fields = clazz.getDeclaredFields();
                    for (int i = 0; i < fields.length; i++) {
                        Field field = fields[i];
                        int mods = field.getModifiers();
                        if (field.getType().isPrimitive() || (field.getName().indexOf("$") != -1)) {
                            continue;
                        }
                        if (Modifier.isStatic(mods)) {
                            try {
                                // do not clear the log field on this class yet
                                if (clazz.equals(OpenmrsClassLoader.class) && "log".equals(field.getName())) {
                                    continue;
                                }
                                field.setAccessible(true);
                                if (Modifier.isFinal(mods)) {
                                    if (!(field.getType().getName().startsWith("javax."))) {
                                        nullInstance(field.get(null));
                                    }
                                } else {
                                    field.set(null, null);
                                    if (log.isDebugEnabled()) {
                                        log.debug("Set field " + field.getName() + " to null in class "
                                                + clazz.getName());
                                    }
                                }
                            } catch (Exception t) {
                                if (log.isDebugEnabled()) {
                                    log.debug("Could not set field " + field.getName() + " to null in class "
                                            + clazz.getName(), t);
                                }
                            }
                        }
                    }
                } catch (Exception t) {
                    if (log.isDebugEnabled()) {
                        log.debug("Could not clean fields for class " + clazz.getName(), t);
                    }
                }
            }
        }

        // now we can clear the log field on this class
        OpenmrsClassLoader.log = null;

        getInstance().cachedClasses.clear();
    }

    /**
     * Used by {@link #clearReferences()} upon application close. <br>
     * <br>
     * Borrowed from Tomcat's WebappClassLoader.
     *
     * @param instance the object whose fields need to be nulled out
     */
    protected static void nullInstance(Object instance) {
        if (instance == null) {
            return;
        }
        Field[] fields = instance.getClass().getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            Field field = fields[i];
            int mods = field.getModifiers();
            if (field.getType().isPrimitive() || (field.getName().indexOf("$") != -1)) {
                continue;
            }
            try {
                field.setAccessible(true);
                if (Modifier.isStatic(mods) && Modifier.isFinal(mods)) {
                    // Doing something recursively is too risky
                    continue;
                } else {
                    Object value = field.get(instance);
                    if (null != value) {
                        Class<?> valueClass = value.getClass();
                        if (!loadedByThisOrChild(valueClass)) {
                            if (log.isDebugEnabled()) {
                                log.debug("Not setting field " + field.getName() + " to null in object of class "
                                        + instance.getClass().getName()
                                        + " because the referenced object was of type " + valueClass.getName()
                                        + " which was not loaded by this WebappClassLoader.");
                            }
                        } else {
                            field.set(instance, null);
                            if (log.isDebugEnabled()) {
                                log.debug("Set field " + field.getName() + " to null in class "
                                        + instance.getClass().getName());
                            }
                        }
                    }
                }
            } catch (Exception e) {
                if (log.isDebugEnabled()) {
                    log.debug("Could not set field " + field.getName() + " to null in object instance of class "
                            + instance.getClass().getName(), e);
                }
            }
        }
    }

    /**
     * Determine whether a class was loaded by this class loader or one of its child class loaders. <br>
     * <br>
     * Borrowed from Tomcat's WebappClassLoader
     */
    protected static boolean loadedByThisOrChild(Class<?> clazz) {
        boolean result = false;
        for (ClassLoader classLoader = clazz.getClassLoader(); null != classLoader; classLoader = classLoader
                .getParent()) {
            if (classLoader.equals(getInstance())) {
                result = true;
                break;
            }
        }
        return result;
    }

    /**
     * This method should be called before destroying the instance
     *
     * @see #destroyInstance()
     */
    public static void saveState() {
        try {
            String key = SchedulerService.class.getName();
            if (!Context.isRefreshingContext()) {
                mementos.put(key, Context.getSchedulerService().saveToMemento());
            }
        } catch (Exception t) {
            // pass
        }
    }

    /**
     * This method should be called after restoring the instance
     *
     * @see #destroyInstance()
     * @see #saveState()
     */
    public static void restoreState() {
        try {
            String key = SchedulerService.class.getName();
            Context.getSchedulerService().restoreFromMemento(mementos.get(key));
        } catch (APIException e) {
            // pass
        }
        mementos.clear();
    }

    /**
     * All objects depending on the old classloader should be restarted here Should be called after
     * destoryInstance() and after the service is restarted
     *
     * @see #destroyInstance()
     */
    public static void flushInstance() {
        try {
            SchedulerService service = null;
            try {
                service = Context.getSchedulerService();
            } catch (APIException e2) {
                // if there isn't a scheduler service yet, ignore error
                log.warn("Unable to get scheduler service", e2);
            }
            if (service != null) {
                service.rescheduleAllTasks();
            }
        } catch (SchedulerException e) {
            log.error("Failed to restart scheduler tasks", e);
        }
    }

    /**
     * Get the temporary "work" directory for expanded jar files
     *
     * @return temporary location for storing the libraries
     */
    public static File getLibCacheFolder() {
        // cache the location for all calls until OpenMRS is restarted
        if (libCacheFolder != null) {
            return libCacheFolderInitialized ? libCacheFolder : null;
        }

        synchronized (ModuleClassLoader.class) {
            libCacheFolder = new File(System.getProperty("java.io.tmpdir"),
                    System.currentTimeMillis() + LIBCACHESUFFIX);

            if (log.isDebugEnabled()) {
                log.debug("libraries cache folder is " + libCacheFolder);
            }

            File lockFile = new File(libCacheFolder, "lock");
            if (lockFile.exists()) {
                log.error("can't initialize libraries cache folder " + libCacheFolder
                        + " as lock file indicates that it" + " is owned by another openmrs instance");
                return null;
            }

            if (libCacheFolder.exists()) {
                // clean up and empty the folder if it exists (and is not locked)
                try {
                    OpenmrsUtil.deleteDirectory(libCacheFolder);
                } catch (IOException io) {
                    log.warn("Unable to delete: " + libCacheFolder.getName());
                }
            } else {
                // delete old lib cache folders
                deleteOldLibCaches(libCacheFolder);
                // otherwise just create the dir structure
                libCacheFolder.mkdirs();
            }

            // create the lock file in the lib cache folder to prevent other caches
            // from being created here
            try {
                if (!lockFile.createNewFile()) {
                    log.error("can't create lock file in JPF libraries cache folder" + libCacheFolder);
                    return null;
                }
            } catch (IOException ioe) {
                log.error("can't create lock file in JPF libraries cache folder " + libCacheFolder, ioe);
                return null;
            }

            // mark the lock and entire library cache to be deleted when the jvm exits
            lockFile.deleteOnExit();
            libCacheFolder.deleteOnExit();

            // mark the lib cache folder as ready
            libCacheFolderInitialized = true;
        }

        return libCacheFolder;
    }

    /**
     * Deletes the old lib cache folders that might not have been deleted when OpenMRS closed
     * 
     * @param libCacheFolder
     */
    public static void deleteOldLibCaches(File libCacheFolder) {

        FilenameFilter cacheDirFilter = new FilenameFilter() {

            @Override
            public boolean accept(File dir, String name) {
                return name.endsWith(LIBCACHESUFFIX);
            }
        };
        FilenameFilter lockFilter = new FilenameFilter() {

            @Override
            public boolean accept(File dir, String name) {
                return "lock".equals(name);
            }
        };
        File tempLocation = libCacheFolder.getParentFile();
        File[] listFiles = tempLocation.listFiles(cacheDirFilter);
        if (listFiles != null) {
            for (File cacheDir : listFiles) {
                //check if it is a directory, but is not the current lib cache
                if (cacheDir.isDirectory() && !cacheDir.equals(libCacheFolder)
                        && cacheDir.list(lockFilter).length == 0) {
                    // check if its not locked by another running openmrs instance
                    try {
                        OpenmrsUtil.deleteDirectory(cacheDir);
                    } catch (IOException io) {
                        log.warn("Unable to delete: " + cacheDir.getName());
                    }
                }
            }
        }
    }

    /**
     * Expand the given URL into the given folder
     *
     * @param result URL of the file to expand
     * @param folder File (directory) to place the expanded file
     * @return the URL at the expanded location
     */
    public static URL expandURL(URL result, File folder) {
        String extForm = result.toExternalForm();
        // trim out "jar:file:/ and ascii spaces"
        if (OpenmrsConstants.UNIX_BASED_OPERATING_SYSTEM) {
            extForm = extForm.replaceFirst("jar:file:", "").replaceAll("%20", " ");
        } else {
            extForm = extForm.replaceFirst("jar:file:/", "").replaceAll("%20", " ");
        }

        if (log.isDebugEnabled()) {
            log.debug("url external form: " + extForm);
        }

        int i = extForm.indexOf("!");
        String jarPath = extForm.substring(0, i);
        String filePath = extForm.substring(i + 2); // skip over both the '!' and the '/'

        if (log.isDebugEnabled()) {
            log.debug("jarPath: " + jarPath);
            log.debug("filePath: " + filePath);
        }

        File file = new File(folder, filePath);

        if (log.isDebugEnabled()) {
            log.debug("absolute path: " + file.getAbsolutePath());
        }

        try {
            // if the file has been expanded already, return that
            if (file.exists()) {
                return file.toURI().toURL();
            } else {
                // expand the url and return a url to the temp file
                File jarFile = new File(jarPath);
                if (!jarFile.exists()) {
                    log.warn("Cannot find jar at: " + jarFile + " for url: " + result);
                    return null;
                }

                ModuleUtil.expandJar(jarFile, folder, filePath, true);
                return file.toURI().toURL();
            }
        } catch (IOException io) {
            log.warn("Unable to expand url: " + result, io);
            return null;
        }
    }

    /**
     * This class exists solely so OpenmrsClassLoader can call the (should be static) method
     * <code>URLConnection.setDefaultUseCaches(Boolean)</code>. This causes jars opened to not be
     * locked (and allows for the webapp to be reloadable).
     */
    private class OpenmrsURLConnection extends URLConnection {

        public OpenmrsURLConnection() {
            super(null);
        }

        @Override
        public void connect() throws IOException {

        }

    }
}