ModuleClassLoader.java

/**
 * The contents of this file are subject to the OpenMRS Public License
 * Version 1.0 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://license.openmrs.org
 *
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations
 * under the License.
 *
 * Copyright (C) OpenMRS, LLC.  All Rights Reserved.
 */
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.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandlerFactory;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
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.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.api.APIException;
import org.openmrs.util.OpenmrsClassLoader;
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<URL, File> libraryCache;
	
	private boolean probeParentLoaderLast = true;
	
	private Set<String> additionalPackages = new LinkedHashSet<String>();
	
	/**
	 * @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 (log.isDebugEnabled())
			log.debug("URLs length: " + urls.size());
		
		this.module = module;
		collectRequiredModuleImports();
		collectAwareOfModuleImports();
		collectFilters();
		libraryCache = new WeakHashMap<URL, File>();
	}
	
	/**
	 * @param module the <code>Module</code> to load
	 * @param urls <code>List<URL></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);
		
		for (URL url : urls) {
			addAllAdditionalPackages(ModuleUtil.getPackagesFromFile(OpenmrsUtil.url2file(url)));
		}
	}
	
	/**
	 * @param module the <code>Module</code> to load
	 * @param urls <code>List<URL></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;
	}
	
	/**
	 * 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<URL> of all urls found (and cached) in the module
	 */
	private static List<URL> getUrls(final Module module) {
		List<URL> result = new LinkedList<URL>();
		
		File tmpModuleDir = getLibCacheFolderForModule(module);
		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()) {
				// recursively get files
				Collection<File> files = (Collection<File>) FileUtils.listFiles(libdir, new String[] { "jar" }, true);
				for (File file : files) {
					if (log.isDebugEnabled())
						log.debug("Adding file to results: " + file.getAbsolutePath());
					result.add(ModuleUtil.file2url(file));
				}
			}
		}
		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;
	}
	
	/**
	 * 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<URL> 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 void collectRequiredModuleImports() {
		// collect imported modules (exclude duplicates)
		Map<String, Module> publicImportsMap = new WeakHashMap<String, Module>(); //<module ID, Module>
		
		for (String moduleId : ModuleConstants.CORE_MODULES.keySet()) {
			Module module = ModuleFactory.getModuleById(moduleId);
			
			if (module == null && !ModuleUtil.ignoreCoreModules()) {
				log.error("Unable to find an openmrs core loaded module with id: " + moduleId);
				throw new APIException(
				        "Should not be here.  All 'core' required modules by the api should be started and their classloaders should be available");
			}
			
			// if this is already the classloader for one of the core modules, don't put it on the import list
			if (module != null && !moduleId.equals(this.getModule().getModuleId())) {
				publicImportsMap.put(moduleId, module);
			}
		}
		
		for (String requiredPackage : getModule().getRequiredModules()) {
			Module requiredModule = ModuleFactory.getModuleByPackage(requiredPackage);
			if (ModuleFactory.isModuleStarted(requiredModule)) {
				publicImportsMap.put(requiredModule.getModuleId(), requiredModule);
			}
		}
		requiredModules = 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 void collectAwareOfModuleImports() {
		// collect imported modules (exclude duplicates)
		Map<String, Module> publicImportsMap = new WeakHashMap<String, Module>(); //<module ID, Module>
		
		for (String awareOfPackage : getModule().getAwareOfModules()) {
			Module awareOfModule = ModuleFactory.getModuleByPackage(awareOfPackage);
			if (ModuleFactory.isModuleStarted(awareOfModule)) {
				publicImportsMap.put(awareOfModule.getModuleId(), awareOfModule);
			}
		}
		awareOfModules = publicImportsMap.values().toArray(new Module[publicImportsMap.size()]);
		
	}
	
	/**
	 * Get and cache the filters for this module (not currently implemented)
	 */
	protected void collectFilters() {
		//		if (resourceFilters == null) {
		//			resourceFilters = new WeakHashMap<URL, ResourceFilter>();
		//		} else {
		//			resourceFilters.clear();
		//		}
		
		// TODO even need to iterate over libraries here?
		//for (Library lib : getModule().getLibraries()) {
		//resourceFilters.put(
		//		ModuleFactory.getPathResolver().resolvePath(lib,
		//				lib.getPath()), new ResourceFilter(lib));
		//}
	}
	
	/**
	 * @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());
		}
		collectRequiredModuleImports();
		collectAwareOfModuleImports();
		// repopulate resource URLs
		//resourceLoader = ModuleResourceLoader.get(getModule());
		collectFilters();
		for (Iterator<Map.Entry<URL, 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();
		//resourceFilters.clear();
		requiredModules = null;
		awareOfModules = null;
		//resourceLoader = null;
	}
	
	/**
	 * 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 {
		Class<?> result = null;
		if (probeParentLoaderLast) {
			try {
				result = loadClass(name, resolve, this, null);
			}
			catch (ClassNotFoundException cnfe) {
				if (getParent() != null)
					result = getParent().loadClass(name);
			}
			catch (NullPointerException e) {
				log.debug("Error while attempting to load class: " + name + " from: " + this.toString());
			}
			if (result == null) {
				if (getParent() != null)
					result = getParent().loadClass(name);
			}
		} else {
			try {
				if (getParent() != null)
					result = getParent().loadClass(name);
			}
			catch (ClassNotFoundException cnfe) {
				result = loadClass(name, resolve, this, null);
			}
		}
		
		if (result != null)
			return result;
		
		throw new ClassNotFoundException(name);
	}
	
	/**
	 * 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<String> moduleIds that have been tried already
	 * @return Class that has been loaded or null if none
	 * @throws ClassNotFoundException if no class found
	 */
	protected 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());
		}
		
		if ((seenModules != null) && seenModules.contains(getModule().getModuleId())) {
			return null;
		}
		
		// make sure the module is started
		if ((this != requestor) && !ModuleFactory.isModuleStarted(getModule())) {
			String msg = "can't load class " + name + ", module " + getModule() + " is not started yet";
			log.warn(msg);
			
			throw new ClassNotFoundException(msg);
		}
		
		// the class ultimately returned (if found)
		Class<?> result = null;
		
		result = findLoadedClass(name);
		
		if (result != null) {
			checkClassVisibility(result, requestor);
			
			/*if (resolve) {
				resolveClass(result);
			}*/

			// found an already loaded class in this moduleclassloader
			return result;
		}
		
		// we didn't find a loaded class and this isn't a class
		// from another module
		try {
			result = findClass(name);
		}
		catch (LinkageError le) {
			throw le;
		}
		catch (ClassNotFoundException cnfe) {
			// ignore
		}
		
		// we were able to "find" a class
		if (result != null) {
			checkClassVisibility(result, requestor);
			
			if (resolve) {
				resolveClass(result);
			}
			
			return result; // found class in this module
		}
		
		// initialize the array if need be
		if (seenModules == null)
			seenModules = new HashSet<String>();
		
		// add this module to the list of modules we've tried already
		seenModules.add(getModule().getModuleId());
		
		// look through this module's imports to see if the class
		// can be loaded from them
		if (requiredModules != null) {
			for (Module publicImport : requiredModules) {
				if (seenModules.contains(publicImport.getModuleId()))
					continue;
				
				ModuleClassLoader mcl = ModuleFactory.getModuleClassLoader(publicImport);
				
				// the mcl will be null if a required module isn't started yet (like at openmrs startup)
				if (mcl != null) {
					result = mcl.loadClass(name, resolve, requestor, seenModules);
				}
				
				if (result != null) {
					/*if (resolve) {
						resolveClass(result);
					}*/
					return result; // found class in required module
				}
			}
		}
		
		// look through this module's aware of imports to see if the class
		// can be loaded from them.
		for (Module publicImport : awareOfModules) {
			if (seenModules.contains(publicImport.getModuleId()))
				continue;
			
			ModuleClassLoader mcl = ModuleFactory.getModuleClassLoader(publicImport);
			
			// the mcl will be null if an aware of module isn't started yet (like at openmrs startup)
			if (mcl != null) {
				result = mcl.loadClass(name, resolve, requestor, seenModules);
			}
			
			if (result != null) {
				/*if (resolve) {
					resolveClass(result);
				}*/
				return result; // found class in aware of module
			}
		}
		
		return result;
	}
	
	/**
	 * 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);
		} else {
			//			ResourceFilter filter = (ResourceFilter) resourceFilters.get(lib);
			//			if (filter == null) {
			//				log.warn("class not visible, no class filter found, lib=" + lib
			//						+ ", class=" + cls + ", this=" + this
			//						+ ", requestor=" + requestor);
			//				throw new ClassNotFoundException("class "
			//						+ cls.getName() + " is not visible for module "
			//						+ requestor.getModule().getModuleId()
			//						+ ", no filter found for library " + lib);
			//			}
			//			if (!filter.isClassVisible(cls.getName())) {
			//				log.warn("class not visible, lib=" + lib
			//						+ ", class=" + cls + ", this=" + this
			//						+ ", requestor=" + requestor);
			//				throw new ClassNotFoundException("class "
			//						+ cls.getName() + " is not visible for module "
			//						+ requestor.getModule().getModuleId());
			//			}
		}
	}
	
	/**
	 * @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();
		if (libraryCache.containsKey(libUrl)) {
			return libraryCache.get(libUrl);
		}
		
		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(libUrl, 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(libUrl, 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<String> 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()) {
			if (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 (resourceLoader != null) {
		//			result = resourceLoader.findResource(name);
		//			log.debug("Result from resourceLoader: " + result);
		//			if (result != null) { // found resource in this module resource libraries
		//				if (isResourceVisible(name, result, requestor)) {
		//					return result;
		//				}
		//				log.debug("result from resourceLoader 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
				}
			}
		} else {
			// do something here so I can put a breakpoint in
			result = result;
		}
		
		//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<String> 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) {
		/*log.debug("isResourceVisible(URL, ModuleClassLoader): URL=" + url
				+ ", requestor=" + requestor);*/
		if (this == requestor) {
			return true;
		}
		@SuppressWarnings("unused")
		URL lib;
		try {
			String file = url.getFile();
			lib = 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;
		}
		//		ResourceFilter filter = (ResourceFilter) resourceFilters.get(lib);
		//		if (filter == null) {
		//			log.warn("no resource filter found for library "
		//					+ lib + ", name=" + name
		//					+ ", URL=" + url + ", this=" + this
		//					+ ", requestor=" + requestor);
		//			return false;
		//		}
		//		if (!filter.isResourceVisible(name)) {
		//			log.warn("resource not visible, name=" + name
		//					+ ", URL=" + url + ", this=" + this
		//					+ ", requestor=" + requestor);
		//			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);
	}
	
	/**
	 * Package names that this module should try to load. All classes/packages within the omod and
	 * the lib folder are already checked, this method/variable are used for extreme circumstances
	 * where an omod needs to know about another after being loaded
	 * 
	 * @return the additionalPackages
	 */
	public Set<String> getAdditionalPackages() {
		return additionalPackages;
	}
	
	/**
	 * @param additionalPackages the package names to set that this module contains that are outside
	 *            the normal omod and omod/lib folders
	 */
	public void setAdditionalPackages(Set<String> additionalPackages) {
		this.additionalPackages = additionalPackages;
	}
	
	/**
	 * Convenience method to add another package name to the list of packages provided by this
	 * module
	 * 
	 * @param additionalPackage string package name
	 * @see #setProvidedPackages(Set)
	 */
	public void addAdditionalPackage(String additionalPackage) {
		if (this.additionalPackages == null)
			this.additionalPackages = new LinkedHashSet<String>();
		
		// its pointless to add a package that is below the module's package
		// name because we are automatically looking at that in the classloader
		if (!additionalPackage.startsWith(module.getPackageName()))
			this.additionalPackages.add(additionalPackage);
	}
	
	/**
	 * Convenience method to add a bunch of package names to the list of packages provided by this
	 * module
	 * 
	 * @param providedPackages list/set of strings that are package names
	 * @see #setProvidedPackages(Set)
	 */
	public void addAllAdditionalPackages(Collection<String> providedPackages) {
		if (this.additionalPackages == null)
			this.additionalPackages = new LinkedHashSet<String>();
		
		for (String provPackage : providedPackages)
			// its pointless to add a package that is below the module's package
			// name because we are automatically looking at that in the classloader
			addAdditionalPackage(provPackage);
	}
	
	/**
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		return "{ModuleClassLoader: uid=" + System.identityHashCode(this) + "; " + module + "}";
	}
	
}