/** * 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.rmi.activation.Activator; 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.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.slf4j.Logger; import org.slf4j.LoggerFactory; 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 Logger log = LoggerFactory.getLogger(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 * @should load module if it is currently not loaded * @should not load module if already loaded * @should always load module if replacement is wanted * @should not load an older version of the same module * @should load a newer version of the same module * @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 * @should not crash when file is not found or broken * @should setup requirement mappings for every module * @should not start the loaded modules */ public static void loadModules(List<File> modulesToLoad) { // loop over the modules and load all the modules that we can for (File f : modulesToLoad) { if (f.exists()) { // 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); } } } else { log.debug("Could not find file in module directory: " + f); } } //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().isEmpty()) { List<Module> modules = getModulesThatShouldStart(); try { modules = getModulesInStartupOrder(modules); } catch (CycleException ex) { String message = getCyclicDependenciesMessage(ex.getMessage()); log.error(message, ex); notifySuperUsersAboutCyclicDependencies(ex); modules = (List<Module>)ex.getExtraData(); } // 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); } } } } /** * 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 || "true".equals(startedProp) || "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(Exception ex) { try { Context.addProxyPrivilege(PrivilegeConstants.MANAGE_ALERTS); Context.getAlertService().notifySuperUsers("Module.error.cyclicDependencies", ex, ex.getMessage()); } 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<Module></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<String> 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<Module></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<ModuleId, Module> * * @return map<ModuleId, Module> */ 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<PackageName, Module> * * @return map<PackageName, Module> */ 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<Module></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<ModuleId, * Module> * * @return Map<ModuleId, Module> */ 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 startModule(module, false, null); } /** * 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 { if (!requiredModulesStarted(module)) { int missingModules = 0; for (String packageName : module.getRequiredModulesMap().keySet()) { Module mod = getModuleByPackage(packageName); // mod not installed if (mod == null) { missingModules++; continue; } if (!mod.isStarted()) { startModule(mod); } } if (missingModules > 0) { String message = getFailedToStartModuleMessage(module); log.error(message); module.setStartupErrorMessage(message); notifySuperUsersAboutModuleFailure(module); // instead of return null, i realized that Daemon.startModule() always returns a Module // object,irrespective of whether the startup succeeded return module; } } 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().isEmpty() || !module.getGlobalProperties().isEmpty()) { 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(); } } 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(String message) { return Context.getMessageSourceService().getMessage("Module.error.cyclicDependencies", new Object[]{ message }, 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); } } } /** * 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 not run 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 <moduleid>.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) && isModuleRequiredByAnother(dependentModule, 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); 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(); } } 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.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; } /** * Checks if a module is required by another * * @param dependentModule the module whose required modules are to be checked * @param modulePackage the package of the module to check if required by another * @return true if the module is required, else false */ private static boolean isModuleRequiredByAnother(Module dependentModule, String modulePackage) { return dependentModule.getRequiredModules() != null && dependentModule.getRequiredModules().contains(modulePackage); } 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<Privilege></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<GlobalProperty></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<ModuleClassLoader> 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<Module, ModuleClassLoader> */ 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<String, List<Extension>> */ 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<dependentModules> 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; } }