001package io.ebean.config;
002
003import io.ebean.EbeanVersion;
004import io.ebean.annotation.Platform;
005import io.ebean.migration.MigrationConfig;
006import io.ebean.migration.MigrationRunner;
007import io.ebean.util.StringHelper;
008import org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011import java.time.ZonedDateTime;
012import java.time.format.DateTimeFormatter;
013import java.util.Map;
014import java.util.Properties;
015
016/**
017 * Configuration for the DB migration processing.
018 */
019public class DbMigrationConfig {
020
021  protected static final Logger logger = LoggerFactory.getLogger(DbMigrationConfig.class);
022
023  protected MigrationConfig runnerConfig = new MigrationConfig();
024
025  /**
026   * The database platform to generate migration DDL for.
027   */
028  protected Platform platform;
029
030  /**
031   * Resource path for the migration xml and sql.
032   */
033  protected String migrationPath = "dbmigration";
034
035  protected String migrationInitPath = "dbinit";
036
037  /**
038   * Subdirectory the model xml files go into.
039   */
040  protected String modelPath = "model";
041
042  protected String applySuffix = ".sql";
043
044  /**
045   * Set this to "V" to be compatible with FlywayDB.
046   */
047  protected String applyPrefix = "";
048
049  protected String modelSuffix = ".model.xml";
050
051  /**
052   * For running migration the DB table that holds migration execution status.
053   */
054  protected String metaTable = "db_migration";
055
056  /**
057   * Flag set to true means to run any outstanding migrations on startup.
058   */
059  protected boolean runMigration;
060
061  /**
062   * Comma and equals delimited key/value placeholders to replace in DDL scripts.
063   */
064  protected String runPlaceholders;
065
066  /**
067   * Map of key/value placeholders to replace in DDL scripts.
068   */
069  protected Map<String, String> runPlaceholderMap;
070
071  /**
072   * DB schema used for the migration (and testing).
073   */
074  protected String dbSchema;
075
076  /**
077   * Set to true if we consider this the 'default schema' (Postgres schema that matches DB username)
078   */
079  protected boolean defaultDbSchema;
080
081  /**
082   * DB user used to run the DB migration.
083   */
084  protected String dbUsername;
085
086  /**
087   * DB password used to run the DB migration.
088   */
089  protected String dbPassword;
090
091  protected String patchInsertOn;
092
093  protected String patchResetChecksumOn;
094
095  /**
096   * Mode used to check non-null columns added via migration have a default value specified etc.
097   */
098  protected boolean strictMode = true;
099
100  /**
101   * Contains the DDL-header information.
102   */
103  protected String ddlHeader;
104
105  /**
106   * Return the DB platform to generate migration DDL for.
107   * <p>
108   * We typically need to explicitly specify this as migration can often be generated
109   * when running against H2.
110   */
111  public Platform getPlatform() {
112    return platform;
113  }
114
115  /**
116   * Set the DB platform to generate migration DDL for.
117   */
118  public void setPlatform(Platform platform) {
119    this.platform = platform;
120  }
121
122  /**
123   * Return the path for normal migrations or dbinit migrations.
124   *
125   * @param dbinitMigration When true return the path for dbinit migrations.
126   */
127  public String getMigrationPath(boolean dbinitMigration) {
128    return dbinitMigration ? migrationInitPath : migrationPath;
129  }
130
131  /**
132   * Return the resource path for db migrations.
133   */
134  public String getMigrationPath() {
135    return migrationPath;
136  }
137
138  /**
139   * Set the resource path for db migrations.
140   * <p>
141   * The default of "dbmigration" is reasonable in most cases. You may look to set this
142   * to be something like "dbmigration/myapp" where myapp gives it a unique resource path
143   * in the case there are multiple EbeanServer applications in the single classpath.
144   * </p>
145   */
146  public void setMigrationPath(String migrationPath) {
147    this.migrationPath = migrationPath;
148  }
149
150  /**
151   * Return the relative path for the model files (defaults to model).
152   */
153  public String getModelPath() {
154    return modelPath;
155  }
156
157  /**
158   * Set the relative path for the model files.
159   */
160  public void setModelPath(String modelPath) {
161    this.modelPath = modelPath;
162  }
163
164  /**
165   * Return the model suffix (defaults to model.xml)
166   */
167  public String getModelSuffix() {
168    return modelSuffix;
169  }
170
171  /**
172   * Set the model suffix.
173   */
174  public void setModelSuffix(String modelSuffix) {
175    this.modelSuffix = modelSuffix;
176  }
177
178  /**
179   * Return the apply script suffix (defaults to sql).
180   */
181  public String getApplySuffix() {
182    return applySuffix;
183  }
184
185  /**
186   * Set the apply script suffix (defaults to sql).
187   */
188  public void setApplySuffix(String applySuffix) {
189    this.applySuffix = applySuffix;
190  }
191
192  /**
193   * Return the apply prefix.
194   */
195  public String getApplyPrefix() {
196    return applyPrefix;
197  }
198
199  /**
200   * Set the apply prefix. This might be set to "V" for use with FlywayDB.
201   */
202  public void setApplyPrefix(String applyPrefix) {
203    this.applyPrefix = applyPrefix;
204  }
205
206  /**
207   * Return the table name that holds the migration run details
208   * (used by DB Migration runner only).
209   */
210  public String getMetaTable() {
211    return metaTable;
212  }
213
214  /**
215   * Set the table name that holds the migration run details
216   * (used by DB Migration runner only).
217   */
218  public void setMetaTable(String metaTable) {
219    this.metaTable = metaTable;
220  }
221
222  /**
223   * Return a comma and equals delimited placeholders that are substituted in SQL scripts when running migration
224   * (used by DB Migration runner only).
225   */
226  public String getRunPlaceholders() {
227    // environment properties take precedence
228    String placeholders = readEnvironment("ddl.migration.placeholders");
229    if (placeholders != null) {
230      return placeholders;
231    }
232    return runPlaceholders;
233  }
234
235  /**
236   * Set a comma and equals delimited placeholders that are substituted in SQL scripts when running migration
237   * (used by DB Migration runner only).
238   */
239  public void setRunPlaceholders(String runPlaceholders) {
240    this.runPlaceholders = runPlaceholders;
241  }
242
243  /**
244   * Return a map of placeholder values that are substituted in SQL scripts when running migration
245   * (used by DB Migration runner only).
246   */
247  public Map<String, String> getRunPlaceholderMap() {
248    return runPlaceholderMap;
249  }
250
251  /**
252   * Set a map of placeholder values that are substituted when running migration
253   * (used by DB Migration runner only).
254   */
255  public void setRunPlaceholderMap(Map<String, String> runPlaceholderMap) {
256    this.runPlaceholderMap = runPlaceholderMap;
257  }
258
259  /**
260   * Return true if the DB migration should be run on startup.
261   */
262  public boolean isRunMigration() {
263    // environment properties take precedence
264    String run = readEnvironment("ddl.migration.run");
265    if (run != null) {
266      return "true".equalsIgnoreCase(run.trim());
267    }
268    return runMigration;
269  }
270
271  /**
272   * Set to true to run the DB migration on startup.
273   */
274  public void setRunMigration(boolean runMigration) {
275    this.runMigration = runMigration;
276  }
277
278  /**
279   * Return the DB username to use for running DB migrations.
280   */
281  public String getDbUsername() {
282    // environment properties take precedence
283    String user = readEnvironment("ddl.migration.user");
284    if (user != null) {
285      return user;
286    }
287    return dbUsername;
288  }
289
290  /**
291   * Set the DB username to use for running DB migrations.
292   */
293  public void setDbUsername(String dbUsername) {
294    this.dbUsername = dbUsername;
295  }
296
297  /**
298   * Return the DB password to use for running DB migrations.
299   */
300  public String getDbPassword() {
301    String user = readEnvironment("ddl.migration.password");
302    if (user != null) {
303      return user;
304    }
305    return dbPassword;
306  }
307
308  /**
309   * Set the DB password to use for running DB migrations.
310   */
311  public void setDbPassword(String dbPassword) {
312    this.dbPassword = dbPassword;
313  }
314
315  /**
316   * Return the DB schema to use (for migration, testing etc).
317   */
318  public String getDbSchema() {
319    String schema = readEnvironment("ddl.migration.schema");
320    if (schema != null) {
321      return schema;
322    }
323    return dbSchema;
324  }
325
326  /**
327   * Set the Db schema to use.
328   */
329  public void setDbSchema(String dbSchema) {
330    this.dbSchema = dbSchema;
331  }
332
333  /**
334   * Set the Db schema if it hasn't already been defined.
335   */
336  public void setDefaultDbSchema(String dbSchema) {
337    this.defaultDbSchema = true;
338    this.dbSchema = dbSchema;
339  }
340
341  /**
342   * Return true if this is considered the default DB schema (Postgres schema matching DB username).
343   */
344  public boolean isDefaultDbSchema() {
345    return defaultDbSchema;
346  }
347
348  /**
349   * Return migration versions that should be added to history without running.
350   */
351  public String getPatchInsertOn() {
352    return patchInsertOn;
353  }
354
355  /**
356   * Set migration versions that should be added to history without running.
357   * <p>
358   * Value can be a string containing comma delimited list of version numbers.
359   * </p>
360   */
361  public void setPatchInsertOn(String patchInsertOn) {
362    this.patchInsertOn = patchInsertOn;
363  }
364
365  /**
366   * Return migration versions that should have their checksum reset and not run.
367   */
368  public String getPatchResetChecksumOn() {
369    return patchResetChecksumOn;
370  }
371
372  /**
373   * Returns a DDL header prepend for each DDL. E.g. for copyright headers
374   * You can use placeholders like ${version} or ${timestamp} in properties file.
375   */
376  public String getDdlHeader() {
377    if (ddlHeader != null && !ddlHeader.isEmpty()) {
378      ddlHeader = StringHelper.replaceString(ddlHeader, "${version}", EbeanVersion.getVersion());
379      ddlHeader = StringHelper.replaceString(ddlHeader, "${timestamp}", ZonedDateTime.now().format( DateTimeFormatter.ISO_INSTANT ));
380    }
381    return ddlHeader;
382  }
383
384  /**
385   * Set the header prepended to the DDL.
386   */
387  public void setDdlHeader(String ddlHeader) {
388    this.ddlHeader = ddlHeader;
389  }
390
391  /**
392   * Set migration versions that should have their checksum reset and not run.
393   * <p>
394   * Value can be a string containing comma delimited list of version numbers.
395   * </p>
396   */
397  public void setPatchResetChecksumOn(String patchResetChecksumOn) {
398    this.patchResetChecksumOn = patchResetChecksumOn;
399  }
400
401  /**
402   * Return true if strict mode is used which includes a check that non-null columns have a default value.
403   */
404  public boolean isStrictMode() {
405    String envValue = readEnvironment("ddl.migration.strictMode");
406    if (!isEmpty(envValue)) {
407      return Boolean.parseBoolean(envValue.trim());
408    }
409    return strictMode;
410  }
411
412  /**
413   * Set to false to turn off strict mode allowing non-null columns to not have a default value.
414   */
415  public void setStrictMode(boolean strictMode) {
416    this.strictMode = strictMode;
417  }
418
419  /**
420   * Return the underlying migration runner configuration allowing for more advanced settings.
421   */
422  public MigrationConfig getRunnerConfig() {
423    return runnerConfig;
424  }
425
426  /**
427   * Load the settings from the PropertiesWrapper.
428   */
429  public void loadSettings(PropertiesWrapper properties, String serverName) {
430
431    migrationPath = properties.get("migration.migrationPath", migrationPath);
432    migrationInitPath = properties.get("migration.migrationInitPath", migrationInitPath);
433    modelPath = properties.get("migration.modelPath", modelPath);
434    applyPrefix = properties.get("migration.applyPrefix", applyPrefix);
435    applySuffix = properties.get("migration.applySuffix", applySuffix);
436    modelSuffix = properties.get("migration.modelSuffix", modelSuffix);
437
438    platform = properties.getEnum(Platform.class, "migration.platform", platform);
439    patchInsertOn = properties.get("migration.patchInsertOn", patchInsertOn);
440    patchResetChecksumOn = properties.get("migration.patchResetChecksumOn", patchResetChecksumOn);
441
442    runMigration = properties.getBoolean("migration.run", runMigration);
443    metaTable = properties.get("migration.metaTable", metaTable);
444    runPlaceholders = properties.get("migration.placeholders", runPlaceholders);
445    dbSchema = properties.get("migration.dbSchema", dbSchema);
446
447    //Do not set user and pass from "datasource.db.username"
448    //There is a null test in MigrationRunner::getConnection to handle this
449    //String adminUser = properties.get("datasource." + serverName + ".username", dbUsername);
450    String adminUser = properties.get("datasource." + serverName + ".adminusername", dbUsername);
451    dbUsername = properties.get("migration.dbusername", adminUser);
452
453    //String adminPwd = properties.get("datasource." + serverName + ".password", dbPassword);
454    String adminPwd = properties.get("datasource." + serverName + ".adminpassword", dbPassword);
455    dbPassword = properties.get("migration.dbpassword", adminPwd);
456    ddlHeader = properties.get("ddl.header", ddlHeader);
457  }
458
459  /**
460   * Return the system or environment property.
461   */
462  protected String readEnvironment(String key) {
463
464    String val = System.getProperty(key);
465    if (val == null) {
466      val = System.getenv(key);
467    }
468    return val;
469  }
470
471  /**
472   * Return true if the string is null or empty.
473   */
474  protected boolean isEmpty(String val) {
475    return val == null || val.trim().isEmpty();
476  }
477
478  /**
479   * Create the MigrationRunner to run migrations if necessary.
480   */
481  public MigrationRunner createRunner(ClassLoader classLoader, Properties properties) {
482
483    runnerConfig.setMetaTable(metaTable);
484    runnerConfig.setApplySuffix(applySuffix);
485    runnerConfig.setMigrationPath(migrationPath);
486    runnerConfig.setMigrationInitPath(migrationInitPath);
487    runnerConfig.setRunPlaceholderMap(runPlaceholderMap);
488    runnerConfig.setRunPlaceholders(runPlaceholders);
489    runnerConfig.setDbUsername(getDbUsername());
490    runnerConfig.setDbPassword(getDbPassword());
491    runnerConfig.setDbSchema(getDbSchema());
492    if (defaultDbSchema) {
493      runnerConfig.setSetCurrentSchema(false);
494    }
495    runnerConfig.setClassLoader(classLoader);
496    if (patchInsertOn != null) {
497      runnerConfig.setPatchInsertOn(patchInsertOn);
498    }
499    if (patchResetChecksumOn != null) {
500      runnerConfig.setPatchResetChecksumOn(patchResetChecksumOn);
501    }
502    if (properties != null) {
503      runnerConfig.load(properties);
504    }
505    return new MigrationRunner(runnerConfig);
506  }
507}