001package io.ebean.migration;
002
003import io.ebean.migration.runner.LocalMigrationResource;
004import io.ebean.migration.runner.LocalMigrationResources;
005import io.ebean.migration.runner.MigrationPlatform;
006import io.ebean.migration.runner.MigrationSchema;
007import io.ebean.migration.runner.MigrationTable;
008import io.ebean.migration.util.JdbcClose;
009import org.slf4j.Logger;
010import org.slf4j.LoggerFactory;
011
012import javax.sql.DataSource;
013import java.sql.Connection;
014import java.sql.SQLException;
015import java.util.List;
016
017/**
018 * Runs the DB migration typically on application start.
019 */
020public class MigrationRunner {
021
022  private static final Logger logger = LoggerFactory.getLogger(MigrationRunner.class);
023
024  private final MigrationConfig migrationConfig;
025
026  private List<LocalMigrationResource> checkMigrations;
027
028  public MigrationRunner(MigrationConfig migrationConfig) {
029    this.migrationConfig = migrationConfig;
030  }
031
032  /**
033   * Return the migrations that would be applied if the migration is run.
034   */
035  public List<LocalMigrationResource> checkState() {
036    run(migrationConfig.createConnection(), true);
037    return checkMigrations;
038  }
039
040  /**
041   * Return the migrations that would be applied if the migration is run.
042   */
043  public List<LocalMigrationResource> checkState(DataSource dataSource) {
044    run(getConnection(dataSource), true);
045    return checkMigrations;
046  }
047
048  /**
049   * Return the migrations that would be applied if the migration is run.
050   */
051  public List<LocalMigrationResource> checkState(Connection connection) {
052    run(connection, true);
053    return checkMigrations;
054  }
055
056  /**
057   * Run by creating a DB connection from driver, url, username defined in MigrationConfig.
058   */
059  public void run() {
060    run(migrationConfig.createConnection());
061  }
062
063  /**
064   * Run using the connection from the DataSource.
065   */
066  public void run(DataSource dataSource) {
067    run(getConnection(dataSource));
068  }
069
070  /**
071   * Run the migrations if there are any that need running.
072   */
073  public void run(Connection connection) {
074    run(connection, false);
075  }
076
077  private Connection getConnection(DataSource dataSource) {
078    String username = migrationConfig.getDbUsername();
079    try {
080      if (username == null) {
081        return dataSource.getConnection();
082      }
083      logger.debug("using db user [{}] to run migrations ...", username);
084      return dataSource.getConnection(username, migrationConfig.getDbPassword());
085    } catch (SQLException e) {
086      String msgSuffix = (username == null) ? "" : " using user [" + username + "]";
087      throw new IllegalArgumentException("Error trying to connect to database for DB Migration" + msgSuffix, e);
088    }
089  }
090
091  /**
092   * Run the migrations if there are any that need running.
093   */
094  private void run(Connection connection, boolean checkStateMode) {
095
096    LocalMigrationResources resources = new LocalMigrationResources(migrationConfig);
097    if (!resources.readResources()) {
098      logger.debug("no migrations to check");
099      return;
100    }
101
102    try {
103      connection.setAutoCommit(false);
104      MigrationPlatform platform = derivePlatformName(migrationConfig, connection);
105
106      new MigrationSchema(migrationConfig, connection).createAndSetIfNeeded();
107
108      MigrationTable table = new MigrationTable(migrationConfig, connection, checkStateMode, platform);
109      table.createIfNeededAndLock();
110
111      runMigrations(resources, table, checkStateMode);
112      connection.commit();
113
114      table.runNonTransactional();
115
116    } catch (MigrationException e) {
117      JdbcClose.rollback(connection);
118      throw e;
119
120    } catch (Exception e) {
121      JdbcClose.rollback(connection);
122      throw new RuntimeException(e);
123
124    } finally {
125      JdbcClose.close(connection);
126    }
127  }
128
129  /**
130   * Run all the migrations as needed.
131   */
132  private void runMigrations(LocalMigrationResources resources, MigrationTable table, boolean checkStateMode) throws SQLException {
133
134    // get the migrations in version order
135    List<LocalMigrationResource> localVersions = resources.getVersions();
136
137    if (table.isEmpty()) {
138      LocalMigrationResource initVersion = getInitVersion();
139      if (initVersion != null) {
140        // run using a dbinit script
141        logger.info("dbinit migration version:{}  local migrations:{}  checkState:{}", initVersion, localVersions.size(), checkStateMode);
142        checkMigrations = table.runInit(initVersion, localVersions);
143        return;
144      }
145    }
146
147    logger.info("local migrations:{}  existing migrations:{}  checkState:{}", localVersions.size(), table.size(), checkStateMode);
148    checkMigrations = table.runAll(localVersions);
149  }
150
151  /**
152   * Return the last init migration.
153   */
154  private LocalMigrationResource getInitVersion() {
155    LocalMigrationResources initResources = new LocalMigrationResources(migrationConfig);
156    if (initResources.readInitResources()) {
157      List<LocalMigrationResource> initVersions = initResources.getVersions();
158      if (!initVersions.isEmpty()) {
159        return initVersions.get(initVersions.size() - 1);
160      }
161    }
162    return null;
163  }
164
165  /**
166   * Return the platform deriving from connection if required.
167   */
168  private MigrationPlatform derivePlatformName(MigrationConfig migrationConfig, Connection connection) {
169
170    String platformName = migrationConfig.getPlatformName();
171    if (platformName == null) {
172      platformName = DbNameUtil.normalise(connection);
173      migrationConfig.setPlatformName(platformName);
174    }
175
176    return DbNameUtil.platform(platformName);
177  }
178
179}