001package com.avaje.ebean.dbmigration; 002 003import com.avaje.ebean.Transaction; 004import com.avaje.ebean.config.ServerConfig; 005import com.avaje.ebean.dbmigration.model.CurrentModel; 006import com.avaje.ebeaninternal.api.SpiEbeanPlugin; 007import com.avaje.ebeaninternal.api.SpiEbeanServer; 008import org.slf4j.Logger; 009import org.slf4j.LoggerFactory; 010 011import javax.persistence.PersistenceException; 012import java.io.BufferedReader; 013import java.io.File; 014import java.io.FileReader; 015import java.io.FileWriter; 016import java.io.IOException; 017import java.io.LineNumberReader; 018import java.io.StringReader; 019import java.sql.Connection; 020import java.sql.PreparedStatement; 021import java.sql.SQLException; 022import java.util.ArrayList; 023import java.util.List; 024 025/** 026 * Controls the generation of DDL and potentially runs the resulting scripts. 027 */ 028public class DdlGenerator implements SpiEbeanPlugin { 029 030 private static final Logger logger = LoggerFactory.getLogger(DdlGenerator.class); 031 032 private SpiEbeanServer server; 033 034 private boolean generateDdl; 035 private boolean runDdl; 036 037 private CurrentModel currentModel; 038 private String dropContent; 039 private String createContent; 040 041 public void setup(SpiEbeanServer server, ServerConfig serverConfig) { 042 this.server = server; 043 this.generateDdl = serverConfig.isDdlGenerate(); 044 this.runDdl = serverConfig.isDdlRun(); 045 } 046 047 /** 048 * Generate the DDL and then run the DDL based on property settings 049 * (ebean.ddl.generate and ebean.ddl.run etc). 050 */ 051 public void execute(boolean online) { 052 generateDdl(); 053 if (online) { 054 runDdl(); 055 } 056 } 057 058 /** 059 * Generate the DDL drop and create scripts if the properties have been set. 060 */ 061 public void generateDdl() { 062 if (generateDdl) { 063 writeDrop(getDropFileName()); 064 writeCreate(getCreateFileName()); 065 } 066 } 067 068 /** 069 * Run the DDL drop and DDL create scripts if properties have been set. 070 */ 071 public void runDdl() { 072 073 if (runDdl) { 074 try { 075 if (dropContent == null) { 076 dropContent = readFile(getDropFileName()); 077 } 078 if (createContent == null) { 079 createContent = readFile(getCreateFileName()); 080 } 081 runScript(true, dropContent); 082 runScript(false, createContent); 083 084 } catch (IOException e) { 085 String msg = "Error reading drop/create script from file system"; 086 throw new RuntimeException(msg, e); 087 } 088 } 089 } 090 091 protected void writeDrop(String dropFile) { 092 093 try { 094 String c = generateDropDdl(); 095 writeFile(dropFile, c); 096 } catch (IOException e) { 097 throw new PersistenceException("Error generating Drop DDL", e); 098 } 099 } 100 101 protected void writeCreate(String createFile) { 102 103 try { 104 String c = generateCreateDdl(); 105 writeFile(createFile, c); 106 } catch (IOException e) { 107 throw new PersistenceException("Error generating Create DDL", e); 108 } 109 } 110 111 public String generateDropDdl() { 112 113 try { 114 dropContent = currentModel().getDropDdl(); 115 return dropContent; 116 } catch (IOException e) { 117 throw new RuntimeException(e); 118 } 119 } 120 121 public String generateCreateDdl() { 122 123 try { 124 createContent = currentModel().getCreateDdl(); 125 return createContent; 126 } catch (IOException e) { 127 throw new RuntimeException(e); 128 } 129 } 130 131 protected String getDropFileName() { 132 return server.getName() + "-drop-all.sql"; 133 } 134 135 protected String getCreateFileName() { 136 return server.getName() + "-create-all.sql"; 137 } 138 139 protected CurrentModel currentModel() { 140 if (currentModel == null) { 141 currentModel = new CurrentModel(server); 142 } 143 return currentModel; 144 } 145 146 protected void writeFile(String fileName, String fileContent) throws IOException { 147 148 File f = new File(fileName); 149 150 FileWriter fw = new FileWriter(f); 151 try { 152 fw.write(fileContent); 153 fw.flush(); 154 } finally { 155 fw.close(); 156 } 157 } 158 159 protected String readFile(String fileName) throws IOException { 160 161 File f = new File(fileName); 162 if (!f.exists()) { 163 return null; 164 } 165 166 StringBuilder buf = new StringBuilder(); 167 168 FileReader fr = new FileReader(f); 169 LineNumberReader lr = new LineNumberReader(fr); 170 try { 171 String s; 172 while ((s = lr.readLine()) != null) { 173 buf.append(s).append("\n"); 174 } 175 } finally { 176 lr.close(); 177 } 178 179 return buf.toString(); 180 } 181 182 /** 183 * Execute all the DDL statements in the script. 184 */ 185 public void runScript(boolean expectErrors, String content) { 186 187 StringReader sr = new StringReader(content); 188 List<String> statements = parseStatements(sr); 189 190 Transaction t = server.createTransaction(); 191 try { 192 Connection connection = t.getConnection(); 193 194 logger.info("Running DDL"); 195 196 runStatements(expectErrors, statements, connection); 197 198 logger.info("Running DDL Complete"); 199 200 t.commit(); 201 202 } catch (Exception e) { 203 throw new PersistenceException("Error: " + e.getMessage(), e); 204 } finally { 205 t.end(); 206 } 207 } 208 209 /** 210 * Execute the list of statements. 211 */ 212 private void runStatements(boolean expectErrors, List<String> statements, Connection c) { 213 List<String> noDuplicates = new ArrayList<String>(); 214 215 for (String statement : statements) { 216 if (!noDuplicates.contains(statement)) { 217 noDuplicates.add(statement); 218 } 219 } 220 221 for (int i = 0; i < noDuplicates.size(); i++) { 222 String xOfy = (i + 1) + " of " + noDuplicates.size(); 223 runStatement(expectErrors, xOfy, noDuplicates.get(i), c); 224 } 225 } 226 227 /** 228 * Execute the statement. 229 */ 230 private void runStatement(boolean expectErrors, String oneOf, String stmt, Connection c) { 231 232 PreparedStatement pstmt = null; 233 try { 234 235 // trim and remove trailing ; or / 236 stmt = stmt.trim(); 237 if (stmt.endsWith(";")) { 238 stmt = stmt.substring(0, stmt.length() - 1); 239 } else if (stmt.endsWith("/")) { 240 stmt = stmt.substring(0, stmt.length() - 1); 241 } 242 243 logger.info("executing " + oneOf + " " + getSummary(stmt)); 244 245 pstmt = c.prepareStatement(stmt); 246 pstmt.execute(); 247 248 } catch (Exception e) { 249 if (expectErrors) { 250 logger.info(" ... ignoring error executing " + getSummary(stmt) + " error: " + e.getMessage()); 251 } else { 252 String msg = "Error executing stmt[" + stmt + "] error[" + e.getMessage() + "]"; 253 throw new RuntimeException(msg, e); 254 } 255 } finally { 256 if (pstmt != null) { 257 try { 258 pstmt.close(); 259 } catch (SQLException e) { 260 logger.error("Error closing pstmt", e); 261 } 262 } 263 } 264 } 265 266 /** 267 * Local utility used to detect the end of statements / separate statements. 268 * This is often just the semicolon character but for trigger/procedures this 269 * detects the $$ demarcation used in the history DDL generation for MySql and 270 * Postgres. 271 */ 272 static class StatementsSeparator { 273 274 ArrayList<String> statements = new ArrayList<String>(); 275 276 boolean trimDelimiter; 277 278 boolean inDbProcedure; 279 280 StringBuilder sb = new StringBuilder(); 281 282 void lineContainsDollars(String line) { 283 if (inDbProcedure) { 284 if (trimDelimiter) { 285 line = line.replace("$$",""); 286 } 287 endOfStatement(line); 288 } else { 289 // MySql style delimiter needs to be trimmed/removed 290 trimDelimiter = line.equals("delimiter $$"); 291 if (!trimDelimiter) { 292 sb.append(line).append(" "); 293 } 294 } 295 inDbProcedure = !inDbProcedure; 296 } 297 298 void endOfStatement(String line) { 299 // end of Db procedure 300 sb.append(line); 301 statements.add(sb.toString().trim()); 302 sb = new StringBuilder(); 303 } 304 305 void nextLine(String line) { 306 307 if (line.contains("$$")) { 308 lineContainsDollars(line); 309 return; 310 } 311 312 if (inDbProcedure) { 313 sb.append(line).append(" "); 314 return; 315 } 316 317 int semiPos = line.indexOf(';'); 318 if (semiPos == -1) { 319 sb.append(line).append(" "); 320 321 } else if (semiPos == line.length() - 1) { 322 // semicolon at end of line 323 endOfStatement(line); 324 325 } else { 326 // semicolon in middle of line 327 String preSemi = line.substring(0, semiPos); 328 endOfStatement(preSemi); 329 sb.append(line.substring(semiPos + 1)); 330 } 331 } 332 } 333 334 /** 335 * Break up the sql in reader into a list of statements using the semi-colon 336 * character; 337 */ 338 protected List<String> parseStatements(StringReader reader) { 339 340 try { 341 BufferedReader br = new BufferedReader(reader); 342 StatementsSeparator statements = new StatementsSeparator(); 343 344 String s; 345 while ((s = br.readLine()) != null) { 346 s = s.trim(); 347 statements.nextLine(s); 348 } 349 350 return statements.statements; 351 352 } catch (IOException e) { 353 throw new PersistenceException(e); 354 } 355 } 356 357 private String getSummary(String s) { 358 if (s.length() > 80) { 359 return s.substring(0, 80).trim() + "..."; 360 } 361 return s; 362 } 363}