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}