+ * The logged SQL is accessed by LoggedSql. + *
+ */ +public class CapturingLoggerFactory implements SpiLoggerFactory { + + @Override + public SpiLogger create(String name) { + + DSpiLogger logger = new DSpiLogger(LoggerFactory.getLogger(name)); + if (name.equals("io.ebean.SQL")) { + return LoggedSql.register(logger); + } + return logger; + } +} diff --git a/ebean-test/src/main/java/io/ebean/test/DbJson.java b/ebean-test/src/main/java/io/ebean/test/DbJson.java new file mode 100644 index 000000000..b7fae39bb --- /dev/null +++ b/ebean-test/src/main/java/io/ebean/test/DbJson.java @@ -0,0 +1,110 @@ +package io.ebean.test; + +import io.ebean.DB; +import io.ebean.migration.util.IOUtils; + +import java.io.IOException; +import java.io.InputStream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Helper for testing to assert that the JSON form of an entity + * or list of entities match a String / typically test resource. + * + *{@code
+ *
+ * DbJson.of(timedEntries)
+ * .replace("id", "eventTime")
+ * .assertContentMatches("/assertJson/full-1-timed.json");
+ *
+ * }
+ */
+public class DbJson {
+
+ /**
+ * Create a PrettyJson object that has the JSON form of the
+ * entity bean or beans.
+ *
+ * {@code
+ *
+ * DbJson.of(timedEntries)
+ * .replace("id", "eventTime")
+ * .assertContentMatches("/assertJson/full-1-timed.json");
+ *
+ * }
+ */
+ public static PrettyJson of(Object bean) {
+ return new PrettyJson(DB.json().toJsonPretty(bean));
+ }
+
+ /**
+ * Read the content for the given resource path.
+ */
+ public static String readResource(String resourcePath) {
+ InputStream is = DbJson.class.getResourceAsStream(resourcePath);
+ try {
+ return IOUtils.readUtf8(is).trim();
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Contains the JSON of beans(s).
+ */
+ public static class PrettyJson {
+
+ private String placeHolder = "\"*\"";
+ private String rawJson;
+
+ PrettyJson(String rawJson) {
+ this.rawJson = rawJson;
+ }
+
+ /**
+ * Set the placeHolder to use when replacing property values.
+ */
+ public PrettyJson withPlaceholder(String placeHolder) {
+ this.placeHolder = placeHolder;
+ return this;
+ }
+
+ /**
+ * Replace the values of the given properties with a placeholder value.
+ * + * Typically we do this on generated properties such as id and timestamp properties. + *
+ */ + public PrettyJson replace(String... propertyNames) { + for (String propertyName : propertyNames) { + String placeholder = "\"" + propertyName + "\": " + placeHolder; + rawJson = rawJson.replaceAll("\"" + propertyName + "\": (\\d+)", placeholder); + rawJson = rawJson.replaceAll("\"" + propertyName + "\": \"(.*?)\"", placeholder); + } + return this; + } + + /** + * Return the JSON content. + */ + public String asJson() { + return rawJson; + } + + /** + * Assert the json matches the content at the given resource path. + * + *{@code
+ *
+ * DbJson.of(timedEntries)
+ * .replace("id", "eventTime")
+ * .assertContentMatches("/assertJson/full-1-timed.json");
+ *
+ * }
+ */
+ public void assertContentMatches(String resourcePath) {
+ assertThat(rawJson).isEqualTo(readResource(resourcePath));
+ }
+ }
+}
diff --git a/ebean-test/src/main/java/io/ebean/test/ForTests.java b/ebean-test/src/main/java/io/ebean/test/ForTests.java
new file mode 100644
index 000000000..2bf7a9fb5
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/ForTests.java
@@ -0,0 +1,115 @@
+package io.ebean.test;
+
+import io.ebean.DB;
+import io.ebean.Transaction;
+import io.ebeaninternal.api.HelpScopeTrans;
+
+/**
+ * Helper methods for testing.
+ */
+public class ForTests {
+
+ /**
+ * Enable or disable @Transactional methods.
+ * + * This is intended for testing purposes such that tests + * on code with {@code @Transactional} methods don't actually + * start or complete transactions. + *
+ * + * @param enable Set false to disable {@code @Transactional} methods + */ + public static void enableTransactional(boolean enable) { + HelpScopeTrans.setEnabled(enable); + } + + /** + * Run the closure with@Transactional methods
+ * effectively disabled (they won't create/commit transactions).
+ */
+ public static void noTransactional(Runnable run) {
+ try {
+ enableTransactional(false);
+ run.run();
+ } finally {
+ enableTransactional(true);
+ }
+ }
+
+ /**
+ * All transactions started in the closure are effectively rolled back.
+ * + * This creates a wrapping transaction that uses {@link Transaction#setNestedUseSavepoint()}. + * All nested transactions are created as savepoints. On completion the wrapping + * transaction is rolled back. + *
+ * + * @param run Closure that runs such that all the transactions are rolled back. + */ + public static void rollbackAll(Runnable run) { + + try (Transaction transaction = DB.beginTransaction()) { + transaction.setNestedUseSavepoint(); + run.run(); + + transaction.rollback(); + } + } + + /** + * Create and return a RollbackAll which should be closed at the end of the test(s). + *
+ * In tests for @Before we create the rollbackAll and on
+ * @After we close() it effectively rolling
+ * back all changes made during test execution.
+ *
{@code
+ *
+ * private ForTests.RollbackAll rollbackAll;
+ *
+ * @Before
+ * public void before() {
+ * rollbackAll = ForTests.createRollbackAll();
+ * }
+ *
+ * @After
+ * public void after() {
+ * rollbackAll.close();
+ * }
+ *
+ * ... tests execute and everything is rolled back
+ *
+ *
+ * }
+ */
+ public static RollbackAll createRollbackAll() {
+
+ final Transaction transaction = DB.beginTransaction();
+ transaction.setNestedUseSavepoint();
+ return new RollbackAll(transaction);
+ }
+
+ /**
+ * A wrapping transaction used in test code to rollback all changes.
+ *
+ * We must ensure that close() is called.
+ *
io.ebean.SQL.
+ * + * This is here to allow easy access to the SQL that was executed during testing and + * if desired we can use that to perform asserts in tests. + *
+ *{@code
+ *
+ * // start capturing SQL log messages
+ * LoggedSql.start();
+ *
+ * List customers =
+ * Customer.find.where()
+ * .name.ilike("rob%")
+ * .findList();
+ *
+ * assertNotNull(customers);
+ *
+ * // perform an insert
+ * new Product("ad", "asd").save()
+ *
+ *
+ * // return the captured SQL log messages
+ * // since LoggedSql.start()
+ * List sql = LoggedSql.stop();
+ *
+ * assertThat(sql).hasSize(2);
+ * assertThat(sql.get(0)).contains("from customer");
+ * assertThat(sql.get(1)).contains("into product");
+ *
+ * }
+ */
+public class LoggedSql {
+
+ private static CaptureLogger sqlLogger;
+
+ /**
+ * Internal use - register the logger for io.ebean.SQL.
+ */
+ static SpiLogger register(DSpiLogger logger) {
+ if (sqlLogger == null) {
+ sqlLogger = new CaptureLogger(logger);
+ }
+ return sqlLogger;
+ }
+
+ /**
+ * Start the capture of the io.ebean.SQL messages.
+ */
+ public static Listio.ebean.SQL messages and return the messages/sql
+ * that was captured since the call to start().
+ */
+ public static List+ * Unlike stop() collection of messages will continue. + *
+ */ + public static List+ * That is, the ebean-test-config plugin will check if there ia a CurrentUserProvider and if not + * automatically set one and that provider reads the 'current user' from this UserContext. + *
{@code
+ *
+ * // set the current userId which will be put
+ * // into 'WhoCreated' and 'WhoModified' properties
+ *
+ * UserContext.setUserId("U1");
+ *
+ * // persist bean that has ... a 'WhoModified' property
+ * Content content = new Content();
+ * content.setName("hello");
+ *
+ * content.save();
+ *
+ * }
+ */
+public class UserContext {
+
+ private static final UserContextThreadLocal local = new UserContextThreadLocal();
+
+ private Object userId;
+ private Object tenantId;
+
+ private UserContext() {
+ }
+
+ /**
+ * Return the current user.
+ */
+ public static Object currentUserId() {
+ return local.get().userId;
+ }
+
+ /**
+ * Return the current tenantId.
+ */
+ public static Object currentTenantId() {
+ return local.get().tenantId;
+ }
+
+ /**
+ * Set the current userId - this value is put into 'WhoCreated' and 'WhoModified' properties.
+ */
+ public static void setUserId(Object userId) {
+ local.get().userId = userId;
+ }
+
+ /**
+ * Set the current tenantId.
+ */
+ public static void setTenantId(Object tenantId) {
+ local.get().tenantId = tenantId;
+ }
+
+ /**
+ * Clear both the current userId and tenantId.
+ */
+ public static void reset() {
+ local.remove();
+ }
+
+ /**
+ * Set both the current userId and current tenantId.
+ */
+ public static void set(Object userId, String tenantId) {
+ UserContext userContext = local.get();
+ userContext.userId = userId;
+ userContext.tenantId = tenantId;
+ }
+
+ private static class UserContextThreadLocal extends ThreadLocal+ * Can setup and execute docker based databases and other containers. + * Can setup DataSource configuration (to match docker db setup). + * Can setup a CurrentUserProvider and CurrentTenantProvider for testing. + * Can setup a EncryptKeyManager for testing purposes with fixed key. + */ +public class AutoConfigureForTesting implements AutoConfigure { + + private static final Logger log = LoggerFactory.getLogger(AutoConfigureForTesting.class); + + /** + * System property that can override the platform. mvn clean test -Ddb=sqlserver + */ + private final String environmentDb = System.getProperty("db"); + + @Override + public void preConfigure(DatabaseConfig config) { + + Properties properties = config.getProperties(); + if (isExtraServer(config, properties)) { + setupExtraDataSourceIfNecessary(config); + return; + } + + String testPlatform = properties.getProperty("ebean.test.platform"); + log.debug("automatic testing config - with ebean.test.platform:{} environment db:{} name:{}", testPlatform, environmentDb, config.getName()); + + if (RunOnceMarker.isRun()) { + setupPlatform(environmentDb, config); + } + } + + @Override + public void postConfigure(DatabaseConfig config) { + setupProviders(config); + } + + /** + * Check if this is not the primary server and return true if that is the case. + */ + private boolean isExtraServer(DatabaseConfig config, Properties properties) { + String extraDb = properties.getProperty("ebean.test.extraDb.dbName", properties.getProperty("ebean.test.extraDb")); + if (extraDb != null && extraDb.equals(config.getName())) { + config.setDefaultServer(false); + return true; + } + return false; + } + + /** + * Setup the DataSource on the extra database if necessary. + */ + private void setupExtraDataSourceIfNecessary(DatabaseConfig config) { + DataSourceConfig dataSourceConfig = config.getDataSourceConfig(); + if (dataSourceConfig == null || dataSourceConfig.getUsername() == null) { + new PlatformAutoConfig(environmentDb, config) + .configExtraDataSource(); + } + } + + /** + * Setup support for Who, Multi-Tenant and DB encryption if they are not already set. + */ + private void setupProviders(DatabaseConfig config) { + new ProviderAutoConfig(config).run(); + } + + /** + * Setup the platform for testing including docker as needed and adjusting datasource config as needed. + */ + private void setupPlatform(String db, DatabaseConfig config) { + new PlatformAutoConfig(db, config).run(); + } +} diff --git a/ebean-test/src/main/java/io/ebean/test/config/RunOnceMarker.java b/ebean-test/src/main/java/io/ebean/test/config/RunOnceMarker.java new file mode 100644 index 000000000..8a9193fa0 --- /dev/null +++ b/ebean-test/src/main/java/io/ebean/test/config/RunOnceMarker.java @@ -0,0 +1,16 @@ +package io.ebean.test.config; + +import io.ebeaninternal.dbmigration.DbOffline; + +class RunOnceMarker { + + private static boolean hasRun; + + static synchronized boolean isRun() { + if (DbOffline.isSet() || hasRun) { + return false; + } + hasRun = true; + return true; + } +} diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/ClickHouseSetup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/ClickHouseSetup.java new file mode 100644 index 000000000..4dcb472b4 --- /dev/null +++ b/ebean-test/src/main/java/io/ebean/test/config/platform/ClickHouseSetup.java @@ -0,0 +1,41 @@ +package io.ebean.test.config.platform; + +import java.util.Properties; + +class ClickHouseSetup implements PlatformSetup { + + @Override + public Properties setup(Config config) { + + config.ddlMode("dropCreate"); + config.setDefaultPort(8123); + config.setUsername("default"); + config.setPassword(""); + config.setUrl("jdbc:clickhouse://localhost:${port}/${databaseName}"); + config.setDriver("ru.yandex.clickhouse.ClickHouseDriver"); + config.datasourceDefaults(); + + return dockerProperties(config); + } + + private Properties dockerProperties(Config dbConfig) { + + if (!dbConfig.isUseDocker()) { + return new Properties(); + } + + dbConfig.setDockerVersion("latest"); + return dbConfig.getDockerProperties(); + } + + @Override + public void setupExtraDbDataSource(Config config) { + // not supported yet + } + + @Override + public boolean isLocal() { + return false; + } + +} diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/CockroachSetup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/CockroachSetup.java new file mode 100644 index 000000000..879f895ee --- /dev/null +++ b/ebean-test/src/main/java/io/ebean/test/config/platform/CockroachSetup.java @@ -0,0 +1,41 @@ +package io.ebean.test.config.platform; + +import java.util.Properties; + +class CockroachSetup implements PlatformSetup { + + @Override + public Properties setup(Config config) { + + config.ddlMode("dropCreate"); + config.setDefaultPort(26257); + config.setUsername("root"); + config.setPassword(""); + config.setUrl("jdbc:postgresql://localhost:${port}/${databaseName}?sslmode=disable"); + config.setDriver("org.postgresql.Driver"); + config.datasourceDefaults(); + + return dockerProperties(config); + } + + private Properties dockerProperties(Config dbConfig) { + + if (!dbConfig.isUseDocker()) { + return new Properties(); + } + + dbConfig.setDockerVersion("v19.1.3"); + return dbConfig.getDockerProperties(); + } + + @Override + public void setupExtraDbDataSource(Config config) { + // not supported yet + } + + @Override + public boolean isLocal() { + return false; + } + +} diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java b/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java new file mode 100644 index 000000000..391de1191 --- /dev/null +++ b/ebean-test/src/main/java/io/ebean/test/config/platform/Config.java @@ -0,0 +1,454 @@ +package io.ebean.test.config.platform; + +import io.ebean.config.DatabaseConfig; +import io.ebean.datasource.DataSourceConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.util.Properties; + +/** + * Config for a database / datasource with associated DDL mode and Docker configuration. + */ +class Config { + + private static final Logger log = LoggerFactory.getLogger(Config.class); + + /** + * Common optional docker parameters that we just transfer to docker properties. + */ + private static final String[] DOCKER_TEST_PARAMS = {"fastStartMode", "inMemory", "initSqlFile", "seedSqlFile", "adminUser", "adminPassword", "extraDb", "extraDb.dbName", "extraDb.username", "extraDb.password", "extraDb.initSqlFile", "extraDb.seedSqlFile"}; + private static final String[] DOCKER_PLATFORM_PARAMS = {"containerName", "image", "internalPort", "startMode", "stopMode", "shutdown", "maxReadyAttempts", "tmpfs", "collation", "characterSet"}; + + private static final String DDL_MODE_OPTIONS = "dropCreate, create, none, migration, createOnly or migrationDropCreate"; + + private final String db; + private final String platform; + private String dockerPlatform; + + private String databaseName; + + private final Properties properties; + + private int port; + + private String url; + private String driver; + private String schema; + private String username; + private String password; + + private final DatabaseConfig config; + + private boolean containerDropCreate; + + private final Properties dockerProperties = new Properties(); + + Config(String db, String platform, String databaseName, DatabaseConfig config) { + this.db = db; + this.platform = platform; + this.dockerPlatform = platform; + this.databaseName = databaseName; + this.config = config; + this.properties = config.getProperties(); + } + + void setSchemaFromDbName(String newDbName) { + this.schema = databaseName; + this.databaseName = newDbName; + } + + /** + * Set the docker platform name (when it is different from the test platform name). + * For example test platform name of "postgis" maps to "postgres" docker platform name. + */ + void setDockerPlatform(String dockerPlatform) { + this.dockerPlatform = dockerPlatform; + } + + void setDefaultPort(int defaultPort) { + String val = getPlatformKey("port", null); + if (val != null) { + port = Integer.parseInt(val); + } else { + port = defaultPort; + } + } + + void ddlMode(String defaultMode) { + String ddlMode = properties.getProperty("ebean.test.ddlMode", defaultMode); + if (ddlMode == null) { + throw new IllegalStateException("No ebean.test.ddlMode set? Expect one of " + DDL_MODE_OPTIONS); + } + switch (ddlMode.toLowerCase()) { + case "none": { + disableMigrationRun(); + break; + } + case "migrationonly": + case "migrationsonly": { + setMigrationRun(); + break; + } + case "migrationdropcreate": + case "migrationsdropcreate": + case "migration": + case "migrations": { + setMigrationRun(); + containerDropCreate = true; + break; + } + case "createonly": { + setCreate(); + break; + } + case "create": { + containerDropCreate = true; + setCreate(); + break; + } + case "dropcreate": { + setDropCreate(); + break; + } + case "runonly": { + setRunOnly(); + break; + } + default: + throw new IllegalStateException("Unknown ebean.test.ddlMode [" + ddlMode + "] expecting one of " + DDL_MODE_OPTIONS); + } + } + + private void setCreate() { + setDropCreate(); + config.setDdlCreateOnly(true); + setDdlProperty("createOnly"); + } + + private void setDropCreate() { + disableMigrationRun(); + config.setDdlGenerate(true); + config.setDdlRun(true); + setDdlProperty("generate"); + setDdlProperty("run"); + setDdlInitSeed(); + } + + private void setRunOnly() { + disableMigrationRun(); + config.setDdlGenerate(false); + config.setDdlRun(true); + setDdlProperty("run"); + setDdlInitSeed(); + } + + private void setDdlInitSeed() { + final String initSql = getKey("initSql", null); + if (initSql != null) { + setProperty("ebean." + db + ".ddl.initSql", initSql); + } + final String seedSql = getKey("seedSql", null); + if (seedSql != null) { + setProperty("ebean." + db + ".ddl.seedSql", seedSql); + } + } + + private void setMigrationRun() { + config.getMigrationConfig().setRunMigration(true); + setProperty("ebean." + db + ".migration.run", "true"); + } + + private void disableMigrationRun() { + System.setProperty("ddl.migration.run", "false"); + } + + /** + * Override the dataSource property. + */ + private void setDdlProperty(String key) { + setProperty("ebean." + db + ".ddl." + key, "true"); + } + + DataSourceConfig datasourceDefaults() { + return datasourceDefaults(platform); + } + + void extraDatasourceDefaults() { + datasourceDefaults("extraDb"); + } + + private DataSourceConfig datasourceDefaults(String platform) { + // default username to databaseName + if (username == null) { + throw new IllegalStateException("username not set?"); + } + if (password == null) { + throw new IllegalStateException("password not set?"); + } + + DataSourceConfig ds = new DataSourceConfig(); + ds.setUsername(datasourceProperty(platform, "username", username)); + ds.setPassword(datasourceProperty(platform, "password", password)); + ds.setOwnerUsername(datasourceProperty(platform, "ownerUsername", null)); + ds.setOwnerPassword(datasourceProperty(platform, "ownerPassword", null)); + ds.setUrl(datasourceProperty(platform, "url", url)); + String driverClass = datasourceProperty(platform, "driver", driver); + ds.setDriver(driverClass); + config.setDataSourceConfig(ds); + + log.info("Using jdbc settings - username:{} url:{} driver:{}", ds.getUsername(), ds.getUrl(), ds.getDriver()); + + if (driverClass != null) { + try { + Class.forName(driverClass); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("JDBC Driver " + driverClass + " does not appear to be in the classpath?"); + } + } + return ds; + } + + String datasourceProperty(String key, String defaultValue) { + return datasourceProperty(platform, key, defaultValue); + } + + /** + * Override the dataSource property. + */ + private String datasourceProperty(String platform, String key, String defaultValue) { + String val = getTestKey(platform, key, defaultValue); + if (val != null) { + setProperty("datasource." + db + "." + key, val); + } + return val; + } + + private void setProperty(String dsKey, String val) { + properties.setProperty(dsKey, val); + } + + void setUrl(String urlPattern) { + String val = getPlatformKey("url", urlPattern); + val = val.replace("${port}", String.valueOf(port)); + val = val.replace("${databaseName}", databaseName); + this.url = val; + } + + /** + * Append to the connection URL. + */ + void urlAppend(String dbSchemaSuffix) { + this.url += dbSchemaSuffix; + } + + void setDriver(String driver) { + this.driver = getPlatformKey("driver", driver); + } + + void setPasswordDefault() { + setPassword("test"); + } + + void setExtraDbPasswordDefault() { + setExtraDbPassword("test"); + } + + private String deriveDbSchema() { + String dbSchema = properties.getProperty("ebean.dbSchema", config.getDbSchema()); + dbSchema = properties.getProperty("ebean.test.dbSchema", dbSchema); + return getPlatformKey("schema", dbSchema); + } + + /** + * Set the username to default to database name. + */ + void setUsernameDefault() { + this.schema = first(deriveDbSchema()); + String defaultValue = schema != null ? schema : getPlatformKey("databaseName", this.databaseName); + this.username = getKey("username", defaultValue); + } + + void setUsernameDefaultSchema() { + this.username = getKey("username", schema); + } + + void setExtraUsernameDefault() { + this.username = getKey("extraDb.username", this.databaseName); + } + + private String first(String dbSchema) { + if (dbSchema == null) { + return null; + } + String[] schemas = dbSchema.split(","); + if (schemas.length > 1) { + // multiple schemas specified so just use the first one + return schemas[0]; + } + return dbSchema; + } + + String getUsername() { + return username; + } + + String getSchema() { + return schema; + } + + private void setExtraDbPassword(String password) { + this.password = getKey("extraDb.password", password); + } + + void setPassword(String password) { + this.password = getKey("password", password); + } + + void setUsername(String username) { + this.username = getPlatformKey("username", username); + } + + void setDatabaseName(String databaseName) { + this.databaseName = getPlatformKey("databaseName", databaseName); + } + + boolean isUseDocker() { + String val = getPlatformKey("useDocker", properties.getProperty("ebean.test.useDocker")); + return val == null || !val.equalsIgnoreCase("false"); + } + + void setDockerVersion(String version) { + String val = getPlatformKey("version", version); + dockerProperties.setProperty(dockerKey("version"), val); + + if (containerDropCreate) { + dockerProperties.setProperty(dockerKey("startMode"), "dropCreate"); + } + String mode = properties.getProperty("ebean.test.containerMode"); + if (mode != null) { + dockerProperties.setProperty(dockerKey("startMode"), mode); + } + initDockerProperties(); + } + + void setDockerContainerName(String containerName) { + dockerProperties.setProperty(dockerKey("containerName"), getPlatformKey("containerName", containerName)); + } + + void setDockerImage(String defaultImage) { + dockerProperties.setProperty(dockerKey("image"), getPlatformKey("image", defaultImage)); + } + + void setExtensions(String defaultValue) { + // ebean.test.postgres.extensions=hstore,pgcrypto + String val = getPlatformKey("extensions", defaultValue); + if (val != null) { + dockerProperties.setProperty(dockerKey("extensions"), trimExtensions(val)); + } + } + + String trimExtensions(String val) { + val = val.replaceAll(" ", ""); + val = val.replaceAll(",,", ","); + return val; + } + + private String getTestKey(String platform, String key, String defaultValue) { + return properties.getProperty("ebean.test." + platform + "." + key, defaultValue); + } + + String getPlatformKey(String key, String defaultValue) { + return properties.getProperty("ebean.test." + platform + "." + key, defaultValue); + } + + private String getKey(String key, String defaultValue) { + defaultValue = properties.getProperty("ebean.test." + key, defaultValue); + return properties.getProperty("ebean.test." + platform + "." + key, defaultValue); + } + + private void initDockerProperties() { + + dockerProperties.setProperty(dockerKey("port"), String.valueOf(port)); + dockerProperties.setProperty(dockerKey("dbName"), databaseName); + if (schema != null) { + dockerProperties.setProperty(dockerKey("schema"), schema); + } + dockerProperties.setProperty(dockerKey("username"), username); + dockerProperties.setProperty(dockerKey("password"), password); + dockerProperties.setProperty(dockerKey("url"), url); + if (driver != null) { + dockerProperties.setProperty(dockerKey("driver"), driver); + } + setDockerOptionalParameters(); + } + + private void setDockerOptionalParameters() { + + // check for shutdown mode on all containers + String mode = properties.getProperty("ebean.test.shutdown"); + if (mode != null && !ignoreDockerShutdown()) { + dockerProperties.setProperty(dockerKey("shutdown"), mode); + } + for (String key : DOCKER_TEST_PARAMS) { + String val = getKey(key, null); + val = properties.getProperty("docker." + platform + "." + key, val); + if (val != null) { + dockerProperties.setProperty(dockerKey(key), val); + } + } + for (String key : DOCKER_PLATFORM_PARAMS) { + String val = getPlatformKey(key, null); + val = properties.getProperty("docker." + platform + "." + key, val); + if (val != null) { + dockerProperties.setProperty(dockerKey(key), val); + } + } + } + + /** + * For local development we might want to ignore docker shutdown. + *
+ * So we just want the shutdown mode to be used on the CI server.
+ */
+ boolean ignoreDockerShutdown() {
+ String localDev = properties.getProperty("ebean.test.localDevelopment", "~/.ebean/ignore-docker-shutdown");
+ return ignoreDockerShutdown(localDev);
+ }
+
+ boolean ignoreDockerShutdown(String localDev) {
+
+ if (localDev.startsWith("~/")) {
+ File homeDir = new File(System.getProperty("user.home"));
+ return new File(homeDir, localDev.substring(2)).exists();
+ }
+
+ return new File(localDev).exists();
+ }
+
+ private String dockerKey(String key) {
+ return dockerPlatform + "." + key;
+ }
+
+ Properties getDockerProperties() {
+ return dockerProperties;
+ }
+
+ /**
+ * Pretty much only for SqlServer as we have the 2 platforms we need to choose from.
+ */
+ void setDatabasePlatformName() {
+ String databasePlatformName = getPlatformKey("databasePlatformName", null);
+ if (databasePlatformName != null) {
+ setProperty("ebean." + db + ".databasePlatformName", databasePlatformName);
+ }
+ }
+
+ /**
+ * Return the docker platform name. Should be a name that ebean-test-docker understands.
+ */
+ String getDockerPlatform() {
+ return dockerPlatform;
+ }
+}
diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/ElasticSearchSetup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/ElasticSearchSetup.java
new file mode 100644
index 000000000..011bfcbd2
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/config/platform/ElasticSearchSetup.java
@@ -0,0 +1,77 @@
+package io.ebean.test.config.platform;
+
+import io.ebean.docker.commands.ElasticConfig;
+import io.ebean.docker.commands.ElasticContainer;
+
+import java.util.Properties;
+
+/**
+ * Setup and start a Docker container for ElasticSearch.
+ */
+class ElasticSearchSetup {
+
+ private static final String[] DOCKER_PARAMS = {"containerName", "image", "internalPort", "startMode", "shutdown"};
+
+ private final Properties config;
+
+ ElasticSearchSetup(Properties config) {
+ this.config = config;
+ }
+
+ void run() {
+ ElasticConfig elasticConfig = readConfig();
+ if (elasticConfig != null) {
+ new ElasticContainer(elasticConfig).start();
+ }
+ }
+
+ ElasticConfig readConfig() {
+
+ String version = read("version", null);
+ if (version == null) {
+ // we need an explicit version to run
+ return null;
+ }
+
+ return new ElasticConfig(version, populateDockerProperties(version));
+ }
+
+ private Properties populateDockerProperties(String version) {
+
+ PropertiesBuilder properties = new PropertiesBuilder();
+
+ String mode = config.getProperty("ebean.test.shutdown");
+ if (mode != null) {
+ properties.set("shutdown", mode);
+ }
+
+ properties.set("version", version);
+ properties.set("port", read("port", "9201"));
+ for (String dockerParam : DOCKER_PARAMS) {
+ String val = read(dockerParam, null);
+ if (val != null) {
+ properties.set(dockerParam, val);
+ }
+ }
+
+ return properties.build();
+ }
+
+ private String read(String key, String defaultValue) {
+ return config.getProperty("ebean.docstore.elastic." + key, defaultValue);
+ }
+
+ private static class PropertiesBuilder {
+
+ private Properties dockerProperties = new Properties();
+
+ private void set(String key, String val) {
+ dockerProperties.setProperty("elastic." + key, val);
+ }
+
+ private Properties build() {
+ return dockerProperties;
+ }
+ }
+
+}
diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/H2Setup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/H2Setup.java
new file mode 100644
index 000000000..5e94ae8d9
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/config/platform/H2Setup.java
@@ -0,0 +1,30 @@
+package io.ebean.test.config.platform;
+
+import java.util.Properties;
+
+class H2Setup implements PlatformSetup {
+
+ @Override
+ public Properties setup(Config config) {
+
+ config.ddlMode("create");
+ config.setUsername("sa");
+ config.setPassword("");
+ config.setUrl("jdbc:h2:mem:${databaseName}");
+ config.setDriver("org.h2.Driver");
+ config.datasourceDefaults();
+
+ // return empty properties
+ return new Properties();
+ }
+
+ @Override
+ public void setupExtraDbDataSource(Config config) {
+ // not supported yet
+ }
+
+ @Override
+ public boolean isLocal() {
+ return true;
+ }
+}
diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/HanaSetup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/HanaSetup.java
new file mode 100644
index 000000000..8ad014b70
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/config/platform/HanaSetup.java
@@ -0,0 +1,67 @@
+package io.ebean.test.config.platform;
+
+import java.util.Properties;
+
+class HanaSetup implements PlatformSetup {
+
+ @Override
+ public Properties setup(Config config) {
+
+ config.setDatabasePlatformName();
+
+ config.ddlMode("dropCreate");
+ int instanceNumber = Integer.parseInt(config.getPlatformKey("instanceNumber", "90"));
+ if (instanceNumber >= 0 && instanceNumber <= 99) {
+ config.setDefaultPort(30017 + (instanceNumber * 100));
+ } else {
+ config.setDefaultPort(39017);
+ }
+ config.setUsernameDefault();
+ config.setUsername("SYSTEM");
+ config.setPassword("HXEHana1");
+ config.setDatabaseName("HXE");
+ config.setUrl("jdbc:sap://localhost:${port}/?databaseName=${databaseName}");
+ String schema = config.getSchema();
+ if (schema != null && !schema.equals(config.getUsername())) {
+ config.urlAppend("¤tSchema=" + schema);
+ }
+ config.setDriver("com.sap.db.jdbc.Driver");
+ config.datasourceDefaults();
+
+ return dockerProperties(config);
+ }
+
+ private Properties dockerProperties(Config dbConfig) {
+
+ if (!dbConfig.isUseDocker()) {
+ return new Properties();
+ }
+
+ dbConfig.setDockerVersion("latest");
+
+ setDockerProperty("agreeToSapLicense", String.valueOf(false), dbConfig);
+ setDockerProperty("passwordsUrl", null, dbConfig);
+ setDockerProperty("mountsDirectory", null, dbConfig);
+ setDockerProperty("instanceNumber", null, dbConfig);
+
+ return dbConfig.getDockerProperties();
+ }
+
+ @Override
+ public boolean isLocal() {
+ return false;
+ }
+
+ @Override
+ public void setupExtraDbDataSource(Config config) {
+ // not supported yet
+ }
+
+ private void setDockerProperty(String key, String defaultValue, Config dbConfig) {
+ String value = dbConfig.getPlatformKey(key, defaultValue);
+ if (value != null) {
+ dbConfig.getDockerProperties().put("hana." + key, value);
+ }
+ }
+
+}
diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/MariaDBSetup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/MariaDBSetup.java
new file mode 100644
index 000000000..4e242d4b0
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/config/platform/MariaDBSetup.java
@@ -0,0 +1,40 @@
+package io.ebean.test.config.platform;
+
+import java.util.Properties;
+
+class MariaDBSetup implements PlatformSetup {
+
+ @Override
+ public Properties setup(Config config) {
+
+ int defaultPort = config.isUseDocker() ? 4306 : 3306;
+
+ config.ddlMode("dropCreate");
+ config.setDefaultPort(defaultPort);
+ config.setUsernameDefault();
+ config.setPasswordDefault();
+ config.setUrl("jdbc:mariadb://localhost:${port}/${databaseName}?useLegacyDatetimeCode=false");
+ config.datasourceDefaults();
+
+ return dockerProperties(config);
+ }
+
+ private Properties dockerProperties(Config dbConfig) {
+ if (!dbConfig.isUseDocker()) {
+ return new Properties();
+ }
+ dbConfig.setDockerVersion("10");
+ return dbConfig.getDockerProperties();
+ }
+
+ @Override
+ public void setupExtraDbDataSource(Config config) {
+ // not supported yet
+ }
+
+ @Override
+ public boolean isLocal() {
+ return false;
+ }
+
+}
diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/MySqlSetup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/MySqlSetup.java
new file mode 100644
index 000000000..3939ffbec
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/config/platform/MySqlSetup.java
@@ -0,0 +1,53 @@
+package io.ebean.test.config.platform;
+
+import java.util.Properties;
+
+class MySqlSetup implements PlatformSetup {
+
+ @Override
+ public Properties setup(Config config) {
+
+ int defaultPort = config.isUseDocker() ? 4306 : 3306;
+
+ config.ddlMode("dropCreate");
+ config.setDefaultPort(defaultPort);
+ config.setUsernameDefault();
+ config.setPasswordDefault();
+ config.setUrl("jdbc:mysql://localhost:${port}/${databaseName}");
+ config.setDriver(defaultDriver());
+ config.datasourceDefaults();
+
+ return dockerProperties(config);
+ }
+
+ private String defaultDriver() {
+ try {
+ String newDriver = "com.mysql.cj.jdbc.Driver";
+ Class.forName(newDriver);
+ return newDriver;
+ } catch (ClassNotFoundException e) {
+ return "com.mysql.jdbc.Driver";
+ }
+ }
+
+ private Properties dockerProperties(Config dbConfig) {
+
+ if (!dbConfig.isUseDocker()) {
+ return new Properties();
+ }
+
+ dbConfig.setDockerVersion("8.0");
+ return dbConfig.getDockerProperties();
+ }
+
+ @Override
+ public void setupExtraDbDataSource(Config config) {
+ // not supported yet
+ }
+
+ @Override
+ public boolean isLocal() {
+ return false;
+ }
+
+}
diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/NuoDBSetup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/NuoDBSetup.java
new file mode 100644
index 000000000..b339c24a2
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/config/platform/NuoDBSetup.java
@@ -0,0 +1,45 @@
+package io.ebean.test.config.platform;
+
+import io.ebean.datasource.DataSourceConfig;
+
+import java.util.Properties;
+
+class NuoDBSetup implements PlatformSetup {
+
+ @Override
+ public Properties setup(Config config) {
+ // use testdb as our standard db name and instead use schema
+ config.setSchemaFromDbName("testdb");
+ config.ddlMode("dropCreate");
+ config.setDefaultPort(8888);
+ config.setUsernameDefaultSchema();
+ config.setPasswordDefault();
+ config.setUrl("jdbc:com.nuodb://localhost/testdb");
+ config.setDriver("com.nuodb.jdbc.Driver");
+
+ final DataSourceConfig dsConfig = config.datasourceDefaults();
+ dsConfig.setSchema(config.getSchema());
+ return dockerProperties(config);
+ }
+
+ private Properties dockerProperties(Config dbConfig) {
+
+ if (!dbConfig.isUseDocker()) {
+ return new Properties();
+ }
+
+ dbConfig.setDockerVersion("4.0.0");
+ return dbConfig.getDockerProperties();
+ }
+
+ @Override
+ public void setupExtraDbDataSource(Config config) {
+ // not supported yet
+ }
+
+ @Override
+ public boolean isLocal() {
+ return false;
+ }
+
+}
diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/OracleSetup.java b/ebean-test/src/main/java/io/ebean/test/config/platform/OracleSetup.java
new file mode 100644
index 000000000..b93660615
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/config/platform/OracleSetup.java
@@ -0,0 +1,41 @@
+package io.ebean.test.config.platform;
+
+import java.util.Properties;
+
+class OracleSetup implements PlatformSetup {
+
+ @Override
+ public Properties setup(Config config) {
+
+ config.ddlMode("dropCreate");
+ config.setDefaultPort(1521);
+ config.setUsernameDefault();
+ config.setPasswordDefault();
+ config.setDatabaseName("XE");
+ config.setUrl("jdbc:oracle:thin:@localhost:${port}:${databaseName}");
+ config.setDriver("oracle.jdbc.driver.OracleDriver");
+ config.datasourceDefaults();
+ return dockerProperties(config);
+ }
+
+ private Properties dockerProperties(Config dbConfig) {
+
+ if (!dbConfig.isUseDocker()) {
+ return new Properties();
+ }
+
+ dbConfig.setDockerVersion("latest");
+ return dbConfig.getDockerProperties();
+ }
+
+ @Override
+ public void setupExtraDbDataSource(Config config) {
+ // not supported yet
+ }
+
+ @Override
+ public boolean isLocal() {
+ return false;
+ }
+
+}
diff --git a/ebean-test/src/main/java/io/ebean/test/config/platform/PlatformAutoConfig.java b/ebean-test/src/main/java/io/ebean/test/config/platform/PlatformAutoConfig.java
new file mode 100644
index 000000000..dc87516f2
--- /dev/null
+++ b/ebean-test/src/main/java/io/ebean/test/config/platform/PlatformAutoConfig.java
@@ -0,0 +1,157 @@
+package io.ebean.test.config.platform;
+
+import io.ebean.config.DatabaseConfig;
+import io.ebean.docker.container.ContainerFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import static java.util.concurrent.CompletableFuture.allOf;
+import static java.util.concurrent.CompletableFuture.runAsync;
+
+public class PlatformAutoConfig {
+
+ private static final Logger log = LoggerFactory.getLogger(PlatformAutoConfig.class);
+
+ /**
+ * Known platforms we can setup locally or via docker container.
+ */
+ private static final Map