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.getServerConfig(); 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.getDatabasePlatform(); 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.getDataSource().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.getDatabasePlatform().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.getDatabasePlatform(); 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.getServerConfig().getDdlInitSql()); 259 } 260 261 protected void runSeedSql(Connection connection) throws IOException { 262 runResourceScript(connection, server.getServerConfig().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.getName() + "-drop-all.sql"; 325 } 326 327 protected String getCreateFileName() { 328 return server.getName() + "-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}