001package io.ebeaninternal.dbmigration;
002
003import io.ebean.annotation.Platform;
004import io.ebean.config.DatabaseConfig;
005import io.ebean.config.dbplatform.DatabasePlatform;
006import io.ebean.ddlrunner.DdlRunner;
007import io.ebean.ddlrunner.ScriptTransform;
008import io.ebean.util.JdbcClose;
009import io.ebeaninternal.api.SpiDdlGenerator;
010import io.ebeaninternal.api.SpiEbeanServer;
011import io.ebeaninternal.dbmigration.model.CurrentModel;
012import io.ebeaninternal.dbmigration.model.MTable;
013import io.ebeaninternal.extraddl.model.ExtraDdlXmlReader;
014import io.ebeaninternal.server.deploy.PartitionMeta;
015import org.slf4j.Logger;
016import org.slf4j.LoggerFactory;
017
018import javax.persistence.PersistenceException;
019import java.io.File;
020import java.io.FileReader;
021import java.io.FileWriter;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.InputStreamReader;
025import java.io.LineNumberReader;
026import java.io.Reader;
027import java.sql.Connection;
028import java.sql.SQLException;
029
030/**
031 * Controls the generation and execution of "Create All" and "Drop All" DDL scripts.
032 * <p>
033 * Typically the "Create All" DDL is executed for running tests etc and has nothing to do
034 * with DB Migration (diff based) DDL.
035 */
036public class DdlGenerator implements SpiDdlGenerator {
037
038  private static final Logger log = LoggerFactory.getLogger(DdlGenerator.class);
039  private static final String[] BUILD_DIRS = {"target", "build"};
040
041  private final SpiEbeanServer server;
042
043  private final boolean generateDdl;
044  private final boolean runDdl;
045  private final boolean extraDdl;
046  private final boolean createOnly;
047  private final boolean jaxbPresent;
048  private final boolean ddlAutoCommit;
049  private final String dbSchema;
050  private final ScriptTransform scriptTransform;
051  private final Platform platform;
052  private final String platformName;
053
054  private CurrentModel currentModel;
055  private String dropAllContent;
056  private String createAllContent;
057  private final File baseDir;
058
059  public DdlGenerator(SpiEbeanServer server) {
060    this.server = server;
061    final DatabaseConfig config = server.config();
062    this.jaxbPresent = Detect.isJAXBPresent(config);
063    this.generateDdl = config.isDdlGenerate();
064    this.extraDdl = config.isDdlExtra();
065    this.createOnly = config.isDdlCreateOnly();
066    this.dbSchema = config.getDbSchema();
067    final DatabasePlatform databasePlatform = server.databasePlatform();
068    this.platform = databasePlatform.getPlatform();
069    this.platformName = platform.base().name();
070    if (!config.getTenantMode().isDdlEnabled() && config.isDdlRun()) {
071      log.warn("DDL can't be run on startup with TenantMode " + config.getTenantMode());
072      this.runDdl = false;
073      this.ddlAutoCommit = false;
074    } else {
075      this.runDdl = config.isDdlRun();
076      this.ddlAutoCommit = databasePlatform.isDdlAutoCommit();
077    }
078    this.scriptTransform = createScriptTransform(config);
079    this.baseDir = initBaseDir();
080  }
081
082  private File initBaseDir() {
083    for (String buildDir : BUILD_DIRS) {
084      File dir = new File(buildDir);
085      if (dir.exists() && dir.isDirectory()) {
086        return dir;
087      }
088    }
089    return new File(".");
090  }
091
092  @Override
093  public void execute(boolean online) {
094    generateDdl();
095    if (online) {
096      runDdl();
097    }
098  }
099
100  /**
101   * Generate the DDL drop and create scripts if the properties have been set.
102   */
103  protected void generateDdl() {
104    if (generateDdl) {
105      if (!createOnly) {
106        writeDrop(getDropFileName());
107      }
108      writeCreate(getCreateFileName());
109    }
110  }
111
112  /**
113   * Run the DDL drop and DDL create scripts if properties have been set.
114   */
115  protected void runDdl() {
116    if (runDdl) {
117      Connection connection = null;
118      try {
119        connection = obtainConnection();
120        runDdlWith(connection);
121      } finally {
122        JdbcClose.rollback(connection);
123        JdbcClose.close(connection);
124      }
125    }
126  }
127
128  private void runDdlWith(Connection connection) {
129    try {
130      if (dbSchema != null) {
131        createSchemaIfRequired(connection);
132      }
133      runInitSql(connection);
134      runDropSql(connection);
135      runCreateSql(connection);
136      runSeedSql(connection);
137    } catch (IOException e) {
138      throw new RuntimeException("Error reading drop/create script from file system", e);
139    }
140  }
141
142  private Connection obtainConnection() {
143    try {
144      return server.dataSource().getConnection();
145    } catch (SQLException e) {
146      throw new PersistenceException("Failed to obtain connection to run DDL", e);
147    }
148  }
149
150  private void createSchemaIfRequired(Connection connection) {
151    try {
152      for (String schema : dbSchema.split(",")) {
153        server.databasePlatform().createSchemaIfNotExists(schema, connection);
154      }
155    } catch (SQLException e) {
156      throw new PersistenceException("Failed to create DB Schema", e);
157    }
158  }
159
160  /**
161   * Execute all the DDL statements in the script.
162   */
163  void runScript(Connection connection, boolean expectErrors, String content, String scriptName) {
164
165    DdlRunner runner = createDdlRunner(expectErrors, scriptName);
166    try {
167      if (expectErrors || ddlAutoCommit) {
168        connection.setAutoCommit(true);
169      }
170      runner.runAll(scriptTransform.transform(content), connection);
171      if (expectErrors || ddlAutoCommit) {
172        connection.setAutoCommit(false);
173      }
174      connection.commit();
175      runner.runNonTransactional(connection);
176
177    } catch (SQLException e) {
178      throw new PersistenceException("Failed to run script", e);
179    } finally {
180      JdbcClose.rollback(connection);
181    }
182  }
183
184  private DdlRunner createDdlRunner(boolean expectErrors, String scriptName) {
185    return new DdlRunner(expectErrors, scriptName, platformName);
186  }
187
188  protected void runDropSql(Connection connection) throws IOException {
189    if (!createOnly) {
190      if (extraDdl && jaxbPresent) {
191        String extraApply = ExtraDdlXmlReader.buildExtra(platform, true);
192        if (extraApply != null) {
193          runScript(connection, false, extraApply, "extra-ddl");
194        }
195      }
196
197      if (dropAllContent == null) {
198        dropAllContent = readFile(getDropFileName());
199      }
200      runScript(connection, true, dropAllContent, getDropFileName());
201    }
202  }
203
204  protected void runCreateSql(Connection connection) throws IOException {
205    if (createAllContent == null) {
206      createAllContent = readFile(getCreateFileName());
207    }
208    runScript(connection, false, createAllContent, getCreateFileName());
209
210    if (extraDdl && jaxbPresent) {
211      if (currentModel().isTablePartitioning()) {
212        String extraPartitioning = ExtraDdlXmlReader.buildPartitioning(platform);
213        if (extraPartitioning != null && !extraPartitioning.isEmpty()) {
214          runScript(connection, false, extraPartitioning, "builtin-partitioning-ddl");
215        }
216      }
217
218      String extraApply = ExtraDdlXmlReader.buildExtra(platform, false);
219      if (extraApply != null) {
220        runScript(connection, false, extraApply, "extra-ddl");
221      }
222
223      if (currentModel().isTablePartitioning()) {
224        checkInitialTablePartitions(connection);
225      }
226    }
227  }
228
229  /**
230   * Check if table partitions exist and if not create some. The expectation is that
231   * extra-ddl.xml should have some partition initialisation but this helps people get going.
232   */
233  private void checkInitialTablePartitions(Connection connection) {
234    DatabasePlatform databasePlatform = server.databasePlatform();
235    try {
236      StringBuilder sb = new StringBuilder();
237      for (MTable table : currentModel.getPartitionedTables()) {
238        String tableName = table.getName();
239        if (!databasePlatform.tablePartitionsExist(connection, tableName)) {
240          log.info("No table partitions for table {}", tableName);
241          PartitionMeta meta = table.getPartitionMeta();
242          String initPart = databasePlatform.tablePartitionInit(tableName, meta.getMode(), meta.getProperty(), table.singlePrimaryKey());
243          sb.append(initPart).append("\n");
244        }
245      }
246
247      String initialPartitionSql = sb.toString();
248      if (!initialPartitionSql.isEmpty()) {
249        runScript(connection, false, initialPartitionSql, "initial table partitions");
250      }
251
252    } catch (SQLException e) {
253      log.error("Error checking initial table partitions", e);
254    }
255  }
256
257  protected void runInitSql(Connection connection) throws IOException {
258    runResourceScript(connection, server.config().getDdlInitSql());
259  }
260
261  protected void runSeedSql(Connection connection) throws IOException {
262    runResourceScript(connection, server.config().getDdlSeedSql());
263  }
264
265  protected void runResourceScript(Connection connection, String sqlScript) throws IOException {
266    if (sqlScript != null) {
267      try (InputStream is = getClassLoader().getResourceAsStream(sqlScript)) {
268        if (is == null) {
269          log.warn("sql script {} was not found as a resource", sqlScript);
270        } else {
271          String content = readContent(new InputStreamReader(is));
272          runScript(connection, false, content, sqlScript);
273        }
274      }
275    }
276  }
277
278  /**
279   * Return the classLoader to use to read sql scripts as resources.
280   */
281  protected ClassLoader getClassLoader() {
282    ClassLoader cl = Thread.currentThread().getContextClassLoader();
283    if (cl == null) {
284      cl = this.getClassLoader();
285    }
286    return cl;
287  }
288
289  protected void writeDrop(String dropFile) {
290    try {
291      writeFile(dropFile, generateDropAllDdl());
292    } catch (IOException e) {
293      throw new PersistenceException("Error generating Drop DDL", e);
294    }
295  }
296
297  protected void writeCreate(String createFile) {
298    try {
299      writeFile(createFile, generateCreateAllDdl());
300    } catch (IOException e) {
301      throw new PersistenceException("Error generating Create DDL", e);
302    }
303  }
304
305  protected String generateDropAllDdl() {
306    try {
307      dropAllContent = currentModel().getDropAllDdl();
308      return dropAllContent;
309    } catch (IOException e) {
310      throw new RuntimeException(e);
311    }
312  }
313
314  protected String generateCreateAllDdl() {
315    try {
316      createAllContent = currentModel().getCreateDdl();
317      return createAllContent;
318    } catch (IOException e) {
319      throw new RuntimeException(e);
320    }
321  }
322
323  protected String getDropFileName() {
324    return server.name() + "-drop-all.sql";
325  }
326
327  protected String getCreateFileName() {
328    return server.name() + "-create-all.sql";
329  }
330
331  protected CurrentModel currentModel() {
332    if (currentModel == null) {
333      currentModel = new CurrentModel(server);
334    }
335    return currentModel;
336  }
337
338  protected void writeFile(String fileName, String fileContent) throws IOException {
339    File f = new File(baseDir, fileName);
340    try (FileWriter fw = new FileWriter(f)) {
341      fw.write(fileContent);
342      fw.flush();
343    }
344  }
345
346  protected String readFile(String fileName) throws IOException {
347    File f = new File(baseDir, fileName);
348    if (!f.exists()) {
349      return null;
350    }
351    return readContent(new FileReader(f));
352  }
353
354  protected String readContent(Reader reader) throws IOException {
355    StringBuilder buf = new StringBuilder();
356    try (LineNumberReader lineReader = new LineNumberReader(reader)) {
357      String s;
358      while ((s = lineReader.readLine()) != null) {
359        buf.append(s).append("\n");
360      }
361      return buf.toString();
362    }
363  }
364
365  /**
366   * Create the ScriptTransform for placeholder key/value replacement.
367   */
368  private ScriptTransform createScriptTransform(DatabaseConfig config) {
369    return ScriptTransform.build(config.getDdlPlaceholders(), config.getDdlPlaceholderMap());
370  }
371
372}