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