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

openmrs / openmrs-core / 13444985538

20 Feb 2025 09:26PM UTC coverage: 64.835% (+0.003%) from 64.832%
13444985538

push

github

web-flow
TRUNK-6308: DatabaseUpdater.releaseDatabaseLocks doesn't release database locks after crashes (#4935)

0 of 1 new or added line in 1 file covered. (0.0%)

5 existing lines in 4 files now uncovered.

23168 of 35734 relevant lines covered (64.83%)

0.65 hits per line

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

67.2
/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.SQLException;
60
import java.util.ArrayList;
61
import java.util.Arrays;
62
import java.util.Calendar;
63
import java.util.Date;
64
import java.util.HashMap;
65
import java.util.HashSet;
66
import java.util.LinkedList;
67
import java.util.List;
68
import java.util.Map;
69
import java.util.Properties;
70
import java.util.Set;
71

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

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

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

218
                int numChangeSetsToRun = new StatusCommandStep()
1✔
219
                        .listUnrunChangeSets(contexts,
1✔
220
                                new LabelExpression(), liquibase.getDatabaseChangeLog(), liquibase.getDatabase()).size();
1✔
221

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

239
                                ChangeLogIterator logIterator = new ChangeLogIterator(changeLog, new ShouldRunChangeSetFilter(database),
1✔
240
                                        new ContextChangeSetFilter(contexts), new DbmsChangeSetFilter(database));
241

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

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

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

441
                        if (changeLogFile == null) {
1✔
442
                                changeLogFile = EMPTY_CHANGE_LOG_FILE;
1✔
443
                        }
444

445
                        configureLiquibaseDuplicateFileMode();
1✔
446

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

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

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

856
        private final static class OpenmrsUpdateVisitor extends UpdateVisitor {
857

858
                private final ChangeSetExecutorCallback callback;
859

860
                private final int numChangeSetsToRun;
861

862
                public OpenmrsUpdateVisitor(Database database, ChangeSetExecutorCallback callback, int numChangeSetsToRun) {
863
                        super(database, null);
1✔
864
                        this.callback = callback;
1✔
865
                        this.numChangeSetsToRun = numChangeSetsToRun;
1✔
866
                }
1✔
867

868
                @Override
869
                public void visit(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database,
870
                        Set<ChangeSetFilterResult> filterResults) throws LiquibaseException {
871
                        if (callback != null) {
1✔
872
                                callback.executing(changeSet, numChangeSetsToRun);
×
873
                        }
874
                        Map<String, Object> scopeValues = new HashMap<>();
1✔
875
                        scopeValues.put(Scope.Attr.resourceAccessor.name(), getCompositeResourceAccessor(null));
1✔
876
                        String scopeId = null;
1✔
877
                        try {
878
                                scopeId = Scope.enter(scopeValues);
1✔
879
                                super.visit(changeSet, databaseChangeLog, database, filterResults);
1✔
880
                        }
881
                        catch (Exception e) {
×
882
                                throw new LiquibaseException("Unable to execute change set: " + changeSet, e);
×
883
                        }
884
                        finally {
885
                                try {
886
                                        Scope.exit(scopeId);
1✔
887
                                }
888
                                catch (Exception e) {
×
889
                                        log.warn("An error occurred trying to exit the liquibase scope", e);
×
890
                                }
1✔
891
                        }
892
                }
1✔
893
        }
894

895
        /**
896
         * @return a resourceAccessor that includes both classpath and filesystem at the application data directory
897
         */
898
        private static CompositeResourceAccessor getCompositeResourceAccessor(ClassLoader classLoader) {
899
                if (classLoader == null) {
1✔
900
                        classLoader = Thread.currentThread().getContextClassLoader();
1✔
901
                        if (!(classLoader instanceof OpenmrsClassLoader) && !(classLoader instanceof ModuleClassLoader)) {
1✔
902
                                classLoader = OpenmrsClassLoader.getInstance();
×
903
                        }
904
                }
905
                
906
                ResourceAccessor openmrsFO = new OpenmrsClassLoaderResourceAccessor(classLoader);
1✔
907
                ResourceAccessor fsFO = new FileSystemResourceAccessor(OpenmrsUtil.getApplicationDataDirectoryAsFile());
1✔
908
                return new CompositeResourceAccessor(openmrsFO, fsFO);
1✔
909
        }
910
        
911
        private static void configureLiquibaseDuplicateFileMode() {
912
                final String dupFlagModeKey = GlobalConfiguration.DUPLICATE_FILE_MODE.getKey();
1✔
913
                final String dupFlagMode = Context.getRuntimeProperties().getProperty(dupFlagModeKey);
1✔
914

915
                if (dupFlagMode != null) {
1✔
916
                        System.setProperty(dupFlagModeKey, dupFlagMode);
×
917
                } else if (System.getProperty(dupFlagModeKey) == null) {
1✔
918
                        System.setProperty(dupFlagModeKey, OpenmrsConstants.LIQUIBASE_DUPLICATE_FILE_MODE_DEFAULT);
1✔
919
                }
920
        }
1✔
921
}
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