001package ca.uhn.fhir.jpa.migrate.taskdef; 002 003/*- 004 * #%L 005 * HAPI FHIR Server - SQL Migration 006 * %% 007 * Copyright (C) 2014 - 2023 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}