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

openmrs / openmrs-core / 17128381054

21 Aug 2025 01:28PM UTC coverage: 64.842% (-0.03%) from 64.868%
17128381054

push

github

dkayiwa
Do not just swallow attribute validation errors

(cherry picked from commit 23f1d4a87)

2 of 2 new or added lines in 1 file covered. (100.0%)

684 existing lines in 16 files now uncovered.

23362 of 36029 relevant lines covered (64.84%)

0.65 hits per line

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

90.79
/api/src/main/java/org/openmrs/liquibase/ChangeLogDetective.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.liquibase;
11

12
import java.util.ArrayList;
13
import java.util.Collections;
14
import java.util.List;
15
import java.util.Map;
16
import liquibase.Contexts;
17
import liquibase.LabelExpression;
18
import liquibase.Liquibase;
19
import liquibase.changelog.ChangeSet;
20
import liquibase.command.core.StatusCommandStep;
21
import org.slf4j.Logger;
22
import org.slf4j.LoggerFactory;
23

24
/**
25
 * Figures out which Liquibase change logs were used to initialise an OpenMRS database and which
26
 * change logs need to be run on top of that when updating the database.
27
 * 
28
 * @since 2.4
29
 */
30
public class ChangeLogDetective {
31
        
32
        /*
33
         * Log statements from this class are to be logged underneath 'org.openmrs.api' as the log level for this
34
         * package is 'INFO', hence the deviation of the actual package and the logger name.
35
         */
36
        private static final Logger log = LoggerFactory.getLogger("org.openmrs.api.ChangeLogDetective");
1✔
37
        
38
        private static final String BEN = "ben";
39
        
40
        private static final String DEFAULT_SNAPSHOT_VERSION = "1.9.x";
41
        
42
        private static final String DISABLE_FOREIGN_KEY_CHECKS = "disable-foreign-key-checks";
43
        
44
        private static final String ENABLE_FOREIGN_KEY_CHECKS = "enable-foreign-key-checks";
45
        
46
        private static final int MAX_NUMBER_OF_CHANGE_SETS_TO_LOG = 10;
47
        
48
        private static final String LIQUIBASE_CORE_DATA_1_9_X_FILENAME = "liquibase-core-data-1.9.x.xml";
49
        
50
        private static final String LIQUIBASE_SCHEMA_ONLY_1_9_X_FILENAME = "liquibase-schema-only-1.9.x.xml";
51
        
52
        private ChangeLogVersionFinder changeLogVersionFinder;
53
        
54
        private String initialSnapshotVersion;
55
        
56
        private List<String> unrunLiquibaseUpdates;
57
        
58
        private ChangeLogDetective() {
1✔
59
                changeLogVersionFinder = new ChangeLogVersionFinder();
1✔
60
        }
1✔
61
        
62
        private static class ChangeLogDetectiveHolder {
63

64
                private ChangeLogDetectiveHolder() {
65
                        
66
                }
67
                
68
                private static ChangeLogDetective INSTANCE = null;        
1✔
69
        }
70
        
71
        public static ChangeLogDetective getInstance() {
72
                if (ChangeLogDetectiveHolder.INSTANCE == null) {
1✔
73
                        ChangeLogDetectiveHolder.INSTANCE = new ChangeLogDetective();
1✔
74
                }
75
                
76
                return ChangeLogDetectiveHolder.INSTANCE;
1✔
77
        }
78
        
79
        /**
80
         * Returns the version of the Liquibase snapshot that had been used to initialise the OpenMRS
81
         * database. The version is needed to determine which Liquibase update files need to be checked for
82
         * un-run change sets and may need to be (re-)run to apply the latest changes to the OpenMRS
83
         * database.
84
         * 
85
         * @param liquibaseProvider provides access to a Liquibase instance
86
         * @return the version of the Liquibase snapshot that had been used to initialise the OpenMRS
87
         *         database
88
         * @throws Exception
89
         */
90
        public String getInitialLiquibaseSnapshotVersion(String context, LiquibaseProvider liquibaseProvider) throws Exception {
91
                
92
                if (initialSnapshotVersion != null) {
1✔
UNCOV
93
                        return initialSnapshotVersion;
×
94
                }
95
                
96
                log.info("identifying the Liquibase snapshot version that had been used to initialize the OpenMRS database...");
1✔
97
                Map<String, List<String>> snapshotCombinations = changeLogVersionFinder.getSnapshotCombinations();
1✔
98
                
99
                if (snapshotCombinations.isEmpty()) {
1✔
100
                        throw new IllegalStateException(
×
101
                                "identifying the Liqubase snapshot version that had been used to initialize the OpenMRS database failed as no candidate change sets were found");
102
                }
103
                
104
                List<String> snapshotVersions = getSnapshotVersionsInDescendingOrder(snapshotCombinations);
1✔
105
                
106
                for (String version : snapshotVersions) {
1✔
107
                        int unrunChangeSetsCount = 0;
1✔
108
                        
109
                        log.info("looking for un-run change sets in snapshot version '{}'", version);
1✔
110
                        List<String> changeSets = snapshotCombinations.get(version);
1✔
111
                        
112
                        Contexts contexts = new Contexts(context);
1✔
113
                        for (String filename : changeSets) {
1✔
114
                                List<ChangeSet> rawUnrunChangeSets = getUnrunChangeSets(filename, contexts, liquibaseProvider);
1✔
115
                                
116
                                List<ChangeSet> refinedUnrunChangeSets = excludeVintageChangeSets(filename, rawUnrunChangeSets);
1✔
117
                                
118
                                log.info("file '{}' contains {} un-run change sets", filename, refinedUnrunChangeSets.size());
1✔
119
                                logUnRunChangeSetDetails(filename, refinedUnrunChangeSets);
1✔
120
                                
121
                                unrunChangeSetsCount += refinedUnrunChangeSets.size();
1✔
122
                        }
1✔
123
                        
124
                        if (unrunChangeSetsCount == 0) {
1✔
125
                                log.info("the Liquibase snapshot version that had been used to initialize the OpenMRS database is '{}'",
1✔
126
                                    version);
127
                                
128
                                initialSnapshotVersion = version;
1✔
129
                                return initialSnapshotVersion;
1✔
130
                        }
131
                }
1✔
132
                
133
                log.info(
1✔
134
                    "the snapshot version that had been used to initialize the OpenMRS database could not be identified, falling back to the default version '{}'",
135
                    DEFAULT_SNAPSHOT_VERSION);
136
                
137
                initialSnapshotVersion = DEFAULT_SNAPSHOT_VERSION;
1✔
138
                return initialSnapshotVersion;
1✔
139
        }
140
        
141
        /**
142
         * Returns a list of Liquibase update files that contain un-run change sets.
143
         *
144
         * @param snapshotVersion the snapshot version that had been used to initialise the OpenMRS database
145
         * @param liquibaseProvider provides access to a Liquibase instance
146
         * @return a list of Liquibase update files that contain un-run change sets.
147
         * @throws Exception
148
         */
149
        public List<String> getUnrunLiquibaseUpdateFileNames(String snapshotVersion, String context,
150
                LiquibaseProvider liquibaseProvider) throws Exception {
151
                
152
                if (unrunLiquibaseUpdates != null && unrunLiquibaseUpdates.isEmpty()) {
1✔
153
                        return unrunLiquibaseUpdates;
×
154
                }
155
                
156
                unrunLiquibaseUpdates = new ArrayList<>();
1✔
157
                
158
                List<String> updateVersions = changeLogVersionFinder.getUpdateVersionsGreaterThan(snapshotVersion);
1✔
159
                List<String> updateFileNames = changeLogVersionFinder.getUpdateFileNames(updateVersions);
1✔
160
                
161
                Contexts contexts = new Contexts(context);
1✔
162
                for (String filename : updateFileNames) {
1✔
163
                        List<ChangeSet> unrunChangeSets = getUnrunChangeSets(filename, contexts, liquibaseProvider);
1✔
164

165
                        log.info("file '{}' contains {} un-run change sets", filename, unrunChangeSets.size());
1✔
166
                        logUnRunChangeSetDetails(filename, unrunChangeSets);
1✔
167
                        
168
                        if (!unrunChangeSets.isEmpty()) {
1✔
169
                                unrunLiquibaseUpdates.add(filename);
1✔
170
                        }
171
                }
1✔
172
                
173
                return unrunLiquibaseUpdates;
1✔
174
        }
175
        
176
        List<String> getSnapshotVersionsInDescendingOrder(Map<String, List<String>> snapshotCombinations) {
177
                List<String> versions = new ArrayList<>(snapshotCombinations.keySet());
1✔
178
                versions.sort(Collections.reverseOrder());
1✔
179
                return versions;
1✔
180
        }
181
        
182
        List<ChangeSet> excludeVintageChangeSets(String filename, List<ChangeSet> changeSets) {
183
                List<ChangeSet> result = new ArrayList<>();
1✔
184
                for (ChangeSet changeSet : changeSets) {
1✔
185
                        if (!isVintageChangeSet(filename, changeSet)) {
1✔
186
                                result.add(changeSet);
1✔
187
                        }
188
                }
1✔
189
                return result;
1✔
190
        }
191
        
192
        List<ChangeSet> getUnrunChangeSets(String filename, Contexts context, LiquibaseProvider liquibaseProvider) throws Exception {
193
                Liquibase liquibase = liquibaseProvider.getLiquibase(filename);
1✔
194

195
                List<ChangeSet> unrunChangeSets;
196
                try {
197
                        unrunChangeSets = new StatusCommandStep()
1✔
198
                                .listUnrunChangeSets(context,
1✔
199
                                        new LabelExpression(), liquibase.getDatabaseChangeLog(), liquibase.getDatabase());
1✔
200

201
                } finally {
202
                        liquibase.close();
1✔
203
                }
204
                
205
                return unrunChangeSets;
1✔
206
        }
207
        
208
        boolean isVintageChangeSet(String filename, ChangeSet changeSet) {
209
                if (filename != null && filename.contains(LIQUIBASE_CORE_DATA_1_9_X_FILENAME) && changeSet.getAuthor().equals(BEN)) {
1✔
210
                        return changeSet.getId().equals(DISABLE_FOREIGN_KEY_CHECKS) || changeSet.getId().equals(ENABLE_FOREIGN_KEY_CHECKS);
1✔
211
                }
212
                
213
                return false;
1✔
214
        }
215
        
216
        /**
217
         * Logs un-run change sets no more than a given number and only for the 1.9.x Liquibase snapshots.
218
         * 
219
         * @return a boolean value indicating whether the change sets were logged. The value is used for
220
         *         testing.
221
         */
222
        boolean logUnRunChangeSetDetails(String filename, List<ChangeSet> changeSets) {
223
                if (changeSets.size() < MAX_NUMBER_OF_CHANGE_SETS_TO_LOG && (filename.contains(LIQUIBASE_CORE_DATA_1_9_X_FILENAME)
1✔
224
                        || filename.contains(LIQUIBASE_SCHEMA_ONLY_1_9_X_FILENAME))) {
1✔
225
                        if (log.isInfoEnabled()) {
1✔
226
                                for (ChangeSet changeSet : changeSets) {
×
227
                                        log.info("file '{}' contains un-run change set with id '{}' by author '{}'", filename, changeSet.getId(),
×
228
                                                changeSet.getAuthor());
×
229
                                }
×
230
                        }
231
                        
232
                        return true;
1✔
233
                }
234
                return false;
1✔
235
        }
236
}
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