• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openmrs / openmrs-core / 20784346756

07 Jan 2026 02:13PM UTC coverage: 63.283%. First build
20784346756

push

github

web-flow
TRUNK-6418 Run liquibase checks and data imports only when version of core or modules changes (#5603)

* TRUNK-6418: Run liquibase checks and data imports only when version of core or modules changes

(cherry picked from commit 4723e71c3)

* TRUNK-6418: Follow up adjustments

---------

Co-authored-by: IamMujuziMoses <mujuzimoses@gmail.com>

49 of 112 new or added lines in 9 files covered. (43.75%)

23080 of 36471 relevant lines covered (63.28%)

0.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

59.13
/api/src/main/java/org/openmrs/util/DatabaseUpdater.java
1
/**
2
 * This Source Code Form is subject to the terms of the Mozilla Public License,
3
 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
4
 * obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
5
 * the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
6
 *
7
 * Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
8
 * graphic logo is a trademark of OpenMRS Inc.
9
 */
10
package org.openmrs.util;
11

12
import liquibase.Contexts;
13
import liquibase.GlobalConfiguration;
14
import liquibase.LabelExpression;
15
import liquibase.Liquibase;
16
import liquibase.RuntimeEnvironment;
17
import liquibase.Scope;
18
import liquibase.changelog.ChangeLogHistoryServiceFactory;
19
import liquibase.changelog.ChangeLogIterator;
20
import liquibase.changelog.ChangeSet;
21
import liquibase.changelog.DatabaseChangeLog;
22
import liquibase.changelog.filter.ChangeSetFilterResult;
23
import liquibase.changelog.filter.ContextChangeSetFilter;
24
import liquibase.changelog.filter.DbmsChangeSetFilter;
25
import liquibase.changelog.filter.ShouldRunChangeSetFilter;
26
import liquibase.changelog.visitor.UpdateVisitor;
27
import liquibase.command.core.StatusCommandStep;
28
import liquibase.database.Database;
29
import liquibase.database.DatabaseFactory;
30
import liquibase.database.jvm.JdbcConnection;
31
import liquibase.exception.LiquibaseException;
32
import liquibase.exception.LockException;
33
import liquibase.lockservice.LockService;
34
import liquibase.lockservice.LockServiceFactory;
35
import liquibase.resource.CompositeResourceAccessor;
36
import liquibase.resource.FileSystemResourceAccessor;
37
import liquibase.resource.ResourceAccessor;
38
import org.apache.commons.io.IOUtils;
39
import org.openmrs.api.context.Context;
40
import org.openmrs.liquibase.ChangeLogDetective;
41
import org.openmrs.liquibase.ChangeLogVersionFinder;
42
import org.openmrs.liquibase.ChangeSetExecutorCallback;
43
import org.openmrs.liquibase.LiquibaseProvider;
44
import org.openmrs.liquibase.OpenmrsClassLoaderResourceAccessor;
45
import org.openmrs.module.ModuleClassLoader;
46
import org.slf4j.Logger;
47
import org.slf4j.LoggerFactory;
48

49
import java.io.BufferedWriter;
50
import java.io.File;
51
import java.io.FileNotFoundException;
52
import java.io.FileOutputStream;
53
import java.io.InputStream;
54
import java.io.OutputStreamWriter;
55
import java.io.PrintWriter;
56
import java.nio.charset.StandardCharsets;
57
import java.sql.Connection;
58
import java.sql.DriverManager;
59
import java.sql.PreparedStatement;
60
import java.sql.ResultSet;
61
import java.sql.SQLException;
62
import java.util.ArrayList;
63
import java.util.Arrays;
64
import java.util.Calendar;
65
import java.util.Date;
66
import java.util.HashMap;
67
import java.util.HashSet;
68
import java.util.LinkedList;
69
import java.util.List;
70
import java.util.Map;
71
import java.util.Objects;
72
import java.util.Properties;
73
import java.util.Set;
74

75
/**
76
 * This class uses Liquibase to update the database. <br>
77
 * <br>
78
 * See src/main/resources/liquibase-update-to-latest.xml for the changes. This class will also run
79
 * arbitrary liquibase xml files on the associated database as well. Details for the database are
80
 * taken from the openmrs runtime properties.
81
 *
82
 * @since 1.5
83
 */
84
public class DatabaseUpdater {
×
85
        
86
        private static final Logger log = LoggerFactory.getLogger(DatabaseUpdater.class);
1✔
87
        
88
        private static final String EMPTY_CHANGE_LOG_FILE = "liquibase-empty-changelog.xml";
89
        
90
        public static final String CONTEXT = "core";
91
        
92
        public static final String DATABASE_UPDATES_LOG_FILE = "liquibaseUpdateLogs.txt";
93
        
94
        private static Integer authenticatedUserId;
95
        
96
        private static final ChangeLogDetective changeLogDetective;
97
        
98
        private static final ChangeLogVersionFinder changeLogVersionFinder;
99
        
100
        private static LiquibaseProvider liquibaseProvider;
101
        
102
        static {
103
                changeLogDetective = ChangeLogDetective.getInstance();
1✔
104
                changeLogVersionFinder = new ChangeLogVersionFinder();
1✔
105
        }
106
        
107
        /**
108
         * Allows to inject a LiquibaseProvider instance for testing purposes.
109
         * 
110
         * @param liquibaseProvider
111
         */
112
        static void setLiquibaseProvider(LiquibaseProvider liquibaseProvider) {
113
                DatabaseUpdater.liquibaseProvider = liquibaseProvider;
1✔
114
        }
1✔
115

116
        /**
117
         * Removes any LiquibaseProvider instance that was set for testing purposes.
118
         */
119
        static void unsetLiquibaseProvider() {
120
                DatabaseUpdater.liquibaseProvider = null;
1✔
121
        }
1✔
122
        
123
        /**
124
         * Holds the update warnings generated by the custom liquibase changesets as they are executed
125
         */
126
        private static volatile List<String> updateWarnings = null;
1✔
127
        
128
        /**
129
         * Convenience method to run the changesets using Liquibase to bring the database up to a version
130
         * compatible with the code
131
         */
132
        public static void executeChangelog() throws DatabaseUpdateException {
133
                final LiquibaseProvider liquibaseProvider = new DatabaseUpdaterLiquibaseProvider();
×
134
                
135
                final List<String> changeLogs;
136
                try {
137
                        final String version = changeLogDetective.getInitialLiquibaseSnapshotVersion(CONTEXT, liquibaseProvider);
×
138
                        log.debug(
×
139
                                "updating the database with versions of liquibase-update-to-latest files greater than '{}'",
140
                                version);
141
                        
142
                        changeLogs = changeLogDetective.getUnrunLiquibaseUpdateFileNames(version, CONTEXT, liquibaseProvider);
×
143
                        log.debug("found applicable Liquibase update change logs: {}", changeLogs);
×
144
                }
145
                catch (Exception e) {
×
146
                        log.error("Error while trying to find database changes to run", e);
×
147
                        throw new DatabaseUpdateException("Error while trying to find database changes to run", e);
×
148
                }
×
149
                
150
                if (changeLogs.isEmpty()) {
×
151
                        return;
×
152
                }
153
                
154
                for (String changeLog : changeLogs) {
×
155
                        log.debug("applying Liquibase changelog '{}'", changeLog);
×
156
                        executeChangelog(changeLog, (ChangeSetExecutorCallback) null);
×
157
                }
×
158
        }
×
159
        
160
        /**
161
         * Run changesets on database using Liquibase to get the database up to the most recent version
162
         *
163
         * @param changelog the liquibase changelog file to use (or null to use the default file)
164
         * @param userInput nullable map from question to user answer. Used if a call to update(null) threw
165
         *            an {@link InputRequiredException}
166
         * @throws DatabaseUpdateException if an error occurs
167
         * @deprecated as of 2.4 see {@link #executeChangelog(String, ChangeSetExecutorCallback)}
168
         */
169
        @Deprecated
170
        public static void executeChangelog(String changelog, Map<String, Object> userInput)
171
                throws DatabaseUpdateException {
172
                log.debug("Executing changelog: {}" , changelog);
×
173
                executeChangelog(changelog, (ChangeSetExecutorCallback) null);
×
174
        }
×
175

176
        /**
177
         * Executes the given changelog file. This file is assumed to be on the classpath.
178
         *
179
         * @param changelog The string filename of a liquibase changelog xml file to run
180
         * @return A list of messages or warnings generated by the executed changesets
181
         */
182
        public static List<String> executeChangelog(String changelog, ChangeSetExecutorCallback callback)
183
                throws DatabaseUpdateException {
184
                log.debug("installing the tables into the database");
1✔
185
                
186
                if (changelog == null) {
1✔
187
                        throw new IllegalArgumentException("changelog must not be null");
1✔
188
                }
189
                
190
                try {
191
                        log.debug("executing liquibase changelog {}", changelog);
1✔
192
                        return executeChangelog(changelog, new Contexts(CONTEXT), callback, null);
1✔
193
                }
194
                catch (Exception e) {
×
195
                        throw new DatabaseUpdateException("There was an error while updating the database to the latest. file: "
×
196
                                + changelog + ". Error: " + e.getMessage(), e);
×
197
                }
198
        }
199
        
200
        /**
201
         * This code was borrowed from the liquibase jar so that we can call the given callback function.
202
         *
203
         * @param changeLogFile the file to execute
204
         * @param contexts the liquibase changeset context
205
         * @param callback the function to call after every changeset
206
         * @return A list of messages or warnings generated by the executed changesets
207
         * @throws Exception
208
         */
209
        public static List<String> executeChangelog(String changeLogFile, Contexts contexts, ChangeSetExecutorCallback callback,
210
                ClassLoader cl) throws Exception {
211
                
212
                if (cl == null) {
1✔
213
                        cl = OpenmrsClassLoader.getInstance();
1✔
214
                }
215
                
216
                Thread.currentThread().setContextClassLoader(cl);
1✔
217
                
218
                log.debug("Setting up liquibase object to run changelog: {}", changeLogFile);
1✔
219
                Liquibase liquibase = getLiquibase(changeLogFile, cl);
1✔
220

221
                int numChangeSetsToRun = new StatusCommandStep()
1✔
222
                        .listUnrunChangeSets(contexts,
1✔
223
                                new LabelExpression(), liquibase.getDatabaseChangeLog(), liquibase.getDatabase()).size();
1✔
224

225
                Database database = null;
1✔
226
                LockService lockHandler = null;
1✔
227
                
228
                try {
229
                        database = liquibase.getDatabase();
1✔
230
                        lockHandler = LockServiceFactory.getInstance().getLockService(database);
1✔
231
                        lockHandler.waitForLock();
1✔
232
                        
233
                        Map<String, Object> scopeValues = new HashMap<>();
1✔
234
                        scopeValues.put(Scope.Attr.resourceAccessor.name(), getCompositeResourceAccessor(null));
1✔
235
                        String scopeId = null;
1✔
236
                        try {
237
                                scopeId = Scope.enter(scopeValues);
1✔
238
                                DatabaseChangeLog changeLog = liquibase.getDatabaseChangeLog();
1✔
239
                                changeLog.setChangeLogParameters(liquibase.getChangeLogParameters());
1✔
240
                                changeLog.validate(database);
1✔
241

242
                                ChangeLogIterator logIterator = new ChangeLogIterator(changeLog, new ShouldRunChangeSetFilter(database),
1✔
243
                                        new ContextChangeSetFilter(contexts), new DbmsChangeSetFilter(database));
244

245
                                // ensure that the change log history service is initialised
246
                                Scope.getCurrentScope().getSingleton(ChangeLogHistoryServiceFactory.class).getChangeLogService(database).init();
1✔
247

248
                                logIterator.run(new OpenmrsUpdateVisitor(database, callback, numChangeSetsToRun),
1✔
249
                                        new RuntimeEnvironment(database, contexts, new LabelExpression()));
250
                        }
251
                        finally {
252
                                try {
253
                                        Scope.exit(scopeId);
1✔
254
                                }
255
                                catch (Exception e) {
×
256
                                        log.warn("An error occurred trying to exit the liquibase scope", e);
×
257
                                }
1✔
258
                        }
259
                }
260
                finally {
261
                        try {
262
                                if (lockHandler != null) {
1✔
263
                                        lockHandler.releaseLock();
1✔
264
                                }
265
                        }
266
                        catch (Exception e) {
×
267
                                log.error("Could not release lock", e);
×
268
                        }
1✔
269

270
                        try {
271
                                if (database != null && database.getConnection() != null) {
1✔
272
                                        database.getConnection().close();
1✔
273
                                }
274
                        }
275
                        catch (Exception e) {
×
276
                                //pass
277
                        }
1✔
278
                }
279
                
280
                return updateWarnings;
1✔
281
        }
282
        
283
        /**
284
         * Determine if Liquibase updates are required. If OpenMRS Core version did not change, then do not run any checks
285
         * unless <b>force.setup</b> runtime property is set to <b>true</b>.
286
         *
287
         * @return true/false whether database updates are required
288
         * @throws Exception when an exception is raised while processing Liquibase changelog files
289
         */
290
        public static boolean updatesRequired() throws Exception {
291
                String storedCoreVersion = null;
1✔
292
                // Using raw SQL to not rely on Context being initialized for this check.
293
                try (Connection con = getConnection()) {
1✔
294
                        try (PreparedStatement ps = con.prepareStatement("SELECT property_value from global_property " +
1✔
295
                                "where property = ?")) {
296
                                ps.setString(1, "core.version");
1✔
297
                                ResultSet resultSet = ps.executeQuery();
1✔
298
                                if (resultSet.next()) {
1✔
NEW
299
                                        storedCoreVersion = resultSet.getString(1);
×
300
                                }
301
                        }
302
                }
303
                String currentCoreVersion = OpenmrsConstants.OPENMRS_VERSION_SHORT;
1✔
304
                boolean forceSetup = Boolean.parseBoolean(Context.getRuntimeProperties().getProperty("force.setup", "false"));
1✔
305

306
                if (!forceSetup && Objects.equals(storedCoreVersion, currentCoreVersion)) {
1✔
NEW
307
                        log.info("No core version changed. Skipping database updates.");
×
NEW
308
                        return false;
×
309
                }
310
                
311
                log.debug("Checking for database updates");
1✔
312
                List<OpenMRSChangeSet> changesets = getUnrunDatabaseChanges(new DatabaseUpdaterLiquibaseProvider());
1✔
313
                
314
                // if the db is locked, it means there was a crash
315
                // or someone is executing db updates right now. either way
316
                // returning true here stops the openmrs startup and shows
317
                // the user the maintenance wizard for updates
318
                if (isLocked() && changesets.isEmpty()) {
1✔
319
                        // if there is a db lock but there are no db changes we undo the
320
                        // lock
321
                        DatabaseUpdater.releaseDatabaseLock();
×
NEW
322
                        log.debug("DB lock found and released automatically");
×
323
                        return false;
×
324
                }
325
                
326
                return !changesets.isEmpty();
1✔
327
        }
328
        
329
        /**
330
         * Ask Liquibase if it needs to do any updates
331
         *
332
         * @param changeLogFilenames the filenames of all files to search for unrun changesets
333
         * @return true/false whether database updates are required <strong>Should</strong> always have a
334
         *         valid update to latest file
335
         */
336
        public static boolean updatesRequired(String... changeLogFilenames) throws Exception {
337
                log.debug("checking for updates");
×
338
                List<OpenMRSChangeSet> changesets = getUnrunDatabaseChanges(changeLogFilenames);
×
339
                return !changesets.isEmpty();
×
340
        }
341
        
342
        /**
343
         * Indicates whether automatic database updates are allowed by this server. Automatic updates are
344
         * disabled by default. In order to enable automatic updates, the admin needs to add
345
         * 'auto_update_database=true' to the runtime properties file.
346
         *
347
         * @return true/false whether the 'auto_update_database' has been enabled.
348
         */
349
        public static Boolean allowAutoUpdate() {
350
                String allowAutoUpdate = Context.getRuntimeProperties()
×
351
                        .getProperty(OpenmrsConstants.AUTO_UPDATE_DATABASE_RUNTIME_PROPERTY, "false");
×
352
                
353
                return "true".equals(allowAutoUpdate);
×
354
                
355
        }
356
        
357
        /**
358
         * Takes the default properties defined in /metadata/api/hibernate/hibernate.default.properties and
359
         * merges it into the user-defined runtime properties
360
         *
361
         * @see org.openmrs.api.db.ContextDAO#mergeDefaultRuntimeProperties(Properties)
362
         */
363
        private static void mergeDefaultRuntimeProperties(Properties runtimeProperties) {
364
                
365
                // loop over runtime properties and precede each with "hibernate" if
366
                // it isn't already
367
                // must do it this way to prevent concurrent mod errors
368
                Set<Object> runtimePropertyKeys = new HashSet<>(runtimeProperties.keySet());
1✔
369
                for (Object key : runtimePropertyKeys) {
1✔
370
                        String prop = (String) key;
1✔
371
                        String value = (String) runtimeProperties.get(key);
1✔
372
                        log.trace("Setting property: " + prop + ":" + value);
1✔
373
                        if (!prop.startsWith("hibernate") && !runtimeProperties.containsKey("hibernate." + prop)) {
1✔
374
                                runtimeProperties.setProperty("hibernate." + prop, value);
1✔
375
                        }
376
                }
1✔
377
                
378
                // load in the default hibernate properties from hibernate.default.properties
379
                InputStream propertyStream = null;
1✔
380
                try {
381
                        Properties props = new Properties();
1✔
382
                        // TODO: This is a dumb requirement to have hibernate in here.  Clean this up
383
                        propertyStream = DatabaseUpdater.class.getClassLoader().getResourceAsStream("hibernate.default.properties");
1✔
384
                        OpenmrsUtil.loadProperties(props, propertyStream);
1✔
385
                        // add in all default properties that don't exist in the runtime
386
                        // properties yet
387
                        for (Map.Entry<Object, Object> entry : props.entrySet()) {
1✔
388
                                if (!runtimeProperties.containsKey(entry.getKey())) {
1✔
389
                                        runtimeProperties.put(entry.getKey(), entry.getValue());
1✔
390
                                }
391
                        }
1✔
392
                }
393
                finally {
394
                        try {
395
                                propertyStream.close();
1✔
396
                        }
397
                        catch (Exception e) {
×
398
                                // pass
399
                        }
1✔
400
                }
401
        }
1✔
402
        
403
        /**
404
         * Exposes Liquibase instances created by this class. When calling
405
         * org.openmrs.util.DatabaseUpdater#getInitialLiquibaseSnapshotVersion(LiquibaseProvider) and
406
         * org.openmrs.util.DatabaseUpdater#getInitialLiquibaseSnapshotVersion(String,LiquibaseProvider), a
407
         * Liquibase instance created by this class is injected into these methods. The Liquibase instance
408
         * is injected into these methods instead of calling
409
         * org.openmrs.util.DatabaseUpdater#getLiquibase(String,ClassLoader) directly. The reason for that
410
         * design decision is that injecting a Liquibase instance (via a Liquibase provider) makes it
411
         * possible to test the two methods mentioned above in isolation. The respective integration test is
412
         * org.openmrs.util.DatabaseUpdateIT.
413
         * 
414
         * @see LiquibaseProvider
415
         * @param changeLogFile name of a Liquibase change log file
416
         * @return a Liquibase instance
417
         * @throws Exception
418
         */
419
        static Liquibase getLiquibase(String changeLogFile) throws Exception {
420
                if (liquibaseProvider != null) {
1✔
421
                        return liquibaseProvider.getLiquibase(changeLogFile);
1✔
422
                }
423
                return getLiquibase(changeLogFile, OpenmrsClassLoader.getInstance());
1✔
424
        }
425
        
426
        /**
427
         * Get a connection to the database through Liquibase. The calling method /must/ close the database
428
         * connection when finished with this Liquibase object.
429
         * liquibase.getDatabase().getConnection().close()
430
         *
431
         * @param changeLogFile the name of the file to look for the on classpath or filesystem
432
         * @param cl the {@link ClassLoader} to use to find the file (or null to use
433
         *            {@link OpenmrsClassLoader})
434
         * @return Liquibase object based on the current connection settings
435
         * @throws Exception
436
         */
437
        private static Liquibase getLiquibase(String changeLogFile, ClassLoader cl) throws Exception {
438
                Connection connection;
439
                try {
440
                        connection = getConnection();
1✔
441
                }
442
                catch (SQLException e) {
×
443
                        throw new Exception(
×
444
                                "Unable to get a connection to the database.  Please check your openmrs runtime properties file and make sure you have the correct connection.username and connection.password set",
445
                                e);
446
                }
1✔
447
                
448
                if (cl == null) {
1✔
449
                        cl = OpenmrsClassLoader.getInstance();
1✔
450
                }
451
                
452
                try {
453
                        Database database = DatabaseFactory.getInstance()
1✔
454
                                .findCorrectDatabaseImplementation(new JdbcConnection(connection));
1✔
455
                        database.setDatabaseChangeLogTableName("liquibasechangelog");
1✔
456
                        database.setDatabaseChangeLogLockTableName("liquibasechangeloglock");
1✔
457
                        
458
                        if (connection.getMetaData().getDatabaseProductName().contains("HSQL Database Engine")
1✔
459
                                || connection.getMetaData().getDatabaseProductName().contains("H2")) {
1✔
460
                                // a hack because hsqldb and h2 seem to be checking table names in the metadata section case sensitively
461
                                database.setDatabaseChangeLogTableName(database.getDatabaseChangeLogTableName().toUpperCase());
1✔
462
                                database.setDatabaseChangeLogLockTableName(database.getDatabaseChangeLogLockTableName().toUpperCase());
1✔
463
                        }
464

465
                        if (changeLogFile == null) {
1✔
466
                                changeLogFile = EMPTY_CHANGE_LOG_FILE;
1✔
467
                        }
468

469
                        configureLiquibaseDuplicateFileMode();
1✔
470

471
                        // ensure that the change log history service is initialised
472
                        Scope.getCurrentScope()
1✔
473
                                .getSingleton(ChangeLogHistoryServiceFactory.class)
1✔
474
                                .getChangeLogService(database)
1✔
475
                                .init();
1✔
476
                        return new Liquibase(changeLogFile, getCompositeResourceAccessor(cl), database);
1✔
477
                }
478
                catch (Exception e) {
×
479
                        // if an error occurs, close the connection
480
                        if (connection != null) {
×
481
                                connection.close();
×
482
                        }
483
                        throw e;
×
484
                }
485
        }
486
        
487
        /**
488
         * Gets a database connection for liquibase to do the updates
489
         *
490
         * @return a java.sql.connection based on the current runtime properties
491
         */
492
        public static Connection getConnection() throws Exception {
493
                Properties props = Context.getRuntimeProperties();
1✔
494
                mergeDefaultRuntimeProperties(props);
1✔
495
                
496
                String driver = props.getProperty("hibernate.connection.driver_class");
1✔
497
                String username = props.getProperty("hibernate.connection.username");
1✔
498
                String password = props.getProperty("hibernate.connection.password");
1✔
499
                String url = props.getProperty("hibernate.connection.url");
1✔
500
                
501
                // hack for mysql to make sure innodb tables are created
502
                if (url.contains("mysql") && !url.contains("InnoDB")) {
1✔
503
                        url = url + "&sessionVariables=default_storage_engine=InnoDB";
×
504
                }
505
                
506
                Class.forName(driver);
1✔
507
                return DriverManager.getConnection(url, username, password);
1✔
508
        }
509
        
510
        /**
511
         * Represents each change in the files referenced by liquibase-update-to-latest
512
         */
513
        public static class OpenMRSChangeSet {
514
                
515
                private String id;
516
                
517
                private String author;
518
                
519
                private String comments;
520
                
521
                private String description;
522
                
523
                private ChangeSet.RunStatus runStatus;
524
                
525
                private Date ranDate;
526
                
527
                /**
528
                 * Create an OpenmrsChangeSet from the given changeset
529
                 *
530
                 * @param changeSet
531
                 * @param database
532
                 */
533
                public OpenMRSChangeSet(ChangeSet changeSet, Database database) throws Exception {
1✔
534
                        setId(changeSet.getId());
1✔
535
                        setAuthor(changeSet.getAuthor());
1✔
536
                        setComments(changeSet.getComments());
1✔
537
                        setDescription(changeSet.getDescription());
1✔
538
                        setRunStatus(database.getRunStatus(changeSet));
1✔
539
                        setRanDate(database.getRanDate(changeSet));
1✔
540
                }
1✔
541
                
542
                /**
543
                 * @return the author
544
                 */
545
                public String getAuthor() {
546
                        return author;
×
547
                }
548
                
549
                /**
550
                 * @param author the author to set
551
                 */
552
                public void setAuthor(String author) {
553
                        this.author = author;
1✔
554
                }
1✔
555
                
556
                /**
557
                 * @return the comments
558
                 */
559
                public String getComments() {
560
                        return comments;
×
561
                }
562
                
563
                /**
564
                 * @param comments the comments to set
565
                 */
566
                public void setComments(String comments) {
567
                        this.comments = comments;
1✔
568
                }
1✔
569
                
570
                /**
571
                 * @return the description
572
                 */
573
                public String getDescription() {
574
                        return description;
×
575
                }
576
                
577
                /**
578
                 * @param description the description to set
579
                 */
580
                public void setDescription(String description) {
581
                        this.description = description;
1✔
582
                }
1✔
583
                
584
                /**
585
                 * @return the runStatus
586
                 */
587
                public ChangeSet.RunStatus getRunStatus() {
588
                        return runStatus;
×
589
                }
590
                
591
                /**
592
                 * @param runStatus the runStatus to set
593
                 */
594
                public void setRunStatus(ChangeSet.RunStatus runStatus) {
595
                        this.runStatus = runStatus;
1✔
596
                }
1✔
597
                
598
                /**
599
                 * @return the ranDate
600
                 */
601
                public Date getRanDate() {
602
                        return ranDate;
×
603
                }
604
                
605
                /**
606
                 * @param ranDate the ranDate to set
607
                 */
608
                public void setRanDate(Date ranDate) {
609
                        this.ranDate = ranDate;
1✔
610
                }
1✔
611
                
612
                /**
613
                 * @return the id
614
                 */
615
                public String getId() {
616
                        return id;
×
617
                }
618
                
619
                /**
620
                 * @param id the id to set
621
                 */
622
                public void setId(String id) {
623
                        this.id = id;
1✔
624
                }
1✔
625
                
626
        }
627
        
628
        /**
629
         * Returns all change sets defined by (a) the Liquibase snapshot files that had been used to
630
         * initialise the OpenMRS database and (b) the Liquibase update files that that are applicable on
631
         * top of the snapshot version.
632
         * 
633
         * @return list of change sets that both have and haven't been run
634
         */
635
        public static List<OpenMRSChangeSet> getDatabaseChanges() throws Exception {
636
                if (Context.isSessionOpen()) { // Do not check privileges if not run in webapp context (e.g. in tests)
×
637
                        Context.requirePrivilege(PrivilegeConstants.GET_DATABASE_CHANGES);
×
638
                }
639
                List<OpenMRSChangeSet> result = new ArrayList<>();
×
640
                
641
                String initialSnapshotVersion = changeLogDetective.getInitialLiquibaseSnapshotVersion(CONTEXT,
×
642
                    new DatabaseUpdaterLiquibaseProvider());
643
                List<String> updateVersions = changeLogVersionFinder.getUpdateVersionsGreaterThan(initialSnapshotVersion);
×
644
                
645
                Map<String, List<String>> snapshotCombinations = changeLogVersionFinder.getSnapshotCombinations();
×
646

647
                List<String> changeLogFileNames = new ArrayList<>();
×
648
                changeLogFileNames.addAll(snapshotCombinations.get(initialSnapshotVersion));
×
649
                changeLogFileNames.addAll(changeLogVersionFinder.getUpdateFileNames(updateVersions));
×
650
                
651
                Liquibase liquibase = null;
×
652
                try {
653
                        for (String filename : changeLogFileNames) {
×
654
                                liquibase = getLiquibase(filename);
×
655
                                List<ChangeSet> changeSets = liquibase.getDatabaseChangeLog().getChangeSets();
×
656
                                
657
                                for (ChangeSet changeSet : changeSets) {
×
658
                                        OpenMRSChangeSet openMRSChangeSet = new OpenMRSChangeSet(changeSet, liquibase.getDatabase());
×
659
                                        result.add(openMRSChangeSet);
×
660
                                }
×
661
                                liquibase.close();
×
662
                        }
×
663
                }
664
                finally {
665
                        if (liquibase != null) {
×
666
                                try {
667
                                        liquibase.close();
×
668
                                }
669
                                catch (Exception e) {
×
670
                                        // ignore exceptions triggered by closing liquibase a second time 
671
                                }
×
672
                        }
673
                }
674
                
675
                return result;
×
676
        }
677
        
678
        /**
679
         * Returns a list of Liquibase change sets were not run yet.
680
         *
681
         * @param liquibaseProvider provides access to a Liquibase instance
682
         * @return list of change sets that were not run yet.
683
         */
684
        public static List<OpenMRSChangeSet> getUnrunDatabaseChanges(LiquibaseProvider liquibaseProvider) throws Exception {
685
                if (Context.isSessionOpen()) { // Do not check privileges if not run in webapp context (e.g. in tests)
1✔
686
                        Context.requirePrivilege(PrivilegeConstants.GET_DATABASE_CHANGES);
1✔
687
                }
688
                String initialSnapshotVersion = changeLogDetective.getInitialLiquibaseSnapshotVersion(CONTEXT, liquibaseProvider);
1✔
689
                log.debug("initial snapshot version is '{}'", initialSnapshotVersion);
1✔
690
                
691
                List<String> liquibaseUpdateFilenames = changeLogDetective.getUnrunLiquibaseUpdateFileNames(initialSnapshotVersion,
1✔
692
                    CONTEXT, liquibaseProvider);
693
                
694
                if (!liquibaseUpdateFilenames.isEmpty()) {
1✔
695
                        return getUnrunDatabaseChanges(liquibaseUpdateFilenames.toArray(new String[0]));
1✔
696
                }
697
                
698
                return new ArrayList<OpenMRSChangeSet>();
×
699
        }
700
        
701
        /**
702
         * Looks at the specified liquibase change log files and returns all changesets in the files that
703
         * have not been run on the database yet. If no argument is specified, then it looks at the current
704
         * liquibase-update-to-latest.xml file
705
         *
706
         * @param changeLogFilenames the filenames of all files to search for unrun changesets
707
         * @return list of change sets
708
         */
709
        public static List<OpenMRSChangeSet> getUnrunDatabaseChanges(String... changeLogFilenames) {
710
                if (Context.isSessionOpen()) { // Do not check privileges if not run in webapp context (e.g. in tests)
1✔
711
                        Context.requirePrivilege(PrivilegeConstants.GET_DATABASE_CHANGES);
1✔
712
                }
713
                log.debug("looking for un-run change sets in '{}'", Arrays.toString(changeLogFilenames));
1✔
714
                
715
                Database database = null;
1✔
716
                try {
717
                        if (changeLogFilenames == null || changeLogFilenames.length == 0) {
1✔
718
                                throw new IllegalArgumentException("changeLogFilenames can neither null nor an empty array");
1✔
719
                        }
720
                        
721
                        List<OpenMRSChangeSet> results = new ArrayList<>();
1✔
722
                        
723
                        for (String changelogFile : changeLogFilenames) {
1✔
724
                                Liquibase liquibase = getLiquibase(changelogFile, null);
1✔
725
                                database = liquibase.getDatabase();
1✔
726
                                
727
                                List<ChangeSet> changeSets = new StatusCommandStep()
1✔
728
                                        .listUnrunChangeSets(new Contexts(CONTEXT),
1✔
729
                                                new LabelExpression(), liquibase.getDatabaseChangeLog(), liquibase.getDatabase());
1✔
730

731
                                for (ChangeSet changeSet : changeSets) {
1✔
732
                                        OpenMRSChangeSet omrschangeset = new OpenMRSChangeSet(changeSet, database);
1✔
733
                                        results.add(omrschangeset);
1✔
734
                                }
1✔
735
                        }
736
                        
737
                        return results;
1✔
738
                        
739
                }
740
                catch (Exception e) {
1✔
741
                        throw new RuntimeException(
1✔
742
                                "Error occurred while trying to get the updates needed for the database. " + e.getMessage(), e);
1✔
743
                }
744
                finally {
745
                        try {
746
                                database.getConnection().close();
1✔
747
                        }
748
                        catch (Exception e) {
1✔
749
                                //pass
750
                        }
1✔
751
                }
752
        }
753
        
754
        /**
755
         * @return the authenticatedUserId
756
         */
757
        public static Integer getAuthenticatedUserId() {
758
                return authenticatedUserId;
×
759
        }
760
        
761
        /**
762
         * @param userId the authenticatedUserId to set
763
         */
764
        public static void setAuthenticatedUserId(Integer userId) {
765
                authenticatedUserId = userId;
×
766
        }
×
767
        
768
        /**
769
         * This method is called by an executing custom changeset to register warning messages.
770
         *
771
         * @param warnings list of warnings to append to the end of the current list
772
         */
773
        public static void reportUpdateWarnings(List<String> warnings) {
774
                if (updateWarnings == null) {
×
775
                        updateWarnings = new LinkedList<>();
×
776
                }
777
                updateWarnings.addAll(warnings);
×
778
        }
×
779
        
780
        /**
781
         * This method writes the given text to the database updates log file located in the application
782
         * data directory.
783
         *
784
         * @param text text to be written to the file
785
         */
786
        public static void writeUpdateMessagesToFile(String text) {
787
                OutputStreamWriter streamWriter = null;
×
788
                PrintWriter writer = null;
×
789
                File destFile = new File(OpenmrsUtil.getApplicationDataDirectory(), DatabaseUpdater.DATABASE_UPDATES_LOG_FILE);
×
790
                try {
791
                        String lineSeparator = System.getProperty("line.separator");
×
792
                        Date date = Calendar.getInstance().getTime();
×
793
                        
794
                        streamWriter = new OutputStreamWriter(new FileOutputStream(destFile, true), StandardCharsets.UTF_8);
×
795
                        writer = new PrintWriter(new BufferedWriter(streamWriter));
×
796
                        writer.write("********** START OF DATABASE UPDATE LOGS AS AT " + date + " **********");
×
797
                        writer.write(lineSeparator);
×
798
                        writer.write(lineSeparator);
×
799
                        writer.write(text);
×
800
                        writer.write(lineSeparator);
×
801
                        writer.write(lineSeparator);
×
802
                        writer.write("*********** END OF DATABASE UPDATE LOGS AS AT " + date + " ***********");
×
803
                        writer.write(lineSeparator);
×
804
                        writer.write(lineSeparator);
×
805
                        
806
                        //check if there was an error while writing to the file
807
                        if (writer.checkError()) {
×
808
                                log.warn("An Error occured while writing warnings to the database update log file'");
×
809
                        }
810
                        
811
                        writer.close();
×
812
                }
813
                catch (FileNotFoundException e) {
×
814
                        log.warn("Failed to find the database update log file", e);
×
815
                }
816
                finally {
817
                        IOUtils.closeQuietly(streamWriter);
×
818
                        IOUtils.closeQuietly(writer);
×
819
                }
820
        }
×
821
        
822
        /**
823
         * This method releases the liquibase db lock, and is intended to be usd after a crashed database update.
824
         * <br>
825
         * This should only be called if the user is sure that no one else is currently running database
826
         * updates. This method should be used if there was a db crash while updates were being written and
827
         * the lock table was never cleaned up.
828
         *
829
         * @throws LockException
830
         */
831
        public static synchronized void releaseDatabaseLock() throws LockException {
832
                Database database = null;
×
833
                try {
834
                        Liquibase liquibase = getLiquibase(null, null);
×
835
                        database = liquibase.getDatabase();
×
836
                        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
×
837
                        if (isLocked()) {
×
838
                                lockService.forceReleaseLock();
×
839
                        }
840
                }
841
                catch (Exception e) {
×
842
                        throw new LockException(e);
×
843
                }
844
                finally {
845
                        try {
846
                                database.getConnection().close();
×
847
                        }
848
                        catch (Exception e) {
×
849
                                // pass
850
                        }
×
851
                }
852
        }
×
853
        
854
        /**
855
         * This method currently checks the liquibasechangeloglock table to see if there is a row with a
856
         * lock in it. This uses the liquibase API to do this
857
         *
858
         * @return true if database is currently locked
859
         */
860
        public static boolean isLocked() {
861
                Database database = null;
1✔
862
                try {
863
                        Liquibase liquibase = getLiquibase(null, null);
1✔
864
                        database = liquibase.getDatabase();
1✔
865
                        return LockServiceFactory.getInstance().getLockService(database).listLocks().length > 0;
1✔
866
                }
867
                catch (Exception e) {
×
868
                        return false;
×
869
                }
870
                finally {
871
                        try {
872
                                database.getConnection().close();
1✔
873
                        }
874
                        catch (Exception e) {
×
875
                                // pass
876
                        }
1✔
877
                }
878
        }
879

880
        private final static class OpenmrsUpdateVisitor extends UpdateVisitor {
881

882
                private final ChangeSetExecutorCallback callback;
883

884
                private final int numChangeSetsToRun;
885

886
                public OpenmrsUpdateVisitor(Database database, ChangeSetExecutorCallback callback, int numChangeSetsToRun) {
887
                        super(database, null);
1✔
888
                        this.callback = callback;
1✔
889
                        this.numChangeSetsToRun = numChangeSetsToRun;
1✔
890
                }
1✔
891

892
                @Override
893
                public void visit(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database,
894
                        Set<ChangeSetFilterResult> filterResults) throws LiquibaseException {
895
                        if (callback != null) {
1✔
896
                                callback.executing(changeSet, numChangeSetsToRun);
×
897
                        }
898
                        Map<String, Object> scopeValues = new HashMap<>();
1✔
899
                        scopeValues.put(Scope.Attr.resourceAccessor.name(), getCompositeResourceAccessor(null));
1✔
900
                        String scopeId = null;
1✔
901
                        try {
902
                                scopeId = Scope.enter(scopeValues);
1✔
903
                                super.visit(changeSet, databaseChangeLog, database, filterResults);
1✔
904
                        }
905
                        catch (Exception e) {
×
906
                                throw new LiquibaseException("Unable to execute change set: " + changeSet, e);
×
907
                        }
908
                        finally {
909
                                try {
910
                                        Scope.exit(scopeId);
1✔
911
                                }
912
                                catch (Exception e) {
×
913
                                        log.warn("An error occurred trying to exit the liquibase scope", e);
×
914
                                }
1✔
915
                        }
916
                }
1✔
917
        }
918

919
        /**
920
         * @return a resourceAccessor that includes both classpath and filesystem at the application data directory
921
         */
922
        private static CompositeResourceAccessor getCompositeResourceAccessor(ClassLoader classLoader) {
923
                if (classLoader == null) {
1✔
924
                        classLoader = Thread.currentThread().getContextClassLoader();
1✔
925
                        if (!(classLoader instanceof OpenmrsClassLoader) && !(classLoader instanceof ModuleClassLoader)) {
1✔
926
                                classLoader = OpenmrsClassLoader.getInstance();
×
927
                        }
928
                }
929
                
930
                ResourceAccessor openmrsFO = new OpenmrsClassLoaderResourceAccessor(classLoader);
1✔
931
                ResourceAccessor fsFO = new FileSystemResourceAccessor(OpenmrsUtil.getApplicationDataDirectoryAsFile());
1✔
932
                return new CompositeResourceAccessor(openmrsFO, fsFO);
1✔
933
        }
934
        
935
        private static void configureLiquibaseDuplicateFileMode() {
936
                final String dupFlagModeKey = GlobalConfiguration.DUPLICATE_FILE_MODE.getKey();
1✔
937
                final String dupFlagMode = Context.getRuntimeProperties().getProperty(dupFlagModeKey);
1✔
938

939
                if (dupFlagMode != null) {
1✔
940
                        System.setProperty(dupFlagModeKey, dupFlagMode);
×
941
                } else if (System.getProperty(dupFlagModeKey) == null) {
1✔
942
                        System.setProperty(dupFlagModeKey, OpenmrsConstants.LIQUIBASE_DUPLICATE_FILE_MODE_DEFAULT);
1✔
943
                }
944
        }
1✔
945
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc