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