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