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