/**
* 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.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.Vector;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.openmrs.GlobalProperty;
import org.openmrs.Privilege;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
/**
* Generic module class that openmrs manipulates
*
* @version 1.0
*/
public final class Module {
private Logger log = LoggerFactory.getLogger(this.getClass());
private String name;
private String moduleId;
private String packageName;
private String description;
private String author;
private String version;
private String updateURL; // should be a URL to an update.rdf file
private String updateVersion = null; // version obtained from the remote update.rdf file
private String downloadURL = null; // will only be populated when the remote file is newer than the current module
private ModuleActivator moduleActivator;
private String activatorName;
private String requireOpenmrsVersion;
private String requireDatabaseVersion;
private Map<String, String> requiredModulesMap;
private Map<String, String> awareOfModulesMap;
private Map<String, String> startBeforeModulesMap;
private List<AdvicePoint> advicePoints = new Vector<AdvicePoint>();
private IdentityHashMap<String, String> extensionNames = new IdentityHashMap<String, String>();
private List<Extension> extensions = new Vector<Extension>();
private Map<String, Properties> messages = new HashMap<String, Properties>();
private List<Privilege> privileges = new Vector<Privilege>();
private List<GlobalProperty> globalProperties = new Vector<GlobalProperty>();
private List<String> mappingFiles = new Vector<String>();
private Set<String> packagesWithMappedClasses = new HashSet<String>();
private Document config = null;
private Document sqldiff = null;
private boolean mandatory = Boolean.FALSE;
private List<ModuleConditionalResource> conditionalResources = new ArrayList<ModuleConditionalResource>();
// keep a reference to the file that we got this module from so we can delete
// it if necessary
private File file = null;
private String startupErrorMessage = null;
/**
* Simple constructor
*
* @param name
*/
public Module(String name) {
this.name = name;
}
/**
* Main constructor
*
* @param name
* @param moduleId
* @param packageName
* @param author
* @param description
* @param version
*/
public Module(String name, String moduleId, String packageName, String author, String description, String version) {
this.name = name;
this.moduleId = moduleId;
this.packageName = packageName;
this.author = author;
this.description = description;
this.version = version;
log.debug("Creating module " + name);
}
@Override
public boolean equals(Object obj) {
if (obj != null && obj instanceof Module) {
Module mod = (Module) obj;
return getModuleId().equals(mod.getModuleId());
}
return false;
}
@Override
public int hashCode() {
return new HashCodeBuilder().append(getModuleId()).toHashCode();
}
/**
* @return the moduleActivator
*/
public ModuleActivator getModuleActivator() {
try {
if (moduleActivator == null) {
ModuleClassLoader classLoader = ModuleFactory.getModuleClassLoader(this);
if (classLoader == null) {
throw new ModuleException("The classloader is null", getModuleId());
}
Class<?> c = classLoader.loadClass(getActivatorName());
Object o = c.newInstance();
if (ModuleActivator.class.isAssignableFrom(o.getClass())) {
setModuleActivator((ModuleActivator) o);
}
}
}
catch (ClassNotFoundException e) {
throw new ModuleException("Unable to load/find moduleActivator: '" + getActivatorName() + "'", name, e);
}
catch (IllegalAccessException e) {
throw new ModuleException("Unable to load/access moduleActivator: '" + getActivatorName() + "'", name, e);
}
catch (InstantiationException e) {
throw new ModuleException("Unable to load/instantiate moduleActivator: '" + getActivatorName() + "'", name, e);
}
catch (NoClassDefFoundError e) {
throw new ModuleException("Unable to load/find moduleActivator: '" + getActivatorName() + "'", name, e);
}
return moduleActivator;
}
/**
* @param moduleActivator the moduleActivator to set
*/
public void setModuleActivator(ModuleActivator moduleActivator) {
this.moduleActivator = moduleActivator;
}
/**
* @return the activatorName
*/
public String getActivatorName() {
return activatorName;
}
/**
* @param activatorName the activatorName to set
*/
public void setActivatorName(String activatorName) {
this.activatorName = activatorName;
}
/**
* @return the author
*/
public String getAuthor() {
return author;
}
/**
* @param author the author to set
*/
public void setAuthor(String author) {
this.author = author;
}
/**
* @return the description
*/
public String getDescription() {
return description;
}
/**
* @param description the description to set
*/
public void setDescription(String description) {
this.description = description;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the requireDatabaseVersion
*/
public String getRequireDatabaseVersion() {
return requireDatabaseVersion;
}
/**
* @param requireDatabaseVersion the requireDatabaseVersion to set
*/
public void setRequireDatabaseVersion(String requireDatabaseVersion) {
this.requireDatabaseVersion = requireDatabaseVersion;
}
/**
* This list of strings is just what is included in the config.xml file, the full package names:
* e.g. org.openmrs.module.formentry
*
* @return the list of requiredModules
*/
public List<String> getRequiredModules() {
return requiredModulesMap == null ? null : new ArrayList<String>(requiredModulesMap.keySet());
}
/**
* Convenience method to get the version of this given module that is required
*
* @return the version of the given required module, or null if there are no version constraints
* @since 1.5
* @should return null if no required modules exist
* @should return null if no required module by given name exists
*/
public String getRequiredModuleVersion(String moduleName) {
return requiredModulesMap == null ? null : requiredModulesMap.get(moduleName);
}
/**
* This is a convenience method to set all the required modules without any version requirements
*
* @param requiredModules the requiredModules to set for this module
* @should set modules when there is a null required modules map
*/
public void setRequiredModules(List<String> requiredModules) {
if (requiredModulesMap == null) {
requiredModulesMap = new HashMap<String, String>();
}
for (String module : requiredModules) {
requiredModulesMap.put(module, null);
}
}
/**
* @param requiredModule the requiredModule to add for this module
* @param version version requiredModule
* @should add module to required modules map
*/
public void addRequiredModule(String requiredModule, String version) {
if (requiredModulesMap != null) {
requiredModulesMap.put(requiredModule, version);
}
}
/**
* @param requiredModulesMap <code>Map<String,String></code> of the <code>requiredModule</code>s
* to set
* @since 1.5
*/
public void setRequiredModulesMap(Map<String, String> requiredModulesMap) {
this.requiredModulesMap = requiredModulesMap;
}
/**
* Get the modules that are required for this module. The keys in this map are the module
* package names. The values in the map are the required version. If no specific version is
* required, it will be null.
*
* @return a map from required module to the version that is required
*/
public Map<String, String> getRequiredModulesMap() {
return requiredModulesMap;
}
/**
* Sets modules that must start after this module
* @param startBeforeModulesMap the startedBefore modules to set
*/
public void setStartBeforeModulesMap(Map<String, String> startBeforeModulesMap) {
this.startBeforeModulesMap = startBeforeModulesMap;
}
/**
* Gets modules which should start after this
* @return map where key is module name and value is module version
*/
public Map<String, String> getStartBeforeModulesMap() {
return this.startBeforeModulesMap;
}
/**
* Gets names of modules which should start after this
* @since 1.11
* @return list of module names or null
*/
public List<String> getStartBeforeModules() {
return this.startBeforeModulesMap == null ? null : new ArrayList<String>(this.startBeforeModulesMap.keySet());
}
/**
* Sets the modules that this module is aware of.
*
* @param awareOfModulesMap <code>Map<String,String></code> of the
* <code>awareOfModulesMap</code>s to set
* @since 1.9
*/
public void setAwareOfModulesMap(Map<String, String> awareOfModulesMap) {
this.awareOfModulesMap = awareOfModulesMap;
}
/**
* This list of strings is just what is included in the config.xml file, the full package names:
* e.g. org.openmrs.module.formentry, for the modules that this module is aware of.
*
* @since 1.9
* @return the list of awareOfModules
*/
public List<String> getAwareOfModules() {
return awareOfModulesMap == null ? null : new ArrayList<String>(awareOfModulesMap.keySet());
}
public String getAwareOfModuleVersion(String awareOfModule) {
return awareOfModulesMap == null ? null : awareOfModulesMap.get(awareOfModule);
}
/**
* @return the requireOpenmrsVersion
*/
public String getRequireOpenmrsVersion() {
return requireOpenmrsVersion;
}
/**
* @param requireOpenmrsVersion the requireOpenmrsVersion to set
*/
public void setRequireOpenmrsVersion(String requireOpenmrsVersion) {
this.requireOpenmrsVersion = requireOpenmrsVersion;
}
/**
* @return the module id
*/
public String getModuleId() {
return moduleId;
}
/**
* @return the module id, with all . replaced with /
*/
public String getModuleIdAsPath() {
return moduleId == null ? null : moduleId.replace('.', '/');
}
/**
* @param moduleId the module id to set
*/
public void setModuleId(String moduleId) {
this.moduleId = moduleId;
}
/**
* @return the packageName
*/
public String getPackageName() {
return packageName;
}
/**
* @param packageName the packageName to set
*/
public void setPackageName(String packageName) {
this.packageName = packageName;
}
/**
* @return the version
*/
public String getVersion() {
return version;
}
/**
* @param version the version to set
*/
public void setVersion(String version) {
this.version = version;
}
/**
* @return the updateURL
*/
public String getUpdateURL() {
return updateURL;
}
/**
* @param updateURL the updateURL to set
*/
public void setUpdateURL(String updateURL) {
this.updateURL = updateURL;
}
/**
* @return the downloadURL
*/
public String getDownloadURL() {
return downloadURL;
}
/**
* @param downloadURL the downloadURL to set
*/
public void setDownloadURL(String downloadURL) {
this.downloadURL = downloadURL;
}
/**
* @return the updateVersion
*/
public String getUpdateVersion() {
return updateVersion;
}
/**
* @param updateVersion the updateVersion to set
*/
public void setUpdateVersion(String updateVersion) {
this.updateVersion = updateVersion;
}
/**
* @return the extensions
*
* @should not expand extensionNames if extensionNames is null
* @should not expand extensionNames if extensionNames is empty
* @should not expand extensionNames if extensions matches extensionNames
* @should expand extensionNames if extensions does not match extensionNames
*/
public List<Extension> getExtensions() {
if (extensionsMatchNames()) {
return extensions;
} else {
return expandExtensionNames();
}
}
/**
* @param extensions the extensions to set
*/
public void setExtensions(List<Extension> extensions) {
this.extensions = extensions;
}
/**
* A map of pointid to classname. The classname is expected to be a class that extends the
* {@link Extension} object. <br>
* <br>
* This map will be expanded into full Extension objects the first time {@link #getExtensions()}
* is called
*
* @param map from pointid to classname
* @see ModuleFileParser
*/
public void setExtensionNames(IdentityHashMap<String, String> map) {
if (log.isDebugEnabled()) {
for (Map.Entry<String, String> entry : extensionNames.entrySet()) {
log.debug("Setting extension names: " + entry.getKey() + " : " + entry.getValue());
}
}
this.extensionNames = map;
}
/**
* Tests whether extensions match the contents of extensionNames. Used to determine
* if expandExtensionNames should to be called.<br>
*
* @return a boolean for whether extensions match the contents of extensionNames
*/
private boolean extensionsMatchNames() {
if (extensionNames != null && extensionNames.size() != 0) {
for (Extension ext : extensions) {
if (extensionNames.get(ext.getPointId()) != ext.getClass().getName()) {
return false;
}
}
if (extensions.size() != extensionNames.size()) {
return false;
}
}
return true;
}
/**
* Expand the temporary extensionNames map of pointid-classname to full pointid-classobject. <br>
* This has to be done after the fact because when the pointid-classnames are parsed, the
* module's objects aren't fully realized yet and so not all classes can be loaded. <br>
* <br>
*
* @return a list of full Extension objects
*/
private List<Extension> expandExtensionNames() {
ModuleClassLoader moduleClsLoader = ModuleFactory.getModuleClassLoader(this);
if (moduleClsLoader == null) {
log.debug(String.format("Module class loader is not available, maybe the module %s is stopped/stopping",
getName()));
} else if (!extensionsMatchNames()) {
extensions.clear();
for (Map.Entry<String, String> entry : extensionNames.entrySet()) {
String point = entry.getKey();
String className = entry.getValue();
final String errorLoadClassString = "Unable to load class for extension: ";
log.debug("expanding extension names: " + point + " : " + className);
try {
Class<?> cls = moduleClsLoader.loadClass(className);
Extension ext = (Extension) cls.newInstance();
ext.setPointId(point);
ext.setModuleId(this.getModuleId());
extensions.add(ext);
log.debug("Added extension: " + ext.getExtensionId() + " : " + ext.getClass());
}
catch (NoClassDefFoundError e) {
log.warn(getModuleId() + ": Unable to find class definition for extension: " + point, e);
}
catch (ClassNotFoundException e) {
log.warn(errorLoadClassString + point, e);
}
catch (IllegalAccessException e) {
log.warn(errorLoadClassString + point, e);
}
catch (InstantiationException e) {
log.warn(errorLoadClassString + point, e);
}
}
}
return extensions;
}
/**
* @return the advicePoints
*/
public List<AdvicePoint> getAdvicePoints() {
return advicePoints;
}
/**
* @param advicePoints the advicePoints to set
*/
public void setAdvicePoints(List<AdvicePoint> advicePoints) {
this.advicePoints = advicePoints;
}
public File getFile() {
return file;
}
public void setFile(File file) {
this.file = file;
}
/**
* Gets a mapping from locale to properties used by this module. The locales are represented as
* a string containing language and country codes.
*
* @return mapping from locales to properties
* @deprecated as of 2.0 because messages are automatically loaded from the classpath
*/
@Deprecated
public Map<String, Properties> getMessages() {
return messages;
}
/**
* Sets the map from locale to properties used by this module.
*
* @param messages map of locale to properties for that locale
* @deprecated as of 2.0 because messages are automatically loaded from the classpath
*/
@Deprecated
public void setMessages(Map<String, Properties> messages) {
this.messages = messages;
}
public List<GlobalProperty> getGlobalProperties() {
return globalProperties;
}
public void setGlobalProperties(List<GlobalProperty> globalProperties) {
this.globalProperties = globalProperties;
}
public List<Privilege> getPrivileges() {
return privileges;
}
public void setPrivileges(List<Privilege> privileges) {
this.privileges = privileges;
}
public Document getConfig() {
return config;
}
public void setConfig(Document config) {
this.config = config;
}
public Document getSqldiff() {
return sqldiff;
}
public void setSqldiff(Document sqldiff) {
this.sqldiff = sqldiff;
}
public List<String> getMappingFiles() {
return mappingFiles;
}
public void setMappingFiles(List<String> mappingFiles) {
this.mappingFiles = mappingFiles;
}
/**
* Packages to scan for classes with JPA annotated classes.
* @return the set of packages to scan
* @since 1.9.2, 1.10
*/
public Set<String> getPackagesWithMappedClasses() {
return packagesWithMappedClasses;
}
/**
* @param packagesToScan
* @see #getPackagesWithMappedClasses()
* @since 1.9.2, 1.10
*/
public void setPackagesWithMappedClasses(Set<String> packagesToScan) {
this.packagesWithMappedClasses = new HashSet<String>(packagesToScan);
}
/**
* This property is set by the module owner to tell OpenMRS that once it is installed, it must
* always startup. This is intended for modules with system-critical monitoring or security
* checks that should always be in place.
*
* @return true if this module has said that it should always start up
*/
public boolean isMandatory() {
return mandatory;
}
public void setMandatory(boolean mandatory) {
this.mandatory = mandatory;
}
/**
* This is a convenience method to know whether this module is core to OpenMRS. A module is
* 'core' when this module is essentially part of the core code and must exist at all times
*
* @return true if this is an OpenMRS core module
* @see ModuleConstants#CORE_MODULES
*/
public boolean isCoreModule() {
return !ModuleUtil.ignoreCoreModules() && ModuleConstants.CORE_MODULES.containsKey(moduleId);
}
public boolean isStarted() {
return ModuleFactory.isModuleStarted(this);
}
/**
* @param e string to set as startup error message
* @should throw exception when message is null
*/
public void setStartupErrorMessage(String e) {
if (e == null) {
throw new ModuleException("Startup error message cannot be null", this.getModuleId());
}
this.startupErrorMessage = e;
}
/**
* Add the given exceptionMessage and throwable as the startup error for this module. This
* method loops over the stacktrace and adds the detailed message
*
* @param exceptionMessage optional. the default message to show on the first line of the error
* message
* @param t throwable stacktrace to include in the error message
*
* @should throw exception when throwable is null
* @should set StartupErrorMessage when exceptionMessage is null
* @should append throwable's message to exceptionMessage
*/
public void setStartupErrorMessage(String exceptionMessage, Throwable t) {
if (t == null) {
throw new ModuleException("Startup error value cannot be null", this.getModuleId());
}
StringBuilder sb = new StringBuilder();
// if exceptionMessage is not null, append it
if (exceptionMessage != null) {
sb.append(exceptionMessage);
sb.append("\n");
}
sb.append(t.getMessage());
sb.append("\n");
this.startupErrorMessage = sb.toString();
}
public String getStartupErrorMessage() {
return startupErrorMessage;
}
public Boolean hasStartupError() {
return (this.startupErrorMessage != null);
}
public void clearStartupError() {
this.startupErrorMessage = null;
}
@Override
public String toString() {
if (moduleId == null) {
return super.toString();
}
return moduleId;
}
/*
* @should dispose all classInstances, not AdvicePoints
*/
public void disposeAdvicePointsClassInstance() {
if (advicePoints == null) {
return;
}
for (AdvicePoint advicePoint : advicePoints) {
advicePoint.disposeClassInstance();
}
}
public List<ModuleConditionalResource> getConditionalResources() {
return conditionalResources;
}
public void setConditionalResources(List<ModuleConditionalResource> conditionalResources) {
this.conditionalResources = conditionalResources;
}
public boolean isCore() {
return ModuleConstants.CORE_MODULES.containsKey(getModuleId());
}
}