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