001package com.avaje.ebean.config;
002
003import com.avaje.ebean.EbeanServer;
004import com.avaje.ebean.config.dbplatform.DbPlatformName;
005import com.avaje.ebean.dbmigration.DbMigration;
006import org.avaje.dbmigration.MigrationConfig;
007import org.avaje.dbmigration.MigrationRunner;
008import org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011import java.util.Map;
012
013/**
014 * Configuration for the DB migration processing.
015 */
016public class DbMigrationConfig {
017
018  protected static final Logger logger = LoggerFactory.getLogger(DbMigrationConfig.class);
019
020  /**
021   * The database platform to generate migration DDL for.
022   */
023  protected DbPlatformName platform;
024
025  /**
026   * Set to true if the DB migration should be generated on server start.
027   */
028  protected boolean generate;
029
030  /**
031   * The migration version name (typically FlywayDb compatible).
032   * <p>
033   * Example: 1.1.1_2
034   * <p>
035   * The version is expected to be the combination of the current pom version plus
036   * a 'feature' id. The combined version must be unique and ordered to work with
037   * FlywayDb so each developer sets a unique version so that the migration script
038   * generated is unique (typically just prior to being submitted as a merge request).
039   */
040  protected String version;
041
042  /**
043   * Description text that can be appended to the version to become the ddl script file name.
044   * <p>
045   * So if the name is "a foo table" then the ddl script file could be:
046   * "1.1.1_2__a-foo-table.sql"
047   * <p>
048   * When the DB migration relates to a git feature (merge request) then this description text
049   * is a short description of the feature.
050   */
051  protected String name;
052
053  /**
054   * Resource path for the migration xml and sql.
055   */
056  protected String migrationPath = "dbmigration";
057
058  /**
059   * Subdirectory the model xml files go into.
060   */
061  protected String modelPath = "model";
062
063  protected String applySuffix = ".sql";
064
065  /**
066   * Set this to "V" to be compatible with FlywayDB.
067   */
068  protected String applyPrefix = "";
069
070  protected String modelSuffix = ".model.xml";
071
072  protected boolean includeGeneratedFileComment;
073
074  /**
075   * The version of a pending drop that should be generated as the next migration.
076   */
077  protected String generatePendingDrop;
078
079  /**
080   * For running migration the DB table that holds migration execution status.
081   */
082  protected String metaTable = "db_migration";
083
084  /**
085   * Flag set to true means to run any outstanding migrations on startup.
086   */
087  protected boolean runMigration;
088
089  /**
090   * Comma and equals delimited key/value placeholders to replace in DDL scripts.
091   */
092  protected String runPlaceholders;
093
094  /**
095   * Map of key/value placeholders to replace in DDL scripts.
096   */
097  protected Map<String,String> runPlaceholderMap;
098
099  /**
100   * DB user used to run the DB migration.
101   */
102  protected String dbUsername;
103
104  /**
105   * DB password used to run the DB migration.
106   */
107  protected String dbPassword;
108
109  /**
110   * Return the DB platform to generate migration DDL for.
111   *
112   * We typically need to explicitly specify this as migration can often be generated
113   * when running against H2.
114   */
115  public DbPlatformName getPlatform() {
116    return platform;
117  }
118
119  /**
120   * Set the DB platform to generate migration DDL for.
121   */
122  public void setPlatform(DbPlatformName platform) {
123    this.platform = platform;
124  }
125
126  /**
127   * Return the resource path for db migrations.
128   */
129  public String getMigrationPath() {
130    return migrationPath;
131  }
132
133  /**
134   * Set the resource path for db migrations.
135   * <p>
136   * The default of "dbmigration" is reasonable in most cases. You may look to set this
137   * to be something like "dbmigration/myapp" where myapp gives it a unique resource path
138   * in the case there are multiple EbeanServer applications in the single classpath.
139   * </p>
140   */
141  public void setMigrationPath(String migrationPath) {
142    this.migrationPath = migrationPath;
143  }
144
145  /**
146   * Return the relative path for the model files (defaults to model).
147   */
148  public String getModelPath() {
149    return modelPath;
150  }
151
152  /**
153   * Set the relative path for the model files.
154   */
155  public void setModelPath(String modelPath) {
156    this.modelPath = modelPath;
157  }
158
159  /**
160   * Return the model suffix (defaults to model.xml)
161   */
162  public String getModelSuffix() {
163    return modelSuffix;
164  }
165
166  /**
167   * Set the model suffix.
168   */
169  public void setModelSuffix(String modelSuffix) {
170    this.modelSuffix = modelSuffix;
171  }
172
173  /**
174   * Return the apply script suffix (defaults to sql).
175   */
176  public String getApplySuffix() {
177    return applySuffix;
178  }
179
180  /**
181   * Set the apply script suffix (defaults to sql).
182   */
183  public void setApplySuffix(String applySuffix) {
184    this.applySuffix = applySuffix;
185  }
186
187  /**
188   * Return the apply prefix.
189   */
190  public String getApplyPrefix() {
191    return applyPrefix;
192  }
193
194  /**
195   * Set the apply prefix. This might be set to "V" for use with FlywayDB.
196   */
197  public void setApplyPrefix(String applyPrefix) {
198    this.applyPrefix = applyPrefix;
199  }
200
201  /**
202   * Return true if the generated file comment should be included.
203   */
204  public boolean isIncludeGeneratedFileComment() {
205    return includeGeneratedFileComment;
206  }
207
208  /**
209   * Set to true if the generated file comment should be included.
210   */
211  public void setIncludeGeneratedFileComment(boolean includeGeneratedFileComment) {
212    this.includeGeneratedFileComment = includeGeneratedFileComment;
213  }
214
215  /**
216   * Return the migration version (or "next") to generate pending drops for.
217   */
218  public String getGeneratePendingDrop() {
219    return generatePendingDrop;
220  }
221
222  /**
223   * Set the migration version (or "next") to generate pending drops for.
224   */
225  public void setGeneratePendingDrop(String generatePendingDrop) {
226    this.generatePendingDrop = generatePendingDrop;
227  }
228
229  /**
230   * Set the migration version.
231   * <p>
232   * Note that version set via System property or environment variable <code>ddl.migration.version</code> takes precedence.
233   */
234  public void setVersion(String version) {
235    this.version = version;
236  }
237
238  /**
239   * Set the migration name.
240   * <p>
241   * Note that name set via System property or environment variable <code>ddl.migration.name</code> takes precedence.
242   */
243  public void setName(String name) {
244    this.name = name;
245  }
246
247  /**
248   * Return the table name that holds the migration run details
249   * (used by DB Migration runner only).
250   */
251  public String getMetaTable() {
252    return metaTable;
253  }
254
255  /**
256   * Set the table name that holds the migration run details
257   * (used by DB Migration runner only).
258   */
259  public void setMetaTable(String metaTable) {
260    this.metaTable = metaTable;
261  }
262
263  /**
264   * Return a comma and equals delimited placeholders that are substituted in SQL scripts when running migration
265   * (used by DB Migration runner only).
266   */
267  public String getRunPlaceholders() {
268    // environment properties take precedence
269    String placeholders = readEnvironment("ddl.migration.placeholders");
270    if (placeholders != null) {
271      return placeholders;
272    }
273    return runPlaceholders;
274  }
275
276  /**
277   * Set a comma and equals delimited placeholders that are substituted in SQL scripts when running migration
278   * (used by DB Migration runner only).
279   */
280  public void setRunPlaceholders(String runPlaceholders) {
281    this.runPlaceholders = runPlaceholders;
282  }
283
284  /**
285   * Return a map of placeholder values that are substituted in SQL scripts when running migration
286   * (used by DB Migration runner only).
287   */
288  public Map<String, String> getRunPlaceholderMap() {
289    return runPlaceholderMap;
290  }
291
292  /**
293   * Set a map of placeholder values that are substituted when running migration
294   * (used by DB Migration runner only).
295   */
296  public void setRunPlaceholderMap(Map<String, String> runPlaceholderMap) {
297    this.runPlaceholderMap = runPlaceholderMap;
298  }
299
300  /**
301   * Return true if the DB migration should be run on startup.
302   */
303  public boolean isRunMigration() {
304    // environment properties take precedence
305    String run = readEnvironment("ddl.migration.run");
306    if (run != null) {
307      return "true".equalsIgnoreCase(run.trim());
308    }
309    return runMigration;
310  }
311
312  /**
313   * Set to true to run the DB migration on startup.
314   */
315  public void setRunMigration(boolean runMigration) {
316    this.runMigration = runMigration;
317  }
318
319  /**
320   * Return the DB username to use for running DB migrations.
321   */
322  public String getDbUsername() {
323    // environment properties take precedence
324    String user = readEnvironment("ddl.migration.user");
325    if (user != null) {
326      return user;
327    }
328    return dbUsername;
329  }
330
331  /**
332   * Set the DB username to use for running DB migrations.
333   */
334  public void setDbUsername(String dbUsername) {
335    this.dbUsername = dbUsername;
336  }
337
338  /**
339   * Return the DB password to use for running DB migrations.
340   */
341  public String getDbPassword() {
342    String user = readEnvironment("ddl.migration.password");
343    if (user != null) {
344      return user;
345    }
346    return dbPassword;
347  }
348
349  /**
350   * Set the DB password to use for running DB migrations.
351   */
352  public void setDbPassword(String dbPassword) {
353    this.dbPassword = dbPassword;
354  }
355
356  /**
357   * Load the settings from the PropertiesWrapper.
358   */
359  public void loadSettings(PropertiesWrapper properties, String serverName) {
360
361    migrationPath = properties.get("migration.migrationPath", migrationPath);
362    modelPath = properties.get("migration.modelPath", modelPath);
363    applyPrefix = properties.get("migration.applyPrefix", applyPrefix);
364    applySuffix = properties.get("migration.applySuffix", applySuffix);
365    modelSuffix = properties.get("migration.modelSuffix", modelSuffix);
366    includeGeneratedFileComment = properties.getBoolean("migration.includeGeneratedFileComment", includeGeneratedFileComment);
367    generatePendingDrop = properties.get("migration.generatePendingDrop", generatePendingDrop);
368
369    platform = properties.getEnum(DbPlatformName.class, "migration.platform", platform);
370
371    generate = properties.getBoolean("migration.generate", generate);
372    version = properties.get("migration.version", version);
373    name = properties.get("migration.name", name);
374
375    runMigration = properties.getBoolean("migration.run", runMigration);
376    metaTable = properties.get("migration.metaTable", metaTable);
377    runPlaceholders = properties.get("migration.placeholders", runPlaceholders);
378
379    String adminUser = properties.get("datasource."+serverName+".username", dbUsername);
380    adminUser = properties.get("datasource."+serverName+".adminusername", adminUser);
381    dbUsername = properties.get("migration.dbusername", adminUser);
382
383    String adminPwd = properties.get("datasource."+serverName+".password", dbPassword);
384    adminPwd = properties.get("datasource."+serverName+".adminpassword", adminPwd);
385    dbPassword = properties.get("migration.dbpassword", adminPwd);
386  }
387
388  /**
389   * Return true if the migration should be generated.
390   * <p>
391   * It is expected that when an environment variable <code>ddl.migration.enabled</code>
392   * is set to <code>true</code> then the DB migration will generate the migration DDL.
393   * </p>
394   */
395  public boolean isGenerateOnStart() {
396
397    // environment properties take precedence
398    String envGenerate = readEnvironment("ddl.migration.generate");
399    if (envGenerate != null) {
400      return "true".equalsIgnoreCase(envGenerate.trim());
401    }
402    return generate;
403  }
404
405  /**
406   * Called by EbeanServer on start.
407   *
408   * <p>
409   * If enabled this generates the migration xml and DDL scripts.
410   * </p>
411   */
412  public void generateOnStart(EbeanServer server) {
413
414    if (isGenerateOnStart()) {
415      if (platform == null) {
416        logger.warn("No platform set for migration DDL generation");
417      } else {
418        // generate the migration xml and platform specific DDL
419        DbMigration migration = new DbMigration(server);
420        migration.setPlatform(platform);
421        try {
422          migration.generateMigration();
423        } catch (Exception e) {
424          throw new RuntimeException("Error generating DB migration", e);
425        }
426      }
427    }
428  }
429
430  /**
431   * Return the migration version (typically FlywayDb compatible).
432   * <p>
433   * Example: 1.1.1_2
434   * <p>
435   * The version is expected to be the combination of the current pom version plus
436   * a 'feature' id. The combined version must be unique and ordered to work with
437   * FlywayDb so each developer sets a unique version so that the migration script
438   * generated is unique (typically just prior to being submitted as a merge request).
439   */
440  public String getVersion() {
441    String envVersion = readEnvironment("ddl.migration.version");
442    if (!isEmpty(envVersion)) {
443      return envVersion.trim();
444    }
445    return version;
446  }
447
448  /**
449   * Return the migration name which is short description text that can be appended to
450   * the migration version to become the ddl script file name.
451   * <p>
452   * So if the name is "a foo table" then the ddl script file could be:
453   * "1.1.1_2__a-foo-table.sql"
454   * </p>
455   * <p>
456   * When the DB migration relates to a git feature (merge request) then this description text
457   * is a short description of the feature.
458   * </p>
459   */
460  public String getName() {
461    String envName = readEnvironment("ddl.migration.name");
462    if (!isEmpty(envName)) {
463      return envName.trim();
464    }
465    return name;
466  }
467
468  /**
469   * Return the system or environment property.
470   */
471  protected String readEnvironment(String key) {
472
473    String val = System.getProperty(key);
474    if (val == null) {
475      val = System.getenv(key);
476    }
477    return val;
478  }
479
480  /**
481   * Return true if the string is null or empty.
482   */
483  protected boolean isEmpty(String val) {
484    return val == null || val.trim().isEmpty();
485  }
486
487  /**
488   * Create the MigrationRunner to run migrations if necessary.
489   */
490  public MigrationRunner createRunner(ClassLoader classLoader) {
491
492    MigrationConfig runnerConfig = new MigrationConfig();
493    runnerConfig.setMetaTable(metaTable);
494    runnerConfig.setApplySuffix(applySuffix);
495    runnerConfig.setMigrationPath(migrationPath);
496    runnerConfig.setRunPlaceholderMap(runPlaceholderMap);
497    runnerConfig.setRunPlaceholders(runPlaceholders);
498    runnerConfig.setDbUsername(getDbUsername());
499    runnerConfig.setDbPassword(getDbPassword());
500    runnerConfig.setClassLoader(classLoader);
501    return new MigrationRunner(runnerConfig);
502  }
503}