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}