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}