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