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