001package io.ebean.migration; 002 003import java.sql.Connection; 004import java.sql.DriverManager; 005import java.sql.SQLException; 006import java.util.HashSet; 007import java.util.Map; 008import java.util.Properties; 009import java.util.Set; 010 011/** 012 * Configuration used to run the migration. 013 */ 014public class MigrationConfig { 015 016 private String migrationPath = "dbmigration"; 017 018 private String migrationInitPath = "dbinit"; 019 020 private String metaTable = "db_migration"; 021 022 private String applySuffix = ".sql"; 023 024 private String runPlaceholders; 025 026 private boolean skipChecksum; 027 028 private Map<String, String> runPlaceholderMap; 029 030 private ClassLoader classLoader; 031 032 private String dbUsername; 033 private String dbPassword; 034 private String dbDriver; 035 private String dbUrl; 036 037 private String dbSchema; 038 039 private boolean createSchemaIfNotExists = true; 040 041 private boolean setCurrentSchema = true; 042 043 private boolean allowErrorInRepeatable; 044 045 private String platformName; 046 047 private JdbcMigrationFactory jdbcMigrationFactory = new DefaultMigrationFactory(); 048 049 /** 050 * Versions that we want to insert into migration history without actually running. 051 */ 052 private Set<String> patchInsertOn; 053 054 /** 055 * Versions that we want to update the checksum on without actually running. 056 */ 057 private Set<String> patchResetChecksumOn; 058 059 /** 060 * The minimum version, that must be in the dbmigration table. If the current maxVersion 061 * in the migration table is less than this version, the MigrationRunner will fail 062 * with a {@link MigrationException} and an optional {@link #minVersionFailMessage} 063 * to enforce certain migration paths. 064 */ 065 private String minVersion; 066 067 /** 068 * The (customizable) fail message, if minVersion is not in database. 069 * (e.g. "To perform an upgrade, you must install APP XY first") 070 */ 071 private String minVersionFailMessage; 072 073 /** 074 * Return the name of the migration table. 075 */ 076 public String getMetaTable() { 077 return metaTable; 078 } 079 080 /** 081 * Set the name of the migration table. 082 */ 083 public void setMetaTable(String metaTable) { 084 this.metaTable = metaTable; 085 } 086 087 /** 088 * Parse as comma delimited versions. 089 */ 090 private Set<String> parseCommaDelimited(String versionsCommaDelimited) { 091 if (versionsCommaDelimited != null) { 092 Set<String> versions = new HashSet<>(); 093 String[] split = versionsCommaDelimited.split(","); 094 for (String version : split) { 095 if (version.startsWith("R__")) { 096 version = version.substring(3); 097 } 098 versions.add(version); 099 } 100 return versions; 101 } 102 return null; 103 } 104 105 /** 106 * Return true if we continue running the migration when a repeatable migration fails. 107 */ 108 public boolean isAllowErrorInRepeatable() { 109 return allowErrorInRepeatable; 110 } 111 112 /** 113 * Set to true to continue running the migration when a repeatable migration fails. 114 */ 115 public void setAllowErrorInRepeatable(boolean allowErrorInRepeatable) { 116 this.allowErrorInRepeatable = allowErrorInRepeatable; 117 } 118 119 /** 120 * Set the migrations that should have their checksum reset as a comma delimited list. 121 */ 122 public void setPatchResetChecksumOn(String versionsCommaDelimited) { 123 patchResetChecksumOn = parseCommaDelimited(versionsCommaDelimited); 124 } 125 126 /** 127 * Set the migrations that should have their checksum reset. 128 */ 129 public void setPatchResetChecksumOn(Set<String> patchResetChecksumOn) { 130 this.patchResetChecksumOn = patchResetChecksumOn; 131 } 132 133 /** 134 * Return the migrations that should have their checksum reset. 135 */ 136 public Set<String> getPatchResetChecksumOn() { 137 return patchResetChecksumOn; 138 } 139 140 /** 141 * Set the migrations that should not be run but inserted into history as if they have run. 142 */ 143 public void setPatchInsertOn(String versionsCommaDelimited) { 144 patchInsertOn = parseCommaDelimited(versionsCommaDelimited); 145 } 146 147 /** 148 * Set the migrations that should not be run but inserted into history as if they have run. 149 * <p> 150 * This can be useful when we need to pull out DDL from a repeatable migration that should really 151 * only run once. We can pull out that DDL as a new migration and add it to history as if it had been 152 * run (we can only do this when we know it exists in all environments including production). 153 * </p> 154 */ 155 public void setPatchInsertOn(Set<String> patchInsertOn) { 156 this.patchInsertOn = patchInsertOn; 157 } 158 159 /** 160 * Return the migrations that should not be run but inserted into history as if they have run. 161 */ 162 public Set<String> getPatchInsertOn() { 163 return patchInsertOn; 164 } 165 166 /** 167 * Return true if checksum check should be skipped (during development). 168 */ 169 public boolean isSkipChecksum() { 170 return skipChecksum; 171 } 172 173 /** 174 * Set to true to skip the checksum check. 175 * <p> 176 * This is intended for use during development only. 177 * </p> 178 */ 179 public void setSkipChecksum(boolean skipChecksum) { 180 this.skipChecksum = skipChecksum; 181 } 182 183 /** 184 * Return a Comma and equals delimited key/value placeholders to replace in DDL scripts. 185 */ 186 public String getRunPlaceholders() { 187 return runPlaceholders; 188 } 189 190 /** 191 * Set a Comma and equals delimited key/value placeholders to replace in DDL scripts. 192 */ 193 public void setRunPlaceholders(String runPlaceholders) { 194 this.runPlaceholders = runPlaceholders; 195 } 196 197 /** 198 * Return a map of name/value pairs that can be expressions replaced in migration scripts. 199 */ 200 public Map<String, String> getRunPlaceholderMap() { 201 return runPlaceholderMap; 202 } 203 204 /** 205 * Set a map of name/value pairs that can be expressions replaced in migration scripts. 206 */ 207 public void setRunPlaceholderMap(Map<String, String> runPlaceholderMap) { 208 this.runPlaceholderMap = runPlaceholderMap; 209 } 210 211 /** 212 * Return the root path used to find migrations. 213 */ 214 public String getMigrationPath() { 215 return migrationPath; 216 } 217 218 /** 219 * Set the root path used to find migrations. 220 */ 221 public void setMigrationPath(String migrationPath) { 222 this.migrationPath = migrationPath; 223 } 224 225 /** 226 * Return the path for containing init migration scripts. 227 */ 228 public String getMigrationInitPath() { 229 return migrationInitPath; 230 } 231 232 /** 233 * Set the path containing init migration scripts. 234 */ 235 public void setMigrationInitPath(String migrationInitPath) { 236 this.migrationInitPath = migrationInitPath; 237 } 238 239 /** 240 * Return the suffix for migration resources (defaults to .sql). 241 */ 242 public String getApplySuffix() { 243 return applySuffix; 244 } 245 246 /** 247 * Set the suffix for migration resources. 248 */ 249 public void setApplySuffix(String applySuffix) { 250 this.applySuffix = applySuffix; 251 } 252 253 /** 254 * Return the DB username. 255 * <p> 256 * Used when a Connection to run the migration is not supplied. 257 * </p> 258 */ 259 public String getDbUsername() { 260 return dbUsername; 261 } 262 263 /** 264 * Set the DB username. 265 * <p> 266 * Used when a Connection to run the migration is not supplied. 267 * </p> 268 */ 269 public void setDbUsername(String dbUsername) { 270 this.dbUsername = dbUsername; 271 } 272 273 /** 274 * Return the DB password. 275 * <p> 276 * Used when creating a Connection to run the migration. 277 * </p> 278 */ 279 public String getDbPassword() { 280 return dbPassword; 281 } 282 283 /** 284 * Set the DB password. 285 * <p> 286 * Used when creating a Connection to run the migration. 287 * </p> 288 */ 289 public void setDbPassword(String dbPassword) { 290 this.dbPassword = dbPassword; 291 } 292 293 /** 294 * Return the DB Driver. 295 * <p> 296 * Used when creating a Connection to run the migration. 297 * </p> 298 */ 299 public String getDbDriver() { 300 return dbDriver; 301 } 302 303 /** 304 * Set the DB Driver. 305 * <p> 306 * Used when creating a Connection to run the migration. 307 * </p> 308 */ 309 public void setDbDriver(String dbDriver) { 310 this.dbDriver = dbDriver; 311 } 312 313 /** 314 * Return the DB connection URL. 315 * <p> 316 * Used when creating a Connection to run the migration. 317 * </p> 318 */ 319 public String getDbUrl() { 320 return dbUrl; 321 } 322 323 /** 324 * Set the DB connection URL. 325 * <p> 326 * Used when creating a Connection to run the migration. 327 * </p> 328 */ 329 public void setDbUrl(String dbUrl) { 330 this.dbUrl = dbUrl; 331 } 332 333 /** 334 * Return the DB connection Schema. 335 * <p> 336 * Used when creating a Connection to run the migration. 337 * </p> 338 */ 339 public String getDbSchema() { 340 return dbSchema; 341 } 342 343 /** 344 * Set the DB connection Schema. 345 * <p> 346 * Used when creating a Connection to run the migration. 347 * </p> 348 */ 349 public void setDbSchema(String dbSchema) { 350 this.dbSchema = dbSchema; 351 } 352 353 /** 354 * Return true if migration should create the schema if it does not exist. 355 */ 356 public boolean isCreateSchemaIfNotExists() { 357 return createSchemaIfNotExists; 358 } 359 360 /** 361 * Set to create Schema if it does not exist. 362 */ 363 public void setCreateSchemaIfNotExists(boolean createSchemaIfNotExists) { 364 this.createSchemaIfNotExists = createSchemaIfNotExists; 365 } 366 367 /** 368 * Return true if the dbSchema should be set as current schema. 369 */ 370 public boolean isSetCurrentSchema() { 371 return setCurrentSchema; 372 } 373 374 /** 375 * Set if the dbSchema should be set as current schema. 376 * <p> 377 * We want to set this to false for the case of Postgres where the dbSchema matches the DB username. 378 * If we set the dbSchema that can mess up the Postgres search path so we turn this off in that case. 379 * </p> 380 */ 381 public void setSetCurrentSchema(boolean setCurrentSchema) { 382 this.setCurrentSchema = setCurrentSchema; 383 } 384 385 /** 386 * Return the DB platform name (used for platform create table and select for update syntax). 387 */ 388 public String getPlatformName() { 389 return platformName; 390 } 391 392 /** 393 * Set a DB platform name (to load specific create table and select for update syntax). 394 */ 395 public void setPlatformName(String platformName) { 396 this.platformName = platformName; 397 } 398 399 /** 400 * Return the ClassLoader to use to load resources. 401 */ 402 public ClassLoader getClassLoader() { 403 if (classLoader == null) { 404 classLoader = Thread.currentThread().getContextClassLoader(); 405 if (classLoader == null) { 406 classLoader = this.getClass().getClassLoader(); 407 } 408 } 409 return classLoader; 410 } 411 412 /** 413 * Set the ClassLoader to use when loading resources. 414 */ 415 public void setClassLoader(ClassLoader classLoader) { 416 this.classLoader = classLoader; 417 } 418 419 /** 420 * Returns the jdbcMigrationFactory. 421 */ 422 public JdbcMigrationFactory getJdbcMigrationFactory() { 423 return jdbcMigrationFactory; 424 } 425 426 /** 427 * Sets the jdbcMigrationFactory. 428 */ 429 public void setJdbcMigrationFactory(JdbcMigrationFactory jdbcMigrationFactory) { 430 this.jdbcMigrationFactory = jdbcMigrationFactory; 431 } 432 433 /** 434 * Returns the minVersion. 435 */ 436 public String getMinVersion() { 437 return minVersion; 438 } 439 440 /** 441 * Set the minVersion. 442 */ 443 public void setMinVersion(String minVersion) { 444 this.minVersion = minVersion; 445 } 446 447 /** 448 * Sets the optional minVersionFailMessage. 449 */ 450 public String getMinVersionFailMessage() { 451 return minVersionFailMessage; 452 } 453 454 /** 455 * @param minVersionFailMessage the minVersionFailMessage to set 456 */ 457 public void setMinVersionFailMessage(String minVersionFailMessage) { 458 this.minVersionFailMessage = minVersionFailMessage; 459 } 460 461 /** 462 * Load configuration from standard properties. 463 */ 464 public void load(Properties props) { 465 466 dbUsername = props.getProperty("dbmigration.username", dbUsername); 467 dbPassword = props.getProperty("dbmigration.password", dbPassword); 468 dbDriver = props.getProperty("dbmigration.driver", dbDriver); 469 dbUrl = props.getProperty("dbmigration.url", dbUrl); 470 dbSchema = props.getProperty("dbmigration.schema", dbSchema); 471 472 String skip = props.getProperty("dbmigration.skipchecksum"); 473 if (skip != null) { 474 skipChecksum = Boolean.parseBoolean(skip); 475 } 476 477 String createSchema = props.getProperty("dbmigration.createSchemaIfNotExists"); 478 if (createSchema != null) { 479 createSchemaIfNotExists = Boolean.parseBoolean(createSchema); 480 } 481 String setSchema = props.getProperty("dbmigration.setCurrentSchema"); 482 if (setSchema != null) { 483 setCurrentSchema = Boolean.parseBoolean(setSchema); 484 } 485 platformName = props.getProperty("dbmigration.platformName", platformName); 486 applySuffix = props.getProperty("dbmigration.applySuffix", applySuffix); 487 metaTable = props.getProperty("dbmigration.metaTable", metaTable); 488 migrationPath = props.getProperty("dbmigration.migrationPath", migrationPath); 489 migrationInitPath = props.getProperty("dbmigration.migrationInitPath", migrationInitPath); 490 runPlaceholders = props.getProperty("dbmigration.placeholders", runPlaceholders); 491 minVersion = props.getProperty("dbmigration.minVersion", minVersion); 492 minVersionFailMessage = props.getProperty("dbmigration.minVersionFailMessage", minVersionFailMessage); 493 494 String patchInsertOn = props.getProperty("dbmigration.patchInsertOn"); 495 if (patchInsertOn != null) { 496 setPatchInsertOn(patchInsertOn); 497 } 498 String patchResetChecksumOn = props.getProperty("dbmigration.patchResetChecksumOn"); 499 if (patchResetChecksumOn != null) { 500 setPatchResetChecksumOn(patchResetChecksumOn); 501 } 502 String runPlaceholders = props.getProperty("dbmigration.runPlaceholders"); 503 if (runPlaceholders != null) { 504 setRunPlaceholders(runPlaceholders); 505 } 506 } 507 508 /** 509 * Create a Connection to the database using the configured driver, url, username etc. 510 * <p> 511 * Used when an existing DataSource or Connection is not supplied. 512 * </p> 513 */ 514 public Connection createConnection() { 515 516 if (dbUsername == null) throw new MigrationException("Database username is null?"); 517 if (dbPassword == null) throw new MigrationException("Database password is null?"); 518 if (dbDriver == null) throw new MigrationException("Database Driver is null?"); 519 if (dbUrl == null) throw new MigrationException("Database connection URL is null?"); 520 521 loadDriver(); 522 523 try { 524 Properties props = new Properties(); 525 props.setProperty("user", dbUsername); 526 props.setProperty("password", dbPassword); 527 return DriverManager.getConnection(dbUrl, props); 528 529 } catch (SQLException e) { 530 throw new MigrationException("Error trying to create Connection", e); 531 } 532 } 533 534 private void loadDriver() { 535 try { 536 Class.forName(dbDriver, true, getClassLoader()); 537 } catch (Throwable e) { 538 throw new MigrationException("Problem loading Database Driver [" + dbDriver + "]: " + e.getMessage(), e); 539 } 540 } 541 542 /** 543 * Default factory. Uses the migration's class loader and injects the config if necessary. 544 * 545 * @author Roland Praml, FOCONIS AG 546 */ 547 public class DefaultMigrationFactory implements JdbcMigrationFactory { 548 549 @Override 550 public JdbcMigration createInstance(String className) { 551 try { 552 Class<?> clazz = Class.forName(className, true, MigrationConfig.this.getClassLoader()); 553 JdbcMigration migration = (JdbcMigration) clazz.newInstance(); 554 if (migration instanceof ConfigurationAware) { 555 ((ConfigurationAware) migration).setMigrationConfig(MigrationConfig.this); 556 } 557 return migration; 558 } catch (Exception e) { 559 throw new IllegalArgumentException(className + " is not a valid JdbcMigration", e); 560 } 561 } 562 } 563 564}