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}