org.openmrs.module.ModuleFactory.java Source code

Java tutorial

Introduction

Here is the source code for org.openmrs.module.ModuleFactory.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.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedMap;
import java.util.Vector;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;

import org.aopalliance.aop.Advice;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openmrs.GlobalProperty;
import org.openmrs.Privilege;
import org.openmrs.api.AdministrationService;
import org.openmrs.api.OpenmrsService;
import org.openmrs.api.context.Context;
import org.openmrs.api.context.Daemon;
import org.openmrs.module.Extension.MEDIA_TYPE;
import org.openmrs.util.CycleException;
import org.openmrs.util.DatabaseUpdateException;
import org.openmrs.util.DatabaseUpdater;
import org.openmrs.util.Graph;
import org.openmrs.util.InputRequiredException;
import org.openmrs.util.OpenmrsClassLoader;
import org.openmrs.util.OpenmrsConstants;
import org.openmrs.util.OpenmrsUtil;
import org.openmrs.util.PrivilegeConstants;
import org.springframework.aop.Advisor;
import org.springframework.context.support.AbstractRefreshableApplicationContext;
import org.springframework.util.StringUtils;

/**
 * Methods for loading, starting, stopping, and storing OpenMRS modules
 */
public class ModuleFactory {

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

    protected static volatile Map<String, Module> loadedModules = new WeakHashMap<String, Module>();

    protected static volatile Map<String, Module> startedModules = new WeakHashMap<String, Module>();

    protected static volatile Map<String, List<Extension>> extensionMap = new HashMap<String, List<Extension>>();

    // maps to keep track of the memory and objects to free/close
    protected static volatile Map<Module, ModuleClassLoader> moduleClassLoaders = new WeakHashMap<Module, ModuleClassLoader>();

    private static Map<String, Set<ModuleClassLoader>> providedPackages = new ConcurrentHashMap<String, Set<ModuleClassLoader>>();

    // the name of the file within a module file
    private static final String MODULE_CHANGELOG_FILENAME = "liquibase.xml";

    private static final Map<String, DaemonToken> daemonTokens = new WeakHashMap<String, DaemonToken>();

    private static volatile Set<String> actualStartupOrder;

    /**
     * Add a module (in the form of a jar file) to the list of openmrs modules Returns null if an
     * error occurred and/or module was not successfully loaded
     * 
     * @param moduleFile
     * @return Module
     */
    public static Module loadModule(File moduleFile) throws ModuleException {

        return loadModule(moduleFile, true);

    }

    /**
     * Add a module (in the form of a jar file) to the list of openmrs modules Returns null if an
     * error occurred and/or module was not successfully loaded
     * 
     * @param moduleFile
     * @param replaceIfExists unload a module that has the same moduleId if one is loaded already
     * @return Module
     */
    public static Module loadModule(File moduleFile, Boolean replaceIfExists) throws ModuleException {
        Module module = getModuleFromFile(moduleFile);

        if (module != null) {
            loadModule(module, replaceIfExists);
        }

        return module;
    }

    /**
     * Add a module to the list of openmrs modules
     * 
     * @param module
     * @param replaceIfExists unload a module that has the same moduleId if one is loaded already
     * @return module the module that was loaded or if the module exists already with the same
     *         version, the old module
     */
    public static Module loadModule(Module module, Boolean replaceIfExists) throws ModuleException {

        if (log.isDebugEnabled()) {
            log.debug("Adding module " + module.getName() + " to the module queue");
        }

        Module oldModule = getLoadedModulesMap().get(module.getModuleId());
        if (oldModule != null) {
            int versionComparison = ModuleUtil.compareVersion(oldModule.getVersion(), module.getVersion());
            if (versionComparison < 0) {
                // if oldModule version is lower, unload it and use the new
                unloadModule(oldModule);
            } else if (versionComparison == 0) {
                if (replaceIfExists) {
                    // if the versions are the same and we're told to replaceIfExists, use the new
                    unloadModule(oldModule);
                } else {
                    // if the versions are equal and we're not told to replaceIfExists, jump out of here in a bad way
                    throw new ModuleException("A module with the same id and version already exists",
                            module.getModuleId());
                }
            } else {
                // if the older (already loaded) module is newer, keep that original one that was loaded. return that one.
                return oldModule;
            }
        }

        getLoadedModulesMap().put(module.getModuleId(), module);

        return module;
    }

    /**
     * Load OpenMRS modules from <code>OpenmrsUtil.getModuleRepository()</code>
     */
    public static void loadModules() {

        // load modules from the user's module repository directory
        File modulesFolder = ModuleUtil.getModuleRepository();

        if (log.isDebugEnabled()) {
            log.debug("Loading modules from: " + modulesFolder.getAbsolutePath());
        }

        if (modulesFolder.isDirectory()) {
            loadModules(Arrays.asList(modulesFolder.listFiles()));
        } else {
            log.error("modules folder: '" + modulesFolder.getAbsolutePath() + "' is not a valid directory");
        }
    }

    /**
     * Attempt to load the given files as OpenMRS modules
     * 
     * @param modulesToLoad the list of files to try and load
     */
    public static void loadModules(List<File> modulesToLoad) {
        // loop over the modules and load all the modules that we can
        for (File f : modulesToLoad) {
            // ignore .svn folder and the like
            if (!f.getName().startsWith(".")) {
                try {
                    Module mod = loadModule(f, true); // last module loaded wins
                    log.debug("Loaded module: " + mod + " successfully");
                } catch (Exception e) {
                    log.debug("Unable to load file in module directory: " + f + ". Skipping file.", e);
                }
            }
        }

        //inform modules, that they can't start before other modules
        Map<String, Module> loadedModulesMap = getLoadedModulesMapPackage();
        for (String key : loadedModules.keySet()) {
            Module m = loadedModules.get(key);
            Map<String, String> startBeforeModules = m.getStartBeforeModulesMap();
            if (startBeforeModules.size() > 0) {
                for (String s : startBeforeModules.keySet()) {
                    Module mod = loadedModulesMap.get(s);
                    if (mod != null) {
                        mod.addRequiredModule(m.getPackageName(), m.getVersion());
                    }
                }
            }
        }
    }

    /**
     * Try to start all of the loaded modules that have the global property <i>moduleId</i>.started
     * is set to "true" or the property does not exist. Otherwise, leave it as only "loaded"<br>
     * <br>
     * Modules that are already started will be skipped.
     */
    public static void startModules() {

        // loop over and try starting each of the loaded modules
        if (getLoadedModules().size() > 0) {

            try {
                List<Module> modules = getModulesThatShouldStart();

                modules = getModulesInStartupOrder(modules);

                // try and start the modules that should be started
                for (Module mod : modules) {

                    if (mod.isStarted()) {
                        continue; // skip over modules that are already started
                    }

                    // Skip module if required ones are not started
                    if (!requiredModulesStarted(mod)) {
                        String message = getFailedToStartModuleMessage(mod);
                        log.error(message);
                        mod.setStartupErrorMessage(message);
                        notifySuperUsersAboutModuleFailure(mod);
                        continue;
                    }

                    try {
                        if (log.isDebugEnabled()) {
                            log.debug("starting module: " + mod.getModuleId());
                        }
                        startModule(mod);
                    } catch (Exception e) {
                        log.error("Error while starting module: " + mod.getName(), e);
                        mod.setStartupErrorMessage("Error while starting module", e);
                        notifySuperUsersAboutModuleFailure(mod);
                    }
                }

            } catch (CycleException e) {
                String message = getCyclicDependenciesMessage();
                log.error(message);
                notifySuperUsersAboutCyclicDependencies();
            }

        }
    }

    /**
     * Obtain the list of modules that should be started
     * 
     * @return list of modules
     */
    private static List<Module> getModulesThatShouldStart() {
        List<Module> modules = new ArrayList<Module>();

        AdministrationService adminService = Context.getAdministrationService();

        for (Module mod : getLoadedModulesCoreFirst()) {

            String key = mod.getModuleId() + ".started";
            String startedProp = adminService.getGlobalProperty(key, null);
            String mandatoryProp = adminService.getGlobalProperty(mod.getModuleId() + ".mandatory", null);

            boolean isCoreToOpenmrs = mod.isCore() && !ModuleUtil.ignoreCoreModules();

            // if a 'moduleid.started' property doesn't exist, start the module anyway
            // as this is probably the first time they are loading it
            if (startedProp == null || startedProp.equals("true") || "true".equalsIgnoreCase(mandatoryProp)
                    || mod.isMandatory() || isCoreToOpenmrs) {
                modules.add(mod);
            }
        }
        return modules;
    }

    /**
     * Sort modules in startup order based on required and aware-of dependencies
     * 
     * @param modules list of modules to sort
     * @return list of modules sorted by dependencies
     * @throws CycleException
     */
    public static List<Module> getModulesInStartupOrder(Collection<Module> modules) throws CycleException {
        Graph<Module> graph = new Graph<Module>();

        for (Module mod : modules) {

            Module toNode = mod;
            graph.addNode(toNode);

            // Required dependencies
            for (String key : mod.getRequiredModules()) {
                Module module = getModuleByPackage(key);
                Module fromNode = graph.getNode(module);
                if (fromNode == null) {
                    fromNode = module;
                }

                if (fromNode != null) {
                    graph.addEdge(graph.new Edge(fromNode, toNode));
                }
            }

            // Aware-of dependencies
            for (String key : mod.getAwareOfModules()) {
                Module module = getModuleByPackage(key);
                Module fromNode = graph.getNode(module);
                if (fromNode == null) {
                    fromNode = module;
                }

                if (fromNode != null) {
                    graph.addEdge(graph.new Edge(fromNode, toNode));
                }
            }
        }

        return graph.topologicalSort();
    }

    /**
     * Send an Alert to all super users that the given module did not start successfully.
     * 
     * @param mod The Module that failed
     */
    private static void notifySuperUsersAboutModuleFailure(Module mod) {
        try {
            // Add the privileges necessary for notifySuperUsers
            Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);

            // Send an alert to all administrators
            Context.getAlertService().notifySuperUsers("Module.startupError.notification.message", null,
                    mod.getName());
        } catch (Exception e) {
            log.error("Unable to send an alert to the super users", e);
        } finally {
            // Remove added privileges
            Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
        }
    }

    /**
     * Send an Alert to all super users that modules did not start due to cyclic dependencies
     */
    private static void notifySuperUsersAboutCyclicDependencies() {
        try {
            Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
            Context.getAlertService().notifySuperUsers("Module.error.cyclicDependencies", null);
        } catch (Exception e) {
            log.error("Unable to send an alert to the super users", e);
        } finally {
            Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS);
        }
    }

    /**
     * Returns all modules found/loaded into the system (started and not started), with the core
     * modules at the start of that list
     * 
     * @return <code>List&lt;Module&gt;</code> of the modules loaded into the system, with the core
     *         modules first.
     */
    public static List<Module> getLoadedModulesCoreFirst() {
        List<Module> list = new ArrayList<Module>(getLoadedModules());
        final Collection<String> coreModuleIds = ModuleConstants.CORE_MODULES.keySet();
        Collections.sort(list, new Comparator<Module>() {

            @Override
            public int compare(Module left, Module right) {
                Integer leftVal = coreModuleIds.contains(left.getModuleId()) ? 0 : 1;
                Integer rightVal = coreModuleIds.contains(right.getModuleId()) ? 0 : 1;
                return leftVal.compareTo(rightVal);
            }
        });
        return list;
    }

    /**
     * Convenience method to return a List of Strings containing a description of which modules the
     * passed module requires but which are not started. The returned description of each module is
     * the moduleId followed by the required version if one is specified
     * 
     * @param module the module to check required modules for
     * @return List&lt;String&gt; of module names + optional required versions:
     *         "org.openmrs.formentry 1.8, org.rg.patientmatching"
     */
    private static List<String> getMissingRequiredModules(Module module) {
        List<String> ret = new ArrayList<String>();
        for (String moduleName : module.getRequiredModules()) {
            boolean started = false;
            for (Module mod : getStartedModules()) {
                if (mod.getPackageName().equals(moduleName)) {
                    String reqVersion = module.getRequiredModuleVersion(moduleName);
                    if (reqVersion == null || ModuleUtil.compareVersion(mod.getVersion(), reqVersion) >= 0) {
                        started = true;
                    }
                    break;
                }
            }

            if (!started) {
                String moduleVersion = module.getRequiredModuleVersion(moduleName);
                moduleName = moduleName.replace("org.openmrs.module.", "").replace("org.openmrs.", "");
                ret.add(moduleName + (moduleVersion != null ? " " + moduleVersion : ""));
            }
        }
        return ret;
    }

    /**
     * Returns all modules found/loaded into the system (started and not started)
     * 
     * @return <code>Collection&lt;Module&gt;</code> of the modules loaded into the system
     */
    public static Collection<Module> getLoadedModules() {
        if (getLoadedModulesMap().size() > 0) {
            return getLoadedModulesMap().values();
        }

        return Collections.emptyList();
    }

    /**
     * Returns all modules found/loaded into the system (started and not started) in the form of a
     * map&lt;ModuleId, Module&gt;
     * 
     * @return map&lt;ModuleId, Module&gt;
     */
    public static Map<String, Module> getLoadedModulesMap() {
        if (loadedModules == null) {
            loadedModules = new WeakHashMap<String, Module>();
        }

        return loadedModules;
    }

    /**
     * Returns all modules found/loaded into the system (started and not started) in the form of a
     * map&lt;PackageName, Module&gt;
     * 
     * @return map&lt;PackageName, Module&gt;
     */
    public static Map<String, Module> getLoadedModulesMapPackage() {
        if (loadedModules == null) {
            loadedModules = new WeakHashMap<String, Module>();
            return loadedModules;
        }

        Map<String, Module> map = new WeakHashMap<String, Module>();
        for (String key : loadedModules.keySet()) {
            map.put(loadedModules.get(key).getPackageName(), loadedModules.get(key));
        }
        return map;
    }

    /**
     * Returns the modules that have been successfully started
     * 
     * @return <code>Collection&lt;Module&gt;</code> of the started modules
     */
    public static Collection<Module> getStartedModules() {
        if (getStartedModulesMap().size() > 0) {
            return getStartedModulesMap().values();
        }

        return Collections.emptyList();
    }

    public static List<Module> getStartedModulesInOrder() {
        List<Module> modules = new ArrayList<Module>();
        if (actualStartupOrder != null) {
            for (String moduleId : actualStartupOrder) {
                modules.add(getStartedModulesMap().get(moduleId));
            }
        } else {
            modules.addAll(getStartedModules());
        }
        return modules;
    }

    /**
     * Returns the modules that have been successfully started in the form of a map&lt;ModuleId,
     * Module&gt;
     * 
     * @return Map&lt;ModuleId, Module&gt;
     */
    public static Map<String, Module> getStartedModulesMap() {
        if (startedModules == null) {
            startedModules = new WeakHashMap<String, Module>();
        }

        return startedModules;
    }

    /**
     * Creates a Module object from the (jar)file pointed to by <code>moduleFile</code> returns null
     * if an error occurred during processing
     * 
     * @param moduleFile
     * @return module Module
     */
    private static Module getModuleFromFile(File moduleFile) throws ModuleException {

        Module module = null;
        try {
            module = new ModuleFileParser(moduleFile).parse();
        } catch (ModuleException e) {
            if (moduleFile != null) {
                log.error("Error getting module object from file " + moduleFile.getName(), e);
            } else {
                log.error("Module was null.", e);
            }
            throw e;
        }

        return module;
    }

    /**
     * @param moduleId
     * @return Module matching module id or null if none
     */
    public static Module getModuleById(String moduleId) {
        return getLoadedModulesMap().get(moduleId);
    }

    /**
     * @param moduleId
     * @return Module matching moduleId, if it is started or null otherwise
     */
    public static Module getStartedModuleById(String moduleId) {
        return getStartedModulesMap().get(moduleId);
    }

    /**
     * @param modulePackage
     * @return Module matching module package or null if none
     */
    public static Module getModuleByPackage(String modulePackage) {
        for (Module mod : getLoadedModulesMap().values()) {
            if (mod.getPackageName().equals(modulePackage)) {
                return mod;
            }
        }
        return null;
    }

    /**
     * @see #startModule(Module, boolean, AbstractRefreshableApplicationContext)
     * @see #startModuleInternal(Module)
     * @see Daemon#startModule(Module)
     */
    public static Module startModule(Module module) throws ModuleException {
        return Daemon.startModule(module);
    }

    /**
     * Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
     * Module's activator. This method is run in a new thread and is authenticated as the Daemon
     * user. If a non null application context is passed in, it gets refreshed to make the module's
     * services available
     * 
     * @param module Module to start
     * @param isOpenmrsStartup Specifies whether this module is being started at application startup
     *            or not, this argument is ignored if a null application context is passed in
     * @param applicationContext the spring application context instance to refresh
     * @throws ModuleException if the module throws any kind of error at startup or in an activator
     * @see #startModuleInternal(Module, boolean, AbstractRefreshableApplicationContext)
     * @see Daemon#startModule(Module, boolean, AbstractRefreshableApplicationContext)
     */
    public static Module startModule(Module module, boolean isOpenmrsStartup,
            AbstractRefreshableApplicationContext applicationContext) throws ModuleException {
        return Daemon.startModule(module, isOpenmrsStartup, applicationContext);
    }

    /**
     * This method should not be called directly.<br>
     * <br>
     * The {@link #startModule(Module)} (and hence {@link Daemon#startModule(Module)}) calls this
     * method in a new Thread and is authenticated as the {@link Daemon} user<br>
     * <br>
     * Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
     * Module's activator.
     * 
     * @param module Module to start
     */
    public static Module startModuleInternal(Module module) throws ModuleException {
        return startModuleInternal(module, false, null);
    }

    /**
     * This method should not be called directly.<br>
     * <br>
     * The {@link #startModule(Module)} (and hence {@link Daemon#startModule(Module)}) calls this
     * method in a new Thread and is authenticated as the {@link Daemon} user<br>
     * <br>
     * Runs through extensionPoints and then calls {@link BaseModuleActivator#willStart()} on the
     * Module's activator. <br>
     * <br>
     * If a non null application context is passed in, it gets refreshed to make the module's
     * services available
     * 
     * @param module Module to start
     * @param isOpenmrsStartup Specifies whether this module is being started at application startup
     *            or not, this argument is ignored if a null application context is passed in
     * @param applicationContext the spring application context instance to refresh
     */
    public static Module startModuleInternal(Module module, boolean isOpenmrsStartup,
            AbstractRefreshableApplicationContext applicationContext) throws ModuleException {

        if (module != null) {

            String moduleId = module.getModuleId();
            try {

                // check to be sure this module can run with our current version
                // of OpenMRS code
                String requireVersion = module.getRequireOpenmrsVersion();
                ModuleUtil.checkRequiredVersion(OpenmrsConstants.OPENMRS_VERSION_SHORT, requireVersion);

                // check for required modules
                if (!requiredModulesStarted(module)) {
                    throw new ModuleException(getFailedToStartModuleMessage(module));
                }

                // fire up the classloader for this module
                ModuleClassLoader moduleClassLoader = new ModuleClassLoader(module,
                        ModuleFactory.class.getClassLoader());
                getModuleClassLoaderMap().put(module, moduleClassLoader);
                registerProvidedPackages(moduleClassLoader);

                // don't load the advice objects into the Context
                // At startup, the spring context isn't refreshed until all modules
                // have been loaded.  This causes errors if called here during a
                // module's startup if one of these advice points is on another
                // module because that other module's service won't have been loaded
                // into spring yet.  All advice for all modules must be reloaded
                // a spring context refresh anyway, so skip the advice loading here
                // loadAdvice(module);

                // map extension point to a list of extensions for this module only
                Map<String, List<Extension>> moduleExtensionMap = new HashMap<String, List<Extension>>();
                for (Extension ext : module.getExtensions()) {

                    String extId = ext.getExtensionId();
                    List<Extension> tmpExtensions = moduleExtensionMap.get(extId);
                    if (tmpExtensions == null) {
                        tmpExtensions = new Vector<Extension>();
                        moduleExtensionMap.put(extId, tmpExtensions);
                    }

                    tmpExtensions.add(ext);
                }

                // Sort this module's extensions, and merge them into the full extensions map
                Comparator<Extension> sortOrder = new Comparator<Extension>() {

                    @Override
                    public int compare(Extension e1, Extension e2) {
                        return Integer.valueOf(e1.getOrder()).compareTo(Integer.valueOf(e2.getOrder()));
                    }
                };
                for (Map.Entry<String, List<Extension>> moduleExtensionEntry : moduleExtensionMap.entrySet()) {
                    // Sort this module's extensions for current extension point
                    List<Extension> sortedModuleExtensions = moduleExtensionEntry.getValue();
                    Collections.sort(sortedModuleExtensions, sortOrder);

                    // Get existing extensions, and append the ones from the new module
                    List<Extension> extensions = getExtensionMap().get(moduleExtensionEntry.getKey());
                    if (extensions == null) {
                        extensions = new Vector<Extension>();
                        getExtensionMap().put(moduleExtensionEntry.getKey(), extensions);
                    }
                    for (Extension ext : sortedModuleExtensions) {
                        log.debug(
                                "Adding to mapping ext: " + ext.getExtensionId() + " ext.class: " + ext.getClass());
                        extensions.add(ext);
                    }
                }

                // run the module's sql update script
                // This and the property updates are the only things that can't
                // be undone at startup, so put these calls after any other
                // calls that might hinder startup
                SortedMap<String, String> diffs = SqlDiffFileParser.getSqlDiffs(module);

                try {
                    // this method must check and run queries against the database.
                    // to do this, it must be "authenticated".  Give the current
                    // "user" the proxy privilege so this can be done. ("user" might
                    // be nobody because this is being run at startup)
                    Context.addProxyPrivilege("");

                    for (Map.Entry<String, String> entry : diffs.entrySet()) {
                        String version = entry.getKey();
                        String sql = entry.getValue();
                        if (StringUtils.hasText(sql)) {
                            runDiff(module, version, sql);
                        }
                    }
                } finally {
                    // take the "authenticated" privilege away from the current "user"
                    Context.removeProxyPrivilege("");
                }

                // run module's optional liquibase.xml immediately after sqldiff.xml
                runLiquibase(module);

                // effectively mark this module as started successfully
                getStartedModulesMap().put(moduleId, module);
                if (actualStartupOrder == null) {
                    actualStartupOrder = new LinkedHashSet<String>();
                }
                actualStartupOrder.add(moduleId);

                try {
                    // save the state of this module for future restarts
                    saveGlobalProperty(moduleId + ".started", "true",
                            getGlobalPropertyStartedDescription(moduleId));

                    // save the mandatory status
                    saveGlobalProperty(moduleId + ".mandatory", String.valueOf(module.isMandatory()),
                            getGlobalPropertyMandatoryModuleDescription(moduleId));
                } catch (Exception e) {
                    // pass over errors because this doesn't really concern startup
                    // passing over this also allows for multiple of the same-named modules
                    // to be loaded in junit tests that are run within one session
                    log.debug("Got an error when trying to set the global property on module startup", e);
                }

                // (this must be done after putting the module in the started
                // list)
                // if this module defined any privileges or global properties,
                // make sure they are added to the database
                // (Unfortunately, placing the call here will duplicate work
                // done at initial app startup)
                if (module.getPrivileges().size() > 0 || module.getGlobalProperties().size() > 0) {
                    log.debug("Updating core dataset");
                    Context.checkCoreDataset();
                    // checkCoreDataset() currently doesn't throw an error. If
                    // it did, it needs to be
                    // caught and the module needs to be stopped and given a
                    // startup error
                }

                // should be near the bottom so the module has all of its stuff
                // set up for it already.
                try {
                    if (module.getModuleActivator() != null) {
                        // if extends BaseModuleActivator
                        module.getModuleActivator().willStart();
                    } else {
                        module.getActivator().startup();//implements old Activator interface
                    }
                } catch (ModuleException e) {
                    // just rethrow module exceptions. This should be used for a
                    // module marking that it had trouble starting
                    throw e;
                } catch (Exception e) {
                    throw new ModuleException("Error while calling module's Activator.startup()/willStart() method",
                            e);
                }

                // erase any previous startup error
                module.clearStartupError();
            } catch (Exception e) {
                log.warn("Error while trying to start module: " + moduleId, e);
                module.setStartupErrorMessage("Error while trying to start module", e);
                notifySuperUsersAboutModuleFailure(module);

                // undo all of the actions in startup
                try {
                    boolean skipOverStartedProperty = false;

                    if (e instanceof ModuleMustStartException) {
                        skipOverStartedProperty = true;
                    }

                    stopModule(module, skipOverStartedProperty, true);
                } catch (Exception e2) {
                    // this will probably occur about the same place as the
                    // error in startup
                    log.debug("Error while stopping module: " + moduleId, e2);
                }
            }

        }

        if (applicationContext != null) {
            ModuleUtil.refreshApplicationContext(applicationContext, isOpenmrsStartup, module);
        }

        return module;
    }

    private static void registerProvidedPackages(ModuleClassLoader moduleClassLoader) {
        for (String providedPackage : moduleClassLoader.getProvidedPackages()) {
            Set<ModuleClassLoader> newSet = new HashSet<ModuleClassLoader>();

            Set<ModuleClassLoader> set = providedPackages.get(providedPackage);
            if (set != null) {
                newSet.addAll(set);
            }

            newSet.add(moduleClassLoader);
            providedPackages.put(providedPackage, newSet);
        }
    }

    private static void unregisterProvidedPackages(ModuleClassLoader moduleClassLoader) {
        for (String providedPackage : moduleClassLoader.getProvidedPackages()) {
            Set<ModuleClassLoader> newSet = new HashSet<ModuleClassLoader>();

            Set<ModuleClassLoader> set = providedPackages.get(providedPackage);
            if (set != null) {
                newSet.addAll(set);
            }
            newSet.remove(moduleClassLoader);

            providedPackages.put(providedPackage, newSet);
        }
    }

    public static Set<ModuleClassLoader> getModuleClassLoadersForPackage(String packageName) {
        Set<ModuleClassLoader> set = providedPackages.get(packageName);
        if (set == null) {
            return Collections.emptySet();
        } else {
            return new HashSet<ModuleClassLoader>(set);
        }
    }

    /**
     * Gets the error message of a module which fails to start.
     * 
     * @param module the module that has failed to start.
     * @return the message text.
     */
    private static String getFailedToStartModuleMessage(Module module) {
        String[] params = { module.getName(), OpenmrsUtil.join(getMissingRequiredModules(module), ", ") };
        return Context.getMessageSourceService().getMessage("Module.error.moduleCannotBeStarted", params,
                Context.getLocale());
    }

    /**
     * Gets the error message of cyclic dependencies between modules
     * 
     * @return the message text.
     */
    private static String getCyclicDependenciesMessage() {
        return Context.getMessageSourceService().getMessage("Module.error.cyclicDependencies", null,
                Context.getLocale());
    }

    /**
     * Loop over the given module's advice objects and load them into the Context This needs to be
     * called for all started modules after every restart of the Spring Application Context
     * 
     * @param module
     */
    public static void loadAdvice(Module module) {
        for (AdvicePoint advice : module.getAdvicePoints()) {
            Class<?> cls = null;
            try {
                cls = Context.loadClass(advice.getPoint());
                Object aopObject = advice.getClassInstance();
                if (Advisor.class.isInstance(aopObject)) {
                    log.debug("adding advisor: " + aopObject.getClass());
                    Context.addAdvisor(cls, (Advisor) aopObject);
                } else {
                    log.debug("Adding advice: " + aopObject.getClass());
                    Context.addAdvice(cls, (Advice) aopObject);
                }
            } catch (ClassNotFoundException e) {
                log.warn("Could not load advice point: " + advice.getPoint(), e);
                //throw new ModuleException("Could not load advice point: " + advice.getPoint(), e);
            }
        }
    }

    /**
     * Execute the given sql diff section for the given module
     * 
     * @param module the module being executed on
     * @param version the version of this sql diff
     * @param sql the actual sql statements to run (separated by semi colons)
     */
    private static void runDiff(Module module, String version, String sql) {
        AdministrationService as = Context.getAdministrationService();

        String key = module.getModuleId() + ".database_version";
        GlobalProperty gp = as.getGlobalPropertyObject(key);

        boolean executeSQL = false;

        // check given version against current version
        if (gp != null && StringUtils.hasLength(gp.getPropertyValue())) {
            String currentDbVersion = gp.getPropertyValue();
            if (log.isDebugEnabled()) {
                log.debug("version:column " + version + ":" + currentDbVersion);
                log.debug("compare: " + ModuleUtil.compareVersion(version, currentDbVersion));
            }
            if (ModuleUtil.compareVersion(version, currentDbVersion) > 0) {
                executeSQL = true;
            }
        } else {
            executeSQL = true;
        }

        // version is greater than the currently installed version. execute this update.
        if (executeSQL) {
            try {
                Context.addProxyPrivilege(PrivilegeConstants.SQL_LEVEL_ACCESS);
                log.debug("Executing sql: " + sql);
                String[] sqlStatements = sql.split(";");
                for (String sqlStatement : sqlStatements) {
                    if (sqlStatement.trim().length() > 0) {
                        as.executeSQL(sqlStatement, false);
                    }
                }
            } finally {
                Context.removeProxyPrivilege(PrivilegeConstants.SQL_LEVEL_ACCESS);
            }

            // save the global property
            try {
                Context.addProxyPrivilege(PrivilegeConstants.MANAGE_GLOBAL_PROPERTIES);

                String description = "DO NOT MODIFY.  Current database version number for the "
                        + module.getModuleId() + " module.";

                if (gp == null) {
                    log.info("Global property " + key + " was not found. Creating one now.");
                    gp = new GlobalProperty(key, version, description);
                    as.saveGlobalProperty(gp);
                } else if (!gp.getPropertyValue().equals(version)) {
                    log.info("Updating global property " + key + " to version: " + version);
                    gp.setDescription(description);
                    gp.setPropertyValue(version);
                    as.saveGlobalProperty(gp);
                } else {
                    log.error("Should not be here. GP property value and sqldiff version should not be equal");
                }

            } finally {
                Context.removeProxyPrivilege(PrivilegeConstants.MANAGE_GLOBAL_PROPERTIES);
            }

        }

    }

    /**
     * Execute all unrun changeSets in liquibase.xml for the given module
     * 
     * @param module the module being executed on
     */
    private static void runLiquibase(Module module) {
        JarFile jarFile = null;
        boolean liquibaseFileExists = false;

        try {
            try {
                jarFile = new JarFile(module.getFile());
            } catch (IOException e) {
                throw new ModuleException("Unable to get jar file", module.getName(), e);
            }

            //check whether module has a liquibase.xml
            InputStream inStream = null;
            ZipEntry entry = null;
            try {
                inStream = ModuleUtil.getResourceFromApi(jarFile, module.getModuleId(), module.getVersion(),
                        MODULE_CHANGELOG_FILENAME);
                if (inStream == null) {
                    // Try the old way. Loading from the root of the omod
                    entry = jarFile.getEntry(MODULE_CHANGELOG_FILENAME);
                }
                liquibaseFileExists = (inStream != null) || (entry != null);
            } finally {
                IOUtils.closeQuietly(inStream);
            }
        } finally {
            try {
                if (jarFile != null) {
                    jarFile.close();
                }
            } catch (IOException e) {
                log.warn("Unable to close jarfile: " + jarFile.getName());
            }
        }

        if (liquibaseFileExists) {
            try {
                // run liquibase.xml by Liquibase API
                DatabaseUpdater.executeChangelog(MODULE_CHANGELOG_FILENAME, null, null, null,
                        getModuleClassLoader(module));
            } catch (InputRequiredException ire) {
                // the user would be stepped through the questions returned here.
                throw new ModuleException("Input during database updates is not yet implemented.", module.getName(),
                        ire);
            } catch (DatabaseUpdateException e) {
                throw new ModuleException("Unable to update data model using liquibase.xml.", module.getName(), e);
            } catch (Exception e) {
                throw new ModuleException("Unable to update data model using liquibase.xml.", module.getName(), e);
            }
        }
    }

    /**
     * Runs through the advice and extension points and removes from api. <br>
     * Also calls mod.Activator.shutdown()
     * 
     * @param mod module to stop
     * @see ModuleFactory#stopModule(Module, boolean, boolean)
     */
    public static void stopModule(Module mod) {
        stopModule(mod, false, false);
    }

    /**
     * Runs through the advice and extension points and removes from api.<br>
     * Also calls mod.Activator.shutdown()
     * 
     * @param mod the module to stop
     * @param isShuttingDown true if this is called during the process of shutting down openmrs
     * @see #stopModule(Module, boolean, boolean)
     */
    public static void stopModule(Module mod, boolean isShuttingDown) {
        stopModule(mod, isShuttingDown, false);
    }

    /**
     * Runs through the advice and extension points and removes from api.<br>
     * <code>skipOverStartedProperty</code> should only be true when openmrs is stopping modules
     * because it is shutting down. When normally stopping a module, use {@link #stopModule(Module)}
     * (or leave value as false). This property controls whether the globalproperty is set for
     * startup/shutdown. <br>
     * Also calls module's {@link Activator#shutdown()}
     * 
     * @param mod module to stop
     * @param skipOverStartedProperty true if we don't want to set &lt;moduleid&gt;.started to false
     * @param isFailedStartup true if this is being called as a cleanup because of a failed module
     *            startup
     * @return list of dependent modules that were stopped because this module was stopped. This
     *         will never be null.
     */
    public static List<Module> stopModule(Module mod, boolean skipOverStartedProperty, boolean isFailedStartup)
            throws ModuleMustStartException {

        List<Module> dependentModulesStopped = new Vector<Module>();

        if (mod != null) {

            if (!ModuleFactory.isModuleStarted(mod)) {
                return dependentModulesStopped;
            }

            try {
                if (mod.getModuleActivator() != null) { // if extends BaseModuleActivator
                    mod.getModuleActivator().willStop();
                }
            } catch (Exception t) {
                log.warn("Unable to call module's Activator.willStop() method", t);
            }

            String moduleId = mod.getModuleId();

            // don't allow mandatory modules to be stopped
            // don't use database checks here because spring might be in a bad state
            if (!isFailedStartup && mod.isMandatory()) {
                throw new MandatoryModuleException(moduleId);
            }

            if (!isFailedStartup && ModuleConstants.CORE_MODULES.containsKey(moduleId)) {
                throw new OpenmrsCoreModuleException(moduleId);
            }

            String modulePackage = mod.getPackageName();

            // stop all dependent modules
            // copy modules to new list to avoid "concurrent modification exception"
            List<Module> startedModulesCopy = new ArrayList<Module>();
            startedModulesCopy.addAll(getStartedModules());
            for (Module dependentModule : startedModulesCopy) {
                if (dependentModule != null && !dependentModule.equals(mod)) {
                    if (dependentModule.getRequiredModules() != null
                            && dependentModule.getRequiredModules().contains(modulePackage)) {
                        dependentModulesStopped.add(dependentModule);
                        dependentModulesStopped
                                .addAll(stopModule(dependentModule, skipOverStartedProperty, isFailedStartup));
                    }
                }
            }

            getStartedModulesMap().remove(moduleId);
            if (actualStartupOrder != null) {
                actualStartupOrder.remove(moduleId);
                for (Module depModule : dependentModulesStopped) {
                    actualStartupOrder.remove(depModule.getModuleId());
                }
            }

            if (!skipOverStartedProperty && !Context.isRefreshingContext()) {
                saveGlobalProperty(moduleId + ".started", "false", getGlobalPropertyStartedDescription(moduleId));
            }

            ModuleClassLoader moduleClassLoader = getModuleClassLoaderMap().get(mod);
            if (moduleClassLoader != null) {
                unregisterProvidedPackages(moduleClassLoader);

                log.debug("Mod was in classloader map.  Removing advice and extensions.");
                // remove all advice by this module
                try {
                    for (AdvicePoint advice : mod.getAdvicePoints()) {
                        Class cls = null;
                        try {
                            cls = Context.loadClass(advice.getPoint());
                            Object aopObject = advice.getClassInstance();
                            if (Advisor.class.isInstance(aopObject)) {
                                log.debug("adding advisor: " + aopObject.getClass());
                                Context.removeAdvisor(cls, (Advisor) aopObject);
                            } else {
                                log.debug("Adding advice: " + aopObject.getClass());
                                Context.removeAdvice(cls, (Advice) aopObject);
                            }
                        } catch (Exception t) {
                            log.warn("Could not remove advice point: " + advice.getPoint(), t);
                        }
                    }
                } catch (Exception t) {
                    log.warn("Error while getting advicePoints from module: " + moduleId, t);
                }

                // remove all extensions by this module
                try {
                    for (Extension ext : mod.getExtensions()) {
                        String extId = ext.getExtensionId();
                        try {
                            List<Extension> tmpExtensions = getExtensions(extId);
                            if (tmpExtensions == null) {
                                tmpExtensions = new Vector<Extension>();
                            }

                            tmpExtensions.remove(ext);
                            getExtensionMap().put(extId, tmpExtensions);
                        } catch (Exception exterror) {
                            log.warn("Error while getting extension: " + ext, exterror);
                        }
                    }
                } catch (Exception t) {
                    log.warn("Error while getting extensions from module: " + moduleId, t);
                }
            }

            //Run the onShutdown() method for openmrs services in this module.
            List<OpenmrsService> services = Context.getModuleOpenmrsServices(modulePackage);
            if (services != null) {
                for (OpenmrsService service : services) {
                    service.onShutdown();
                }
            }

            try {
                if (mod.getModuleActivator() != null) {// extends BaseModuleActivator
                    mod.getModuleActivator().stopped();
                } else {
                    mod.getActivator().shutdown(); // implements old Activator interface
                }
            } catch (Exception t) {
                log.warn("Unable to call module's Activator.shutdown() method", t);
            }

            //Since extensions are loaded by the module class loader which is about to be disposed,
            //we need to clear them, else we shall never be able to unload the class loader until
            //when we unload the module, hence resulting into two problems:
            // 1) Memory leakage for start/stop module.
            // 2) Calls to Context.getService(Service.class) which are made within these extensions 
            //     will throw APIException("Service not found: ") because their calls to Service.class
            //    will pass in a Class from the old module class loader (which loaded them) yet the
            //    ServiceContext will have new services from a new module class loader.
            //
            //Same thing applies to activator, moduleActivator and AdvicePoint classInstance.
            mod.getExtensions().clear();
            mod.setActivator(null);
            mod.setModuleActivator(null);
            mod.disposeAdvicePointsClassInstance();

            ModuleClassLoader cl = removeClassLoader(mod);
            if (cl != null) {
                cl.dispose();
                cl = null;
                // remove files from lib cache
                File folder = OpenmrsClassLoader.getLibCacheFolder();
                File tmpModuleDir = new File(folder, moduleId);
                try {
                    OpenmrsUtil.deleteDirectory(tmpModuleDir);
                } catch (IOException e) {
                    log.warn("Unable to delete libcachefolder for " + moduleId);
                }
            }
        }

        return dependentModulesStopped;
    }

    private static ModuleClassLoader removeClassLoader(Module mod) {
        getModuleClassLoaderMap(); // create map if it is null
        if (!moduleClassLoaders.containsKey(mod)) {
            log.warn("Module: " + mod.getModuleId() + " does not exist");
        }

        return moduleClassLoaders.remove(mod);
    }

    /**
     * Removes module from module repository
     * 
     * @param mod module to unload
     */
    public static void unloadModule(Module mod) {

        // remove this module's advice and extensions
        if (isModuleStarted(mod)) {
            stopModule(mod, true);
        }

        // remove from list of loaded modules
        getLoadedModules().remove(mod);

        if (mod != null) {
            // remove the file from the module repository
            File file = mod.getFile();

            boolean deleted = file.delete();
            if (!deleted) {
                file.deleteOnExit();
                log.warn("Could not delete " + file.getAbsolutePath());
            }

            file = null;
            mod = null;
        }
    }

    /**
     * Return all of the extensions associated with the given <code>pointId</code> Returns empty
     * extension list if no modules extend this pointId
     * 
     * @param pointId
     * @return List of extensions
     */
    public static List<Extension> getExtensions(String pointId) {
        List<Extension> extensions = null;
        Map<String, List<Extension>> extensionMap = getExtensionMap();

        // get all extensions for this exact pointId
        extensions = extensionMap.get(pointId);
        if (extensions == null) {
            extensions = new ArrayList<Extension>();
        }

        // if this pointId doesn't contain the separator character, search
        // for this point prepended with each MEDIA TYPE
        if (!pointId.contains(Extension.extensionIdSeparator)) {
            for (MEDIA_TYPE mediaType : Extension.MEDIA_TYPE.values()) {

                // get all extensions for this type and point id
                List<Extension> tmpExtensions = extensionMap.get(Extension.toExtensionId(pointId, mediaType));

                // 'extensions' should be a unique list
                if (tmpExtensions != null) {
                    for (Extension ext : tmpExtensions) {
                        if (!extensions.contains(ext)) {
                            extensions.add(ext);
                        }
                    }
                }
            }
        }

        log.debug("Getting extensions defined by : " + pointId);
        return extensions;
    }

    /**
     * Return all of the extensions associated with the given <code>pointId</code> Returns
     * getExtension(pointId) if no modules extend this pointId for given media type
     * 
     * @param pointId
     * @param type Extension.MEDIA_TYPE
     * @return List of extensions
     */
    public static List<Extension> getExtensions(String pointId, Extension.MEDIA_TYPE type) {
        String key = Extension.toExtensionId(pointId, type);
        List<Extension> extensions = getExtensionMap().get(key);
        if (extensions != null) {
            log.debug("Getting extensions defined by : " + key);
            return extensions;
        } else {
            return getExtensions(pointId);
        }
    }

    /**
     * Get a list of required Privileges defined by the modules
     * 
     * @return <code>List&lt;Privilege&gt;</code> of the required privileges
     */
    public static List<Privilege> getPrivileges() {

        List<Privilege> privileges = new Vector<Privilege>();

        for (Module mod : getStartedModules()) {
            privileges.addAll(mod.getPrivileges());
        }

        log.debug(privileges.size() + " new privileges");

        return privileges;
    }

    /**
     * Get a list of required GlobalProperties defined by the modules
     * 
     * @return <code>List&lt;GlobalProperty&gt;</code> object of the module's global properties
     */
    public static List<GlobalProperty> getGlobalProperties() {

        List<GlobalProperty> globalProperties = new Vector<GlobalProperty>();

        for (Module mod : getStartedModules()) {
            globalProperties.addAll(mod.getGlobalProperties());
        }

        log.debug(globalProperties.size() + " new global properties");

        return globalProperties;
    }

    /**
     * Checks whether the given module is activated
     * 
     * @param mod Module to check
     * @return true if the module is started, false otherwise
     */
    public static boolean isModuleStarted(Module mod) {
        return getStartedModulesMap().containsValue(mod);
    }

    /**
     * Checks whether the given module, identified by its id, is started.
     * 
     * @param moduleId module id. e.g formentry, logic
     * @since 1.9
     * @return true if the module is started, false otherwise
     */
    public static boolean isModuleStarted(String moduleId) {
        return getStartedModulesMap().containsKey(moduleId);
    }

    /**
     * Get a module's classloader
     * 
     * @param mod Module to fetch the class loader for
     * @return ModuleClassLoader pertaining to this module. Returns null if the module is not
     *         started
     * @throws ModuleException if the module does not have a registered classloader
     */
    public static ModuleClassLoader getModuleClassLoader(Module mod) throws ModuleException {
        ModuleClassLoader mcl = getModuleClassLoaderMap().get(mod);

        if (mcl == null) {
            log.debug("Module classloader not found for module with id: " + mod.getModuleId());
        }

        return mcl;
    }

    /**
     * Get a module's classloader via the module id
     * 
     * @param moduleId <code>String</code> id of the module
     * @return ModuleClassLoader pertaining to this module. Returns null if the module is not
     *         started
     * @throws ModuleException if this module isn't started or doesn't have a classloader
     * @see #getModuleClassLoader(Module)
     */
    public static ModuleClassLoader getModuleClassLoader(String moduleId) throws ModuleException {
        Module mod = getStartedModulesMap().get(moduleId);
        if (mod == null) {
            log.debug("Module id not found in list of started modules: " + moduleId);
        }

        return getModuleClassLoader(mod);
    }

    /**
     * Returns all module classloaders This method will not return null
     * 
     * @return Collection&lt;ModuleClassLoader&gt; all known module classloaders or empty list.
     */
    public static Collection<ModuleClassLoader> getModuleClassLoaders() {
        Map<Module, ModuleClassLoader> classLoaders = getModuleClassLoaderMap();
        if (classLoaders.size() > 0) {
            return classLoaders.values();
        }

        return Collections.emptyList();
    }

    /**
     * Return all current classloaders keyed on module object
     * 
     * @return Map&lt;Module, ModuleClassLoader&gt;
     */
    public static Map<Module, ModuleClassLoader> getModuleClassLoaderMap() {
        if (moduleClassLoaders == null) {
            moduleClassLoaders = new WeakHashMap<Module, ModuleClassLoader>();
        }

        return moduleClassLoaders;
    }

    /**
     * Return the current extension map keyed on extension point id
     * 
     * @return Map&lt;String, List&lt;Extension&gt;&gt;
     */
    public static Map<String, List<Extension>> getExtensionMap() {
        if (extensionMap == null) {
            extensionMap = new WeakHashMap<String, List<Extension>>();
        }

        return extensionMap;
    }

    /**
     * Tests whether all modules mentioned in module.requiredModules are loaded and started already
     * (by being in the startedModules list)
     * 
     * @param module
     * @return true/false boolean whether this module's required modules are all started
     */
    private static boolean requiredModulesStarted(Module module) {
        //required
        for (String reqModPackage : module.getRequiredModules()) {
            boolean started = false;
            for (Module mod : getStartedModules()) {
                if (mod.getPackageName().equals(reqModPackage)) {
                    String reqVersion = module.getRequiredModuleVersion(reqModPackage);
                    if (reqVersion == null || ModuleUtil.compareVersion(mod.getVersion(), reqVersion) >= 0) {
                        started = true;
                    }
                    break;
                }
            }

            if (!started) {
                return false;
            }
        }

        return true;
    }

    /**
     * Update the module: 1) Download the new module 2) Unload the old module 3) Load/start the new
     * module
     * 
     * @param mod
     */
    public static Module updateModule(Module mod) throws ModuleException {
        if (mod.getDownloadURL() == null) {
            return mod;
        }

        URL url = null;
        try {
            url = new URL(mod.getDownloadURL());
        } catch (MalformedURLException e) {
            throw new ModuleException("Unable to download module update", e);
        }

        unloadModule(mod);

        // copy content to a temporary file
        InputStream inputStream = ModuleUtil.getURLStream(url);
        log.warn("url pathname: " + url.getPath());
        String filename = url.getPath().substring(url.getPath().lastIndexOf("/"));
        File moduleFile = ModuleUtil.insertModuleFile(inputStream, filename);

        try {
            // load, and start the new module
            Module newModule = loadModule(moduleFile);
            startModule(newModule);
            return newModule;
        } catch (Exception e) {
            log.warn("Error while unloading old module and loading in new module");
            moduleFile.delete();
            return mod;
        }

    }

    /**
     * Validates the given token.
     * <p>
     * It is thread safe.
     * 
     * @param token
     * @since 1.9.2
     */
    public static boolean isTokenValid(DaemonToken token) {
        if (token == null) {
            return false;
        } else {
            //We need to synchronize to guarantee that the last passed token is valid.
            synchronized (daemonTokens) {
                DaemonToken validToken = daemonTokens.get(token.getId());
                //Compare by reference to defend from overridden equals.
                return validToken == token;
            }
        }
    }

    /**
     * Passes a daemon token to the given module.
     * <p>
     * The token is passed to that module's {@link ModuleActivator} if it implements
     * {@link DaemonTokenAware}.
     * <p>
     * This method is called automatically before {@link ModuleActivator#contextRefreshed()} or
     * {@link ModuleActivator#started()}. Note that it may be called multiple times and there is no
     * guarantee that it will always pass the same token. The last passed token is valid, whereas
     * previously passed tokens may be invalidated.
     * <p>
     * It is thread safe.
     * 
     * @param module
     * @since 1.9.2
     */
    static void passDaemonToken(Module module) {
        ModuleActivator moduleActivator = module.getModuleActivator();
        if (moduleActivator instanceof DaemonTokenAware) {
            DaemonToken daemonToken = getDaemonToken(module);
            ((DaemonTokenAware) module.getModuleActivator()).setDaemonToken(daemonToken);
        }
    }

    /**
     * Gets a new or existing token. Uses weak references for tokens so that they are garbage
     * collected when not needed.
     * <p>
     * It is thread safe.
     * 
     * @param module
     * @return the token
     */
    private static DaemonToken getDaemonToken(Module module) {
        synchronized (daemonTokens) {
            DaemonToken token = daemonTokens.get(module.getModuleId());
            if (token != null) {
                return token;
            }

            token = new DaemonToken(module.getModuleId());
            daemonTokens.put(module.getModuleId(), token);

            return token;
        }
    }

    /**
     * Returns the description for the [moduleId].started global property
     * 
     * @param moduleId
     * @return description to use for the .started property
     */
    private static String getGlobalPropertyStartedDescription(String moduleId) {
        String ret = "DO NOT MODIFY. true/false whether or not the " + moduleId;
        ret += " module has been started.  This is used to make sure modules that were running ";
        ret += " prior to a restart are started again";

        return ret;
    }

    /**
     * Returns the description for the [moduleId].mandatory global property
     * 
     * @param moduleId
     * @return description to use for .mandatory property
     */
    private static String getGlobalPropertyMandatoryModuleDescription(String moduleId) {
        String ret = "true/false whether or not the " + moduleId;
        ret += " module MUST start when openmrs starts.  This is used to make sure that mission critical";
        ret += " modules are always running if openmrs is running.";

        return ret;
    }

    /**
     * Convenience method to save a global property with the given value. Proxy privileges are added
     * so that this can occur at startup.
     * 
     * @param key the property for this global property
     * @param value the value for this global property
     * @param desc the description
     * @see AdministrationService#saveGlobalProperty(GlobalProperty)
     */
    private static void saveGlobalProperty(String key, String value, String desc) {
        try {
            AdministrationService as = Context.getAdministrationService();
            GlobalProperty gp = as.getGlobalPropertyObject(key);
            if (gp == null) {
                gp = new GlobalProperty(key, value, desc);
            } else {
                gp.setPropertyValue(value);
            }

            as.saveGlobalProperty(gp);
        } catch (Exception e) {
            log.warn("Unable to save the global property", e);
        }
    }

    /**
     * Convenience method used to identify module interdependencies and alert the user before
     * modules are shut down.
     * 
     * @param moduleId the moduleId used to identify the module being validated
     * @return List&lt;dependentModules&gt; the list of moduleId's which depend on the module about to be
     *         shutdown.
     * @since 1.10
     */
    public static List<String> getDependencies(String moduleId) {
        List<String> dependentModules = null;
        Module module = getModuleById(moduleId);

        Map<String, Module> startedModules = getStartedModulesMap();
        String modulePackage = module.getPackageName();

        for (Entry<String, Module> entry : startedModules.entrySet()) {
            if (!moduleId.equals(entry.getKey()) && entry.getValue().getRequiredModules().contains(modulePackage)) {
                if (dependentModules == null) {
                    dependentModules = new ArrayList<String>();
                }
                dependentModules.add(entry.getKey() + " " + entry.getValue().getVersion());
            }
        }
        return dependentModules;
    }
}