001/*- 002 * #%L 003 * HAPI FHIR Server - SQL Migration 004 * %% 005 * Copyright (C) 2014 - 2024 Smile CDR, Inc. 006 * %% 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, software 014 * distributed under the License is distributed on an "AS IS" BASIS, 015 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 016 * See the License for the specific language governing permissions and 017 * limitations under the License. 018 * #L% 019 */ 020package ca.uhn.fhir.jpa.migrate.taskdef; 021 022import ca.uhn.fhir.i18n.Msg; 023import ca.uhn.fhir.jpa.migrate.DriverTypeEnum; 024import ca.uhn.fhir.jpa.migrate.HapiMigrationException; 025import ca.uhn.fhir.jpa.migrate.tasks.api.TaskFlagEnum; 026import ca.uhn.fhir.system.HapiSystemProperties; 027import jakarta.annotation.Nonnull; 028import org.apache.commons.lang3.Validate; 029import org.apache.commons.lang3.builder.EqualsBuilder; 030import org.apache.commons.lang3.builder.HashCodeBuilder; 031import org.apache.commons.lang3.builder.ToStringBuilder; 032import org.apache.commons.lang3.builder.ToStringStyle; 033import org.flywaydb.core.api.MigrationVersion; 034import org.intellij.lang.annotations.Language; 035import org.slf4j.Logger; 036import org.slf4j.LoggerFactory; 037import org.springframework.dao.DataAccessException; 038import org.springframework.jdbc.core.JdbcTemplate; 039import org.springframework.transaction.support.TransactionTemplate; 040 041import java.sql.SQLException; 042import java.util.ArrayList; 043import java.util.Arrays; 044import java.util.Collections; 045import java.util.EnumSet; 046import java.util.HashSet; 047import java.util.List; 048import java.util.Set; 049import java.util.regex.Matcher; 050import java.util.regex.Pattern; 051 052public abstract class BaseTask { 053 054 public static final String MIGRATION_VERSION_PATTERN = "\\d{8}\\.\\d+"; 055 private static final Logger ourLog = LoggerFactory.getLogger(BaseTask.class); 056 private static final Pattern versionPattern = Pattern.compile(MIGRATION_VERSION_PATTERN); 057 private final String myProductVersion; 058 private final String mySchemaVersion; 059 private final List<ExecuteTaskPrecondition> myPreconditions = new ArrayList<>(); 060 private final EnumSet<TaskFlagEnum> myFlags = EnumSet.noneOf(TaskFlagEnum.class); 061 private final List<ExecutedStatement> myExecutedStatements = new ArrayList<>(); 062 /** 063 * Whether to check for existing tables 064 * before generating SQL 065 */ 066 protected boolean myCheckForExistingTables = true; 067 /** 068 * Whether to generate the SQL in a 'readable format' 069 */ 070 protected boolean myPrettyPrint = false; 071 072 private DriverTypeEnum.ConnectionProperties myConnectionProperties; 073 private DriverTypeEnum myDriverType; 074 private String myDescription; 075 private Integer myChangesCount = 0; 076 private boolean myDryRun; 077 private boolean myTransactional = true; 078 private Set<DriverTypeEnum> myOnlyAppliesToPlatforms = new HashSet<>(); 079 private boolean myNoColumnShrink; 080 081 protected BaseTask(String theProductVersion, String theSchemaVersion) { 082 myProductVersion = theProductVersion; 083 mySchemaVersion = theSchemaVersion; 084 } 085 086 /** 087 * Adds a flag if it's not already present, otherwise this call is ignored. 088 * 089 * @param theFlag The flag, must not be null 090 */ 091 public BaseTask addFlag(@Nonnull TaskFlagEnum theFlag) { 092 myFlags.add(theFlag); 093 return this; 094 } 095 096 /** 097 * Some migrations can not be run in a transaction. 098 * When this is true, {@link BaseTask#executeSql} will run without a transaction 099 */ 100 public void setTransactional(boolean theTransactional) { 101 myTransactional = theTransactional; 102 } 103 104 public void setPrettyPrint(boolean thePrettyPrint) { 105 myPrettyPrint = thePrettyPrint; 106 } 107 108 public void setOnlyAppliesToPlatforms(Set<DriverTypeEnum> theOnlyAppliesToPlatforms) { 109 Validate.notNull(theOnlyAppliesToPlatforms, "theOnlyAppliesToPlatforms must not be null"); 110 myOnlyAppliesToPlatforms = theOnlyAppliesToPlatforms; 111 } 112 113 public String getProductVersion() { 114 return myProductVersion; 115 } 116 117 public String getSchemaVersion() { 118 return mySchemaVersion; 119 } 120 121 public boolean isNoColumnShrink() { 122 return myNoColumnShrink; 123 } 124 125 public void setNoColumnShrink(boolean theNoColumnShrink) { 126 myNoColumnShrink = theNoColumnShrink; 127 } 128 129 public boolean isDryRun() { 130 return myDryRun; 131 } 132 133 public void setDryRun(boolean theDryRun) { 134 myDryRun = theDryRun; 135 } 136 137 public String getDescription() { 138 if (myDescription == null) { 139 return this.getClass().getSimpleName(); 140 } 141 return myDescription; 142 } 143 144 public BaseTask setDescription(String theDescription) { 145 myDescription = theDescription; 146 return this; 147 } 148 149 public List<ExecutedStatement> getExecutedStatements() { 150 return myExecutedStatements; 151 } 152 153 public int getChangesCount() { 154 return myChangesCount; 155 } 156 157 /** 158 * @param theTableName This is only used for logging currently 159 * @param theSql The SQL statement 160 * @param theArguments The SQL statement arguments 161 */ 162 public void executeSql(String theTableName, @Language("SQL") String theSql, Object... theArguments) { 163 if (!isDryRun()) { 164 Integer changes; 165 if (myTransactional) { 166 changes = getConnectionProperties().getTxTemplate().execute(t -> doExecuteSql(theSql, theArguments)); 167 } else { 168 changes = doExecuteSql(theSql, theArguments); 169 } 170 171 myChangesCount += changes; 172 } 173 174 captureExecutedStatement(theTableName, theSql, theArguments); 175 } 176 177 protected void executeSqlListInTransaction(String theTableName, List<String> theSqlStatements) { 178 if (!isDryRun()) { 179 Integer changes; 180 changes = getConnectionProperties().getTxTemplate().execute(t -> doExecuteSqlList(theSqlStatements)); 181 myChangesCount += changes; 182 } 183 184 for (@Language("SQL") String sqlStatement : theSqlStatements) { 185 captureExecutedStatement(theTableName, sqlStatement); 186 } 187 } 188 189 private Integer doExecuteSqlList(List<String> theSqlStatements) { 190 int changesCount = 0; 191 for (@Language("SQL") String nextSql : theSqlStatements) { 192 changesCount += doExecuteSql(nextSql); 193 } 194 195 return changesCount; 196 } 197 198 private int doExecuteSql(@Language("SQL") String theSql, Object... theArguments) { 199 JdbcTemplate jdbcTemplate = getConnectionProperties().newJdbcTemplate(); 200 // 0 means no timeout -- we use this for index rebuilds that may take time. 201 jdbcTemplate.setQueryTimeout(0); 202 try { 203 int changesCount = jdbcTemplate.update(theSql, theArguments); 204 if (!HapiSystemProperties.isUnitTestModeEnabled()) { 205 logInfo(ourLog, "SQL \"{}\" returned {}", theSql, changesCount); 206 } 207 return changesCount; 208 } catch (DataAccessException e) { 209 if (myFlags.contains(TaskFlagEnum.FAILURE_ALLOWED)) { 210 ourLog.info( 211 "Task {} did not exit successfully on doExecuteSql(), but task is allowed to fail", 212 getMigrationVersion()); 213 ourLog.debug("Error was: {}", e.getMessage(), e); 214 return 0; 215 } else { 216 throw new HapiMigrationException( 217 Msg.code(61) + "Failed during task " + getMigrationVersion() + ": " + e, e); 218 } 219 } 220 } 221 222 protected void captureExecutedStatement( 223 String theTableName, @Language("SQL") String theSql, Object... theArguments) { 224 myExecutedStatements.add(new ExecutedStatement(mySchemaVersion, theTableName, theSql, theArguments)); 225 } 226 227 public DriverTypeEnum.ConnectionProperties getConnectionProperties() { 228 return myConnectionProperties; 229 } 230 231 public BaseTask setConnectionProperties(DriverTypeEnum.ConnectionProperties theConnectionProperties) { 232 myConnectionProperties = theConnectionProperties; 233 return this; 234 } 235 236 public DriverTypeEnum getDriverType() { 237 return myDriverType; 238 } 239 240 public BaseTask setDriverType(DriverTypeEnum theDriverType) { 241 myDriverType = theDriverType; 242 return this; 243 } 244 245 public abstract void validate(); 246 247 public TransactionTemplate getTxTemplate() { 248 return getConnectionProperties().getTxTemplate(); 249 } 250 251 public JdbcTemplate newJdbcTemplate() { 252 return getConnectionProperties().newJdbcTemplate(); 253 } 254 255 public void execute() throws SQLException { 256 if (myFlags.contains(TaskFlagEnum.DO_NOTHING)) { 257 ourLog.info("Skipping stubbed task: {}", getDescription()); 258 return; 259 } 260 if (!myOnlyAppliesToPlatforms.isEmpty()) { 261 if (!myOnlyAppliesToPlatforms.contains(getDriverType())) { 262 ourLog.info("Skipping task {} as it does not apply to {}", getDescription(), getDriverType()); 263 return; 264 } 265 } 266 267 for (ExecuteTaskPrecondition precondition : myPreconditions) { 268 ourLog.debug("precondition to evaluate: {}", precondition); 269 if (!precondition.getPreconditionRunner().get()) { 270 ourLog.info( 271 "Skipping task since one of the preconditions was not met: {}", 272 precondition.getPreconditionReason()); 273 return; 274 } 275 } 276 doExecute(); 277 } 278 279 protected abstract void doExecute() throws SQLException; 280 281 public String getMigrationVersion() { 282 String releasePart = myProductVersion; 283 if (releasePart.startsWith("V")) { 284 releasePart = releasePart.substring(1); 285 } 286 String version = releasePart + "." + mySchemaVersion; 287 MigrationVersion migrationVersion = MigrationVersion.fromVersion(version); 288 return migrationVersion.getVersion(); 289 } 290 291 @SuppressWarnings("StringConcatenationArgumentToLogCall") 292 protected void logInfo(Logger theLog, String theFormattedMessage, Object... theArguments) { 293 theLog.info(getMigrationVersion() + ": " + theFormattedMessage, theArguments); 294 } 295 296 public void validateVersion() { 297 Matcher matcher = versionPattern.matcher(mySchemaVersion); 298 if (!matcher.matches()) { 299 throw new IllegalStateException(Msg.code(62) + "The version " + mySchemaVersion 300 + " does not match the expected pattern " + MIGRATION_VERSION_PATTERN); 301 } 302 } 303 304 public void addPrecondition(ExecuteTaskPrecondition thePrecondition) { 305 myPreconditions.add(thePrecondition); 306 } 307 308 @Override 309 public final int hashCode() { 310 HashCodeBuilder builder = new HashCodeBuilder(); 311 generateHashCode(builder); 312 return builder.hashCode(); 313 } 314 315 protected abstract void generateHashCode(HashCodeBuilder theBuilder); 316 317 @Override 318 public final boolean equals(Object theObject) { 319 if (theObject == null || getClass().equals(theObject.getClass()) == false) { 320 return false; 321 } 322 BaseTask otherObject = (BaseTask) theObject; 323 324 EqualsBuilder b = new EqualsBuilder(); 325 generateEquals(b, otherObject); 326 return b.isEquals(); 327 } 328 329 protected abstract void generateEquals(EqualsBuilder theBuilder, BaseTask theOtherObject); 330 331 public boolean initializedSchema() { 332 return false; 333 } 334 335 public boolean isDoNothing() { 336 return myFlags.contains(TaskFlagEnum.DO_NOTHING); 337 } 338 339 public boolean isHeavyweightSkippableTask() { 340 return myFlags.contains(TaskFlagEnum.HEAVYWEIGHT_SKIP_BY_DEFAULT); 341 } 342 343 public boolean hasFlag(TaskFlagEnum theFlag) { 344 return myFlags.contains(theFlag); 345 } 346 347 public static class ExecutedStatement { 348 private final String mySql; 349 private final List<Object> myArguments; 350 private final String myTableName; 351 private final String mySchemaVersion; 352 353 public ExecutedStatement(String theSchemaVersion, String theDescription, String theSql, Object[] theArguments) { 354 mySchemaVersion = theSchemaVersion; 355 myTableName = theDescription; 356 mySql = theSql; 357 myArguments = theArguments != null ? Arrays.asList(theArguments) : Collections.emptyList(); 358 } 359 360 public String getSchemaVersion() { 361 return mySchemaVersion; 362 } 363 364 public String getTableName() { 365 return myTableName; 366 } 367 368 public String getSql() { 369 return mySql; 370 } 371 372 public List<Object> getArguments() { 373 return myArguments; 374 } 375 376 @Override 377 public String toString() { 378 return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) 379 .append("tableName", myTableName) 380 .append("sql", mySql) 381 .append("arguments", myArguments) 382 .toString(); 383 } 384 } 385}