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 org.slf4j.Logger;
009import org.slf4j.LoggerFactory;
010
011import javax.sql.DataSource;
012import java.sql.Connection;
013import java.sql.SQLException;
014import java.util.List;
015
016/**
017 * Runs the DB migration typically on application start.
018 */
019public class MigrationRunner {
020
021  private static final Logger logger = LoggerFactory.getLogger(MigrationRunner.class);
022
023  private final MigrationConfig migrationConfig;
024
025  private List<LocalMigrationResource> checkMigrations;
026
027  public MigrationRunner(MigrationConfig migrationConfig) {
028    this.migrationConfig = migrationConfig;
029  }
030
031  /**
032   * Return the migrations that would be applied if the migration is run.
033   */
034  public List<LocalMigrationResource> checkState() {
035    run(migrationConfig.createConnection(), true);
036    return checkMigrations;
037  }
038
039  /**
040   * Return the migrations that would be applied if the migration is run.
041   */
042  public List<LocalMigrationResource> checkState(DataSource dataSource) {
043    run(getConnection(dataSource), 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(Connection connection) {
051    run(connection, true);
052    return checkMigrations;
053  }
054
055  /**
056   * Run by creating a DB connection from driver, url, username defined in MigrationConfig.
057   */
058  public void run() {
059    run(migrationConfig.createConnection());
060  }
061
062  /**
063   * Run using the connection from the DataSource.
064   */
065  public void run(DataSource dataSource) {
066    run(getConnection(dataSource));
067  }
068
069  /**
070   * Run the migrations if there are any that need running.
071   */
072  public void run(Connection connection) {
073    run(connection, false);
074  }
075
076  private Connection getConnection(DataSource dataSource) {
077    String username = migrationConfig.getDbUsername();
078    try {
079      if (username == null) {
080        return dataSource.getConnection();
081      }
082      logger.debug("using db user [{}] to run migrations ...", username);
083      return dataSource.getConnection(username, migrationConfig.getDbPassword());
084    } catch (SQLException e) {
085      String msgSuffix = (username == null) ? "" : " using user [" + username + "]";
086      throw new IllegalArgumentException("Error trying to connect to database for DB Migration" + msgSuffix, e);
087    }
088  }
089
090  /**
091   * Run the migrations if there are any that need running.
092   */
093  private void run(Connection connection, boolean checkStateMode) {
094
095    LocalMigrationResources resources = new LocalMigrationResources(migrationConfig);
096    if (!resources.readResources()) {
097      logger.debug("no migrations to check");
098      return;
099    }
100
101    try {
102      connection.setAutoCommit(false);
103      MigrationPlatform platform = derivePlatformName(migrationConfig, connection);
104
105      new MigrationSchema(migrationConfig, connection).createAndSetIfNeeded();
106
107      MigrationTable table = new MigrationTable(migrationConfig, connection, checkStateMode, platform);
108      table.createIfNeededAndLock();
109
110      runMigrations(resources, table, checkStateMode);
111      connection.commit();
112
113      table.runNonTransactional();
114
115    } catch (MigrationException e) {
116      rollback(connection);
117      throw e;
118
119    } catch (Exception e) {
120      rollback(connection);
121      throw new RuntimeException(e);
122
123    } finally {
124      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  /**
179   * Close the connection logging if an error occurs.
180   */
181  private void close(Connection connection) {
182    try {
183      if (connection != null) {
184        connection.close();
185      }
186    } catch (SQLException e) {
187      logger.warn("Error closing connection", e);
188    }
189  }
190
191  /**
192   * Rollback the connection logging if an error occurs.
193   */
194  private void rollback(Connection connection) {
195    try {
196      if (connection != null) {
197        connection.rollback();
198      }
199    } catch (SQLException e) {
200      logger.warn("Error on connection rollback", e);
201    }
202  }
203}