diff --git a/api/src/main/java/org/openmrs/api/AdministrationService.java b/api/src/main/java/org/openmrs/api/AdministrationService.java
index 8426c4e206fe..8cb562c7ccf5 100644
--- a/api/src/main/java/org/openmrs/api/AdministrationService.java
+++ b/api/src/main/java/org/openmrs/api/AdministrationService.java
@@ -17,10 +17,12 @@
import org.openmrs.GlobalProperty;
import org.openmrs.ImplementationId;
+import org.openmrs.module.Module;
import org.openmrs.OpenmrsObject;
import org.openmrs.User;
import org.openmrs.annotation.Authorized;
import org.openmrs.api.db.AdministrationDAO;
+import org.openmrs.util.DatabaseUpdateException;
import org.openmrs.util.HttpClient;
import org.openmrs.util.OpenmrsConstants;
import org.openmrs.util.PrivilegeConstants;
@@ -420,4 +422,33 @@ public interface AdministrationService extends OpenmrsService {
* Should return default common classes if no GPs defined
*/
List getSerializerWhitelistTypes();
+
+ /**
+ * Checks whether a core setup needs to be run due to a version change.
+ *
+ * @return true if core setup should be executed because of a version change, false otherwise
+ */
+ public boolean isCoreSetupOnVersionChangeNeeded();
+
+ /**
+ * Checks whether a module setup needs to be run due to a version change.
+ *
+ * @param moduleId the identifier of the module to check
+ * @return true if the module setup should be executed because of a version change, false otherwise
+ */
+ public boolean isModuleSetupOnVersionChangeNeeded(String moduleId);
+
+ /**
+ * Executes the core setup procedures required after a core version change.
+ *
+ * @throws DatabaseUpdateException
+ */
+ public void runCoreSetupOnVersionChange() throws DatabaseUpdateException;
+
+ /**
+ * Executes the setup procedures required for a module after a module version change.
+ *
+ * @param module the module for which the setup should be executed
+ */
+ public void runModuleSetupOnVersionChange(Module module);
}
diff --git a/api/src/main/java/org/openmrs/api/context/Context.java b/api/src/main/java/org/openmrs/api/context/Context.java
index ffb1e5185bc0..dda1ba53c7e2 100644
--- a/api/src/main/java/org/openmrs/api/context/Context.java
+++ b/api/src/main/java/org/openmrs/api/context/Context.java
@@ -1311,13 +1311,18 @@ private static void checkForDatabaseUpdates(Properties props) throws DatabaseUpd
// this must be the first thing run in case it changes database mappings
if (updatesRequired) {
- if (DatabaseUpdater.allowAutoUpdate()) {
- DatabaseUpdater.executeChangelog();
- } else {
+ if (!DatabaseUpdater.allowAutoUpdate()) {
throw new DatabaseUpdateException(
"Database updates are required. Call Context.updateDatabase() before .startup() to continue.");
}
}
+
+ if (getAdministrationService().isCoreSetupOnVersionChangeNeeded()) {
+ log.info("Detected core version change. Running core setup hooks and Liquibase.");
+ getAdministrationService().runCoreSetupOnVersionChange();
+ }
+
+ log.info("Database update check completed.");
}
/**
diff --git a/api/src/main/java/org/openmrs/api/impl/AdministrationServiceImpl.java b/api/src/main/java/org/openmrs/api/impl/AdministrationServiceImpl.java
index ab9ab42ed8d1..38d1eaa389cc 100644
--- a/api/src/main/java/org/openmrs/api/impl/AdministrationServiceImpl.java
+++ b/api/src/main/java/org/openmrs/api/impl/AdministrationServiceImpl.java
@@ -25,6 +25,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Properties;
import java.util.Set;
import java.util.SortedMap;
@@ -59,6 +60,8 @@
import org.openmrs.module.ModuleUtil;
import org.openmrs.obs.ComplexData;
import org.openmrs.person.PersonMergeLogData;
+import org.openmrs.util.DatabaseUpdateException;
+import org.openmrs.util.DatabaseUpdater;
import org.openmrs.util.HttpClient;
import org.openmrs.util.LocaleUtility;
import org.openmrs.util.OpenmrsConstants;
@@ -1010,4 +1013,99 @@ public List> getRefTypes() {
return Arrays.asList(GlobalProperty.class);
}
+ /**
+ * @see org.openmrs.api.AdministrationService#isCoreSetupOnVersionChangeNeeded()
+ */
+ @Override
+ public boolean isCoreSetupOnVersionChangeNeeded() {
+ String stored = getStoredCoreVersion();
+ String current = OpenmrsConstants.OPENMRS_VERSION_SHORT;
+ boolean forceSetup = Boolean.parseBoolean(getGlobalProperty("force.setup", "false"));
+
+ return forceSetup || !Objects.equals(stored, current);
+ }
+
+ /**
+ * @see org.openmrs.api.AdministrationService#isModuleSetupOnVersionChangeNeeded(String)
+ */
+ @Override
+ public boolean isModuleSetupOnVersionChangeNeeded(String moduleId) {
+ String stored = getStoredModuleVersion(moduleId);
+ Module module = ModuleFactory.getModuleById(moduleId);
+ if (module == null) {
+ return false;
+ }
+ String current = module.getVersion();
+ boolean forceSetup = Boolean.parseBoolean(getGlobalProperty("force.setup", "false"));
+
+ return forceSetup || !Objects.equals(stored, current);
+ }
+
+ /**
+ * @see org.openmrs.api.AdministrationService#runCoreSetupOnVersionChange()
+ */
+ @Override
+ @Transactional
+ public void runCoreSetupOnVersionChange() throws DatabaseUpdateException {
+ DatabaseUpdater.executeChangelog();
+ storeCoreVersion();
+ }
+
+ /**
+ * @see org.openmrs.api.AdministrationService#runModuleSetupOnVersionChange(Module)
+ */
+ @Override
+ @Transactional
+ public void runModuleSetupOnVersionChange(Module module) {
+ if (module == null) {
+ return;
+ }
+
+ String moduleId = module.getModuleId();
+ String prevCoreVersion = getStoredCoreVersion() != null ? getStoredCoreVersion() : OpenmrsConstants.OPENMRS_VERSION_SHORT;
+ String prevModuleVersion = getStoredModuleVersion(moduleId);
+
+ module.getModuleActivator().setupOnVersionChangeBeforeSchemaChanges(prevCoreVersion, prevModuleVersion);
+ ModuleFactory.runLiquibaseForModule(module);
+ module.getModuleActivator().setupOnVersionChange(prevCoreVersion, prevModuleVersion);
+
+ storeModuleVersion(moduleId, module.getVersion());
+ }
+
+ protected String getStoredCoreVersion() {
+ return getGlobalProperty("core.version");
+ }
+
+ protected String getStoredModuleVersion(String moduleId) {
+ return getGlobalProperty("module." + moduleId + ".version");
+ }
+
+ protected void storeCoreVersion() {
+ saveGlobalProperty("core.version", OpenmrsConstants.OPENMRS_VERSION_SHORT, "Saved the state of this core version for future restarts");
+ }
+
+ protected void storeModuleVersion(String moduleId, String version) {
+ String propertyName = "module." + moduleId + ".version";
+ saveGlobalProperty(propertyName, version, "Saved the state of this module version for future restarts");
+ }
+
+ /**
+ * Convenience method to save a global property with the given value. Proxy privileges are added so
+ * that this can occur at startup.
+ */
+ protected void saveGlobalProperty(String key, String value, String desc) {
+ try {
+ GlobalProperty gp = getGlobalPropertyObject(key);
+ if (gp == null) {
+ gp = new GlobalProperty(key, value, desc);
+ } else {
+ gp.setPropertyValue(value);
+ }
+
+ saveGlobalProperty(gp);
+ }
+ catch (Exception e) {
+ log.warn("Unable to save the global property", e);
+ }
+ }
}
diff --git a/api/src/main/java/org/openmrs/module/BaseModuleActivator.java b/api/src/main/java/org/openmrs/module/BaseModuleActivator.java
index f1c1eda0233f..3d29a936c338 100644
--- a/api/src/main/java/org/openmrs/module/BaseModuleActivator.java
+++ b/api/src/main/java/org/openmrs/module/BaseModuleActivator.java
@@ -61,4 +61,17 @@ public void willStart() {
public void willStop() {
}
+ /**
+ * @see org.openmrs.module.ModuleActivator#setupOnVersionChangeBeforeSchemaChanges(String, String)
+ */
+ @Override
+ public void setupOnVersionChangeBeforeSchemaChanges(String previousCoreVersion, String previousModuleVersion) {
+ }
+
+ /**
+ * @see org.openmrs.module.ModuleActivator#setupOnVersionChange(String, String)
+ */
+ @Override
+ public void setupOnVersionChange(String previousCoreVersion, String previousModuleVersion) {
+ }
}
diff --git a/api/src/main/java/org/openmrs/module/ModuleActivator.java b/api/src/main/java/org/openmrs/module/ModuleActivator.java
index 434d7b8b1acb..00485931f768 100644
--- a/api/src/main/java/org/openmrs/module/ModuleActivator.java
+++ b/api/src/main/java/org/openmrs/module/ModuleActivator.java
@@ -56,4 +56,14 @@ public interface ModuleActivator {
*/
public void stopped();
+ /**
+ * Called before Liquibase runs, but only if core or this module version changed.
+ */
+ default void setupOnVersionChangeBeforeSchemaChanges(String previousCoreVersion, String previousModuleVersion) {}
+
+ /**
+ * Called after Liquibase runs, but only if core or this module version changed.
+ */
+ default void setupOnVersionChange(String previousCoreVersion, String previousModuleVersion) {}
+
}
diff --git a/api/src/main/java/org/openmrs/module/ModuleFactory.java b/api/src/main/java/org/openmrs/module/ModuleFactory.java
index aee8358294a9..ec4fe970cc4a 100644
--- a/api/src/main/java/org/openmrs/module/ModuleFactory.java
+++ b/api/src/main/java/org/openmrs/module/ModuleFactory.java
@@ -694,9 +694,10 @@ public static Module startModuleInternal(Module module, boolean isOpenmrsStartup
Context.removeProxyPrivilege("");
}
- // run module's optional liquibase.xml immediately after sqldiff.xml
- log.debug("Run module liquibase: {}", module.getModuleId());
- runLiquibase(module);
+ if (Context.getAdministrationService().isModuleSetupOnVersionChangeNeeded(module.getModuleId())) {
+ log.info("Detected version change for module {}. Running setup hooks and module Liquibase.", module.getModuleId());
+ Context.getAdministrationService().runModuleSetupOnVersionChange(module);
+ }
// effectively mark this module as started successfully
getStartedModulesMap().put(moduleId, module);
@@ -945,6 +946,13 @@ private static void runDiff(Module module, String version, String sql) {
}
}
+
+ /**
+ * This is a convenience method that exposes the private {@link #runLiquibase(Module)} method.
+ */
+ public static void runLiquibaseForModule(Module module) {
+ runLiquibase(module);
+ }
/**
* Execute all not run changeSets in liquibase.xml for the given module
diff --git a/api/src/test/java/org/openmrs/api/AdministrationServiceTest.java b/api/src/test/java/org/openmrs/api/AdministrationServiceTest.java
index 4d59b5afae7f..aab419a09079 100644
--- a/api/src/test/java/org/openmrs/api/AdministrationServiceTest.java
+++ b/api/src/test/java/org/openmrs/api/AdministrationServiceTest.java
@@ -27,6 +27,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
import java.util.ArrayList;
import java.util.Arrays;
@@ -51,6 +52,8 @@
import org.openmrs.customdatatype.datatype.DateDatatype;
import org.openmrs.messagesource.MutableMessageSource;
import org.openmrs.messagesource.impl.MutableResourceBundleMessageSource;
+import org.openmrs.module.Module;
+import org.openmrs.module.ModuleActivator;
import org.openmrs.test.jupiter.BaseContextSensitiveTest;
import org.openmrs.util.HttpClient;
import org.openmrs.util.LocaleUtility;
@@ -1160,4 +1163,29 @@ public void getSerializerWhitelistTypes_shouldReturnDefaultCommonClassesIfNoGPS(
"hierarchyOf:org.openmrs.messagesource.PresentationMessage",
"hierarchyOf:org.openmrs.person.PersonMergeLogData"));
}
+
+ @Test
+ public void runModuleSetupOnVersionChange_shouldExecuteLiquibaseAndStoreNewVersion() {
+ // old version
+ adminService.setGlobalProperty("module.testmodule.version", "1.0.0");
+ assertEquals("1.0.0", adminService.getGlobalProperty("module.testmodule.version"));
+
+ String previousModuleVersion = "1.0.0";
+ String previousCoreVersion = OpenmrsConstants.OPENMRS_VERSION_SHORT;
+
+ Module module = new Module("Test Module");
+ module.setModuleId("testmodule");
+ module.setVersion("1.2.3");
+
+ ModuleActivator activator = mock(ModuleActivator.class);
+ module.setModuleActivator(activator);
+
+ adminService.runModuleSetupOnVersionChange(module);
+
+ assertEquals("1.2.3", adminService.getGlobalProperty("module.testmodule.version"));
+
+ // verify hook methods must be called
+ verify(activator).setupOnVersionChangeBeforeSchemaChanges(previousCoreVersion, previousModuleVersion);
+ verify(activator).setupOnVersionChange(previousCoreVersion, previousModuleVersion);
+ }
}
diff --git a/api/src/test/java/org/openmrs/module/ModuleTestData.java b/api/src/test/java/org/openmrs/module/ModuleTestData.java
index 9ed0ee3e5a2d..17141ca92f51 100644
--- a/api/src/test/java/org/openmrs/module/ModuleTestData.java
+++ b/api/src/test/java/org/openmrs/module/ModuleTestData.java
@@ -27,6 +27,10 @@ public class ModuleTestData {
private Map stoppedCallCount = new HashMap<>();
+ private Map setupOnVersionChangeBeforeSchemaChangesCallCount = new HashMap<>();
+
+ private Map setupOnVersionChangeCallCount = new HashMap<>();
+
private Map willRefreshContextCallTime = new HashMap<>();
private Map contextRefreshedCallTime = new HashMap<>();
@@ -39,6 +43,10 @@ public class ModuleTestData {
private Map stoppedCallTime = new HashMap<>();
+ private Map setupOnVersionChangeBeforeSchemaChangesCallTime = new HashMap<>();
+
+ private Map setupOnVersionChangeCallTime = new HashMap<>();
+
private ModuleTestData() {
}
@@ -59,6 +67,8 @@ public synchronized void init(String moduleId) {
startedCallCount.put(moduleId, 0);
willStopCallCount.put(moduleId, 0);
stoppedCallCount.put(moduleId, 0);
+ setupOnVersionChangeBeforeSchemaChangesCallCount.put(moduleId, 0);
+ setupOnVersionChangeCallCount.put(moduleId, 0);
willRefreshContextCallTime.put(moduleId, 0L);
contextRefreshedCallTime.put(moduleId, 0L);
@@ -66,6 +76,8 @@ public synchronized void init(String moduleId) {
startedCallTime.put(moduleId, 0L);
willStopCallTime.put(moduleId, 0L);
stoppedCallTime.put(moduleId, 0L);
+ setupOnVersionChangeBeforeSchemaChangesCallTime.put(moduleId, 0L);
+ setupOnVersionChangeCallTime.put(moduleId, 0L);
}
public synchronized Integer getWillRefreshContextCallCount(String moduleId) {
@@ -116,6 +128,22 @@ public synchronized Integer getStoppedCallCount(String moduleId) {
return count;
}
+ public synchronized Integer getSetupOnVersionChangeBeforeSchemaChangesCallCount(String moduleId) {
+ Integer count = setupOnVersionChangeBeforeSchemaChangesCallCount.get(moduleId);
+ if (count == null) {
+ count = 0;
+ }
+ return count;
+ }
+
+ public synchronized Integer getSetupOnVersionChangeCallCount(String moduleId) {
+ Integer count = setupOnVersionChangeCallCount.get(moduleId);
+ if (count == null) {
+ count = 0;
+ }
+ return count;
+ }
+
public synchronized void willRefreshContext(String moduleId) {
willRefreshContextCallTime.put(moduleId, new Date().getTime());
@@ -176,6 +204,26 @@ public synchronized void stopped(String moduleId) {
stoppedCallCount.put(moduleId, count + 1);
}
+ public synchronized void setupOnVersionChangeBeforeSchemaChanges(String moduleId) {
+ setupOnVersionChangeBeforeSchemaChangesCallTime.put(moduleId, new Date().getTime());
+
+ Integer count = setupOnVersionChangeBeforeSchemaChangesCallCount.get(moduleId);
+ if (count == null) {
+ count = 0;
+ }
+ setupOnVersionChangeBeforeSchemaChangesCallCount.put(moduleId, count + 1);
+ }
+
+ public synchronized void setupOnVersionChange(String moduleId) {
+ setupOnVersionChangeCallTime.put(moduleId, new Date().getTime());
+
+ Integer count = setupOnVersionChangeCallCount.get(moduleId);
+ if (count == null) {
+ count = 0;
+ }
+ setupOnVersionChangeCallCount.put(moduleId, count + 1);
+ }
+
public synchronized Long getWillRefreshContextCallTime(String moduleId) {
return willRefreshContextCallTime.get(moduleId);
}
@@ -199,4 +247,12 @@ public synchronized Long getWillStopCallTime(String moduleId) {
public synchronized Long getStoppedCallTime(String moduleId) {
return stoppedCallTime.get(moduleId);
}
+
+ public synchronized Long getSetupOnVersionChangeBeforeSchemaChangesCallTime(String moduleId) {
+ return setupOnVersionChangeBeforeSchemaChangesCallTime.get(moduleId);
+ }
+
+ public synchronized Long getSetupOnVersionChangeCallTime(String moduleId) {
+ return setupOnVersionChangeCallTime.get(moduleId);
+ }
}