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   * Run by creating a DB connection from driver, url, username defined in MigrationConfig.
034   */
035  public void run() {
036    run(migrationConfig.createConnection());
037  }
038
039  /**
040   * Return the migrations that would be applied if the migration is run.
041   */
042  public List<LocalMigrationResource> checkState() {
043    run(migrationConfig.createConnection(), true);
044    return checkMigrations;
045  }
046
047  /**
048   * Return the migrations that would be applied if the migration is run.
049   */
050  public List<LocalMigrationResource> checkState(DataSource dataSource) {
051    run(getConnection(dataSource), true);
052    return checkMigrations;
053  }
054
055  /**
056   * Return the migrations that would be applied if the migration is run.
057   */
058  public List<LocalMigrationResource> checkState(Connection connection) {
059    run(connection, true);
060    return checkMigrations;
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  private Connection getConnection(DataSource dataSource) {
071
072    String username = migrationConfig.getDbUsername();
073    try {
074      if (username == null) {
075        return dataSource.getConnection();
076      }
077      logger.debug("using db user [{}] to run migrations ...", username);
078      return dataSource.getConnection(username, migrationConfig.getDbPassword());
079    } catch (SQLException e) {
080      String msgSuffix = (username == null) ? "" : " using user [" + username + "]";
081      throw new IllegalArgumentException("Error trying to connect to database for DB Migration" + msgSuffix, e);
082    }
083  }
084
085  /**
086   * Run the migrations if there are any that need running.
087   */
088  public void run(Connection connection) {
089    run(connection, false);
090  }
091
092  /**
093   * Run the migrations if there are any that need running.
094   */
095  private void run(Connection connection, boolean checkStateMode) {
096
097    LocalMigrationResources resources = new LocalMigrationResources(migrationConfig);
098    if (!resources.readResources()) {
099      logger.debug("no migrations to check");
100      return;
101    }
102
103    try {
104      connection.setAutoCommit(false);
105      MigrationPlatform platform = derivePlatformName(migrationConfig, connection);
106
107      new MigrationSchema(migrationConfig, connection).createAndSetIfNeeded();
108
109      MigrationTable table = new MigrationTable(migrationConfig, connection, checkStateMode);
110      table.createIfNeededAndLock(platform);
111
112      runMigrations(resources, table, checkStateMode);
113      connection.commit();
114
115    } catch (MigrationException e) {
116      JdbcClose.rollback(connection);
117      throw e;
118
119    } catch (Exception e) {
120      JdbcClose.rollback(connection);
121      throw new RuntimeException(e);
122
123    } finally {
124      JdbcClose.close(connection);
125    }
126  }
127
128  /**
129   * Run all the migrations as needed.
130   */
131  private void runMigrations(LocalMigrationResources resources, MigrationTable table, boolean checkStateMode) throws SQLException {
132
133    // get the migrations in version order
134    List<LocalMigrationResource> localVersions = resources.getVersions();
135
136    if (table.isEmpty()) {
137      LocalMigrationResource initVersion = getInitVersion();
138      if (initVersion != null) {
139        // run using a dbinit script
140        logger.info("dbinit migration version:{}  local migrations:{}  checkState:{}", initVersion, localVersions.size(), checkStateMode);
141        checkMigrations = table.runInit(initVersion, localVersions);
142        return;
143      }
144    }
145
146    logger.info("local migrations:{}  existing migrations:{}  checkState:{}", localVersions.size(), table.size(), checkStateMode);
147    checkMigrations = table.runAll(localVersions);
148  }
149
150  /**
151   * Return the last init migration.
152   */
153  private LocalMigrationResource getInitVersion() {
154    LocalMigrationResources initResources = new LocalMigrationResources(migrationConfig);
155    if (initResources.readInitResources()) {
156      List<LocalMigrationResource> initVersions = initResources.getVersions();
157      if (!initVersions.isEmpty()) {
158        return initVersions.get(initVersions.size() - 1);
159      }
160    }
161    return null;
162  }
163
164  /**
165   * Return the platform deriving from connection if required.
166   */
167  private MigrationPlatform derivePlatformName(MigrationConfig migrationConfig, Connection connection) {
168
169    String platformName = migrationConfig.getPlatformName();
170    if (platformName == null) {
171      platformName = DbNameUtil.normalise(connection);
172      migrationConfig.setPlatformName(platformName);
173    }
174
175    return DbNameUtil.platform(platformName);
176  }
177
178}