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