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