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); + } }