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