001package io.ebeaninternal.dbmigration;
002
003import io.ebean.DB;
004import io.ebean.Database;
005import io.ebean.EbeanServer;
006import io.ebean.annotation.Platform;
007import io.ebean.config.DatabaseConfig;
008import io.ebean.config.DbConstraintNaming;
009import io.ebean.config.PlatformConfig;
010import io.ebean.config.PropertiesWrapper;
011import io.ebean.config.dbplatform.DatabasePlatform;
012import io.ebean.config.dbplatform.clickhouse.ClickHousePlatform;
013import io.ebean.config.dbplatform.cockroach.CockroachPlatform;
014import io.ebean.config.dbplatform.db2.DB2Platform;
015import io.ebean.config.dbplatform.h2.H2Platform;
016import io.ebean.config.dbplatform.hana.HanaPlatform;
017import io.ebean.config.dbplatform.hsqldb.HsqldbPlatform;
018import io.ebean.config.dbplatform.mariadb.MariaDbPlatform;
019import io.ebean.config.dbplatform.mysql.MySql55Platform;
020import io.ebean.config.dbplatform.mysql.MySqlPlatform;
021import io.ebean.config.dbplatform.nuodb.NuoDbPlatform;
022import io.ebean.config.dbplatform.oracle.Oracle11Platform;
023import io.ebean.config.dbplatform.oracle.OraclePlatform;
024import io.ebean.config.dbplatform.postgres.Postgres9Platform;
025import io.ebean.config.dbplatform.postgres.PostgresPlatform;
026import io.ebean.config.dbplatform.sqlanywhere.SqlAnywherePlatform;
027import io.ebean.config.dbplatform.sqlite.SQLitePlatform;
028import io.ebean.config.dbplatform.sqlserver.SqlServer16Platform;
029import io.ebean.config.dbplatform.sqlserver.SqlServer17Platform;
030import io.ebean.dbmigration.DbMigration;
031import io.ebeaninternal.api.DbOffline;
032import io.ebeaninternal.api.SpiEbeanServer;
033import io.ebeaninternal.dbmigration.ddlgeneration.DdlOptions;
034import io.ebeaninternal.dbmigration.ddlgeneration.DdlWrite;
035import io.ebeaninternal.dbmigration.migration.Migration;
036import io.ebeaninternal.dbmigration.migrationreader.MigrationXmlWriter;
037import io.ebeaninternal.dbmigration.model.CurrentModel;
038import io.ebeaninternal.dbmigration.model.MConfiguration;
039import io.ebeaninternal.dbmigration.model.MigrationModel;
040import io.ebeaninternal.dbmigration.model.ModelContainer;
041import io.ebeaninternal.dbmigration.model.ModelDiff;
042import io.ebeaninternal.dbmigration.model.PlatformDdlWriter;
043import io.ebeaninternal.extraddl.model.DdlScript;
044import io.ebeaninternal.extraddl.model.ExtraDdl;
045import io.ebeaninternal.extraddl.model.ExtraDdlXmlReader;
046import org.slf4j.Logger;
047import org.slf4j.LoggerFactory;
048
049import java.io.File;
050import java.io.FileWriter;
051import java.io.IOException;
052import java.util.ArrayList;
053import java.util.List;
054import java.util.Properties;
055
056import static io.ebeaninternal.api.PlatformMatch.matchPlatform;
057
058/**
059 * Generates DB Migration xml and sql scripts.
060 * <p>
061 * Reads the prior migrations and compares with the current model of the EbeanServer
062 * and generates a migration 'diff' in the form of xml document with the logical schema
063 * changes and a series of sql scripts to apply, rollback the applied changes if necessary
064 * and drop objects (drop tables, drop columns).
065 * </p>
066 * <p>
067 * This does not run the migration or ddl scripts but just generates them.
068 * </p>
069 * <pre>{@code
070 *
071 *       DbMigration migration = DbMigration.create();
072 *       migration.setPathToResources("src/main/resources");
073 *       migration.setPlatform(Platform.POSTGRES);
074 *
075 *       migration.generateMigration();
076 *
077 * }</pre>
078 */
079public class DefaultDbMigration implements DbMigration {
080
081  protected static final Logger logger = LoggerFactory.getLogger("io.ebean.GenerateMigration");
082
083  private static final String initialVersion = "1.0";
084
085  private static final String GENERATED_COMMENT = "THIS IS A GENERATED FILE - DO NOT MODIFY";
086
087  private boolean logToSystemOut = true;
088
089  /**
090   * Set to true if DefaultDbMigration run with online EbeanServer instance.
091   */
092  protected final boolean online;
093
094  protected SpiEbeanServer server;
095
096  protected String pathToResources = "src/main/resources";
097
098  protected String migrationPath = "dbmigration";
099  protected String migrationInitPath = "dbinit";
100  protected String modelPath = "model";
101  protected String modelSuffix = ".model.xml";
102
103  protected DatabasePlatform databasePlatform;
104
105  private boolean vanillaPlatform;
106
107  protected List<Pair> platforms = new ArrayList<>();
108
109  protected DatabaseConfig databaseConfig;
110
111  protected DbConstraintNaming constraintNaming;
112
113  protected Boolean strictMode;
114  protected Boolean includeGeneratedFileComment;
115  protected String header;
116  protected String applyPrefix = "";
117  protected String version;
118  protected String name;
119  protected String generatePendingDrop;
120  private boolean addForeignKeySkipCheck;
121  private int lockTimeoutSeconds;
122
123  protected boolean includeBuiltInPartitioning = true;
124
125  /**
126   * Create for offline migration generation.
127   */
128  public DefaultDbMigration() {
129    this.online = false;
130  }
131
132  /**
133   * Create using online EbeanServer.
134   */
135  public DefaultDbMigration(EbeanServer server) {
136    this.online = true;
137    setServer(server);
138  }
139
140  /**
141   * Set the path from the current working directory to the application resources.
142   * <p>
143   * This defaults to maven style 'src/main/resources'.
144   */
145  @Override
146  public void setPathToResources(String pathToResources) {
147    this.pathToResources = pathToResources;
148  }
149
150  @Override
151  public void setMigrationPath(String migrationPath) {
152    this.migrationPath = migrationPath;
153  }
154
155  /**
156   * Set the server to use to determine the current model.
157   * Typically this is not called explicitly.
158   */
159  @Override
160  public void setServer(Database database) {
161    this.server = (SpiEbeanServer) database;
162    setServerConfig(server.getServerConfig());
163  }
164
165  /**
166   * Set the DatabaseConfig to use. Typically this is not called explicitly.
167   */
168  @Override
169  public void setServerConfig(DatabaseConfig config) {
170    if (this.databaseConfig == null) {
171      this.databaseConfig = config;
172    }
173    if (constraintNaming == null) {
174      this.constraintNaming = databaseConfig.getConstraintNaming();
175    }
176
177    Properties properties = config.getProperties();
178    if (properties != null) {
179      PropertiesWrapper props = new PropertiesWrapper("ebean", config.getName(), properties, null);
180      migrationPath = props.get("migration.migrationPath", migrationPath);
181      migrationInitPath = props.get("migration.migrationInitPath", migrationInitPath);
182      pathToResources = props.get("migration.pathToResources", pathToResources);
183    }
184  }
185
186  @Override
187  public void setStrictMode(boolean strictMode) {
188    this.strictMode = strictMode;
189  }
190
191  @Override
192  public void setApplyPrefix(String applyPrefix) {
193    this.applyPrefix = applyPrefix;
194  }
195
196  @Override
197  public void setVersion(String version) {
198    this.version = version;
199  }
200
201  @Override
202  public void setName(String name) {
203    this.name = name;
204  }
205
206  @Override
207  public void setAddForeignKeySkipCheck(boolean addForeignKeySkipCheck) {
208    this.addForeignKeySkipCheck = addForeignKeySkipCheck;
209  }
210
211  @Override
212  public void setLockTimeout(int seconds) {
213    this.lockTimeoutSeconds = seconds;
214  }
215
216  @Override
217  public void setGeneratePendingDrop(String generatePendingDrop) {
218    this.generatePendingDrop = generatePendingDrop;
219  }
220
221  @Override
222  public void setIncludeGeneratedFileComment(boolean includeGeneratedFileComment) {
223    this.includeGeneratedFileComment = includeGeneratedFileComment;
224  }
225
226  @Override
227  public void setIncludeBuiltInPartitioning(boolean includeBuiltInPartitioning) {
228    this.includeBuiltInPartitioning = includeBuiltInPartitioning;
229  }
230
231  @Override
232  public void setHeader(String header) {
233    this.header = header;
234  }
235
236  /**
237   * Set the specific platform to generate DDL for.
238   * <p>
239   * If not set this defaults to the platform of the default server.
240   * </p>
241   */
242  @Override
243  public void setPlatform(Platform platform) {
244    vanillaPlatform = true;
245    setPlatform(getPlatform(platform));
246  }
247
248  /**
249   * Set the specific platform to generate DDL for.
250   * <p>
251   * If not set this defaults to the platform of the default server.
252   * </p>
253   */
254  @Override
255  public void setPlatform(DatabasePlatform databasePlatform) {
256    this.databasePlatform = databasePlatform;
257    if (!online) {
258      DbOffline.setPlatform(databasePlatform.getPlatform());
259    }
260  }
261
262  /**
263   * Add an additional platform to write the migration DDL.
264   * <p>
265   * Use this when you want to generate sql scripts for multiple database platforms
266   * from the migration (e.g. generate migration sql for MySql, Postgres and Oracle).
267   * </p>
268   */
269  @Override
270  public void addPlatform(Platform platform, String prefix) {
271    platforms.add(new Pair(getPlatform(platform), prefix));
272  }
273
274  @Override
275  public void addDatabasePlatform(DatabasePlatform databasePlatform, String prefix) {
276    platforms.add(new Pair(databasePlatform, prefix));
277  }
278
279  /**
280   * Generate the next migration xml file and associated apply and rollback sql scripts.
281   * <p>
282   * This does not run the migration or ddl scripts but just generates them.
283   * </p>
284   * <h3>Example: Run for a single specific platform</h3>
285   * <pre>{@code
286   *
287   *       DbMigration migration = DbMigration.create();
288   *       migration.setPathToResources("src/main/resources");
289   *       migration.setPlatform(DbPlatformName.ORACLE);
290   *
291   *       migration.generateMigration();
292   *
293   * }</pre>
294   * <p>
295   * <h3>Example: Run migration generating DDL for multiple platforms</h3>
296   * <pre>{@code
297   *
298   *       DbMigration migration = DbMigration.create();
299   *       migration.setPathToResources("src/main/resources");
300   *
301   *       migration.addPlatform(DbPlatformName.POSTGRES, "pg");
302   *       migration.addPlatform(DbPlatformName.MYSQL, "mysql");
303   *       migration.addPlatform(DbPlatformName.ORACLE, "mysql");
304   *
305   *       migration.generateMigration();
306   *
307   * }</pre>
308   *
309   * @return the generated migration or null
310   */
311  @Override
312  public String generateMigration() throws IOException {
313    return generateMigrationFor(false);
314  }
315
316  @Override
317  public String generateInitMigration() throws IOException {
318    return generateMigrationFor(true);
319  }
320
321  private String generateMigrationFor(boolean dbinitMigration) throws IOException {
322
323    // use this flag to stop other plugins like full DDL generation
324    if (!online) {
325      DbOffline.setGenerateMigration();
326      if (databasePlatform == null && !platforms.isEmpty()) {
327        // for multiple platform generation the first platform
328        // is used to generate the "logical" model diff
329        setPlatform(platforms.get(0).platform);
330      }
331    }
332    setDefaults();
333    if (!platforms.isEmpty()) {
334      configurePlatforms();
335    }
336    try {
337      Request request = createRequest(dbinitMigration);
338      if (!dbinitMigration) {
339        // repeatable migrations
340        if (platforms.isEmpty()) {
341          generateExtraDdl(request.migrationDir, databasePlatform, request.isTablePartitioning());
342        } else {
343          for (Pair pair : platforms) {
344            PlatformDdlWriter platformWriter = createDdlWriter(pair.platform);
345            File subPath = platformWriter.subPath(request.migrationDir, pair.prefix);
346            generateExtraDdl(subPath, pair.platform, request.isTablePartitioning());
347          }
348        }
349      }
350
351      String pendingVersion = generatePendingDrop();
352      if (pendingVersion != null) {
353        return generatePendingDrop(request, pendingVersion);
354      } else {
355        return generateDiff(request);
356      }
357
358    } catch (UnknownResourcePathException e) {
359      logError("ERROR - " + e.getMessage());
360      logError("Check the working directory or change dbMigration.setPathToResources() value?");
361      return null;
362
363    } finally {
364      if (!online) {
365        DbOffline.reset();
366      }
367    }
368  }
369
370  /**
371   * Return the versions containing pending drops.
372   */
373  @Override
374  public List<String> getPendingDrops() {
375    if (!online) {
376      DbOffline.setGenerateMigration();
377    }
378    setDefaults();
379    try {
380      return createRequest(false).getPendingDrops();
381    } finally {
382      if (!online) {
383        DbOffline.reset();
384      }
385    }
386  }
387
388  /**
389   * Load the configuration for each of the target platforms.
390   */
391  private void configurePlatforms() {
392    for (Pair pair : platforms) {
393      PlatformConfig config = databaseConfig.newPlatformConfig("dbmigration.platform", pair.prefix);
394      pair.platform.configure(config);
395    }
396  }
397
398  /**
399   * Generate "repeatable" migration scripts.
400   * <p>
401   * These take scrips from extra-ddl.xml (typically views) and outputs "repeatable"
402   * migration scripts (starting with "R__") to be run by FlywayDb or Ebean's own
403   * migration runner.
404   * </p>
405   */
406  private void generateExtraDdl(File migrationDir, DatabasePlatform dbPlatform, boolean tablePartitioning) throws IOException {
407
408    if (dbPlatform != null) {
409      if (tablePartitioning && includeBuiltInPartitioning) {
410        generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.readBuiltinTablePartitioning());
411      }
412      generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.readBuiltin());
413      generateExtraDdlFor(migrationDir, dbPlatform, ExtraDdlXmlReader.read());
414    }
415  }
416
417  private void generateExtraDdlFor(File migrationDir, DatabasePlatform dbPlatform, ExtraDdl extraDdl) throws IOException {
418    if (extraDdl != null) {
419      List<DdlScript> ddlScript = extraDdl.getDdlScript();
420      for (DdlScript script : ddlScript) {
421        if (!script.isDrop() && matchPlatform(dbPlatform.getPlatform(), script.getPlatforms())) {
422          writeExtraDdl(migrationDir, script);
423        }
424      }
425    }
426  }
427
428  /**
429   * Write (or override) the "repeatable" migration script.
430   */
431  private void writeExtraDdl(File migrationDir, DdlScript script) throws IOException {
432
433    String fullName = repeatableMigrationName(script.isInit(), script.getName());
434    logger.debug("writing repeatable script {}", fullName);
435
436    File file = new File(migrationDir, fullName);
437    try (FileWriter writer = new FileWriter(file)) {
438      writer.write(script.getValue());
439      writer.flush();
440    }
441  }
442
443  @Override
444  public void setLogToSystemOut(boolean logToSystemOut) {
445    this.logToSystemOut = logToSystemOut;
446  }
447
448  private void logError(String message) {
449    if (logToSystemOut) {
450      System.out.println("DbMigration> " + message);
451    } else {
452      logger.error(message);
453    }
454  }
455
456  private void logInfo(String message, Object value) {
457    if (value != null) {
458      message = String.format(message, value);
459    }
460    if (logToSystemOut) {
461      System.out.println("DbMigration> " + message);
462    } else {
463      logger.info(message);
464    }
465  }
466
467  private String repeatableMigrationName(boolean init, String scriptName) {
468    StringBuilder sb = new StringBuilder();
469    if (init) {
470      sb.append("I__");
471    } else {
472      sb.append("R__");
473    }
474    sb.append(scriptName.replace(' ', '_'));
475    sb.append(".sql");
476    return sb.toString();
477  }
478
479  /**
480   * Generate the diff migration.
481   */
482  private String generateDiff(Request request) throws IOException {
483
484    List<String> pendingDrops = request.getPendingDrops();
485    if (!pendingDrops.isEmpty()) {
486      logInfo("Pending un-applied drops in versions %s", pendingDrops);
487    }
488
489    Migration migration = request.createDiffMigration();
490    if (migration == null) {
491      logInfo("no changes detected - no migration written", null);
492      return null;
493    } else {
494      // there were actually changes to write
495      return generateMigration(request, migration, null);
496    }
497  }
498
499  /**
500   * Generate the migration based on the pendingDrops from a prior version.
501   */
502  private String generatePendingDrop(Request request, String pendingVersion) throws IOException {
503
504    Migration migration = request.migrationForPendingDrop(pendingVersion);
505
506    String version = generateMigration(request, migration, pendingVersion);
507
508    List<String> pendingDrops = request.getPendingDrops();
509    if (!pendingDrops.isEmpty()) {
510      logInfo("... remaining pending un-applied drops in versions %s", pendingDrops);
511    }
512    return version;
513  }
514
515  private Request createRequest(boolean dbinitMigration) {
516    return new Request(dbinitMigration);
517  }
518
519  private class Request {
520
521    final boolean dbinitMigration;
522    final File migrationDir;
523    final File modelDir;
524    final CurrentModel currentModel;
525    final ModelContainer migrated;
526    final ModelContainer current;
527
528    private Request(boolean dbinitMigration) {
529      this.dbinitMigration = dbinitMigration;
530      this.currentModel = new CurrentModel(server, constraintNaming);
531      this.current = currentModel.read();
532      this.migrationDir = getMigrationDirectory(dbinitMigration);
533      if (dbinitMigration) {
534        this.modelDir = null;
535        this.migrated = new ModelContainer();
536      } else {
537        this.modelDir = getModelDirectory(migrationDir);
538        MigrationModel migrationModel = new MigrationModel(modelDir, modelSuffix);
539        this.migrated = migrationModel.read(dbinitMigration);
540      }
541    }
542
543    boolean isTablePartitioning() {
544      return current.isTablePartitioning();
545    }
546
547    /**
548     * Return the next migration version (based on existing migration versions).
549     */
550    String nextVersion() {
551      // always read the next version using the main migration directory (not dbinit)
552      File migDirectory = getMigrationDirectory(false);
553      File modelDir = getModelDirectory(migDirectory);
554      return LastMigration.nextVersion(migDirectory, modelDir, dbinitMigration);
555    }
556
557    /**
558     * Return the migration for the pending drops for a given version.
559     */
560    Migration migrationForPendingDrop(String pendingVersion) {
561
562      Migration migration = migrated.migrationForPendingDrop(pendingVersion);
563
564      // register any remaining pending drops
565      migrated.registerPendingHistoryDropColumns(current);
566      return migration;
567    }
568
569    /**
570     * Return the list of versions that have pending un-applied drops.
571     */
572    public List<String> getPendingDrops() {
573      return migrated.getPendingDrops();
574    }
575
576    /**
577     * Create and return the diff of the current model to the migration model.
578     */
579    Migration createDiffMigration() {
580      ModelDiff diff = new ModelDiff(migrated);
581      diff.compareTo(current);
582      return diff.isEmpty() ? null : diff.getMigration();
583    }
584  }
585
586  private String generateMigration(Request request, Migration dbMigration, String dropsFor) throws IOException {
587
588    String fullVersion = getFullVersion(request.nextVersion(), dropsFor);
589
590    logInfo("generating migration:%s", fullVersion);
591    if (!request.dbinitMigration && !writeMigrationXml(dbMigration, request.modelDir, fullVersion)) {
592      logError("migration already exists, not generating DDL");
593      return null;
594    } else {
595      if (!platforms.isEmpty()) {
596        writeExtraPlatformDdl(fullVersion, request.currentModel, dbMigration, request.migrationDir);
597
598      } else if (databasePlatform != null) {
599        // writer needs the current model to provide table/column details for
600        // history ddl generation (triggers, history tables etc)
601        DdlOptions options = new DdlOptions(addForeignKeySkipCheck);
602        DdlWrite write = new DdlWrite(new MConfiguration(), request.current, options);
603        PlatformDdlWriter writer = createDdlWriter(databasePlatform);
604        writer.processMigration(dbMigration, write, request.migrationDir, fullVersion);
605      }
606      return fullVersion;
607    }
608  }
609
610  /**
611   * Return true if the next pending drop changeSet should be generated as the next migration.
612   */
613  private String generatePendingDrop() {
614    String nextDrop = System.getProperty("ddl.migration.pendingDropsFor");
615    if (nextDrop != null) {
616      return nextDrop;
617    }
618    return generatePendingDrop;
619  }
620
621  /**
622   * Return the full version for the migration being generated.
623   * <p>
624   * The full version can contain a comment suffix after a "__" double underscore.
625   */
626  private String getFullVersion(String nextVersion, String dropsFor) {
627
628    String version = getVersion();
629    if (version == null) {
630      version = (nextVersion != null) ? nextVersion : initialVersion;
631    }
632
633    String fullVersion = applyPrefix + version;
634    String name = getName();
635    if (name != null) {
636      fullVersion += "__" + toUnderScore(name);
637
638    } else if (dropsFor != null) {
639      fullVersion += "__" + toUnderScore("dropsFor_" + trimDropsFor(dropsFor));
640
641    } else if (version.equals(initialVersion)) {
642      fullVersion += "__initial";
643    }
644    return fullVersion;
645  }
646
647  String trimDropsFor(String dropsFor) {
648    if (dropsFor.startsWith("V") || dropsFor.startsWith("v")) {
649      dropsFor = dropsFor.substring(1);
650    }
651    int commentStart = dropsFor.indexOf("__");
652    if (commentStart > -1) {
653      // trim off the trailing comment
654      dropsFor = dropsFor.substring(0, commentStart);
655    }
656    return dropsFor;
657  }
658
659  /**
660   * Replace spaces with underscores.
661   */
662  private String toUnderScore(String name) {
663    return name.replace(' ', '_');
664  }
665
666  /**
667   * Write any extra platform ddl.
668   */
669  private void writeExtraPlatformDdl(String fullVersion, CurrentModel currentModel, Migration dbMigration, File writePath) throws IOException {
670    DdlOptions options = new DdlOptions(addForeignKeySkipCheck);
671    for (Pair pair : platforms) {
672      DdlWrite platformBuffer = new DdlWrite(new MConfiguration(), currentModel.read(), options);
673      PlatformDdlWriter platformWriter = createDdlWriter(pair.platform);
674      File subPath = platformWriter.subPath(writePath, pair.prefix);
675      platformWriter.processMigration(dbMigration, platformBuffer, subPath, fullVersion);
676    }
677  }
678
679  private PlatformDdlWriter createDdlWriter(DatabasePlatform platform) {
680    return new PlatformDdlWriter(platform, databaseConfig, lockTimeoutSeconds);
681  }
682
683  /**
684   * Write the migration xml.
685   */
686  private boolean writeMigrationXml(Migration dbMigration, File resourcePath, String fullVersion) {
687    String modelFile = fullVersion + modelSuffix;
688    File file = new File(resourcePath, modelFile);
689    if (file.exists()) {
690      return false;
691    }
692    String comment = Boolean.TRUE.equals(includeGeneratedFileComment) ? GENERATED_COMMENT : null;
693    MigrationXmlWriter xmlWriter = new MigrationXmlWriter(comment);
694    xmlWriter.write(dbMigration, file);
695    return true;
696  }
697
698  /**
699   * Set default server and platform if necessary.
700   */
701  private void setDefaults() {
702    if (server == null) {
703      setServer(DB.getDefault());
704    }
705    if (vanillaPlatform || databasePlatform == null) {
706      // not explicitly set so use the platform of the server
707      databasePlatform = server.getDatabasePlatform();
708    }
709    if (databaseConfig != null) {
710      if (strictMode != null) {
711        databaseConfig.setDdlStrictMode(strictMode);
712      }
713      if (header != null) {
714        databaseConfig.setDdlHeader(header);
715      }
716    }
717  }
718
719  /**
720   * Return the migration version (typically FlywayDb compatible).
721   * <p>
722   * Example: 1.1.1_2
723   * <p>
724   * The version is expected to be the combination of the current pom version plus
725   * a 'feature' id. The combined version must be unique and ordered to work with
726   * FlywayDb so each developer sets a unique version so that the migration script
727   * generated is unique (typically just prior to being submitted as a merge request).
728   */
729  private String getVersion() {
730    String envVersion = readEnvironment("ddl.migration.version");
731    if (!isEmpty(envVersion)) {
732      return envVersion.trim();
733    }
734    return version;
735  }
736
737  /**
738   * Return the migration name which is short description text that can be appended to
739   * the migration version to become the ddl script file name.
740   * <p>
741   * So if the name is "a foo table" then the ddl script file could be:
742   * "1.1.1_2__a-foo-table.sql"
743   * </p>
744   * <p>
745   * When the DB migration relates to a git feature (merge request) then this description text
746   * is a short description of the feature.
747   * </p>
748   */
749  private String getName() {
750    String envName = readEnvironment("ddl.migration.name");
751    if (!isEmpty(envName)) {
752      return envName.trim();
753    }
754    return name;
755  }
756
757  /**
758   * Return true if the string is null or empty.
759   */
760  private boolean isEmpty(String val) {
761    return val == null || val.trim().isEmpty();
762  }
763
764  /**
765   * Return the system or environment property.
766   */
767  private String readEnvironment(String key) {
768    String val = System.getProperty(key);
769    if (val == null) {
770      val = System.getenv(key);
771    }
772    return val;
773  }
774
775  /**
776   * Return the main migration directory.
777   */
778  File getMigrationDirectory() {
779    return getMigrationDirectory(false);
780  }
781
782  /**
783   * Return the file path to write the xml and sql to.
784   */
785  File getMigrationDirectory(boolean dbinitMigration) {
786
787    // path to src/main/resources in typical maven project
788    File resourceRootDir = new File(pathToResources);
789    if (!resourceRootDir.exists()) {
790      String msg = String.format("Error - path to resources %s does not exist. Absolute path is %s", pathToResources, resourceRootDir.getAbsolutePath());
791      throw new UnknownResourcePathException(msg);
792    }
793    String resourcePath = getMigrationPath(dbinitMigration);
794
795    // expect to be a path to something like - src/main/resources/dbmigration/model
796    File path = new File(resourceRootDir, resourcePath);
797    if (!path.exists()) {
798      if (!path.mkdirs()) {
799        logInfo("Warning - Unable to ensure migration directory exists at %s", path.getAbsolutePath());
800      }
801    }
802    return path;
803  }
804
805  private String getMigrationPath(boolean dbinitMigration) {
806    return dbinitMigration ? migrationInitPath : migrationPath;
807  }
808
809  /**
810   * Return the model directory (relative to the migration directory).
811   */
812  private File getModelDirectory(File migrationDirectory) {
813    if (modelPath == null || modelPath.isEmpty()) {
814      return migrationDirectory;
815    }
816    File modelDir = new File(migrationDirectory, modelPath);
817    if (!modelDir.exists() && !modelDir.mkdirs()) {
818      logInfo("Warning - Unable to ensure migration model directory exists at %s", modelDir.getAbsolutePath());
819    }
820    return modelDir;
821  }
822
823  /**
824   * Return the DatabasePlatform given the platform key.
825   */
826  protected DatabasePlatform getPlatform(Platform platform) {
827    switch (platform) {
828      case H2:
829        return new H2Platform();
830      case HSQLDB:
831        return new HsqldbPlatform();
832      case POSTGRES9:
833        return new Postgres9Platform();
834      case POSTGRES:
835        return new PostgresPlatform();
836      case MARIADB:
837        return new MariaDbPlatform();
838      case MYSQL55:
839        return new MySql55Platform();
840      case MYSQL:
841        return new MySqlPlatform();
842      case ORACLE:
843        return new OraclePlatform();
844      case ORACLE11:
845        return new Oracle11Platform();
846      case SQLANYWHERE:
847        return new SqlAnywherePlatform();
848      case SQLSERVER16:
849        return new SqlServer16Platform();
850      case SQLSERVER17:
851        return new SqlServer17Platform();
852      case SQLSERVER:
853        throw new IllegalArgumentException("Please choose the more specific SQLSERVER16 or SQLSERVER17 platform. Refer to issue #1340 for details");
854      case DB2:
855        return new DB2Platform();
856      case SQLITE:
857        return new SQLitePlatform();
858      case HANA:
859        return new HanaPlatform();
860      case NUODB:
861        return new NuoDbPlatform();
862      case COCKROACH:
863        return new CockroachPlatform();
864      case CLICKHOUSE:
865        return new ClickHousePlatform();
866
867      case GENERIC:
868        return new DatabasePlatform();
869
870      default:
871        throw new IllegalArgumentException("Platform missing? " + platform);
872    }
873  }
874
875  /**
876   * Holds a platform and prefix. Used to generate multiple platform specific DDL
877   * for a single migration.
878   */
879  static class Pair {
880
881    /**
882     * The platform to generate the DDL for.
883     */
884    final DatabasePlatform platform;
885
886    /**
887     * A prefix included into the file/resource names indicating the platform.
888     */
889    final String prefix;
890
891    Pair(DatabasePlatform platform, String prefix) {
892      this.platform = platform;
893      this.prefix = prefix;
894    }
895  }
896
897}