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