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}