diff --git a/pom.xml b/pom.xml index 607ed216b..f23a67d56 100644 --- a/pom.xml +++ b/pom.xml @@ -64,7 +64,7 @@ org.avaje avaje-datasource - 1.1.1 + 1.1.2 @@ -92,12 +92,6 @@ [1.7.1,1.7.99) - - org.avaje - avaje-classpath-scanner - 1.1.1 - - org.springframework.boot diff --git a/src/main/java/com/avaje/ebean/config/ClassLoadConfig.java b/src/main/java/com/avaje/ebean/config/ClassLoadConfig.java index f960de5b5..9c2342f08 100644 --- a/src/main/java/com/avaje/ebean/config/ClassLoadConfig.java +++ b/src/main/java/com/avaje/ebean/config/ClassLoadConfig.java @@ -74,6 +74,9 @@ public class ClassLoadConfig { } } + /** + * Return the resources for the given name. + */ public Enumeration getResources(String name) throws IOException { return context.getResources(name); } diff --git a/src/main/java/com/avaje/ebean/config/DbMigrationConfig.java b/src/main/java/com/avaje/ebean/config/DbMigrationConfig.java index 4356058a2..6e890186a 100644 --- a/src/main/java/com/avaje/ebean/config/DbMigrationConfig.java +++ b/src/main/java/com/avaje/ebean/config/DbMigrationConfig.java @@ -6,6 +6,8 @@ import com.avaje.ebean.dbmigration.DbMigration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.Map; + /** * Configuration for the DB migration processing. */ @@ -67,6 +69,36 @@ public class DbMigrationConfig { */ protected String generatePendingDrop; + /** + * For running migration the DB table that holds migration execution status. + */ + protected String metaTable = "db_migration"; + + /** + * Flag set to true means to run any outstanding migrations on startup. + */ + protected boolean runMigration; + + /** + * Comma and equals delimited key/value placeholders to replace in DDL scripts. + */ + protected String runPlaceholders; + + /** + * Map of key/value placeholders to replace in DDL scripts. + */ + protected Map runPlaceholderMap; + + /** + * DB user used to run the DB migration. + */ + protected String dbUser; + + /** + * DB password used to run the DB migration. + */ + protected String dbPassword; + /** * Return the DB platform to generate migration DDL for. * @@ -199,10 +231,119 @@ public class DbMigrationConfig { this.modelPath = ""; } + /** + * Return the table name that holds the migration run details + * (used by DB Migration runner only). + */ + public String getMetaTable() { + return metaTable; + } + + /** + * Set the table name that holds the migration run details + * (used by DB Migration runner only). + */ + public void setMetaTable(String metaTable) { + this.metaTable = metaTable; + } + + /** + * Return a comma and equals delimited placeholders that are substituted in SQL scripts when running migration + * (used by DB Migration runner only). + */ + public String getRunPlaceholders() { + // environment properties take precedence + String placeholders = readEnvironment("ddl.migration.placeholders"); + if (placeholders != null) { + return placeholders; + } + return runPlaceholders; + } + + /** + * Set a comma and equals delimited placeholders that are substituted in SQL scripts when running migration + * (used by DB Migration runner only). + */ + public void setRunPlaceholders(String runPlaceholders) { + this.runPlaceholders = runPlaceholders; + } + + /** + * Return a map of placeholder values that are substituted in SQL scripts when running migration + * (used by DB Migration runner only). + */ + public Map getRunPlaceholderMap() { + return runPlaceholderMap; + } + + /** + * Set a map of placeholder values that are substituted when running migration + * (used by DB Migration runner only). + */ + public void setRunPlaceholderMap(Map runPlaceholderMap) { + this.runPlaceholderMap = runPlaceholderMap; + } + + /** + * Return true if the DB migration should be run on startup. + */ + public boolean isRunMigration() { + // environment properties take precedence + String run = readEnvironment("ddl.migration.run"); + if (run != null) { + return "true".equalsIgnoreCase(run.trim()); + } + return runMigration; + } + + /** + * Set to true to run the DB migration on startup. + */ + public void setRunMigration(boolean runMigration) { + this.runMigration = runMigration; + } + + /** + * Return the DB user to use for running DB migrations. + */ + public String getDbUser() { + // environment properties take precedence + String user = readEnvironment("ddl.migration.user"); + if (user != null) { + return user; + } + return dbUser; + } + + /** + * Set the DB user to use for running DB migrations. + */ + public void setDbUser(String dbUser) { + this.dbUser = dbUser; + } + + /** + * Return the DB password to use for running DB migrations. + */ + public String getDbPassword() { + String user = readEnvironment("ddl.migration.password"); + if (user != null) { + return user; + } + return dbPassword; + } + + /** + * Set the DB password to use for running DB migrations. + */ + public void setDbPassword(String dbPassword) { + this.dbPassword = dbPassword; + } + /** * Load the settings from the PropertiesWrapper. */ - public void loadSettings(PropertiesWrapper properties) { + public void loadSettings(PropertiesWrapper properties, String serverName) { migrationPath = properties.get("migration.migrationPath", migrationPath); if (properties.getBoolean("migration.singleDirectory", false)) { @@ -219,7 +360,19 @@ public class DbMigrationConfig { generate = properties.getBoolean("migration.generate", generate); version = properties.get("migration.version", version); - name = properties.get("migration.name", name); + this.name = properties.get("migration.name", this.name); + + runMigration = properties.getBoolean("migration.run", runMigration); + metaTable = properties.get("migration.metaTable", metaTable); + runPlaceholders = properties.get("migration.placeholders", runPlaceholders); + + String adminUser = properties.get("datasource."+serverName+".user", dbUser); + adminUser = properties.get("datasource."+serverName+".adminuser", adminUser); + dbUser = properties.get("migration.dbuser", adminUser); + + String adminPwd = properties.get("datasource."+serverName+".password", dbPassword); + adminPwd = properties.get("datasource."+serverName+".adminpassword", adminPwd); + dbPassword = properties.get("migration.dbpassword", adminPwd); } /** diff --git a/src/main/java/com/avaje/ebean/config/ServerConfig.java b/src/main/java/com/avaje/ebean/config/ServerConfig.java index e51f81cfd..9322e29a2 100644 --- a/src/main/java/com/avaje/ebean/config/ServerConfig.java +++ b/src/main/java/com/avaje/ebean/config/ServerConfig.java @@ -2267,7 +2267,7 @@ public class ServerConfig { */ protected void loadSettings(PropertiesWrapper p) { - migrationConfig.loadSettings(p); + migrationConfig.loadSettings(p, name); namingConvention = createNamingConvention(p, namingConvention); if (namingConvention != null) { diff --git a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java index ef378fb55..922536d63 100644 --- a/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java +++ b/src/main/java/com/avaje/ebean/config/dbplatform/DatabasePlatform.java @@ -540,6 +540,9 @@ public class DatabasePlatform { } } + /** + * Close the resultSet. + */ protected void close(ResultSet resultSet) { try { resultSet.close(); diff --git a/src/main/java/com/avaje/ebean/dbmigration/MigrationRunner.java b/src/main/java/com/avaje/ebean/dbmigration/MigrationRunner.java index c00b4c77f..dac27e033 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/MigrationRunner.java +++ b/src/main/java/com/avaje/ebean/dbmigration/MigrationRunner.java @@ -1,7 +1,6 @@ package com.avaje.ebean.dbmigration; import com.avaje.ebean.EbeanServer; -import com.avaje.ebean.Transaction; import com.avaje.ebean.config.DbMigrationConfig; import com.avaje.ebean.config.ServerConfig; import com.avaje.ebean.dbmigration.runner.LocalMigrationResource; @@ -11,62 +10,91 @@ import com.avaje.ebeaninternal.util.JdbcClose; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.sql.DataSource; +import java.io.IOException; import java.sql.Connection; +import java.sql.SQLException; import java.util.List; /** - * + * Runs the DB migration typically on application start. */ public class MigrationRunner { private static final Logger logger = LoggerFactory.getLogger(MigrationRunner.class); - final EbeanServer server; - final ServerConfig config; + private final EbeanServer server; + + private final ServerConfig config; private final DbMigrationConfig migrationConfig; - public MigrationRunner(EbeanServer server) { + public MigrationRunner(EbeanServer server, DbMigrationConfig migrationConfig) { this.server = server; this.config = server.getPluginApi().getServerConfig(); - migrationConfig = this.config.getMigrationConfig(); + this.migrationConfig = migrationConfig; } + /** + * Run the migrations if there are any that need running. + */ public void run() { - LocalMigrationResources resources = new LocalMigrationResources(config); + LocalMigrationResources resources = new LocalMigrationResources(config, migrationConfig); if (!resources.readResources()) { logger.debug("no migrations to check"); return; } - Transaction transaction = server.createTransaction(); - Connection connection = transaction.getConnection(); + String migrationUser = migrationConfig.getDbUser(); + String migrationPwd = migrationConfig.getDbPassword(); + + DataSource dataSource = server.getPluginApi().getDataSource(); + + Connection connection; try { - MigrationTable table = new MigrationTable(server, migrationConfig, connection); - table.createIfNeeded(); + connection = dataSource.getConnection(migrationUser, migrationPwd); + } catch (SQLException e) { + throw new IllegalArgumentException("Error trying to connect to database using DB Migration user [" + migrationUser + "]", e); + } - LocalMigrationResource priorVersion = null; - List localVersions = resources.getVersions(); - for (int i = 0; i < localVersions.size(); i++) { - LocalMigrationResource localVersion = localVersions.get(i); - if (i > 0) { - priorVersion = localVersions.get(i-1); - } - if (!table.shouldRun(localVersion, priorVersion)) { - break; - } - } + try { + connection.setAutoCommit(false); - table.commit(); + runMigrations(resources, connection); + + connection.commit(); } catch (Exception e) { + JdbcClose.rollback(connection); throw new RuntimeException(e); } finally { JdbcClose.close(connection); } + } + /** + * Run all the migrations as needed. + */ + private void runMigrations(LocalMigrationResources resources, Connection connection) throws SQLException, IOException { + + MigrationTable table = new MigrationTable(server, migrationConfig, connection); + table.createIfNeeded(); + + // get the migrations in version order + List localVersions = resources.getVersions(); + + LocalMigrationResource priorVersion = null; + + // run migrations in order + for (int i = 0; i < localVersions.size(); i++) { + LocalMigrationResource localVersion = localVersions.get(i); + if (!table.shouldRun(localVersion, priorVersion)) { + break; + } + priorVersion = localVersion; + } } } diff --git a/src/main/java/com/avaje/ebean/dbmigration/runner/LocalMigrationResource.java b/src/main/java/com/avaje/ebean/dbmigration/runner/LocalMigrationResource.java index 979552e07..de67d6744 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/runner/LocalMigrationResource.java +++ b/src/main/java/com/avaje/ebean/dbmigration/runner/LocalMigrationResource.java @@ -4,7 +4,7 @@ import com.avaje.ebean.dbmigration.model.MigrationVersion; import org.avaje.classpath.scanner.Resource; /** - * A DB migration resource. + * A DB migration resource (DDL script with version). */ public class LocalMigrationResource implements Comparable { @@ -14,6 +14,9 @@ public class LocalMigrationResource implements Comparable versions = new ArrayList(); - public LocalMigrationResources(ServerConfig serverConfig) { + /** + * Construct with configuration options. + */ + public LocalMigrationResources(ServerConfig serverConfig, DbMigrationConfig migrationConfig) { this.serverConfig = serverConfig; - this.migrationConfig = serverConfig.getMigrationConfig(); + this.migrationConfig = migrationConfig; } + /** + * Read all the migration resources (SQL scripts) returning true if there are versions. + */ public boolean readResources() { String migrationPath = migrationConfig.getMigrationPath(); @@ -59,14 +64,20 @@ public class LocalMigrationResources { return !versions.isEmpty(); } + /** + * Return the list of migration resources in version order. + */ public List getVersions() { return versions; } - static class Match implements MatchResource { + /** + * Filter used to find the migration scripts. + */ + private static class Match implements ResourceFilter { - final DbMigrationConfig migrationConfig; + private final DbMigrationConfig migrationConfig; Match(DbMigrationConfig migrationConfig) { this.migrationConfig = migrationConfig; @@ -74,12 +85,7 @@ public class LocalMigrationResources { @Override public boolean isMatch(String name) { - - return name.endsWith(migrationConfig.getApplySuffix()) - || name.endsWith(migrationConfig.getModelSuffix()) - || name.endsWith(migrationConfig.getDropSuffix()) - || name.endsWith(migrationConfig.getRollbackSuffix()); - + return name.endsWith(migrationConfig.getApplySuffix()); } } } diff --git a/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationMetaRow.java b/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationMetaRow.java index 5a066ac87..ae3beff8b 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationMetaRow.java +++ b/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationMetaRow.java @@ -1,27 +1,100 @@ package com.avaje.ebean.dbmigration.runner; +import com.avaje.ebean.SqlRow; +import com.avaje.ebean.SqlUpdate; + import java.sql.Timestamp; /** - * + * Bean holding migration execution details stored in the migration table. */ -public class MigrationMetaRow implements Comparable { +class MigrationMetaRow { - int id; - String status; - String runVersion; - String depVersion; - String comment; - int checksum; - Timestamp runOn; - String runBy; + private int id; - @Override - public int compareTo(MigrationMetaRow o) { - return (id < o.id) ? -1 : ((id == o.id) ? 0 : 1); + private String status; + + private String runVersion; + + private String depVersion; + + private String comment; + + private int checksum; + + private Timestamp runOn; + + private String runBy; + + /** + * Construct for inserting into table. + */ + MigrationMetaRow(int id, String runVersion, String priorVersion, String comment, int checksum, String runBy) { + this.id = id; + this.runVersion = runVersion; + this.depVersion = priorVersion; + this.checksum = checksum; + this.comment = comment; + this.runBy = runBy; + this.runOn = new Timestamp(System.currentTimeMillis()); } + /** + * Construct from the SqlRow (read from table). + */ + MigrationMetaRow(SqlRow row) { + id = row.getInteger("id"); + status = row.getString("status"); + runVersion = row.getString("row_version"); + depVersion = row.getString("dep_version"); + comment = row.getString("comment"); + checksum = row.getInteger("checksum"); + runOn = row.getTimestamp("run_on"); + runBy = row.getString("run_by"); + } - //String sql = "insert into ebean_migration (id,status,run_version,dep_version,comment,checksum,run_on,run_by,run_ip) "+ - // "values (?,?,?,?,?,?,?,?,?)"; + /** + * Return the id for this migration. + */ + int getId() { + return id; + } + + /** + * Return the normalised version for this migration. + */ + String getRunVersion() { + return runVersion; + } + + /** + * Return the checksum for this migration. + */ + int getChecksum() { + return checksum; + } + + /** + * Bind to the insert statement. + */ + void bindInsert(SqlUpdate insert) { + insert.setParameter(1, id); + insert.setParameter(2, "success"); + insert.setParameter(3, runVersion); + insert.setParameter(4, depVersion); + insert.setParameter(5, comment); + insert.setParameter(6, checksum); + insert.setParameter(7, runOn); + insert.setParameter(8, runBy); + insert.setParameter(9, "ip"); + } + + /** + * Return the SQL insert given the table migration meta data is stored in. + */ + static String insertSql(String table) { + return "insert into " + table + + " (id, status, run_version, dep_version, comment, checksum, run_on, run_by, run_ip)" + + " values (?,?,?,?,?,?,?,?,?)"; + } } diff --git a/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationScriptRunner.java b/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationScriptRunner.java index f52ae6caf..4ef20cd7c 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationScriptRunner.java +++ b/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationScriptRunner.java @@ -5,12 +5,15 @@ import com.avaje.ebean.dbmigration.DdlRunner; import java.sql.Connection; /** - * Created by rob on 5/02/16. + * Runs the DDL migration scripts. */ public class MigrationScriptRunner { - final Connection connection; + private final Connection connection; + /** + * Construct with a given connection. + */ public MigrationScriptRunner(Connection connection) { this.connection = connection; } diff --git a/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationTable.java b/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationTable.java index c5d15a6ad..c97e8cede 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationTable.java +++ b/src/main/java/com/avaje/ebean/dbmigration/runner/MigrationTable.java @@ -17,12 +17,13 @@ import java.io.IOException; import java.net.URL; import java.sql.Connection; import java.sql.SQLException; -import java.sql.Timestamp; import java.util.Enumeration; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; /** - * Created by rob on 5/02/16. + * Manages the migration table. */ public class MigrationTable { @@ -34,34 +35,53 @@ public class MigrationTable { private final DatabasePlatform databasePlatform; - private final DbMigrationConfig migrationConfig; - private final String catalog; private final String schema; private final String table; private final ServerConfig serverConfig; private final String envUserName; - int currentSearchPosition; + private final ScriptTransform scriptTransform; - List metaRows; + private final String insertSql; + private final LinkedHashMap migrations; + + private MigrationMetaRow lastMigration; + + /** + * Construct with server, configuration and jdbc connection (DB admin user). + */ public MigrationTable(EbeanServer server, DbMigrationConfig migrationConfig, Connection connection) { this.connection = connection; this.server = server; + this.migrations = new LinkedHashMap(); SpiServer pluginApi = server.getPluginApi(); this.serverConfig = pluginApi.getServerConfig(); this.databasePlatform = pluginApi.getDatabasePlatform(); - this.migrationConfig = migrationConfig; + this.catalog = null; this.schema = null; - this.table = "ebean_migration"; + this.table = migrationConfig.getMetaTable(); + this.insertSql = MigrationMetaRow.insertSql(table); + this.scriptTransform = createScriptTransform(migrationConfig); this.envUserName = System.getProperty("user.name"); } + /** + * Create the ScriptTransform for placeholder key/value replacement. + */ + private ScriptTransform createScriptTransform(DbMigrationConfig config) { + Map map = PlaceholderBuilder.build(config.getRunPlaceholders(), config.getRunPlaceholderMap()); + return new ScriptTransform(map); + } + + /** + * Create the table is it does not exist. + */ public void createIfNeeded() throws SQLException, IOException { if (!tableExists(connection)) { @@ -69,20 +89,39 @@ public class MigrationTable { } ExternalJdbcTransaction t = new ExternalJdbcTransaction(connection); - SqlQuery sqlQuery = server.createSqlQuery("select * from ebean_migration order by id for update"); - metaRows = server.findList(sqlQuery, t); + SqlQuery sqlQuery = server.createSqlQuery("select * from "+table+" order by id for update"); + List metaRows = server.findList(sqlQuery, t); + + for (SqlRow row : metaRows) { + addMigration(new MigrationMetaRow(row)); + } } + private void createTable(Connection connection) throws IOException { - String script = getCreateTableScript(); + String script = ScriptTransform.table(table, getCreateTableScript()); + MigrationScriptRunner run = new MigrationScriptRunner(connection); - run.runScript(false, script, "create migration tables"); + run.runScript(false, script, "create migration table"); } + /** + * Return the create table script. + */ private String getCreateTableScript() throws IOException { + // supply a script to override the default table create script + String script = readResource("migration-support/create-table.sql"); + if (script == null) { + // no, just use the default script + script = readResource("migration-support/default-create-table.sql"); + } + return script; + } - Enumeration resources = serverConfig.getClassLoadConfig().getResources("migration-support/create.sql"); + private String readResource(String location) throws IOException { + + Enumeration resources = serverConfig.getClassLoadConfig().getResources(location); if (resources.hasMoreElements()) { URL url = resources.nextElement(); return IOUtils.readUtf8(url.openStream()); @@ -90,75 +129,106 @@ public class MigrationTable { return null; } - public boolean tableExists(Connection connection) throws SQLException { - + /** + * Return true if the table exists. + */ + private boolean tableExists(Connection connection) throws SQLException { return databasePlatform.tableExists(connection, catalog, schema, table); } - + /** + * Return true if the migration ran successfully and false if the migration failed. + */ public boolean shouldRun(LocalMigrationResource localVersion, LocalMigrationResource priorVersion) { - // if prior != null check previous version installed - // if previous version not installed ... error - missing version (prior version) - -// if (!priorVersionNotInstalled(priorVersion)) { -// -// } -// -// // if localVersion installed -// // check checksum and return ok, or re-installable? -// // else -// // install version and continue -// -// if (runPosition >= metaRows.size()) { -// logger.debug("No matching row"); -// runMigration(runPosition, localVersion); -// return true; -// } - - return false; - } - - private void checkInstalled(LocalMigrationResource priorVersion) { if (priorVersion != null) { - searchFor(priorVersion); + // check priorVersion is installed + MigrationMetaRow existing = migrations.get(priorVersion.getVersion().normalised()); + if (existing == null) { + logger.warn("Migration {} requires prior migration {} which has not been run", localVersion.getVersion(), priorVersion.getVersion()); + return false; + } + } + + MigrationMetaRow existing = migrations.get(localVersion.getVersion().normalised()); + if (existing == null) { + runMigration(localVersion, priorVersion); + return true; + + } else { + // check checksum and return ok, or re-run if repeatable script? + existing.getChecksum(); + return true; } } - private void searchFor(LocalMigrationResource priorVersion) { - - } - - public void commit() throws SQLException { - connection.commit(); - } - - private void runMigration(int runPosition, LocalMigrationResource localVersion) { + /** + * Run the migration script. + */ + private void runMigration(LocalMigrationResource localVersion, LocalMigrationResource prior) { logger.debug("run migration "+localVersion.getLocation()); - - String script = localVersion.getContent(); + String script = convertScript(localVersion.getContent()); MigrationScriptRunner run = new MigrationScriptRunner(connection); run.runScript(false, script, "run migration version: "+localVersion.getVersion()); - String sql = "insert into ebean_migration (id,status,run_version,dep_version,comment,checksum,run_on,run_by,run_ip) "+ - "values (?,?,?,?,?,?,?,?,?)"; - SqlUpdate sqlUpdate = server.createSqlUpdate(sql); - sqlUpdate.setParameter(1, runPosition+1); - sqlUpdate.setParameter(2, "success"); - sqlUpdate.setParameter(3, localVersion.getVersion().getFull()); - sqlUpdate.setParameter(4, "na"); - sqlUpdate.setParameter(5, "comm"); - sqlUpdate.setParameter(6, 0); - sqlUpdate.setParameter(7, new Timestamp(System.currentTimeMillis())); - sqlUpdate.setParameter(8, envUserName); - sqlUpdate.setParameter(9, "userIp"); + int checksum = Checksum.calculate(script); + MigrationMetaRow metaRow = createMetaRow(localVersion, prior, checksum); - ExternalJdbcTransaction t = new ExternalJdbcTransaction(connection); - server.execute(sqlUpdate, t); + SqlUpdate insert = server.createSqlUpdate(insertSql); + metaRow.bindInsert(insert); + server.execute(insert, new ExternalJdbcTransaction(connection)); + addMigration(metaRow); + } + + /** + * Create the MigrationMetaRow for this migration. + */ + private MigrationMetaRow createMetaRow(LocalMigrationResource localVersion, LocalMigrationResource prior, int checksum) { + + int nextId = 1; + if (lastMigration != null) { + nextId = lastMigration.getId() + 1; + } + + String runVersion = localVersion.getVersion().normalised(); + String comment = getMigrationComment(localVersion); + String priorVersion = getMigrationPriorVersion(prior); + + return new MigrationMetaRow(nextId, runVersion, priorVersion, comment, checksum, envUserName); + } + + /** + * Return the prior migration normalised version. + */ + private String getMigrationPriorVersion(LocalMigrationResource prior) { + return prior != null ? prior.getVersion().normalised() : "-"; + } + + /** + * Return the migration comment. + */ + private String getMigrationComment(LocalMigrationResource localVersion) { + String comment = localVersion.getVersion().getComment(); + return comment == null || comment.isEmpty() ? "-" : comment; + } + + /** + * Apply the placeholder key/value replacement on the script. + */ + private String convertScript(String script) { + return scriptTransform.transform(script); + } + + /** + * Register the successfully executed migration (to allow dependant scripts to run). + */ + private void addMigration(MigrationMetaRow metaRow) { + lastMigration = metaRow; + migrations.put(metaRow.getRunVersion(), metaRow); } } diff --git a/src/main/java/com/avaje/ebean/dbmigration/runner/PlaceholderBuilder.java b/src/main/java/com/avaje/ebean/dbmigration/runner/PlaceholderBuilder.java index 0789b474d..eec825394 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/runner/PlaceholderBuilder.java +++ b/src/main/java/com/avaje/ebean/dbmigration/runner/PlaceholderBuilder.java @@ -1,7 +1,53 @@ package com.avaje.ebean.dbmigration.runner; +import java.util.HashMap; +import java.util.Map; + /** - * Created by rob on 1/04/16. + * Joins placeholder map and comma/equals delimited string. */ -public class PlaceholderBuilder { +class PlaceholderBuilder { + + private final Map map = new HashMap(); + + /** + * Create with raw comma and equals delimited pairs plus map of key value pairs. + */ + public static Map build(String commaDelimited, Map placeholders) { + + PlaceholderBuilder builder = new PlaceholderBuilder(); + builder.add(commaDelimited); + builder.add(placeholders); + + return builder.map; + } + + private PlaceholderBuilder() { + + } + + /** + * Add a comma and equals delimited string to parse for key value pairs. + */ + public void add(String commaDelimited) { + + if (commaDelimited != null) { + String[] split = commaDelimited.split("[,;]"); + for (String keyValue : split) { + String[] pair = keyValue.split("="); + if (pair.length == 2) { + map.put(pair[0].trim(), pair[1].trim()); + } + } + } + } + + /** + * Add a map of key value placeholder pairs. + */ + public void add(Map placeholders) { + if (placeholders != null) { + map.putAll(placeholders); + } + } } diff --git a/src/main/java/com/avaje/ebean/dbmigration/runner/ScriptTransform.java b/src/main/java/com/avaje/ebean/dbmigration/runner/ScriptTransform.java index d208527d0..31a0419fd 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/runner/ScriptTransform.java +++ b/src/main/java/com/avaje/ebean/dbmigration/runner/ScriptTransform.java @@ -1,7 +1,47 @@ package com.avaje.ebean.dbmigration.runner; +import java.util.HashMap; +import java.util.Map; + /** - * Created by rob on 1/04/16. + * Transforms a SQL script given a map of key/value substitutions. */ -public class ScriptTransform { +class ScriptTransform { + + /** + * Transform just ${table} with the table name. + */ + public static String table(String tableName, String script) { + return script.replace("${table}", tableName); + } + + private final Map placeholders = new HashMap(); + + ScriptTransform(Map map) { + for (Map.Entry entry : map.entrySet()) { + placeholders.put(wrapKey(entry.getKey()), entry.getValue()); + } + } + + private String wrapKey(String key) { + return "${"+key+"}"; + } + + /** + * Return true if this contains no placeholders. + */ + boolean isEmpty() { + return placeholders.isEmpty(); + } + + /** + * Transform the script replacing placeholders in the form ${key} with value. + */ + String transform(String source) { + + for (Map.Entry entry : placeholders.entrySet()) { + source = source.replace(entry.getKey(), entry.getValue()); + } + return source; + } } diff --git a/src/main/java/com/avaje/ebean/plugin/SpiServer.java b/src/main/java/com/avaje/ebean/plugin/SpiServer.java index 12a196339..73dcacecb 100644 --- a/src/main/java/com/avaje/ebean/plugin/SpiServer.java +++ b/src/main/java/com/avaje/ebean/plugin/SpiServer.java @@ -4,6 +4,7 @@ import com.avaje.ebean.EbeanServer; import com.avaje.ebean.config.ServerConfig; import com.avaje.ebean.config.dbplatform.DatabasePlatform; +import javax.sql.DataSource; import java.util.List; /** @@ -40,4 +41,9 @@ public interface SpiServer extends EbeanServer { * Return the bean type for a given doc store queueId. */ BeanType getBeanTypeForQueueId(String queueId); + + /** + * Return the associated DataSource for this EbeanServer instance. + */ + DataSource getDataSource(); } diff --git a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java index 657bbd20c..c9ef5f557 100644 --- a/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java +++ b/src/main/java/com/avaje/ebeaninternal/server/core/DefaultServer.java @@ -20,8 +20,8 @@ import com.avaje.ebean.event.readaudit.ReadAuditLogger; import com.avaje.ebean.event.readaudit.ReadAuditPrepare; import com.avaje.ebean.meta.MetaInfoManager; import com.avaje.ebean.plugin.BeanType; -import com.avaje.ebean.plugin.SpiServer; import com.avaje.ebean.plugin.Plugin; +import com.avaje.ebean.plugin.SpiServer; import com.avaje.ebean.text.csv.CsvReader; import com.avaje.ebean.text.json.JsonContext; import com.avaje.ebeaninternal.api.LoadBeanRequest; @@ -73,6 +73,7 @@ import org.slf4j.LoggerFactory; import javax.persistence.NonUniqueResultException; import javax.persistence.OptimisticLockException; import javax.persistence.PersistenceException; +import javax.sql.DataSource; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -80,7 +81,6 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.ServiceLoader; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.FutureTask; @@ -308,6 +308,11 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { return autoTuneService; } + @Override + public DataSource getDataSource() { + return transactionManager.getDataSource(); + } + @Override public ReadAuditPrepare getReadAuditPrepare() { return readAuditPrepare; @@ -341,8 +346,9 @@ public final class DefaultServer implements SpiServer, SpiEbeanServer { migrationConfig.generateOnStart(this); } - MigrationRunner migrationRunner = new MigrationRunner(this); - migrationRunner.run(); + if (migrationConfig.isRunMigration()) { + new MigrationRunner(this, migrationConfig).run(); + } } /** diff --git a/src/main/java/com/avaje/ebeaninternal/util/EncodeUtil.java b/src/main/java/com/avaje/ebeaninternal/util/EncodeUtil.java index e5c45aff9..946066c7e 100644 --- a/src/main/java/com/avaje/ebeaninternal/util/EncodeUtil.java +++ b/src/main/java/com/avaje/ebeaninternal/util/EncodeUtil.java @@ -15,7 +15,6 @@ public final class EncodeUtil { /* no instances */ } - /** * URL-encodes the specified UTF-8 string. */ diff --git a/src/main/java/com/avaje/ebeaninternal/util/IOUtils.java b/src/main/java/com/avaje/ebeaninternal/util/IOUtils.java index 4d89e28b3..41a74b8e8 100644 --- a/src/main/java/com/avaje/ebeaninternal/util/IOUtils.java +++ b/src/main/java/com/avaje/ebeaninternal/util/IOUtils.java @@ -7,9 +7,10 @@ import java.io.OutputStream; import java.io.Reader; /** - * Created by rob on 5/02/16. + * Utilities for IO. */ public class IOUtils { + /** * Reads the entire contents of the specified input stream and returns them * as a byte array. @@ -44,6 +45,9 @@ public class IOUtils { return EncodeUtil.decodeBytes(read(in), encoding); } + /** + * Read the entire contents from the reader returning as a String. + */ public static String read(Reader reader) throws IOException { StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/com/avaje/ebeaninternal/util/JdbcClose.java b/src/main/java/com/avaje/ebeaninternal/util/JdbcClose.java index bfdb83532..e9a9cbd34 100644 --- a/src/main/java/com/avaje/ebeaninternal/util/JdbcClose.java +++ b/src/main/java/com/avaje/ebeaninternal/util/JdbcClose.java @@ -23,4 +23,15 @@ public class JdbcClose { logger.warn("Error closing connection", e); } } + + /** + * Rollback the connection logging if an error occurs. + */ + public static void rollback(Connection connection) { + try { + connection.rollback(); + } catch (SQLException e) { + logger.warn("Error on connection rollback", e); + } + } } diff --git a/src/main/resources/migration-support/default-create-table.sql b/src/main/resources/migration-support/default-create-table.sql index e69de29bb..747975589 100644 --- a/src/main/resources/migration-support/default-create-table.sql +++ b/src/main/resources/migration-support/default-create-table.sql @@ -0,0 +1,13 @@ +create table ${table} ( + id integer not null, + status varchar(10) not null, + run_version varchar(150) not null, + dep_version varchar(150) not null, + comment varchar(150), + checksum integer not null, + run_on timestamp not null, + run_by varchar(30) not null, + run_ip varchar(30), + constraint pk_${table} primary key (id) +); + diff --git a/src/test/java/com/avaje/ebean/dbmigration/MigrationRunnerTest.java b/src/test/java/com/avaje/ebean/dbmigration/MigrationRunnerTest.java index aafc2da51..c3189cea0 100644 --- a/src/test/java/com/avaje/ebean/dbmigration/MigrationRunnerTest.java +++ b/src/test/java/com/avaje/ebean/dbmigration/MigrationRunnerTest.java @@ -1,10 +1,27 @@ package com.avaje.ebean.dbmigration; -import static org.junit.Assert.*; +import com.avaje.ebean.Ebean; +import com.avaje.ebean.EbeanServer; +import com.avaje.ebean.config.DbMigrationConfig; +import org.junit.Test; + -/** - * Created by rob on 1/04/16. - */ public class MigrationRunnerTest { + + @Test + public void test() { + + EbeanServer server = Ebean.getDefaultServer(); + + DbMigrationConfig config = new DbMigrationConfig(); + config.setMigrationPath("test-dbmigration"); + config.setRunMigration(true); + config.setDbUser("sa"); + config.setDbPassword(""); + + MigrationRunner runner = new MigrationRunner(server, config); + + runner.run(); + } } \ No newline at end of file diff --git a/src/test/java/com/avaje/ebean/dbmigration/runner/PlaceholderBuilderTest.java b/src/test/java/com/avaje/ebean/dbmigration/runner/PlaceholderBuilderTest.java index b12c63002..48f49b466 100644 --- a/src/test/java/com/avaje/ebean/dbmigration/runner/PlaceholderBuilderTest.java +++ b/src/test/java/com/avaje/ebean/dbmigration/runner/PlaceholderBuilderTest.java @@ -1,16 +1,87 @@ package com.avaje.ebean.dbmigration.runner; +import org.assertj.core.data.MapEntry; import org.junit.Test; -import static org.junit.Assert.*; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + -/** - * Created by rob on 1/04/16. - */ public class PlaceholderBuilderTest { - @Test - public void build() throws Exception { + @Test + public void empty() throws Exception { + + assertThat(PlaceholderBuilder.build(null, null)).isEmpty(); } + @Test + @SuppressWarnings("unchecked") + public void comma() throws Exception { + + assertThat(PlaceholderBuilder.build("a=1", null)) + .containsExactly(MapEntry.entry("a","1")); + } + + @Test + @SuppressWarnings("unchecked") + public void comma_withSpace() throws Exception { + + assertThat(PlaceholderBuilder.build(" a=1 ", null)) + .containsExactly(MapEntry.entry("a","1")); + } + + @Test + @SuppressWarnings("unchecked") + public void comma_withSpace_withSemi() throws Exception { + + assertThat(PlaceholderBuilder.build(" a=1 ; ", null)) + .containsExactly(MapEntry.entry("a","1")); + } + + @Test + @SuppressWarnings("unchecked") + public void commaPair() throws Exception { + + assertThat(PlaceholderBuilder.build("a=1;b=2", null)) + .containsExactly(MapEntry.entry("a","1"),MapEntry.entry("b","2")); + } + + + @Test + @SuppressWarnings("unchecked") + public void mapPair() throws Exception { + + Map in = new HashMap(); + in.put("a", "1"); + in.put("b", "2"); + + assertThat(PlaceholderBuilder.build(null, in)) + .containsExactly(MapEntry.entry("a","1"),MapEntry.entry("b","2")); + } + + @Test + @SuppressWarnings("unchecked") + public void mapPair_override() throws Exception { + + Map in = new HashMap(); + in.put("a", "1"); + in.put("b", "2"); + + assertThat(PlaceholderBuilder.build("a=11;b=12", in)) + .containsExactly(MapEntry.entry("a","1"),MapEntry.entry("b","2")); + } + + @Test + @SuppressWarnings("unchecked") + public void mapPair_join() throws Exception { + + Map in = new HashMap(); + in.put("c", "3"); + + assertThat(PlaceholderBuilder.build("a=1;b=2", in)) + .containsExactly(MapEntry.entry("a","1"),MapEntry.entry("b","2"), MapEntry.entry("c","3")); + } } \ No newline at end of file diff --git a/src/test/java/com/avaje/ebean/dbmigration/runner/ScriptTransformTest.java b/src/test/java/com/avaje/ebean/dbmigration/runner/ScriptTransformTest.java index 96309c25d..3aeff2413 100644 --- a/src/test/java/com/avaje/ebean/dbmigration/runner/ScriptTransformTest.java +++ b/src/test/java/com/avaje/ebean/dbmigration/runner/ScriptTransformTest.java @@ -2,15 +2,31 @@ package com.avaje.ebean.dbmigration.runner; import org.junit.Test; +import java.util.HashMap; +import java.util.Map; + import static org.junit.Assert.*; -/** - * Created by rob on 1/04/16. - */ public class ScriptTransformTest { + @Test public void transform() throws Exception { + Map map = new HashMap(); + map.put("one", "PLACE1"); + map.put("two", "PLACE2"); + + ScriptTransform transform = new ScriptTransform(map); + + assertEquals(transform.transform("${one}"), "PLACE1"); + assertEquals(transform.transform("${two}"), "PLACE2"); + assertEquals(transform.transform("${one}${two}"), "PLACE1PLACE2"); + assertEquals(transform.transform("${two}${one}"), "PLACE2PLACE1"); + assertEquals(transform.transform("A${one}B${two}C"), "APLACE1BPLACE2C"); + assertEquals(transform.transform(" ${one} ${two} "), " PLACE1 PLACE2 "); + + assertEquals(transform.transform("$${one}"), "$PLACE1"); + assertEquals(transform.transform("$${one}}"), "$PLACE1}"); } } \ No newline at end of file