org.openmrs.module.ModuleClassLoader.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.ModuleClassLoader.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.module;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandlerFactory;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.apache.commons.io.FileUtils;
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.util.OpenmrsClassLoader;
import org.openmrs.util.OpenmrsConstants;
import org.openmrs.util.OpenmrsUtil;

/**
 * Standard implementation of module class loader. <br>
 * Code adapted from the Java Plug-in Framework (JPF) - LGPL - Copyright (C)<br>
 * 2004-2006 Dmitry Olshansky
 */
public class ModuleClassLoader extends URLClassLoader {

    static Log log = LogFactory.getLog(ModuleClassLoader.class);

    private final Module module;

    private Module[] requiredModules;

    private Module[] awareOfModules;

    private Map<URI, File> libraryCache;

    private boolean probeParentLoaderLast = true;

    private Set<String> providedPackages = new LinkedHashSet<String>();

    private boolean disposed = false;

    /**
     * Holds a list of all classes for this classloader so that they can be cleaned up.
     * This is also used to fix: https://tickets.openmrs.org/browse/TRUNK-4053
     */
    private Map<String, Class<?>> loadedClasses = new HashMap<String, Class<?>>();

    /**
     * @param module Module
     * @param urls resources "managed" by this class loader
     * @param parent parent class loader
     * @param factory URL stream handler factory
     * @see URLClassLoader#URLClassLoader(java.net.URL[], java.lang.ClassLoader,
     *      java.net.URLStreamHandlerFactory)
     */
    protected ModuleClassLoader(final Module module, final List<URL> urls, final ClassLoader parent,
            final URLStreamHandlerFactory factory) {
        super(urls.toArray(new URL[urls.size()]), parent, factory);

        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");
        }

        if (log.isDebugEnabled()) {
            log.debug("URLs length: " + urls.size());
        }
        this.module = module;
        requiredModules = collectRequiredModuleImports(module);
        awareOfModules = collectAwareOfModuleImports(module);
        libraryCache = new WeakHashMap<URI, File>();
    }

    /**
     * @param module the <code>Module</code> to load
     * @param urls <code>List&lt;URL&gt;</code> of the resources "managed" by this class loader
     * @param parent parent <code>ClassLoader</code>
     * @see URLClassLoader#URLClassLoader(java.net.URL[], java.lang.ClassLoader)
     */
    protected ModuleClassLoader(final Module module, final List<URL> urls, final ClassLoader parent) {
        this(module, urls, parent, null);

        File devDir = ModuleUtil.getDevelopmentDirectory(module.getModuleId());
        if (devDir != null) {
            File dir = new File(devDir,
                    "api" + File.separator + "target" + File.separator + "classes" + File.separator);
            Collection<File> files = FileUtils.listFiles(dir, new String[] { "class" }, true);
            addClassFilePackages(files, dir.getAbsolutePath().length() + 1);

            dir = new File(devDir,
                    "omod" + File.separator + "target" + File.separator + "classes" + File.separator);
            files = FileUtils.listFiles(dir, new String[] { "class" }, true);
            addClassFilePackages(files, dir.getAbsolutePath().length() + 1);
        } else {
            for (URL url : urls) {
                providedPackages.addAll(ModuleUtil.getPackagesFromFile(OpenmrsUtil.url2file(url)));
            }
        }
    }

    private void addClassFilePackages(Collection<File> files, int dirLength) {
        for (File file : files) {
            String name = file.getAbsolutePath().substring(dirLength);
            Integer indexOfLastSlash = name.lastIndexOf(File.separator);
            if (indexOfLastSlash > 0) {
                String packageName = name.substring(0, indexOfLastSlash);
                packageName = packageName.replaceAll(File.separator, ".");
                providedPackages.add(packageName);
            }
        }
    }

    /**
     * @param module the <code>Module</code> to load
     * @param urls <code>List&lt;URL&gt;</code> of thee resources "managed" by this class loader
     * @see URLClassLoader#URLClassLoader(java.net.URL[])
     */
    protected ModuleClassLoader(final Module module, final List<URL> urls) {
        this(module, urls, null);
    }

    /**
     * Creates class instance configured to load classes and resources for given module.
     *
     * @param module the <code>Module</code> to load
     * @param parent parent <code>ClassLoader</code>
     */
    public ModuleClassLoader(final Module module, final ClassLoader parent) {
        this(module, getUrls(module), parent);
    }

    /**
     * @return returns this classloader's module
     */
    public Module getModule() {
        return module;
    }

    public boolean isDisposed() {
        return disposed;
    }

    /**
     * Get the base class url of the given <code>cls</code>. Used for checking against system class
     * loads vs classloader loads
     *
     * @param cls Class name
     * @return URL to the class
     */
    private static URL getClassBaseUrl(final Class<?> cls) {
        ProtectionDomain pd = cls.getProtectionDomain();

        if (pd != null) {
            CodeSource cs = pd.getCodeSource();
            if (cs != null) {
                return cs.getLocation();
            }

        }

        return null;
    }

    /**
     * Get all urls for all files in the given <code>module</code>
     *
     * @param module Module in which to look
     * @return List&lt;URL&gt; of all urls found (and cached) in the module
     */
    private static List<URL> getUrls(final Module module) {
        List<URL> result = new LinkedList<URL>();

        //if in dev mode, add development folder to the classpath
        File devDir = ModuleUtil.getDevelopmentDirectory(module.getModuleId());
        try {
            if (devDir != null) {
                File dir = new File(devDir,
                        "omod" + File.separator + "target" + File.separator + "classes" + File.separator);
                result.add(dir.toURI().toURL());

                dir = new File(devDir,
                        "api" + File.separator + "target" + File.separator + "classes" + File.separator);
                result.add(dir.toURI().toURL());
            }
        } catch (MalformedURLException ex) {
            log.error("Failed to add development folder to the classpath", ex);
        }

        File tmpModuleDir = getLibCacheFolderForModule(module);

        //add module jar to classpath only if we are not in dev mode
        if (devDir == null) {
            File tmpModuleJar = new File(tmpModuleDir, module.getModuleId() + ".jar");

            if (!tmpModuleJar.exists()) {
                try {
                    tmpModuleJar.createNewFile();
                } catch (IOException io) {
                    log.warn("Unable to create tmpModuleFile", io);
                }
            }

            // copy the module jar into that temporary folder
            FileInputStream in = null;
            FileOutputStream out = null;
            try {
                in = new FileInputStream(module.getFile());
                out = new FileOutputStream(tmpModuleJar);
                OpenmrsUtil.copyFile(in, out);
            } catch (IOException io) {
                log.warn("Unable to copy tmpModuleFile", io);
            } finally {
                try {
                    in.close();
                } catch (Exception e) {
                    /* pass */}
                try {
                    out.close();
                } catch (Exception e) {
                    /* pass */}
            }

            // add the module jar as a url in the classpath of the classloader
            URL moduleFileURL = null;
            try {
                moduleFileURL = ModuleUtil.file2url(tmpModuleJar);
                result.add(moduleFileURL);
            } catch (MalformedURLException e) {
                log.warn("Unable to add files from module to URL list: " + module.getModuleId(), e);
            }
        }

        // add each defined jar in the /lib folder, add as a url in the classpath of the classloader
        try {
            if (log.isDebugEnabled()) {
                log.debug("Expanding /lib folder in module");
            }

            ModuleUtil.expandJar(module.getFile(), tmpModuleDir, "lib", true);
            File libdir = new File(tmpModuleDir, "lib");

            if (libdir != null && libdir.exists()) {
                Map<String, String> startedRelatedModules = new HashMap<String, String>();
                for (Module requiredModule : collectRequiredModuleImports(module)) {
                    startedRelatedModules.put(requiredModule.getModuleId(), requiredModule.getVersion());
                }
                for (Module awareOfModule : collectAwareOfModuleImports(module)) {
                    startedRelatedModules.put(awareOfModule.getModuleId(), awareOfModule.getVersion());
                }

                // recursively get files
                Collection<File> files = (Collection<File>) FileUtils.listFiles(libdir, new String[] { "jar" },
                        true);
                for (File file : files) {

                    //if in dev mode, do not put the api jar file in the class path
                    if (devDir != null) {
                        if (file.getName().equals(module.getModuleId() + "-api-" + module.getVersion() + ".jar")) {
                            continue; //e.g uiframework-api-3.3-SNAPSHOT.jar
                        }
                    }

                    URL fileUrl = ModuleUtil.file2url(file);

                    boolean include = shouldResourceBeIncluded(module, fileUrl,
                            OpenmrsConstants.OPENMRS_VERSION_SHORT, startedRelatedModules);

                    if (include) {
                        if (log.isDebugEnabled()) {
                            log.debug("Including file in classpath: " + fileUrl);
                        }
                        result.add(fileUrl);
                    } else {
                        if (log.isDebugEnabled()) {
                            log.debug("Excluding file from classpath: " + fileUrl);
                        }
                    }
                }
            }
        } catch (MalformedURLException e) {
            log.warn("Error while adding module 'lib' folder to URL result list");
        } catch (IOException io) {
            log.warn("Error while expanding lib folder", io);
        }

        // add each xml document to the url list

        return result;
    }

    /**
     * Determines whether or not the given resource should be available on the classpath based on
     * OpenMRS version and/or modules' version. It uses the conditionalResources section specified in 
     * config.xml. Resources that are not mentioned as conditional resources are included by 
     * default. All conditions for a conditional resource to be included must match.
     *
     * @param module
     * @param fileUrl
     * @return true if it should be included
     * @should return true if file matches and openmrs version matches
     * @should return false if file matches but openmrs version does not
     * @should return true if file does not match and openmrs version does not match
     * @should return true if file matches and module version matches
     * @should return false if file matches and module version does not match
     * @should return false if file matches and openmrs version matches but module version does not 
     *          match
     * @should return false if file matches and module not found
     * @should return true if file does not match and module version does not match
     */
    static boolean shouldResourceBeIncluded(Module module, URL fileUrl, String openmrsVersion,
            Map<String, String> startedRelatedModules) {
        boolean include = true; //all resources are included by default

        for (ModuleConditionalResource conditionalResource : module.getConditionalResources()) {
            if (fileUrl.getPath().matches(".*" + conditionalResource.getPath() + "$")) {
                include = false; //if a resource matches a path of contidionalResource then it must meet all conditions

                if (StringUtils.isNotBlank(conditionalResource.getOpenmrsPlatformVersion())) { //openmrsPlatformVersion is optional
                    include = ModuleUtil.matchRequiredVersions(openmrsVersion,
                            conditionalResource.getOpenmrsPlatformVersion());

                    if (!include) {
                        return false;
                    }
                }

                if (conditionalResource.getModules() != null) { //modules are optional
                    for (ModuleConditionalResource.ModuleAndVersion conditionalModuleResource : conditionalResource
                            .getModules()) {
                        String moduleVersion = startedRelatedModules.get(conditionalModuleResource.getModuleId());
                        if (moduleVersion != null) {
                            include = ModuleUtil.matchRequiredVersions(moduleVersion,
                                    conditionalModuleResource.getVersion());

                            if (!include) {
                                return false;
                            }
                        }
                    }

                }
            }
        }

        return include;
    }

    /**
     * Get the library cache folder for the given module. Each module has a different cache folder
     * to ease cleanup when unloading a module while openmrs is running
     *
     * @param module Module which the cache will be used for
     * @return File directory where the files will be placed
     */
    public static File getLibCacheFolderForModule(Module module) {
        File tmpModuleDir = new File(OpenmrsClassLoader.getLibCacheFolder(), module.getModuleId());

        // each module gets its own folder named /moduleId/
        if (!tmpModuleDir.exists()) {
            tmpModuleDir.mkdir();
            tmpModuleDir.deleteOnExit();
        }
        return tmpModuleDir;
    }

    /**
     * Get all urls for the given <code>module</code> that are not already in the
     * <code>existingUrls</code>
     *
     * @param module Module in which to get urls
     * @param existingUrls Array of URLs to skip
     * @return List&lt;URL&gt; of new unique urls
     * @see #getUrls(Module)
     */
    private static List<URL> getUrls(final Module module, final URL[] existingUrls) {
        List<URL> urls = Arrays.asList(existingUrls);
        List<URL> result = new LinkedList<URL>();
        for (URL url : getUrls(module)) {
            if (!urls.contains(url)) {
                result.add(url);
            }
        }
        return result;
    }

    /**
     * Get and cache the imports for this module. The imports should just be the modules that set as
     * "required" by this module
     */
    protected static Module[] collectRequiredModuleImports(Module module) {
        // collect imported modules (exclude duplicates)
        Map<String, Module> publicImportsMap = new WeakHashMap<String, Module>(); //<module ID, Module>

        for (String moduleId : ModuleConstants.CORE_MODULES.keySet()) {
            Module coreModule = ModuleFactory.getModuleById(moduleId);

            if (coreModule == null && !ModuleUtil.ignoreCoreModules()) {
                log.error("Unable to find an openmrs core loaded module with id: " + moduleId);
                throw new APIException("Module.error.shouldNotBeHere", (Object[]) null);
            }

            // if this is already the classloader for one of the core modules, don't put it on the import list
            if (coreModule != null && !moduleId.equals(module.getModuleId())) {
                publicImportsMap.put(moduleId, coreModule);
            }
        }

        for (String requiredPackage : module.getRequiredModules()) {
            Module requiredModule = ModuleFactory.getModuleByPackage(requiredPackage);
            if (ModuleFactory.isModuleStarted(requiredModule)) {
                publicImportsMap.put(requiredModule.getModuleId(), requiredModule);
            }
        }
        return publicImportsMap.values().toArray(new Module[publicImportsMap.size()]);

    }

    /**
     * Get and cache the imports for this module. The imports should just be the modules that set as
     * "aware of" by this module
     */
    protected static Module[] collectAwareOfModuleImports(Module module) {
        // collect imported modules (exclude duplicates)
        Map<String, Module> publicImportsMap = new WeakHashMap<String, Module>(); //<module ID, Module>

        for (String awareOfPackage : module.getAwareOfModules()) {
            Module awareOfModule = ModuleFactory.getModuleByPackage(awareOfPackage);
            if (ModuleFactory.isModuleStarted(awareOfModule)) {
                publicImportsMap.put(awareOfModule.getModuleId(), awareOfModule);
            }
        }
        return publicImportsMap.values().toArray(new Module[publicImportsMap.size()]);

    }

    /**
     * @see org.openmrs.module.ModuleClassLoader#modulesSetChanged()
     */
    protected void modulesSetChanged() {
        List<URL> newUrls = getUrls(getModule(), getURLs());
        for (URL u : newUrls) {
            addURL(u);
        }

        if (log.isDebugEnabled()) {
            StringBuffer buf = new StringBuffer();
            buf.append("New code URL's populated for module " + getModule() + ":\r\n");
            for (URL u : newUrls) {
                buf.append("\t");
                buf.append(u);
                buf.append("\r\n");
            }
            log.debug(buf.toString());
        }
        requiredModules = collectRequiredModuleImports(getModule());
        awareOfModules = collectAwareOfModuleImports(getModule());
        for (Iterator<Map.Entry<URI, File>> it = libraryCache.entrySet().iterator(); it.hasNext();) {
            if (it.next().getValue() == null) {
                it.remove();
            }
        }
    }

    /**
     * @see org.openmrs.module.ModuleFactory#stopModule(Module, boolean)
     */
    public void dispose() {
        if (log.isDebugEnabled())
            log.debug("Disposing of ModuleClassLoader: " + this);

        for (Iterator<File> it = libraryCache.values().iterator(); it.hasNext();) {
            it.next().delete();
        }

        libraryCache.clear();
        requiredModules = null;
        awareOfModules = null;
        disposed = true;
    }

    /**
     * Allow the probe parent loader last variable to be set. Usually this is set to true to allow
     * modules to override and create their own classes
     *
     * @param value boolean true/false whether or not to look at the parent classloader last
     */
    public void setProbeParentLoaderLast(final boolean value) {
        probeParentLoaderLast = value;
    }

    /**
     * @see java.lang.ClassLoader#loadClass(java.lang.String, boolean)
     */
    @Override
    protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
        // Check if the class has already been loaded by this class loader
        Class<?> result = findLoadedClass(name);
        if (result == null) {
            if (probeParentLoaderLast) {
                try {
                    result = loadClass(name, resolve, this, null);
                } catch (ClassNotFoundException cnfe) {
                    // Continue trying...
                }

                if (result == null && getParent() != null) {
                    result = getParent().loadClass(name);
                }
            } else {
                try {
                    if (getParent() != null) {
                        result = getParent().loadClass(name);
                    }
                } catch (ClassNotFoundException cnfe) {
                    // Continue trying...
                }

                if (result == null) {
                    result = loadClass(name, resolve, this, null);
                }
            }
        }

        if (resolve) {
            resolveClass(result);
        }

        return result;
    }

    /**
     * Custom loadClass implementation to allow for loading from a given ModuleClassLoader and skip
     * the modules that have been tried already
     * 
     * @param name String path and name of the class to load
     * @param resolve boolean whether or not to resolve this class before returning
     * @param requestor ModuleClassLoader with which to try loading
     * @param seenModules Set&lt;String&gt; moduleIds that have been tried already
     * @return Class that has been loaded
     * @throws ClassNotFoundException if no class found
     */
    protected synchronized Class<?> loadClass(final String name, final boolean resolve,
            final ModuleClassLoader requestor, Set<String> seenModules) throws ClassNotFoundException {

        if (log.isTraceEnabled()) {
            log.trace("Loading " + name + " " + getModule() + ", seenModules: " + seenModules + ", requestor: "
                    + requestor + ", resolve? " + resolve);
            StringBuilder output = new StringBuilder();
            for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
                if (element.getClassName().contains("openmrs")) {
                    output.append("+ ");
                }
                output.append(element);
                output.append("\n");
            }
            log.trace("Stacktrace: " + output.toString());
        }

        // Check if we already tried this class loader
        if ((seenModules != null) && seenModules.contains(getModule().getModuleId())) {
            throw new ClassNotFoundException("Can't load class " + name + " from module "
                    + getModule().getModuleId() + ". It has been tried before.");
        }

        // Make sure the module is started
        if ((this != requestor) && !ModuleFactory.isModuleStarted(getModule())) {
            String msg = "Can't load class " + name + ", because module " + getModule().getModuleId()
                    + " is not yet started.";
            log.warn(msg);

            throw new ClassNotFoundException(msg);
        }

        // Check if the class has already been loaded by this class loader
        Class<?> result = findLoadedClass(name);

        // Try loading the class with this class loader 
        if (result == null) {
            try {
                result = findClass(name);
            } catch (ClassNotFoundException e) {
                // Continue trying...
            }
        }

        // We were able to "find" a class
        if (result != null) {
            checkClassVisibility(result, requestor);

            return result;
        }

        // Look through this module's imports to see if the class
        // can be loaded from them.

        if (seenModules == null) {
            seenModules = new HashSet<String>();
        }

        // Add this module to the list of modules we've tried already
        seenModules.add(getModule().getModuleId());

        List<Module> importedModules = new ArrayList<Module>();
        if (requiredModules != null) {
            Collections.addAll(importedModules, requiredModules);
        }
        if (awareOfModules != null) {
            Collections.addAll(importedModules, awareOfModules);
        }

        for (Module importedModule : importedModules) {
            if (seenModules.contains(importedModule.getModuleId())) {
                continue;
            }

            ModuleClassLoader moduleClassLoader = ModuleFactory.getModuleClassLoader(importedModule);

            // Module class loader may be null if module has not been started yet
            if (moduleClassLoader != null) {
                try {
                    result = moduleClassLoader.loadClass(name, resolve, requestor, seenModules);

                    return result;
                } catch (ClassNotFoundException e) {
                    // Continue trying...
                }
            }
        }

        throw new ClassNotFoundException(name);
    }

    /**
     * Checking the given class's visibility in this module
     *
     * @param cls Class to check
     * @param requestor ModuleClassLoader to check against
     * @throws ClassNotFoundException
     */
    protected void checkClassVisibility(final Class<?> cls, final ModuleClassLoader requestor)
            throws ClassNotFoundException {

        if (this == requestor) {
            return;
        }

        URL lib = getClassBaseUrl(cls);

        if (lib == null) {
            return; // cls is a system class
        }

        ClassLoader loader = cls.getClassLoader();

        if (!(loader instanceof ModuleClassLoader)) {
            return;
        }

        if (loader != this) {
            ((ModuleClassLoader) loader).checkClassVisibility(cls, requestor);
        }
    }

    /**
     * @see java.lang.ClassLoader#findLibrary(java.lang.String)
     */
    @Override
    protected String findLibrary(final String name) {
        if ((name == null) || "".equals(name.trim())) {
            return null;
        }

        if (log.isTraceEnabled()) {
            log.trace("findLibrary(String): name=" + name + ", this=" + this);
        }
        String libname = System.mapLibraryName(name);
        String result = null;
        //TODO
        //PathResolver pathResolver = ModuleFactory.getPathResolver();
        //      for (Library lib : getModule().getLibraries()) {
        //         if (lib.isCodeLibrary()) {
        //            continue;
        //         }
        //         URL libUrl = null; //pathResolver.resolvePath(lib, lib.getPath() + libname);
        //         if (log.isDebugEnabled()) {
        //            log.debug("findLibrary(String): trying URL " + libUrl);
        //         }
        //         File libFile = OpenmrsUtil.url2file(libUrl);
        //         if (libFile != null) {
        //            if (log.isDebugEnabled()) {
        //               log.debug("findLibrary(String): URL " + libUrl
        //                     + " resolved as local file " + libFile);
        //            }
        //            if (libFile.isFile()) {
        //               result = libFile.getAbsolutePath();
        //               break;
        //            }
        //            continue;
        //         }
        //         // we have some kind of non-local URL
        //         // try to copy it to local temporary file
        //         libFile = (File) libraryCache.get(libUrl);
        //         if (libFile != null) {
        //            if (libFile.isFile()) {
        //               result = libFile.getAbsolutePath();
        //               break;
        //            }
        //            libraryCache.remove(libUrl);
        //         }
        //         if (libraryCache.containsKey(libUrl)) {
        //            // already tried to cache this library
        //            break;
        //         }
        //         libFile = cacheLibrary(libUrl, libname);
        //         if (libFile != null) {
        //            result = libFile.getAbsolutePath();
        //            break;
        //         }
        //      }
        if (log.isTraceEnabled()) {
            log.trace("findLibrary(String): name=" + name + ", libname=" + libname + ", result=" + result
                    + ", this=" + this);
        }

        return result;
    }

    /**
     * Saves the given library in the openmrs cache. This prevents locking of jars/files by servlet
     * container
     *
     * @param libUrl URL to the library/jar file
     * @param libname name of the jar that will be the name of the cached file
     * @return file that is now copied and cached
     */
    protected File cacheLibrary(final URL libUrl, final String libname) {
        File cacheFolder = OpenmrsClassLoader.getLibCacheFolder();

        URI libUri;
        try {
            libUri = libUrl.toURI();
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(libUrl.getPath() + " is not a valid URI", e);
        }

        if (libraryCache.containsKey(libUri)) {
            return libraryCache.get(libUri);
        }

        File result = null;
        try {
            if (cacheFolder == null) {
                throw new IOException("can't initialize libraries cache folder");
            }

            // create the directory to hold the jar's files
            File libCacheModuleFolder = new File(cacheFolder, getModule().getModuleId());

            // error while creating the file
            if (!libCacheModuleFolder.exists() && !libCacheModuleFolder.mkdirs()) {
                throw new IOException("can't create cache folder " + libCacheModuleFolder);
            }

            // directory within the specific folder within the cache
            result = new File(libCacheModuleFolder, libname);

            // copy the file over to the cache
            InputStream in = OpenmrsUtil.getResourceInputStream(libUrl);
            try {
                FileOutputStream fileOut = new FileOutputStream(result);
                OutputStream out = new BufferedOutputStream(fileOut);
                try {
                    OpenmrsUtil.copyFile(in, out);
                } finally {
                    try {
                        out.close();
                    } catch (Exception e) {
                        /* pass */}
                    try {
                        fileOut.close();
                    } catch (Exception e) {
                    }
                }
            } finally {
                try {
                    in.close();
                } catch (Exception e) {
                    /* pass */}
            }

            // save a link to the cached file
            libraryCache.put(libUri, result);

            if (log.isDebugEnabled()) {
                log.debug("library " + libname + " successfully cached from URL " + libUrl
                        + " and saved to local file " + result);
            }

        } catch (IOException ioe) {
            log.error("can't cache library " + libname + " from URL " + libUrl, ioe);
            libraryCache.put(libUri, null);
            result = null;
        }

        return result;
    }

    /**
     * If a resource is found within a jar, that jar URL is converted to a temporary file and a URL
     * to that is returned
     *
     * @see java.lang.ClassLoader#findResource(java.lang.String)
     */
    @Override
    public URL findResource(final String name) {
        URL result = findResource(name, this, null);

        return expandIfNecessary(result);
    }

    /**
     * @see java.lang.ClassLoader#findResources(java.lang.String)
     */
    @Override
    public Enumeration<URL> findResources(final String name) throws IOException {
        List<URL> result = new LinkedList<URL>();
        findResources(result, name, this, null);

        // expand all of the "jar" urls
        for (URL url : result) {
            url = expandIfNecessary(url);
        }

        return Collections.enumeration(result);
    }

    /**
     * Find a resource (image, file, etc) in the module structure
     *
     * @param name String path and name of the file
     * @param requestor ModuleClassLoader in which to look
     * @param seenModules Set&lt;String&gt; modules that have been checked already
     * @return URL to resource
     * @see #findResource(String)
     */
    protected URL findResource(final String name, final ModuleClassLoader requestor, Set<String> seenModules) {
        if (log.isTraceEnabled() && name != null && name.contains("starter")) {
            if (seenModules != null) {
                log.trace("seenModules.size: " + seenModules.size());
            }
            log.trace("name: " + name);
            for (URL url : getURLs()) {
                log.trace("url: " + url);
            }
        }

        if ((seenModules != null) && seenModules.contains(getModule().getModuleId())) {
            return null;
        }

        URL result = super.findResource(name);
        if (result != null) { // found resource in this module class path
            if (isResourceVisible(name, result, requestor)) {
                return result;
            }
            log.debug("Resource is not visible");
            return null;
        }

        if (seenModules == null) {
            seenModules = new HashSet<String>();
        }

        seenModules.add(getModule().getModuleId());

        if (requiredModules != null) {
            for (Module publicImport : requiredModules) {
                if (seenModules.contains(publicImport.getModuleId()))
                    continue;

                ModuleClassLoader mcl = ModuleFactory.getModuleClassLoader(publicImport);

                if (mcl != null) {
                    result = mcl.findResource(name, requestor, seenModules);
                }

                if (result != null) {
                    return result; // found resource in required module
                }
            }
        }

        //look through the aware of modules.
        for (Module publicImport : awareOfModules) {
            if (seenModules.contains(publicImport.getModuleId())) {
                continue;
            }

            ModuleClassLoader mcl = ModuleFactory.getModuleClassLoader(publicImport);

            if (mcl != null) {
                result = mcl.findResource(name, requestor, seenModules);
            }

            if (result != null) {
                return result; // found resource in aware of module
            }
        }

        return result;
    }

    /**
     * Find all occurrences of a resource (image, file, etc) in the module structure
     *
     * @param result URL of the file found
     * @param name String path and name of the file to find
     * @param requestor ModuleClassLoader in which to start
     * @param seenModules Set&lt;String&gt; moduleIds that have been checked already
     * @throws IOException
     * @see #findResources(String)
     * @see #findResource(String, ModuleClassLoader, Set)
     */
    protected void findResources(final List<URL> result, final String name, final ModuleClassLoader requestor,
            Set<String> seenModules) throws IOException {
        if ((seenModules != null) && seenModules.contains(getModule().getModuleId())) {
            return;
        }
        for (Enumeration<URL> enm = super.findResources(name); enm.hasMoreElements();) {
            URL url = enm.nextElement();
            if (isResourceVisible(name, url, requestor)) {
                result.add(url);
            }
        }
        //      if (resourceLoader != null) {
        //         for (Enumeration enm = resourceLoader.findResources(name);
        //               enm.hasMoreElements();) {
        //            URL url = (URL) enm.nextElement();
        //            if (isResourceVisible(name, url, requestor)) {
        //               result.add(url);
        //            }
        //         }
        //      }
        if (seenModules == null) {
            seenModules = new HashSet<String>();
        }
        seenModules.add(getModule().getModuleId());
        if (requiredModules != null) {
            for (Module publicImport : requiredModules) {
                if (seenModules.contains(publicImport.getModuleId())) {
                    continue;
                }

                ModuleClassLoader mcl = ModuleFactory.getModuleClassLoader(publicImport);

                if (mcl != null) {
                    mcl.findResources(result, name, requestor, seenModules);
                }
            }
        }

        //look through the aware of modules.
        for (Module publicImport : awareOfModules) {
            if (seenModules.contains(publicImport.getModuleId())) {
                continue;
            }

            ModuleClassLoader mcl = ModuleFactory.getModuleClassLoader(publicImport);

            if (mcl != null) {
                mcl.findResources(result, name, requestor, seenModules);
            }
        }
    }

    /**
     * Check if the given resource (image, file, etc) is visible by this classloader
     *
     * @param name String path and name to check
     * @param url URL of the library file
     * @param requestor ModuleClassLoader in which to look
     * @return true/false whether this resource is visibile by this classloader
     */
    protected boolean isResourceVisible(final String name, final URL url, final ModuleClassLoader requestor) {
        if (this == requestor) {
            return true;
        }
        try {
            String file = url.getFile();
            new URL(url.getProtocol(), url.getHost(), file.substring(0, file.length() - name.length()));
        } catch (MalformedURLException mue) {
            log.error("can't get resource library URL", mue);
            return false;
        }

        return true;
    }

    /**
     * Expands the URL into the temporary folder if the URL points to a resource inside of a jar
     * file
     *
     * @param result
     * @return URL to the expanded result or null if an error occurred
     */
    private URL expandIfNecessary(URL result) {
        if (result == null || !"jar".equals(result.getProtocol())) {
            return result;
        }

        File tmpFolder = getLibCacheFolderForModule(module);

        return OpenmrsClassLoader.expandURL(result, tmpFolder);
    }

    /**
     * Contains all class packages provided by the module, including those contained in jars.
     * <p>
     * It is used by {@link OpenmrsClassLoader#loadClass(String, boolean)} and in particular 
     * {@link ModuleFactory#getModuleClassLoadersForPackage(String)} to quickly find
     * possible loaders for the given class. Although it takes some time to extract all provided
     * packages from a module, it pays off when loading classes. It is much faster to query a map of
     * packages than iterating over all class loaders to find which one to use.
     * 
     * @return the provided packages
     */
    public Set<String> getProvidedPackages() {
        return providedPackages;
    }

    /**
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "{ModuleClassLoader: uid=" + System.identityHashCode(this) + "; " + module + "}";
    }

}