From c48008c4b2df7b37edd3c026b77a4202094090f9 Mon Sep 17 00:00:00 2001 From: Robin Bygrave Date: Thu, 7 Jan 2016 12:48:55 +1300 Subject: [PATCH] #515 - ENH: Add ebean.ddl.seedSql=xxx.sql ... such that a sql script will execute to insert seed data typically for testing --- .../com/avaje/ebean/config/ServerConfig.java | 25 ++ .../avaje/ebean/dbmigration/DdlGenerator.java | 264 ++++-------------- .../avaje/ebean/dbmigration/DdlParser.java | 105 +++++++ .../avaje/ebean/dbmigration/DdlRunner.java | 138 +++++++++ 4 files changed, 328 insertions(+), 204 deletions(-) create mode 100644 src/main/java/com/avaje/ebean/dbmigration/DdlParser.java create mode 100644 src/main/java/com/avaje/ebean/dbmigration/DdlRunner.java diff --git a/src/main/java/com/avaje/ebean/config/ServerConfig.java b/src/main/java/com/avaje/ebean/config/ServerConfig.java index 24e4ff093..39f2c2c04 100644 --- a/src/main/java/com/avaje/ebean/config/ServerConfig.java +++ b/src/main/java/com/avaje/ebean/config/ServerConfig.java @@ -214,6 +214,8 @@ public class ServerConfig { private boolean ddlCreateOnly; + private String ddlSeedSql; + private boolean useJtaTransactionManager; /** @@ -1570,6 +1572,28 @@ public class ServerConfig { this.ddlCreateOnly = ddlCreateOnly; } + /** + * Return SQL script to execute after the "create all" DDL has been run. + *

+ * Typically this is a sql script that inserts test seed data when running tests. + * Place a sql script in src/test/resources that inserts test seed data. + *

+ */ + public String getDdlSeedSql() { + return ddlSeedSql; + } + + /** + * Set a SQL script to execute after the "create all" DDL has been run. + *

+ * Typically this is a sql script that inserts test seed data when running tests. + * Place a sql script in src/test/resources that inserts test seed data. + *

+ */ + public void setDdlSeedSql(String ddlSeedSql) { + this.ddlSeedSql = ddlSeedSql; + } + /** * Return true if the DDL should be generated. */ @@ -2261,6 +2285,7 @@ public class ServerConfig { ddlGenerate = p.getBoolean("ddl.generate", ddlGenerate); ddlRun = p.getBoolean("ddl.run", ddlRun); ddlCreateOnly = p.getBoolean("ddl.createOnly", ddlCreateOnly); + ddlSeedSql = p.get("ddl.seedSql", ddlSeedSql); classes = getClasses(p); } diff --git a/src/main/java/com/avaje/ebean/dbmigration/DdlGenerator.java b/src/main/java/com/avaje/ebean/dbmigration/DdlGenerator.java index 852ce7527..ee4c7432c 100644 --- a/src/main/java/com/avaje/ebean/dbmigration/DdlGenerator.java +++ b/src/main/java/com/avaje/ebean/dbmigration/DdlGenerator.java @@ -1,34 +1,25 @@ package com.avaje.ebean.dbmigration; -import com.avaje.ebean.Transaction; import com.avaje.ebean.config.ServerConfig; import com.avaje.ebean.dbmigration.model.CurrentModel; import com.avaje.ebeaninternal.api.SpiEbeanPlugin; import com.avaje.ebeaninternal.api.SpiEbeanServer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.persistence.PersistenceException; -import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.io.LineNumberReader; -import java.io.StringReader; -import java.sql.Connection; -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; +import java.io.Reader; /** * Controls the generation of DDL and potentially runs the resulting scripts. */ public class DdlGenerator implements SpiEbeanPlugin { - private static final Logger logger = LoggerFactory.getLogger(DdlGenerator.class); - private SpiEbeanServer server; private boolean generateDdl; @@ -76,16 +67,9 @@ public class DdlGenerator implements SpiEbeanPlugin { if (runDdl) { try { - if (dropContent == null) { - dropContent = readFile(getDropFileName()); - } - if (createContent == null) { - createContent = readFile(getCreateFileName()); - } - if (!createOnly) { - runScript(true, dropContent); - } - runScript(false, createContent); + runDropSql(); + runCreateSql(); + runSeedSql(); } catch (IOException e) { String msg = "Error reading drop/create script from file system"; @@ -94,6 +78,46 @@ public class DdlGenerator implements SpiEbeanPlugin { } } + protected void runDropSql() throws IOException { + if (!createOnly) { + if (dropContent == null) { + dropContent = readFile(getDropFileName()); + } + runScript(true, dropContent, getDropFileName()); + } + } + + protected void runCreateSql() throws IOException { + if (createContent == null) { + createContent = readFile(getCreateFileName()); + } + runScript(false, createContent, getCreateFileName()); + } + + protected void runSeedSql() throws IOException { + + String seedSql = server.getServerConfig().getDdlSeedSql(); + if (seedSql != null) { + InputStream is = getClassLoader().getResourceAsStream(seedSql); + if (is != null) { + DdlRunner runner = new DdlRunner(false, seedSql); + String content = readContent(new InputStreamReader(is)); + runner.runAll(content, server); + } + } + } + + /** + * Return the classLoader to use to read sql scripts as resources. + */ + protected ClassLoader getClassLoader() { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (cl == null) { + cl = this.getClassLoader(); + } + return cl; + } + protected void writeDrop(String dropFile) { try { @@ -169,201 +193,33 @@ public class DdlGenerator implements SpiEbeanPlugin { return null; } + return readContent(new FileReader(f)); + } + + private String readContent(Reader reader) throws IOException { + StringBuilder buf = new StringBuilder(); - FileReader fr = new FileReader(f); - LineNumberReader lr = new LineNumberReader(fr); + LineNumberReader lineReader = new LineNumberReader(reader); try { String s; - while ((s = lr.readLine()) != null) { + while ((s = lineReader.readLine()) != null) { buf.append(s).append("\n"); } - } finally { - lr.close(); - } + return buf.toString(); - return buf.toString(); + } finally { + lineReader.close(); + } } /** * Execute all the DDL statements in the script. */ - public void runScript(boolean expectErrors, String content) { + public int runScript(boolean expectErrors, String content, String scriptName) { - StringReader sr = new StringReader(content); - List statements = parseStatements(sr); - - Transaction t = server.createTransaction(); - try { - Connection connection = t.getConnection(); - - logger.info("Running DDL"); - - runStatements(expectErrors, statements, connection); - - logger.info("Running DDL Complete"); - - t.commit(); - - } catch (Exception e) { - throw new PersistenceException("Error: " + e.getMessage(), e); - } finally { - t.end(); - } + DdlRunner runner = new DdlRunner(expectErrors, scriptName); + return runner.runAll(content, server); } - /** - * Execute the list of statements. - */ - private void runStatements(boolean expectErrors, List statements, Connection c) { - List noDuplicates = new ArrayList(); - - for (String statement : statements) { - if (!noDuplicates.contains(statement)) { - noDuplicates.add(statement); - } - } - - for (int i = 0; i < noDuplicates.size(); i++) { - String xOfy = (i + 1) + " of " + noDuplicates.size(); - runStatement(expectErrors, xOfy, noDuplicates.get(i), c); - } - } - - /** - * Execute the statement. - */ - private void runStatement(boolean expectErrors, String oneOf, String stmt, Connection c) { - - PreparedStatement pstmt = null; - try { - - // trim and remove trailing ; or / - stmt = stmt.trim(); - if (stmt.endsWith(";")) { - stmt = stmt.substring(0, stmt.length() - 1); - } else if (stmt.endsWith("/")) { - stmt = stmt.substring(0, stmt.length() - 1); - } - - logger.info("executing " + oneOf + " " + getSummary(stmt)); - - pstmt = c.prepareStatement(stmt); - pstmt.execute(); - - } catch (Exception e) { - if (expectErrors) { - logger.info(" ... ignoring error executing " + getSummary(stmt) + " error: " + e.getMessage()); - } else { - String msg = "Error executing stmt[" + stmt + "] error[" + e.getMessage() + "]"; - throw new RuntimeException(msg, e); - } - } finally { - if (pstmt != null) { - try { - pstmt.close(); - } catch (SQLException e) { - logger.error("Error closing pstmt", e); - } - } - } - } - - /** - * Local utility used to detect the end of statements / separate statements. - * This is often just the semicolon character but for trigger/procedures this - * detects the $$ demarcation used in the history DDL generation for MySql and - * Postgres. - */ - static class StatementsSeparator { - - ArrayList statements = new ArrayList(); - - boolean trimDelimiter; - - boolean inDbProcedure; - - StringBuilder sb = new StringBuilder(); - - void lineContainsDollars(String line) { - if (inDbProcedure) { - if (trimDelimiter) { - line = line.replace("$$",""); - } - endOfStatement(line); - } else { - // MySql style delimiter needs to be trimmed/removed - trimDelimiter = line.equals("delimiter $$"); - if (!trimDelimiter) { - sb.append(line).append(" "); - } - } - inDbProcedure = !inDbProcedure; - } - - void endOfStatement(String line) { - // end of Db procedure - sb.append(line); - statements.add(sb.toString().trim()); - sb = new StringBuilder(); - } - - void nextLine(String line) { - - if (line.contains("$$")) { - lineContainsDollars(line); - return; - } - - if (inDbProcedure) { - sb.append(line).append(" "); - return; - } - - int semiPos = line.indexOf(';'); - if (semiPos == -1) { - sb.append(line).append(" "); - - } else if (semiPos == line.length() - 1) { - // semicolon at end of line - endOfStatement(line); - - } else { - // semicolon in middle of line - String preSemi = line.substring(0, semiPos); - endOfStatement(preSemi); - sb.append(line.substring(semiPos + 1)); - } - } - } - - /** - * Break up the sql in reader into a list of statements using the semi-colon - * character; - */ - protected List parseStatements(StringReader reader) { - - try { - BufferedReader br = new BufferedReader(reader); - StatementsSeparator statements = new StatementsSeparator(); - - String s; - while ((s = br.readLine()) != null) { - s = s.trim(); - statements.nextLine(s); - } - - return statements.statements; - - } catch (IOException e) { - throw new PersistenceException(e); - } - } - - private String getSummary(String s) { - if (s.length() > 80) { - return s.substring(0, 80).trim() + "..."; - } - return s; - } } diff --git a/src/main/java/com/avaje/ebean/dbmigration/DdlParser.java b/src/main/java/com/avaje/ebean/dbmigration/DdlParser.java new file mode 100644 index 000000000..2f9f2cf25 --- /dev/null +++ b/src/main/java/com/avaje/ebean/dbmigration/DdlParser.java @@ -0,0 +1,105 @@ +package com.avaje.ebean.dbmigration; + +import javax.persistence.PersistenceException; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.List; + +/** + * Parses string content into separate SQL/DDL statements. + */ +public class DdlParser { + + /** + * Break up the sql in reader into a list of statements using the semi-colon and $$ delimiters; + */ + public List parse(StringReader reader) { + + try { + BufferedReader br = new BufferedReader(reader); + StatementsSeparator statements = new StatementsSeparator(); + + String s; + while ((s = br.readLine()) != null) { + s = s.trim(); + statements.nextLine(s); + } + + return statements.statements; + + } catch (IOException e) { + throw new PersistenceException(e); + } + } + + + /** + * Local utility used to detect the end of statements / separate statements. + * This is often just the semicolon character but for trigger/procedures this + * detects the $$ demarcation used in the history DDL generation for MySql and + * Postgres. + */ + static class StatementsSeparator { + + ArrayList statements = new ArrayList(); + + boolean trimDelimiter; + + boolean inDbProcedure; + + StringBuilder sb = new StringBuilder(); + + void lineContainsDollars(String line) { + if (inDbProcedure) { + if (trimDelimiter) { + line = line.replace("$$",""); + } + endOfStatement(line); + } else { + // MySql style delimiter needs to be trimmed/removed + trimDelimiter = line.equals("delimiter $$"); + if (!trimDelimiter) { + sb.append(line).append(" "); + } + } + inDbProcedure = !inDbProcedure; + } + + void endOfStatement(String line) { + // end of Db procedure + sb.append(line); + statements.add(sb.toString().trim()); + sb = new StringBuilder(); + } + + void nextLine(String line) { + + if (line.contains("$$")) { + lineContainsDollars(line); + return; + } + + if (inDbProcedure) { + sb.append(line).append(" "); + return; + } + + int semiPos = line.indexOf(';'); + if (semiPos == -1) { + sb.append(line).append(" "); + + } else if (semiPos == line.length() - 1) { + // semicolon at end of line + endOfStatement(line); + + } else { + // semicolon in middle of line + String preSemi = line.substring(0, semiPos); + endOfStatement(preSemi); + sb.append(line.substring(semiPos + 1)); + } + } + } +} diff --git a/src/main/java/com/avaje/ebean/dbmigration/DdlRunner.java b/src/main/java/com/avaje/ebean/dbmigration/DdlRunner.java new file mode 100644 index 000000000..f1b1d2b58 --- /dev/null +++ b/src/main/java/com/avaje/ebean/dbmigration/DdlRunner.java @@ -0,0 +1,138 @@ +package com.avaje.ebean.dbmigration; + +import com.avaje.ebean.Transaction; +import com.avaje.ebeaninternal.api.SpiEbeanServer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.persistence.PersistenceException; +import java.io.StringReader; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +/** + * Runs DDL scripts. + */ +public class DdlRunner { + + protected static final Logger logger = LoggerFactory.getLogger(DdlRunner.class); + + protected DdlParser ddlParser = new DdlParser(); + + protected final String scriptName; + + protected final boolean expectErrors; + + /** + * Construct with a script name (for logging) and flag indicating if errors are expected. + */ + public DdlRunner(boolean expectErrors, String scriptName) { + this.expectErrors = expectErrors; + this.scriptName = scriptName; + } + + /** + * Parse the content into sql statements and execute them in a transaction. + */ + public int runAll(String content, SpiEbeanServer server) { + + List statements = ddlParser.parse(new StringReader(content)); + return runStatements(statements, server); + } + + /** + * Execute all the statements in a single transaction. + */ + public int runStatements(List statements, SpiEbeanServer server) { + + Transaction t = server.createTransaction(); + try { + int statementCount = runStatements(expectErrors, statements, t.getConnection()); + t.commit(); + + return statementCount; + + } catch (Exception e) { + throw new PersistenceException("Error: " + e.getMessage(), e); + + } finally { + t.end(); + } + } + + /** + * Execute the list of statements. + */ + private int runStatements(boolean expectErrors, List statements, Connection c) { + + List noDuplicates = new ArrayList(); + + for (String statement : statements) { + if (!noDuplicates.contains(statement)) { + noDuplicates.add(statement); + } + } + + logger.info("Executing {} - {} statements", scriptName, noDuplicates.size()); + + for (int i = 0; i < noDuplicates.size(); i++) { + String xOfy = (i + 1) + " of " + noDuplicates.size(); + runStatement(expectErrors, xOfy, noDuplicates.get(i), c); + } + + return noDuplicates.size(); + } + + /** + * Execute the statement. + */ + private void runStatement(boolean expectErrors, String oneOf, String stmt, Connection c) { + + PreparedStatement pstmt = null; + try { + + // trim and remove trailing ; or / + stmt = stmt.trim(); + if (stmt.endsWith(";")) { + stmt = stmt.substring(0, stmt.length() - 1); + } else if (stmt.endsWith("/")) { + stmt = stmt.substring(0, stmt.length() - 1); + } + + if (logger.isDebugEnabled()) { + logger.debug("executing " + oneOf + " " + getSummary(stmt)); + } + + pstmt = c.prepareStatement(stmt); + pstmt.execute(); + + } catch (Exception e) { + if (expectErrors) { + logger.debug(" ... ignoring error executing " + getSummary(stmt) + " error: " + e.getMessage()); + } else { + String msg = "Error executing stmt[" + stmt + "] error[" + e.getMessage() + "]"; + throw new RuntimeException(msg, e); + } + + } finally { + if (pstmt != null) { + try { + pstmt.close(); + } catch (SQLException e) { + logger.error("Error closing pstmt", e); + } + } + } + } + + private String getSummary(String s) { + if (s.length() > 80) { + return s.substring(0, 80).trim() + "..."; + } + return s; + } + +}