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

openmrs / openmrs-core / 13444999516

20 Feb 2025 09:27PM UTC coverage: 63.69% (-0.02%) from 63.705%
13444999516

push

github

ibacher
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%)

7 existing lines in 7 files now uncovered.

21977 of 34506 relevant lines covered (63.69%)

0.64 hits per line

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

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

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

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

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

172
        /**
173
         * Executes the given changelog file. This file is assumed to be on the classpath.
174
         *
175
         * @param changelog The string filename of a liquibase changelog xml file to run
176
         * @return A list of messages or warnings generated by the executed changesets
177
         */
178
        public static List<String> executeChangelog(String changelog, ChangeSetExecutorCallback callback)
179
                throws DatabaseUpdateException {
180
                log.debug("installing the tables into the database");
1✔
181
                
182
                if (changelog == null) {
1✔
183
                        throw new IllegalArgumentException("changelog must not be null");
1✔
184
                }
185
                
186
                try {
187
                        log.debug("executing liquibase changelog {}", changelog);
1✔
188
                        return executeChangelog(changelog, new Contexts(CONTEXT), callback, null);
1✔
189
                }
190
                catch (Exception e) {
×
191
                        throw new DatabaseUpdateException("There was an error while updating the database to the latest. file: "
×
192
                                + changelog + ". Error: " + e.getMessage(), e);
×
193
                }
194
        }
195
        
196
        /**
197
         * This code was borrowed from the liquibase jar so that we can call the given callback function.
198
         *
199
         * @param changeLogFile the file to execute
200
         * @param contexts the liquibase changeset context
201
         * @param callback the function to call after every changeset
202
         * @return A list of messages or warnings generated by the executed changesets
203
         * @throws Exception
204
         */
205
        public static List<String> executeChangelog(String changeLogFile, Contexts contexts, ChangeSetExecutorCallback callback,
206
                ClassLoader cl) throws Exception {
207
                
208
                if (cl == null) {
1✔
209
                        cl = OpenmrsClassLoader.getInstance();
1✔
210
                }
211
                
212
                Thread.currentThread().setContextClassLoader(cl);
1✔
213
                
214
                log.debug("Setting up liquibase object to run changelog: {}", changeLogFile);
1✔
215
                Liquibase liquibase = getLiquibase(changeLogFile, cl);
1✔
216
                
217
                int numChangeSetsToRun = liquibase.listUnrunChangeSets(contexts, new LabelExpression()).size();
1✔
218
                Database database = null;
1✔
219
                LockService lockHandler = null;
1✔
220
                
221
                try {
222
                        database = liquibase.getDatabase();
1✔
223
                        lockHandler = LockServiceFactory.getInstance().getLockService(database);
1✔
224
                        lockHandler.waitForLock();
1✔
225
                        
226
                        DatabaseChangeLog changeLog = liquibase.getDatabaseChangeLog();
1✔
227
                        changeLog.setChangeLogParameters(liquibase.getChangeLogParameters());
1✔
228
                        changeLog.validate(database);
1✔
229
                        
230
                        ChangeLogIterator logIterator = new ChangeLogIterator(changeLog, new ShouldRunChangeSetFilter(database),
1✔
231
                                new ContextChangeSetFilter(contexts), new DbmsChangeSetFilter(database));
232
                        
233
                        // ensure that the change log history service is initialised
234
                        //
235
                        ChangeLogHistoryServiceFactory.getInstance().getChangeLogService(database).init();
1✔
236
                        
237
                        logIterator.run(new OpenmrsUpdateVisitor(database, callback, numChangeSetsToRun),
1✔
238
                            new RuntimeEnvironment(database, contexts, new LabelExpression()));
239
                }
240
                finally {
241
                        try {
242
                                if (lockHandler != null) {
1✔
243
                                        lockHandler.releaseLock();
1✔
244
                                }
245
                        }
246
                        catch (Exception e) {
×
247
                                log.error("Could not release lock", e);
×
248
                        }
1✔
249

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

424
                        if (changeLogFile == null) {
1✔
425
                                changeLogFile = EMPTY_CHANGE_LOG_FILE;
1✔
426
                        }
427

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

601
                List<String> changeLogFileNames = new ArrayList<>();
1✔
602
                changeLogFileNames.addAll(snapshotCombinations.get(initialSnapshotVersion));
1✔
603
                changeLogFileNames.addAll(changeLogVersionFinder.getUpdateFileNames(updateVersions));
1✔
604
                
605
                Liquibase liquibase = null;
1✔
606
                try {
607
                        for (String filename : changeLogFileNames) {
1✔
608
                                liquibase = getLiquibase(filename);
1✔
609
                                List<ChangeSet> changeSets = liquibase.getDatabaseChangeLog().getChangeSets();
1✔
610
                                
611
                                for (ChangeSet changeSet : changeSets) {
1✔
612
                                        OpenMRSChangeSet openMRSChangeSet = new OpenMRSChangeSet(changeSet, liquibase.getDatabase());
1✔
613
                                        result.add(openMRSChangeSet);
1✔
614
                                }
1✔
615
                                liquibase.close();
1✔
616
                        }
1✔
617
                }
618
                finally {
619
                        if (liquibase != null) {
1✔
620
                                try {
621
                                        liquibase.close();
×
622
                                }
623
                                catch (Exception e) {
1✔
624
                                        // ignore exceptions triggered by closing liquibase a second time 
625
                                }
×
626
                        }
627
                }
628
                
629
                return result;
1✔
630
        }
631
        
632
        /**
633
         * Returns a list of Liquibase change sets were not run yet.
634
         *
635
         * @param liquibaseProvider provides access to a Liquibase instance
636
         * @return list of change sets that were not run yet.
637
         */
638
        public static List<OpenMRSChangeSet> getUnrunDatabaseChanges(LiquibaseProvider liquibaseProvider) throws Exception {
639
                if (Context.isSessionOpen()) { // Do not check privileges if not run in webapp context (e.g. in tests)
1✔
640
                        Context.requirePrivilege(PrivilegeConstants.GET_DATABASE_CHANGES);
1✔
641
                }
642
                String initialSnapshotVersion = changeLogDetective.getInitialLiquibaseSnapshotVersion(CONTEXT, liquibaseProvider);
1✔
643
                log.debug("initial snapshot version is '{}'", initialSnapshotVersion);
1✔
644
                
645
                List<String> liquibaseUpdateFilenames = changeLogDetective.getUnrunLiquibaseUpdateFileNames(initialSnapshotVersion,
1✔
646
                    CONTEXT, liquibaseProvider);
647
                
648
                if (!liquibaseUpdateFilenames.isEmpty()) {
1✔
649
                        return getUnrunDatabaseChanges(liquibaseUpdateFilenames.toArray(new String[0]));
1✔
650
                }
651
                
652
                return new ArrayList<OpenMRSChangeSet>();
×
653
        }
654
        
655
        /**
656
         * Looks at the specified liquibase change log files and returns all changesets in the files that
657
         * have not been run on the database yet. If no argument is specified, then it looks at the current
658
         * liquibase-update-to-latest.xml file
659
         *
660
         * @param changeLogFilenames the filenames of all files to search for unrun changesets
661
         * @return list of change sets
662
         */
663
        public static List<OpenMRSChangeSet> getUnrunDatabaseChanges(String... changeLogFilenames) {
664
                if (Context.isSessionOpen()) { // Do not check privileges if not run in webapp context (e.g. in tests)
1✔
665
                        Context.requirePrivilege(PrivilegeConstants.GET_DATABASE_CHANGES);
1✔
666
                }
667
                log.debug("looking for un-run change sets in '{}'", Arrays.toString(changeLogFilenames));
1✔
668
                
669
                Database database = null;
1✔
670
                try {
671
                        if (changeLogFilenames == null || changeLogFilenames.length == 0) {
1✔
672
                                throw new IllegalArgumentException("changeLogFilenames can neither null nor an empty array");
1✔
673
                        }
674
                        
675
                        List<OpenMRSChangeSet> results = new ArrayList<>();
1✔
676
                        
677
                        for (String changelogFile : changeLogFilenames) {
1✔
678
                                Liquibase liquibase = getLiquibase(changelogFile, null);
1✔
679
                                database = liquibase.getDatabase();
1✔
680
                                
681
                                List<ChangeSet> changeSets = liquibase.listUnrunChangeSets(new Contexts(CONTEXT), new LabelExpression());
1✔
682
                                
683
                                for (ChangeSet changeSet : changeSets) {
1✔
684
                                        OpenMRSChangeSet omrschangeset = new OpenMRSChangeSet(changeSet, database);
1✔
685
                                        results.add(omrschangeset);
1✔
686
                                }
1✔
687
                        }
688
                        
689
                        return results;
1✔
690
                        
691
                }
692
                catch (Exception e) {
1✔
693
                        throw new RuntimeException(
1✔
694
                                "Error occurred while trying to get the updates needed for the database. " + e.getMessage(), e);
1✔
695
                }
696
                finally {
697
                        try {
698
                                database.getConnection().close();
1✔
699
                        }
700
                        catch (Exception e) {
1✔
701
                                //pass
702
                        }
1✔
703
                }
704
        }
705
        
706
        /**
707
         * @return the authenticatedUserId
708
         */
709
        public static Integer getAuthenticatedUserId() {
710
                return authenticatedUserId;
1✔
711
        }
712
        
713
        /**
714
         * @param userId the authenticatedUserId to set
715
         */
716
        public static void setAuthenticatedUserId(Integer userId) {
717
                authenticatedUserId = userId;
×
718
        }
×
719
        
720
        /**
721
         * This method is called by an executing custom changeset to register warning messages.
722
         *
723
         * @param warnings list of warnings to append to the end of the current list
724
         */
725
        public static void reportUpdateWarnings(List<String> warnings) {
726
                if (updateWarnings == null) {
1✔
727
                        updateWarnings = new LinkedList<>();
1✔
728
                }
729
                updateWarnings.addAll(warnings);
1✔
730
        }
1✔
731
        
732
        /**
733
         * This method writes the given text to the database updates log file located in the application
734
         * data directory.
735
         *
736
         * @param text text to be written to the file
737
         */
738
        public static void writeUpdateMessagesToFile(String text) {
739
                OutputStreamWriter streamWriter = null;
×
740
                PrintWriter writer = null;
×
741
                File destFile = new File(OpenmrsUtil.getApplicationDataDirectory(), DatabaseUpdater.DATABASE_UPDATES_LOG_FILE);
×
742
                try {
743
                        String lineSeparator = System.getProperty("line.separator");
×
744
                        Date date = Calendar.getInstance().getTime();
×
745
                        
746
                        streamWriter = new OutputStreamWriter(new FileOutputStream(destFile, true), StandardCharsets.UTF_8);
×
747
                        writer = new PrintWriter(new BufferedWriter(streamWriter));
×
748
                        writer.write("********** START OF DATABASE UPDATE LOGS AS AT " + date + " **********");
×
749
                        writer.write(lineSeparator);
×
750
                        writer.write(lineSeparator);
×
751
                        writer.write(text);
×
752
                        writer.write(lineSeparator);
×
753
                        writer.write(lineSeparator);
×
754
                        writer.write("*********** END OF DATABASE UPDATE LOGS AS AT " + date + " ***********");
×
755
                        writer.write(lineSeparator);
×
756
                        writer.write(lineSeparator);
×
757
                        
758
                        //check if there was an error while writing to the file
759
                        if (writer.checkError()) {
×
760
                                log.warn("An Error occured while writing warnings to the database update log file'");
×
761
                        }
762
                        
763
                        writer.close();
×
764
                }
765
                catch (FileNotFoundException e) {
×
766
                        log.warn("Failed to find the database update log file", e);
×
767
                }
768
                catch (IOException e) {
×
769
                        log.warn("Failed to write to the database update log file", e);
×
770
                }
771
                finally {
772
                        IOUtils.closeQuietly(streamWriter);
×
773
                        IOUtils.closeQuietly(writer);
×
774
                }
775
        }
×
776
        
777
        /**
778
         * This method releases the liquibase db lock, and is intended to be usd after a crashed database update.
779
         * <br>
780
         * This should only be called if the user is sure that no one else is currently running database
781
         * updates. This method should be used if there was a db crash while updates were being written and
782
         * the lock table was never cleaned up.
783
         *
784
         * @throws LockException
785
         */
786
        public static synchronized void releaseDatabaseLock() throws LockException {
787
                Database database = null;
×
788
                try {
UNCOV
789
                        Liquibase liquibase = getLiquibase(null, null);
×
790
                        database = liquibase.getDatabase();
×
791
                        LockService lockService = LockServiceFactory.getInstance().getLockService(database);
×
NEW
792
                        if (isLocked()) {
×
793
                                lockService.forceReleaseLock();
×
794
                        }
795
                }
796
                catch (Exception e) {
×
797
                        throw new LockException(e);
×
798
                }
799
                finally {
800
                        try {
801
                                database.getConnection().close();
×
802
                        }
803
                        catch (Exception e) {
×
804
                                // pass
805
                        }
×
806
                }
807
        }
×
808
        
809
        /**
810
         * This method currently checks the liquibasechangeloglock table to see if there is a row with a
811
         * lock in it. This uses the liquibase API to do this
812
         *
813
         * @return true if database is currently locked
814
         */
815
        public static boolean isLocked() {
816
                Database database = null;
1✔
817
                try {
818
                        Liquibase liquibase = getLiquibase(null, null);
1✔
819
                        database = liquibase.getDatabase();
1✔
820
                        return LockServiceFactory.getInstance().getLockService(database).listLocks().length > 0;
1✔
821
                }
822
                catch (Exception e) {
×
823
                        return false;
×
824
                }
825
                finally {
826
                        try {
827
                                database.getConnection().close();
1✔
828
                        }
829
                        catch (Exception e) {
×
830
                                // pass
831
                        }
1✔
832
                }
833
        }
834

835
        private final static class OpenmrsUpdateVisitor extends UpdateVisitor {
836

837
                private final ChangeSetExecutorCallback callback;
838

839
                private final int numChangeSetsToRun;
840

841
                public OpenmrsUpdateVisitor(Database database, ChangeSetExecutorCallback callback, int numChangeSetsToRun) {
842
                        super(database, null);
1✔
843
                        this.callback = callback;
1✔
844
                        this.numChangeSetsToRun = numChangeSetsToRun;
1✔
845
                }
1✔
846

847
                @Override
848
                public void visit(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database,
849
                        Set<ChangeSetFilterResult> filterResults) throws LiquibaseException {
850
                        if (callback != null) {
1✔
851
                                callback.executing(changeSet, numChangeSetsToRun);
×
852
                        }
853
                        Map<String, Object> scopeValues = new HashMap<>();
1✔
854
                        scopeValues.put(Scope.Attr.resourceAccessor.name(), getCompositeResourceAccessor(null));
1✔
855
                        String scopeId = null;
1✔
856
                        try {
857
                                scopeId = Scope.enter(scopeValues);
1✔
858
                                super.visit(changeSet, databaseChangeLog, database, filterResults);
1✔
859
                        }
860
                        catch (Exception e) {
×
861
                                throw new LiquibaseException("Unable to execute change set: " + changeSet, e);
×
862
                        }
863
                        finally {
864
                                try {
865
                                        Scope.exit(scopeId);
1✔
866
                                }
867
                                catch (Exception e) {
×
868
                                        log.warn("An error occurred trying to exit the liquibase scope", e);
×
869
                                }
1✔
870
                        }
871
                }
1✔
872
        }
873

874
        /**
875
         * @return a resourceAccessor that includes both classpath and filesystem at the application data directory
876
         */
877
        private static CompositeResourceAccessor getCompositeResourceAccessor(ClassLoader classLoader) {
878
                if (classLoader == null) {
1✔
879
                        classLoader = Thread.currentThread().getContextClassLoader();
1✔
880
                        if (!(classLoader instanceof OpenmrsClassLoader) && !(classLoader instanceof ModuleClassLoader)) {
1✔
881
                                classLoader = OpenmrsClassLoader.getInstance();
×
882
                        }
883
                }
884
                
885
                ResourceAccessor openmrsFO = new OpenmrsClassLoaderResourceAccessor(classLoader);
1✔
886
                ResourceAccessor fsFO = new FileSystemResourceAccessor(OpenmrsUtil.getApplicationDataDirectoryAsFile());
1✔
887
                return new CompositeResourceAccessor(openmrsFO, fsFO);
1✔
888
        }
889
}
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