001package ca.uhn.fhir.jpa.migrate.taskdef;
002
003/*-
004 * #%L
005 * HAPI FHIR Server - SQL Migration
006 * %%
007 * Copyright (C) 2014 - 2022 Smile CDR, Inc.
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 *
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 *
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import ca.uhn.fhir.i18n.Msg;
024import ca.uhn.fhir.jpa.migrate.DriverTypeEnum;
025import ca.uhn.fhir.jpa.migrate.HapiMigrationException;
026import org.apache.commons.lang3.Validate;
027import org.apache.commons.lang3.builder.EqualsBuilder;
028import org.apache.commons.lang3.builder.HashCodeBuilder;
029import org.flywaydb.core.api.MigrationVersion;
030import org.intellij.lang.annotations.Language;
031import org.slf4j.Logger;
032import org.slf4j.LoggerFactory;
033import org.springframework.dao.DataAccessException;
034import org.springframework.jdbc.core.JdbcTemplate;
035import org.springframework.transaction.support.TransactionTemplate;
036
037import java.sql.SQLException;
038import java.util.ArrayList;
039import java.util.Arrays;
040import java.util.Collections;
041import java.util.HashSet;
042import java.util.List;
043import java.util.Set;
044import java.util.regex.Matcher;
045import java.util.regex.Pattern;
046
047public abstract class BaseTask {
048
049        public static final String MIGRATION_VERSION_PATTERN = "\\d{8}\\.\\d+";
050        private static final Logger ourLog = LoggerFactory.getLogger(BaseTask.class);
051        private static final Pattern versionPattern = Pattern.compile(MIGRATION_VERSION_PATTERN);
052        private final String myProductVersion;
053        private final String mySchemaVersion;
054        private DriverTypeEnum.ConnectionProperties myConnectionProperties;
055        private DriverTypeEnum myDriverType;
056        private String myDescription;
057        private Integer myChangesCount = 0;
058        private boolean myDryRun;
059
060        /**
061         * Some migrations can not be run in a transaction.
062         * When this is true, {@link BaseTask#executeSql} will run without a transaction
063         */
064        public void setTransactional(boolean theTransactional) {
065                myTransactional = theTransactional;
066        }
067
068        private boolean myTransactional = true;
069        private boolean myDoNothing;
070        private List<ExecutedStatement> myExecutedStatements = new ArrayList<>();
071        private Set<DriverTypeEnum> myOnlyAppliesToPlatforms = new HashSet<>();
072        private boolean myNoColumnShrink;
073        private boolean myFailureAllowed;
074        private boolean myRunDuringSchemaInitialization;
075
076        protected BaseTask(String theProductVersion, String theSchemaVersion) {
077                myProductVersion = theProductVersion;
078                mySchemaVersion = theSchemaVersion;
079        }
080
081        public boolean isRunDuringSchemaInitialization() {
082                return myRunDuringSchemaInitialization;
083        }
084
085        /**
086         * Should this task run even if we're doing the very first initialization of an empty schema. By
087         * default we skip most tasks during that pass, since they just take up time and the
088         * schema should be fully initialized by the {@link InitializeSchemaTask}
089         */
090        public void setRunDuringSchemaInitialization(boolean theRunDuringSchemaInitialization) {
091                myRunDuringSchemaInitialization = theRunDuringSchemaInitialization;
092        }
093
094        public void setOnlyAppliesToPlatforms(Set<DriverTypeEnum> theOnlyAppliesToPlatforms) {
095                Validate.notNull(theOnlyAppliesToPlatforms);
096                myOnlyAppliesToPlatforms = theOnlyAppliesToPlatforms;
097        }
098
099        public String getProductVersion() {
100                return myProductVersion;
101        }
102
103        public String getSchemaVersion() {
104                return mySchemaVersion;
105        }
106
107        public boolean isNoColumnShrink() {
108                return myNoColumnShrink;
109        }
110
111        public void setNoColumnShrink(boolean theNoColumnShrink) {
112                myNoColumnShrink = theNoColumnShrink;
113        }
114
115        public boolean isDryRun() {
116                return myDryRun;
117        }
118
119        public void setDryRun(boolean theDryRun) {
120                myDryRun = theDryRun;
121        }
122
123        public String getDescription() {
124                if (myDescription == null) {
125                        return this.getClass().getSimpleName();
126                }
127                return myDescription;
128        }
129
130        public BaseTask setDescription(String theDescription) {
131                myDescription = theDescription;
132                return this;
133        }
134
135        public List<ExecutedStatement> getExecutedStatements() {
136                return myExecutedStatements;
137        }
138
139        public int getChangesCount() {
140                return myChangesCount;
141        }
142
143        /**
144         * @param theTableName This is only used for logging currently
145         * @param theSql       The SQL statement
146         * @param theArguments The SQL statement arguments
147         */
148        public void executeSql(String theTableName, @Language("SQL") String theSql, Object... theArguments) {
149                if (!isDryRun()) {
150                        Integer changes;
151                        if (myTransactional) {
152                                changes = getConnectionProperties().getTxTemplate().execute(t -> doExecuteSql(theSql, theArguments));
153                        } else {
154                                changes = doExecuteSql(theSql, theArguments);
155                        }
156
157                        myChangesCount += changes;
158                }
159
160                captureExecutedStatement(theTableName, theSql, theArguments);
161        }
162
163        protected void executeSqlListInTransaction(String theTableName, List<String> theSqlStatements) {
164                if (!isDryRun()) {
165                        Integer changes;
166                        changes = getConnectionProperties().getTxTemplate().execute(t -> doExecuteSqlList(theSqlStatements));
167                        myChangesCount += changes;
168                }
169
170                for (@Language("SQL") String sqlStatement : theSqlStatements) {
171                        captureExecutedStatement(theTableName, sqlStatement);
172                }
173        }
174
175        private Integer doExecuteSqlList(List<String> theSqlStatements) {
176                int changesCount = 0;
177                for (String nextSql : theSqlStatements) {
178                        changesCount += doExecuteSql(nextSql);
179                }
180
181                return changesCount;
182        }
183
184        private int doExecuteSql(@Language("SQL") String theSql, Object... theArguments) {
185                JdbcTemplate jdbcTemplate = getConnectionProperties().newJdbcTemplate();
186                // 0 means no timeout -- we use this for index rebuilds that may take time.
187                jdbcTemplate.setQueryTimeout(0);
188                try {
189                        int changesCount = jdbcTemplate.update(theSql, theArguments);
190                        if (!"true".equals(System.getProperty("unit_test_mode"))) {
191                                logInfo(ourLog, "SQL \"{}\" returned {}", theSql, changesCount);
192                        }
193                        return changesCount;
194                } catch (DataAccessException e) {
195                        if (myFailureAllowed) {
196                                ourLog.info("Task {} did not exit successfully, but task is allowed to fail", getMigrationVersion());
197                                ourLog.debug("Error was: {}", e.getMessage(), e);
198                                return 0;
199                        } else {
200                                throw new HapiMigrationException(Msg.code(61) + "Failed during task " + getMigrationVersion() + ": " + e, e);
201                        }
202                }
203        }
204
205        protected void captureExecutedStatement(String theTableName, @Language("SQL") String theSql, Object... theArguments) {
206                myExecutedStatements.add(new ExecutedStatement(theTableName, theSql, theArguments));
207        }
208
209        public DriverTypeEnum.ConnectionProperties getConnectionProperties() {
210                return myConnectionProperties;
211        }
212
213        public BaseTask setConnectionProperties(DriverTypeEnum.ConnectionProperties theConnectionProperties) {
214                myConnectionProperties = theConnectionProperties;
215                return this;
216        }
217
218        public DriverTypeEnum getDriverType() {
219                return myDriverType;
220        }
221
222        public BaseTask setDriverType(DriverTypeEnum theDriverType) {
223                myDriverType = theDriverType;
224                return this;
225        }
226
227        public abstract void validate();
228
229        public TransactionTemplate getTxTemplate() {
230                return getConnectionProperties().getTxTemplate();
231        }
232
233        public JdbcTemplate newJdbcTemplate() {
234                return getConnectionProperties().newJdbcTemplate();
235        }
236
237        public void execute() throws SQLException {
238                if (myDoNothing) {
239                        ourLog.info("Skipping stubbed task: {}", getDescription());
240                        return;
241                }
242                if (!myOnlyAppliesToPlatforms.isEmpty()) {
243                        if (!myOnlyAppliesToPlatforms.contains(getDriverType())) {
244                                ourLog.debug("Skipping task {} as it does not apply to {}", getDescription(), getDriverType());
245                                return;
246                        }
247                }
248                doExecute();
249        }
250
251        protected abstract void doExecute() throws SQLException;
252
253        protected boolean isFailureAllowed() {
254                return myFailureAllowed;
255        }
256
257        public void setFailureAllowed(boolean theFailureAllowed) {
258                myFailureAllowed = theFailureAllowed;
259        }
260
261        public String getMigrationVersion() {
262                String releasePart = myProductVersion;
263                if (releasePart.startsWith("V")) {
264                        releasePart = releasePart.substring(1);
265                }
266                String version = releasePart + "." + mySchemaVersion;
267                MigrationVersion migrationVersion = MigrationVersion.fromVersion(version);
268                return migrationVersion.getVersion();
269        }
270
271        protected void logInfo(Logger theLog, String theFormattedMessage, Object... theArguments) {
272                theLog.info(getMigrationVersion() + ": " + theFormattedMessage, theArguments);
273        }
274
275        public void validateVersion() {
276                Matcher matcher = versionPattern.matcher(mySchemaVersion);
277                if (!matcher.matches()) {
278                        throw new IllegalStateException(Msg.code(62) + "The version " + mySchemaVersion + " does not match the expected pattern " + MIGRATION_VERSION_PATTERN);
279                }
280        }
281
282        public boolean isDoNothing() {
283                return myDoNothing;
284        }
285
286        public BaseTask setDoNothing(boolean theDoNothing) {
287                myDoNothing = theDoNothing;
288                return this;
289        }
290
291        @Override
292        public final int hashCode() {
293                HashCodeBuilder builder = new HashCodeBuilder();
294                generateHashCode(builder);
295                return builder.hashCode();
296        }
297
298        protected abstract void generateHashCode(HashCodeBuilder theBuilder);
299
300        @Override
301        public final boolean equals(Object theObject) {
302                if (theObject == null || getClass().equals(theObject.getClass()) == false) {
303                        return false;
304                }
305                @SuppressWarnings("unchecked")
306                BaseTask otherObject = (BaseTask) theObject;
307
308                EqualsBuilder b = new EqualsBuilder();
309                generateEquals(b, otherObject);
310                return b.isEquals();
311        }
312
313        protected abstract void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject);
314
315        public boolean initializedSchema() {
316                return false;
317        }
318
319        public static class ExecutedStatement {
320                private final String mySql;
321                private final List<Object> myArguments;
322                private final String myTableName;
323
324                public ExecutedStatement(String theDescription, String theSql, Object[] theArguments) {
325                        myTableName = theDescription;
326                        mySql = theSql;
327                        myArguments = theArguments != null ? Arrays.asList(theArguments) : Collections.emptyList();
328                }
329
330                public String getTableName() {
331                        return myTableName;
332                }
333
334                public String getSql() {
335                        return mySql;
336                }
337
338                public List<Object> getArguments() {
339                        return myArguments;
340                }
341        }
342}