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

openmrs / openmrs-core / 12828342905

17 Jan 2025 11:38AM UTC coverage: 63.785% (+0.04%) from 63.741%
12828342905

push

github

dkayiwa
TRUNK-6258: Fix tests

(cherry picked from commit 0b8035be0)

5 of 6 new or added lines in 1 file covered. (83.33%)

54 existing lines in 2 files now uncovered.

21710 of 34036 relevant lines covered (63.79%)

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.module.ModuleClassLoader;
43
import org.slf4j.Logger;
44
import org.slf4j.LoggerFactory;
45

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

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

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

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

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

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

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

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

837
        private final static class OpenmrsUpdateVisitor extends UpdateVisitor {
838

839
                private final ChangeSetExecutorCallback callback;
840

841
                private final int numChangeSetsToRun;
842

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

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

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