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}