├── .gitignore ├── src ├── main │ ├── resources │ │ └── gradle-plugins │ │ │ └── com.revolut.jooq-docker.properties │ ├── kotlin │ │ └── com │ │ │ └── revolut │ │ │ └── jooq │ │ │ ├── JooqDockerPlugin.kt │ │ │ ├── DatabaseHostResolver.kt │ │ │ ├── FlywaySchemaVersionProvider.kt │ │ │ ├── WaitForPortStrategy.kt │ │ │ ├── SchemaPackageRenameGeneratorStrategy.kt │ │ │ ├── JooqExtension.kt │ │ │ ├── Docker.kt │ │ │ └── GenerateJooqClassesTask.kt │ └── java │ │ └── com │ │ └── revolut │ │ └── shaded │ │ └── org │ │ └── testcontainers │ │ ├── utility │ │ ├── AuthConfigUtil.java │ │ ├── DockerImageName.java │ │ └── RegistryAuthLocator.java │ │ └── dockerclient │ │ └── auth │ │ └── AuthDelegatingDockerClientConfig.java └── test │ ├── resources │ ├── V01__init.sql │ ├── V02__add_bar.sql │ ├── V01__init_mysql.sql │ ├── V01__init_with_placeholders.sql │ └── V01__init_multiple_schemas.sql │ └── groovy │ ├── SchemaPackageRenameGeneratorStrategySpec.groovy │ ├── WaitForPortStrategySpec.groovy │ ├── DatabaseHostResolverSpec.groovy │ └── JooqDockerPluginSpec.groovy ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── .travis.yml ├── .gitattributes ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle 2 | .gradle/ 3 | build/ 4 | 5 | #Intellij project files 6 | .idea/ 7 | out/ 8 | *.iml -------------------------------------------------------------------------------- /src/main/resources/gradle-plugins/com.revolut.jooq-docker.properties: -------------------------------------------------------------------------------- 1 | implementation-class=com.revolut.jooq.JooqDockerPlugin -------------------------------------------------------------------------------- /src/test/resources/V01__init.sql: -------------------------------------------------------------------------------- 1 | create table foo 2 | ( 3 | id UUID primary key, 4 | data JSONB not null 5 | ); -------------------------------------------------------------------------------- /src/test/resources/V02__add_bar.sql: -------------------------------------------------------------------------------- 1 | create table bar 2 | ( 3 | id UUID primary key, 4 | data JSONB not null 5 | ); -------------------------------------------------------------------------------- /src/test/resources/V01__init_mysql.sql: -------------------------------------------------------------------------------- 1 | create table foo 2 | ( 3 | id INT PRIMARY KEY, 4 | data JSON not null 5 | ); -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/revolut-engineering/jooq-plugin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/resources/V01__init_with_placeholders.sql: -------------------------------------------------------------------------------- 1 | create table foo 2 | ( 3 | id INT primary key, 4 | data TEXT not null 5 | ); 6 | 7 | insert into foo(id, data) 8 | values (1, 'some value with placeholder ${placeholder}') -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.gradle.enterprise") version "3.16.2" 3 | } 4 | 5 | 6 | gradleEnterprise { 7 | buildScan { 8 | termsOfServiceUrl = "https://gradle.com/terms-of-service" 9 | termsOfServiceAgree = "yes" 10 | } 11 | } 12 | 13 | rootProject.name = "jooq-plugin" 14 | -------------------------------------------------------------------------------- /src/test/resources/V01__init_multiple_schemas.sql: -------------------------------------------------------------------------------- 1 | create schema if not exists public; 2 | create schema if not exists other; 3 | 4 | create table public.foo 5 | ( 6 | id UUID primary key, 7 | data JSONB not null 8 | ); 9 | 10 | create table other.bar 11 | ( 12 | id UUID primary key, 13 | data JSONB not null 14 | ); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: true 3 | 4 | services: 5 | - docker 6 | 7 | jdk: 8 | - oraclejdk17 9 | 10 | script: 11 | - ./gradlew build jacocoTestReport --scan -s 12 | 13 | before_cache: 14 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 15 | - rm -rf $HOME/.gradle/caches/*/plugin-resolution/ 16 | 17 | cache: 18 | directories: 19 | - $HOME/.gradle/caches/ 20 | - $HOME/.gradle/wrapper 21 | 22 | after_success: 23 | - bash <(curl -s https://codecov.io/bash) 24 | -------------------------------------------------------------------------------- /src/main/kotlin/com/revolut/jooq/JooqDockerPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.revolut.jooq 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | 6 | open class JooqDockerPlugin : Plugin { 7 | 8 | override fun apply(project: Project) { 9 | project.extensions.create("jooq", JooqExtension::class.java, project.name) 10 | project.configurations.create("jdbc") 11 | project.tasks.create("generateJooqClasses", GenerateJooqClassesTask::class.java) { 12 | group = "jooq" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/revolut/jooq/DatabaseHostResolver.kt: -------------------------------------------------------------------------------- 1 | package com.revolut.jooq 2 | 3 | import java.net.URI 4 | 5 | class DatabaseHostResolver(private val dbHostOverride: String?) { 6 | 7 | @Throws(IllegalStateException::class) 8 | fun resolveHost(dockerHost: URI): String { 9 | return dbHostOverride ?: resolveFromDockerHost(dockerHost) 10 | } 11 | 12 | @Throws(IllegalStateException::class) 13 | private fun resolveFromDockerHost(dockerHost: URI): String { 14 | return when (dockerHost.scheme) { 15 | in arrayOf("http", "https", "tcp") -> dockerHost.host 16 | in arrayOf("unix", "npipe") -> "localhost" 17 | else -> throw IllegalStateException("could not resolve docker host for $dockerHost, " + 18 | "please override it in plugin config \"jooq.db.hostOverride\"") 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # These settings are for any web project 2 | 3 | # Handle line endings automatically for files detected as text 4 | # and leave all files detected as binary untouched. 5 | * text=auto 6 | 7 | # Force the following filetypes to have unix eols, so Windows does not break them 8 | *.* text eol=lf 9 | 10 | # Windows forced line-endings 11 | /.idea/* text eol=crlf 12 | gradlew.bat text eol=crlf 13 | 14 | # 15 | ## These files are binary and should be left untouched 16 | # 17 | 18 | # (binary is a macro for -text -diff) 19 | *.png binary 20 | *.jpg binary 21 | *.jpeg binary 22 | *.gif binary 23 | *.ico binary 24 | *.mov binary 25 | *.mp4 binary 26 | *.mp3 binary 27 | *.flv binary 28 | *.fla binary 29 | *.swf binary 30 | *.gz binary 31 | *.zip binary 32 | *.7z binary 33 | *.ttf binary 34 | *.eot binary 35 | *.woff binary 36 | *.pyc binary 37 | *.pdf binary 38 | *.ez binary 39 | *.bz2 binary 40 | *.swp binary 41 | *.jar binary 42 | -------------------------------------------------------------------------------- /src/main/kotlin/com/revolut/jooq/FlywaySchemaVersionProvider.kt: -------------------------------------------------------------------------------- 1 | package com.revolut.jooq 2 | 3 | import org.jooq.impl.DSL.* 4 | import org.jooq.meta.SchemaDefinition 5 | import org.jooq.meta.SchemaVersionProvider 6 | 7 | class FlywaySchemaVersionProvider : SchemaVersionProvider { 8 | companion object { 9 | private val defaultSchemaName = ThreadLocal() 10 | private val flywayTableName = ThreadLocal() 11 | 12 | fun setup(defaultSchemaName: String, flywayTableName: String) { 13 | this.defaultSchemaName.set(defaultSchemaName) 14 | this.flywayTableName.set(flywayTableName) 15 | } 16 | } 17 | 18 | override fun version(schema: SchemaDefinition): String? { 19 | return schema.database.create() 20 | .select(max(field("version")).`as`("max_version")) 21 | .from(table(name(defaultSchemaName.get(), flywayTableName.get()))) 22 | .fetchSingle("max_version", String::class.java) 23 | } 24 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/revolut/jooq/WaitForPortStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.revolut.jooq 2 | 3 | import java.io.IOException 4 | import java.net.Socket 5 | import java.time.Duration 6 | import java.time.Instant 7 | import java.util.concurrent.TimeoutException 8 | 9 | class WaitForPortStrategy { 10 | companion object { 11 | @JvmStatic 12 | fun wait(dbHost: String, port: Int, timeout: Duration) { 13 | val start = Instant.now() 14 | while (!isPortAvailable(dbHost, port)) { 15 | if (start.plus(timeout).isBefore(Instant.now())) { 16 | throw TimeoutException("Database is not available under ${dbHost}:${port}") 17 | } 18 | Thread.sleep(100) 19 | } 20 | } 21 | 22 | private fun isPortAvailable(dbHost: String, port: Int): Boolean { 23 | try { 24 | Socket(dbHost, port).close() 25 | } catch (e: IOException) { 26 | return false 27 | } 28 | return true 29 | } 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/revolut/jooq/SchemaPackageRenameGeneratorStrategy.kt: -------------------------------------------------------------------------------- 1 | package com.revolut.jooq 2 | 3 | import org.jooq.codegen.DefaultGeneratorStrategy 4 | import org.jooq.meta.CatalogDefinition 5 | import org.jooq.meta.Definition 6 | import org.jooq.meta.SchemaDefinition 7 | 8 | class SchemaPackageRenameGeneratorStrategy : DefaultGeneratorStrategy() { 9 | companion object { 10 | val schemaToPackageMapping: ThreadLocal> = ThreadLocal.withInitial { emptyMap() } 11 | } 12 | 13 | override fun getJavaIdentifier(definition: Definition?): String { 14 | if (isEligibleForRename(definition)) { 15 | return getMappingForDefinition(definition)!! 16 | } else { 17 | return super.getJavaIdentifier(definition) 18 | } 19 | } 20 | 21 | private fun isEligibleForRename(definition: Definition?) = 22 | isSchemaOrCatalog(definition) && getMappingForDefinition(definition) != null 23 | 24 | private fun isSchemaOrCatalog(definition: Definition?) = 25 | definition is SchemaDefinition || definition is CatalogDefinition 26 | 27 | private fun getMappingForDefinition(definition: Definition?) = 28 | schemaToPackageMapping.get()[definition?.getSchemaName()] 29 | 30 | private fun Definition.getSchemaName(): String { 31 | return inputName 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/main/java/com/revolut/shaded/org/testcontainers/utility/AuthConfigUtil.java: -------------------------------------------------------------------------------- 1 | package com.revolut.shaded.org.testcontainers.utility; 2 | 3 | import com.github.dockerjava.api.model.AuthConfig; 4 | import com.google.common.base.MoreObjects; 5 | import org.jetbrains.annotations.NotNull; 6 | 7 | import javax.annotation.Generated; 8 | 9 | import static com.google.common.base.Strings.isNullOrEmpty; 10 | 11 | /** 12 | * TODO: Javadocs 13 | */ 14 | @Generated("https://github.com/testcontainers/testcontainers-java/blob/d127fd799bccbb4ee4d006dc2edd0f56c0c908c2/core/src/main/java/org/testcontainers/utility/AuthConfigUtil.java") 15 | public class AuthConfigUtil { 16 | 17 | public static String toSafeString(AuthConfig authConfig) { 18 | if (authConfig == null) { 19 | return "null"; 20 | } 21 | 22 | return MoreObjects.toStringHelper(authConfig) 23 | .add("username", authConfig.getUsername()) 24 | .add("password", obfuscated(authConfig.getPassword())) 25 | .add("auth", obfuscated(authConfig.getAuth())) 26 | .add("email", authConfig.getEmail()) 27 | .add("registryAddress", authConfig.getRegistryAddress()) 28 | .add("registryToken", obfuscated(authConfig.getRegistrytoken())) 29 | .toString(); 30 | } 31 | 32 | @NotNull 33 | private static String obfuscated(String value) { 34 | return isNullOrEmpty(value) ? "blank" : "hidden non-blank value"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/groovy/SchemaPackageRenameGeneratorStrategySpec.groovy: -------------------------------------------------------------------------------- 1 | import com.revolut.jooq.SchemaPackageRenameGeneratorStrategy 2 | import org.jooq.meta.CatalogDefinition 3 | import org.jooq.meta.Database 4 | import org.jooq.meta.SchemaDefinition 5 | import org.jooq.meta.postgres.PostgresTableDefinition 6 | import spock.lang.Specification 7 | 8 | class SchemaPackageRenameGeneratorStrategySpec extends Specification { 9 | 10 | def underSpec = new SchemaPackageRenameGeneratorStrategy() 11 | 12 | def "returns name from mapping when schema or catalog encountered"() { 13 | given: 14 | SchemaPackageRenameGeneratorStrategy.schemaToPackageMapping.set(["other": "newName"]) 15 | 16 | when: 17 | def result = underSpec.getJavaIdentifier(definition) 18 | 19 | then: 20 | result == expectedResult 21 | 22 | where: 23 | definition || expectedResult 24 | new SchemaDefinition(Mock(Database), "other", "") || "newName" 25 | new SchemaDefinition(Mock(Database), "some", "") || "DEFAULT_SCHEMA" 26 | new CatalogDefinition(Mock(Database), "other", "") || "newName" 27 | new CatalogDefinition(Mock(Database), "some", "") || "DEFAULT_CATALOG" 28 | new PostgresTableDefinition(new SchemaDefinition(Mock(Database), "", ""), "table_name", "") || "TABLE_NAME" 29 | } 30 | } -------------------------------------------------------------------------------- /src/test/groovy/WaitForPortStrategySpec.groovy: -------------------------------------------------------------------------------- 1 | import spock.lang.Specification 2 | 3 | import java.time.Instant 4 | import java.util.concurrent.TimeoutException 5 | 6 | import static com.revolut.jooq.WaitForPortStrategy.wait 7 | import static java.time.Duration.ofSeconds 8 | import static java.util.concurrent.Executors.newFixedThreadPool 9 | 10 | class WaitForPortStrategySpec extends Specification { 11 | 12 | private String host = "localhost" 13 | 14 | def "should throw exception when port is not available"() { 15 | given: 16 | def port = findRandomPort() 17 | 18 | when: 19 | wait(host, port, ofSeconds(1)) 20 | 21 | then: 22 | def e = thrown(TimeoutException.class) 23 | e.message == "Database is not available under localhost:${port}" 24 | } 25 | 26 | def "should do nothing when port is available"() { 27 | given: 28 | def port = openSocket() 29 | 30 | when: 31 | wait(host, port.localPort, ofSeconds(1)) 32 | 33 | then: 34 | noExceptionThrown() 35 | 36 | cleanup: 37 | port.close() 38 | } 39 | 40 | def "should wait for port to be open"() { 41 | given: 42 | def pool = newFixedThreadPool(1) 43 | def port = findRandomPort() 44 | def startupDelay = 5_000 45 | 46 | pool.submit { 47 | Thread.sleep(startupDelay) 48 | openSocket(port) 49 | } 50 | 51 | when: 52 | def start = Instant.now() 53 | wait(host, port, ofSeconds(60)) 54 | 55 | then: 56 | noExceptionThrown() 57 | start.plusMillis(startupDelay) <= Instant.now() 58 | } 59 | 60 | private static openSocket(port = 0) { 61 | return new ServerSocket(port) 62 | } 63 | 64 | private static findRandomPort() { 65 | def socket = openSocket() 66 | def port = socket.localPort 67 | socket.close() 68 | return port 69 | } 70 | } -------------------------------------------------------------------------------- /src/test/groovy/DatabaseHostResolverSpec.groovy: -------------------------------------------------------------------------------- 1 | import com.revolut.jooq.DatabaseHostResolver 2 | import spock.lang.Specification 3 | 4 | class DatabaseHostResolverSpec extends Specification { 5 | def "resolves database host based on docker host"() { 6 | given: 7 | def underSpec = new DatabaseHostResolver(null) 8 | 9 | when: 10 | def result = underSpec.resolveHost(dockerHost) 11 | 12 | then: 13 | result == expectedResult 14 | 15 | where: 16 | dockerHost || expectedResult 17 | new URI("http://hostname:6789") || "hostname" 18 | new URI("https://fancyhost:6789") || "fancyhost" 19 | new URI("tcp://another:6789") || "another" 20 | new URI("unix:///var/run/docker.sock") || "localhost" 21 | new URI("npipe:////./pipe/docker_engine") || "localhost" 22 | } 23 | 24 | def "throws IllegalStateException when unable to resolve database host from docker host"() { 25 | given: 26 | def underSpec = new DatabaseHostResolver(null) 27 | 28 | when: 29 | underSpec.resolveHost(host) 30 | 31 | then: 32 | thrown(IllegalStateException) 33 | 34 | where: 35 | host | _ 36 | new URI("unknown://host") | _ 37 | new URI("host") | _ 38 | } 39 | 40 | def "overrides database host when override provided"() { 41 | given: 42 | def dbHostOverride = "someoverride" 43 | def underSpec = new DatabaseHostResolver(dbHostOverride) 44 | 45 | when: 46 | def result = underSpec.resolveHost(new URI("http://localhost:8080")) 47 | 48 | then: 49 | result == dbHostOverride 50 | } 51 | 52 | def "does not throw exception when databe host cannot be resolved, but override was provided"() { 53 | given: 54 | def dbHostOverride = "someoverride" 55 | def underSpec = new DatabaseHostResolver(dbHostOverride) 56 | 57 | when: 58 | def result = underSpec.resolveHost(new URI("unknown://host")) 59 | 60 | then: 61 | result == dbHostOverride 62 | } 63 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/revolut/jooq/JooqExtension.kt: -------------------------------------------------------------------------------- 1 | package com.revolut.jooq 2 | 3 | import org.gradle.api.Action 4 | import java.io.Serializable 5 | import java.net.ServerSocket 6 | 7 | open class JooqExtension(projectName: String) : Serializable { 8 | val jdbc = Jdbc() 9 | val db = Database(jdbc) 10 | val image = Image(db, projectName) 11 | 12 | fun db(configure: Action) { 13 | configure.execute(db) 14 | } 15 | 16 | fun image(configure: Action) { 17 | configure.execute(image) 18 | } 19 | 20 | fun jdbc(configure: Action) { 21 | configure.execute(jdbc) 22 | } 23 | 24 | class Jdbc : Serializable { 25 | var schema = "jdbc:postgresql" 26 | var driverClassName = "org.postgresql.Driver" 27 | var jooqMetaName = "org.jooq.meta.postgres.PostgresDatabase" 28 | var urlQueryParams = "" 29 | } 30 | 31 | class Database(private val jdbc: Jdbc) : Serializable { 32 | var username = "postgres" 33 | var password = "postgres" 34 | var name = "postgres" 35 | var hostOverride: String? = null 36 | var port = 5432 37 | var exposedPort = lookupFreePort() 38 | 39 | internal fun getUrl(host: String): String { 40 | return "${jdbc.schema}://$host:$exposedPort/$name${jdbc.urlQueryParams}" 41 | } 42 | 43 | private fun lookupFreePort(): Int { 44 | ServerSocket(0).use { 45 | return it.localPort 46 | } 47 | } 48 | } 49 | 50 | class Image(private val db: Database, projectName: String) : Serializable { 51 | var repository = "postgres" 52 | var tag = "14.5-alpine" 53 | var envVars: Map = mapOf("POSTGRES_USER" to db.username, "POSTGRES_PASSWORD" to db.password, "POSTGRES_DB" to db.name) 54 | var containerName = "jooq-docker-container-${projectName}" 55 | var readinessProbeHost = "127.0.0.1" 56 | var readinessProbe = { host: String, port: Int -> 57 | arrayOf("sh", "-c", "until pg_isready -h $host -p $port; do echo waiting for db; sleep 1; done;") 58 | } 59 | 60 | internal fun getReadinessCommand(): Array { 61 | return readinessProbe(readinessProbeHost, db.port) 62 | } 63 | 64 | internal fun getImageName(): String { 65 | return "$repository:$tag" 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/main/java/com/revolut/shaded/org/testcontainers/dockerclient/auth/AuthDelegatingDockerClientConfig.java: -------------------------------------------------------------------------------- 1 | package com.revolut.shaded.org.testcontainers.dockerclient.auth; 2 | 3 | import com.github.dockerjava.api.model.AuthConfig; 4 | import com.github.dockerjava.api.model.AuthConfigurations; 5 | import com.github.dockerjava.core.DockerClientConfig; 6 | import com.github.dockerjava.core.RemoteApiVersion; 7 | import com.github.dockerjava.core.SSLConfig; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import com.revolut.shaded.org.testcontainers.utility.DockerImageName; 11 | import com.revolut.shaded.org.testcontainers.utility.RegistryAuthLocator; 12 | 13 | import javax.annotation.Generated; 14 | import java.lang.invoke.MethodHandles; 15 | import java.net.URI; 16 | 17 | import static com.revolut.shaded.org.testcontainers.utility.AuthConfigUtil.toSafeString; 18 | 19 | /** 20 | * Facade implementation for {@link DockerClientConfig} which overrides how authentication 21 | * configuration is obtained. A delegate {@link DockerClientConfig} will be called first 22 | * to try and obtain auth credentials, but after that {@link RegistryAuthLocator} will be 23 | * used to try and improve the auth resolution (e.g. using credential helpers). 24 | * 25 | * @deprecated should not be used publicly, to be moved to docker-java 26 | */ 27 | @Deprecated 28 | @Generated("https://github.com/testcontainers/testcontainers-java/blob/7d5f4c9e35b5d671f24125395aed3f741f6c3d9e/core/src/main/java/org/testcontainers/dockerclient/auth/AuthDelegatingDockerClientConfig.java") 29 | public class AuthDelegatingDockerClientConfig implements DockerClientConfig { 30 | 31 | private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); 32 | 33 | private final DockerClientConfig delegate; 34 | 35 | public AuthDelegatingDockerClientConfig(DockerClientConfig delegate) { 36 | this.delegate = delegate; 37 | } 38 | 39 | @Override 40 | public URI getDockerHost() { 41 | return delegate.getDockerHost(); 42 | } 43 | 44 | @Override 45 | public RemoteApiVersion getApiVersion() { 46 | return delegate.getApiVersion(); 47 | } 48 | 49 | @Override 50 | public String getRegistryUsername() { 51 | return delegate.getRegistryUsername(); 52 | } 53 | 54 | @Override 55 | public String getRegistryPassword() { 56 | return delegate.getRegistryPassword(); 57 | } 58 | 59 | @Override 60 | public String getRegistryEmail() { 61 | return delegate.getRegistryEmail(); 62 | } 63 | 64 | @Override 65 | public String getRegistryUrl() { 66 | return delegate.getRegistryUrl(); 67 | } 68 | 69 | @Override 70 | public AuthConfig effectiveAuthConfig(String imageName) { 71 | // allow docker-java auth config to be used as a fallback 72 | AuthConfig fallbackAuthConfig; 73 | try { 74 | fallbackAuthConfig = delegate.effectiveAuthConfig(imageName); 75 | } catch (Exception e) { 76 | log.debug("Delegate call to effectiveAuthConfig failed with cause: '{}'. " + 77 | "Resolution of auth config will continue using RegistryAuthLocator.", 78 | e.getMessage()); 79 | fallbackAuthConfig = new AuthConfig(); 80 | } 81 | 82 | // try and obtain more accurate auth config using our resolution 83 | final DockerImageName parsed = new DockerImageName(imageName); 84 | final AuthConfig effectiveAuthConfig = RegistryAuthLocator.instance() 85 | .lookupAuthConfig(parsed, fallbackAuthConfig); 86 | 87 | log.debug("Effective auth config [{}]", toSafeString(effectiveAuthConfig)); 88 | return effectiveAuthConfig; 89 | } 90 | 91 | @Override 92 | public AuthConfigurations getAuthConfigurations() { 93 | return delegate.getAuthConfigurations(); 94 | } 95 | 96 | @Override 97 | public SSLConfig getSSLConfig() { 98 | return delegate.getSSLConfig(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/kotlin/com/revolut/jooq/Docker.kt: -------------------------------------------------------------------------------- 1 | package com.revolut.jooq 2 | 3 | import com.github.dockerjava.api.DockerClient 4 | import com.github.dockerjava.api.command.PullImageResultCallback 5 | import com.github.dockerjava.api.model.ExposedPort 6 | import com.github.dockerjava.api.model.HostConfig.newHostConfig 7 | import com.github.dockerjava.api.model.Ports 8 | import com.github.dockerjava.api.model.Ports.Binding.bindPort 9 | import com.github.dockerjava.core.DefaultDockerClientConfig 10 | import com.github.dockerjava.core.DockerClientConfig 11 | import com.github.dockerjava.core.DockerClientImpl 12 | import com.github.dockerjava.core.command.ExecStartResultCallback 13 | import com.github.dockerjava.okhttp.OkHttpDockerCmdExecFactory 14 | import com.revolut.jooq.WaitForPortStrategy.Companion.wait 15 | import org.gradle.api.Action 16 | import com.revolut.shaded.org.testcontainers.dockerclient.auth.AuthDelegatingDockerClientConfig 17 | import java.io.Closeable 18 | import java.lang.System.* 19 | import java.time.Duration 20 | import java.util.UUID.randomUUID 21 | 22 | class Docker( 23 | private val imageName: String, 24 | private val env: Map, 25 | private val portBinding: Pair, 26 | private val readinessCommand: Array, 27 | private val databaseHostResolver: DatabaseHostResolver, 28 | private val containerName: String = randomUUID().toString(), 29 | private val offline: Boolean 30 | ) : Closeable { 31 | // https://github.com/docker-java/docker-java/issues/1048 32 | private val config: DockerClientConfig = AuthDelegatingDockerClientConfig(DefaultDockerClientConfig 33 | .createDefaultConfigBuilder() 34 | .build()) 35 | private val docker: DockerClient = DockerClientImpl.getInstance(config) 36 | .withDockerCmdExecFactory(OkHttpDockerCmdExecFactory()) 37 | 38 | fun runInContainer(action: Action) { 39 | try { 40 | val dbHost = resolveDbHost() 41 | removeContainer() 42 | prepareDockerizedDb(dbHost) 43 | action.execute(dbHost) 44 | } finally { 45 | removeContainer() 46 | } 47 | } 48 | 49 | private fun prepareDockerizedDb(dbHost: String) { 50 | pullImage() 51 | startContainer() 52 | awaitContainerStart(dbHost) 53 | } 54 | 55 | private fun pullImage() { 56 | if (!offline || !docker.listImagesCmd().withReferenceFilter(imageName).exec().any()) { 57 | val callback = PullImageResultCallback() 58 | docker.pullImageCmd(imageName).exec(callback) 59 | callback.awaitCompletion() 60 | } 61 | } 62 | 63 | private fun startContainer() { 64 | val dbPort = ExposedPort.tcp(portBinding.first) 65 | docker.createContainerCmd(imageName) 66 | .withName(containerName) 67 | .withEnv(env.map { "${it.key}=${it.value}" }) 68 | .withExposedPorts(dbPort) 69 | .withHostConfig(newHostConfig().withPortBindings(Ports(dbPort, bindPort(portBinding.second)))) 70 | .exec() 71 | docker.startContainerCmd(containerName).exec() 72 | } 73 | 74 | private fun awaitContainerStart(dbHost: String) { 75 | val execCreate = docker.execCreateCmd(containerName) 76 | .withCmd(*readinessCommand) 77 | .withAttachStdout(true) 78 | .exec() 79 | docker.execStartCmd(execCreate.id) 80 | .exec(ExecStartResultCallback(out, err)) 81 | .awaitCompletion() 82 | wait(dbHost, portBinding.second, Duration.ofSeconds(20)) 83 | } 84 | 85 | private fun resolveDbHost(): String { 86 | return databaseHostResolver.resolveHost(config.dockerHost) 87 | } 88 | 89 | private fun removeContainer() { 90 | try { 91 | docker.removeContainerCmd(containerName) 92 | .withRemoveVolumes(true) 93 | .withForce(true) 94 | .exec() 95 | } catch (e: Exception) { 96 | } 97 | } 98 | 99 | override fun close() { 100 | docker.close() 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/revolut/shaded/org/testcontainers/utility/DockerImageName.java: -------------------------------------------------------------------------------- 1 | package com.revolut.shaded.org.testcontainers.utility; 2 | 3 | import com.google.common.net.HostAndPort; 4 | 5 | import javax.annotation.Generated; 6 | import java.util.regex.Pattern; 7 | 8 | @Generated("https://github.com/testcontainers/testcontainers-java/blob/7d5f4c9e35b5d671f24125395aed3f741f6c3d9e/core/src/main/java/org/testcontainers/utility/DockerImageName.java") 9 | public final class DockerImageName { 10 | 11 | /* Regex patterns used for validation */ 12 | private static final String ALPHA_NUMERIC = "[a-z0-9]+"; 13 | private static final String SEPARATOR = "([\\.]{1}|_{1,2}|-+)"; 14 | private static final String REPO_NAME_PART = ALPHA_NUMERIC + "(" + SEPARATOR + ALPHA_NUMERIC + ")*"; 15 | private static final Pattern REPO_NAME = Pattern.compile(REPO_NAME_PART + "(/" + REPO_NAME_PART + ")*"); 16 | 17 | private final String rawName; 18 | private final String registry; 19 | private final String repo; 20 | private final Versioning versioning; 21 | 22 | public DockerImageName(String name) { 23 | this.rawName = name; 24 | final int slashIndex = name.indexOf('/'); 25 | 26 | String remoteName; 27 | if (slashIndex == -1 || 28 | (!name.substring(0, slashIndex).contains(".") && 29 | !name.substring(0, slashIndex).contains(":") && 30 | !name.substring(0, slashIndex).equals("localhost"))) { 31 | registry = ""; 32 | remoteName = name; 33 | } else { 34 | registry = name.substring(0, slashIndex); 35 | remoteName = name.substring(slashIndex + 1); 36 | } 37 | 38 | if (remoteName.contains("@sha256:")) { 39 | repo = remoteName.split("@sha256:")[0]; 40 | versioning = new Sha256Versioning(remoteName.split("@sha256:")[1]); 41 | } else if (remoteName.contains(":")) { 42 | repo = remoteName.split(":")[0]; 43 | versioning = new TagVersioning(remoteName.split(":")[1]); 44 | } else { 45 | repo = remoteName; 46 | versioning = new TagVersioning("latest"); 47 | } 48 | } 49 | 50 | public DockerImageName(String name, String tag) { 51 | this.rawName = name; 52 | final int slashIndex = name.indexOf('/'); 53 | 54 | String remoteName; 55 | if (slashIndex == -1 || 56 | (!name.substring(0, slashIndex).contains(".") && 57 | !name.substring(0, slashIndex).contains(":") && 58 | !name.substring(0, slashIndex).equals("localhost"))) { 59 | registry = ""; 60 | remoteName = name; 61 | } else { 62 | registry = name.substring(0, slashIndex); 63 | remoteName = name.substring(slashIndex + 1); 64 | } 65 | 66 | if (tag.startsWith("sha256:")) { 67 | repo = remoteName; 68 | versioning = new Sha256Versioning(tag.replace("sha256:", "")); 69 | } else { 70 | repo = remoteName; 71 | versioning = new TagVersioning(tag); 72 | } 73 | } 74 | 75 | /** 76 | * @return the unversioned (non 'tag') part of this name 77 | */ 78 | public String getUnversionedPart() { 79 | if (!"".equals(registry)) { 80 | return registry + "/" + repo; 81 | } else { 82 | return repo; 83 | } 84 | } 85 | 86 | /** 87 | * @return the versioned part of this name (tag or sha256) 88 | */ 89 | public String getVersionPart() { 90 | return versioning.toString(); 91 | } 92 | 93 | @Override 94 | public String toString() { 95 | if (versioning == null) { 96 | return getUnversionedPart(); 97 | } else { 98 | return getUnversionedPart() + versioning.getSeparator() + versioning.toString(); 99 | } 100 | } 101 | 102 | /** 103 | * Is the image name valid? 104 | * 105 | * @throws IllegalArgumentException if not valid 106 | */ 107 | public void assertValid() { 108 | HostAndPort.fromString(registry); 109 | if (!REPO_NAME.matcher(repo).matches()) { 110 | throw new IllegalArgumentException(repo + " is not a valid Docker image name (in " + rawName + ")"); 111 | } 112 | if (versioning == null) { 113 | throw new IllegalArgumentException("No image tag was specified in docker image name " + 114 | "(" + rawName + "). Please provide a tag; this may be 'latest' or a specific version"); 115 | } 116 | if (!versioning.isValid()) { 117 | throw new IllegalArgumentException(versioning + " is not a valid image versioning identifier (in " + rawName + ")"); 118 | } 119 | } 120 | 121 | public String getRegistry() { 122 | return registry; 123 | } 124 | 125 | 126 | 127 | private interface Versioning { 128 | 129 | boolean isValid(); 130 | 131 | String getSeparator(); 132 | } 133 | 134 | private static class TagVersioning implements Versioning { 135 | 136 | public static final String TAG_REGEX = "[\\w][\\w\\.\\-]{0,127}"; 137 | private final String tag; 138 | 139 | TagVersioning(String tag) { 140 | this.tag = tag; 141 | } 142 | 143 | @Override 144 | public boolean isValid() { 145 | return tag.matches(TAG_REGEX); 146 | } 147 | 148 | @Override 149 | public String getSeparator() { 150 | return ":"; 151 | } 152 | 153 | @Override 154 | public String toString() { 155 | return tag; 156 | } 157 | } 158 | 159 | private class Sha256Versioning implements Versioning { 160 | 161 | public static final String HASH_REGEX = "[0-9a-fA-F]{32,}"; 162 | private final String hash; 163 | 164 | Sha256Versioning(String hash) { 165 | this.hash = hash; 166 | } 167 | 168 | @Override 169 | public boolean isValid() { 170 | return hash.matches(HASH_REGEX); 171 | } 172 | 173 | @Override 174 | public String getSeparator() { 175 | return "@"; 176 | } 177 | 178 | @Override 179 | public String toString() { 180 | return "sha256:" + hash; 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gradle Docker jOOQ Plugin 2 | 3 | [![Build Status](https://travis-ci.com/revolut-engineering/jooq-plugin.svg?branch=master)](https://travis-ci.com/revolut-engineering/jooq-plugin) 4 | [![codecov](https://codecov.io/gh/revolut-engineering/jooq-plugin/branch/master/graph/badge.svg)](https://codecov.io/gh/revolut-engineering/jooq-plugin) 5 | [![Gradle Plugins Release](https://img.shields.io/github/release/revolut-engineering/jooq-plugin.svg)](https://plugins.gradle.org/plugin/com.revolut.jooq-docker) 6 | 7 | This repository contains Gradle plugin for generating jOOQ classes in dockerized databases. 8 | Plugin registers task `generateJooqClasses` that does following steps: 9 | * pulls docker image 10 | * starts database container 11 | * runs migrations using Flyway 12 | * generates jOOQ classes 13 | 14 | **Use**: 15 | - **`0.3.x` and later for jOOQ versions `3.12.x` and later** 16 | - **`0.2.x` and later releases for jOOQ versions `3.11.x` and later** 17 | - **For earlier versions use `0.1.x` release** 18 | 19 | # Examples 20 | 21 | By default plugin is configured to work with PostgreSQL, so the following minimal config is enough: 22 | ```kotlin 23 | plugins { 24 | id("com.revolut.jooq-docker") 25 | } 26 | 27 | repositories { 28 | mavenCentral() 29 | } 30 | 31 | dependencies { 32 | implementation("org.jooq:jooq:3.14.15") 33 | jdbc("org.postgresql:postgresql:42.2.5") 34 | } 35 | ``` 36 | It will look for migration files in `src/main/resources/db/migration` and will output generated classes 37 | to `build/generated-jooq` in package `org.jooq.generated`. All of that details can be configured on the task itself 38 | as shown in examples below. 39 | 40 | Configuring schema names and other parameters of the task: 41 | ```kotlin 42 | plugins { 43 | id("com.revolut.jooq-docker") 44 | } 45 | 46 | repositories { 47 | mavenCentral() 48 | } 49 | 50 | tasks { 51 | generateJooqClasses { 52 | schemas = arrayOf("public", "other_schema") 53 | basePackageName = "org.jooq.generated" 54 | inputDirectory.setFrom(project.files("src/main/resources/db/migration")) 55 | outputDirectory.set(project.layout.buildDirectory.dir("generated-jooq")) 56 | flywayProperties = mapOf("flyway.placeholderReplacement" to "false") 57 | excludeFlywayTable = true 58 | outputSchemaToDefault = setOf("public") 59 | schemaToPackageMapping = mapOf("public" to "fancy_name") 60 | customizeGenerator { 61 | /* "this" here is the org.jooq.meta.jaxb.Generator configure it as you please */ 62 | } 63 | } 64 | } 65 | 66 | dependencies { 67 | implementation("org.jooq:jooq:3.14.15") 68 | jdbc("org.postgresql:postgresql:42.2.5") 69 | } 70 | ``` 71 | 72 | To configure the plugin to work with another DB like MySQL following config can be applied: 73 | ```kotlin 74 | plugins { 75 | id("com.revolut.jooq-docker") 76 | } 77 | 78 | repositories { 79 | mavenCentral() 80 | } 81 | 82 | jooq { 83 | image { 84 | repository = "mysql" 85 | tag = "8.0.15" 86 | envVars = mapOf( 87 | "MYSQL_ROOT_PASSWORD" to "mysql", 88 | "MYSQL_DATABASE" to "mysql") 89 | containerName = "uniqueMySqlContainerName" 90 | readinessProbe = { host: String, port: Int -> 91 | arrayOf("sh", "-c", "until mysqladmin -h$host -P$port -uroot -pmysql ping; do echo wait; sleep 1; done;") 92 | } 93 | } 94 | 95 | db { 96 | username = "root" 97 | password = "mysql" 98 | name = "mysql" 99 | port = 3306 100 | } 101 | 102 | jdbc { 103 | schema = "jdbc:mysql" 104 | driverClassName = "com.mysql.cj.jdbc.Driver" 105 | jooqMetaName = "org.jooq.meta.mysql.MySQLDatabase" 106 | urlQueryParams = "?useSSL=false" 107 | } 108 | } 109 | 110 | dependencies { 111 | implementation("org.jooq:jooq:3.14.15") 112 | jdbc("mysql:mysql-connector-java:8.0.15") 113 | } 114 | ``` 115 | 116 | To register custom types: 117 | ```kotlin 118 | plugins { 119 | id("com.revolut.jooq-docker") 120 | } 121 | 122 | repositories { 123 | mavenCentral() 124 | } 125 | 126 | tasks { 127 | generateJooqClasses { 128 | customizeGenerator { 129 | database.withForcedTypes( 130 | ForcedType() 131 | .withUserType("com.google.gson.JsonElement") 132 | .withBinding("com.example.PostgresJSONGsonBinding") 133 | .withTypes("JSONB") 134 | ) 135 | } 136 | } 137 | } 138 | 139 | dependencies { 140 | implementation("org.jooq:jooq:3.14.15") 141 | jdbc("org.postgresql:postgresql:42.2.5") 142 | } 143 | ``` 144 | 145 | To exclude flyway schema history table from generated classes: 146 | ```kotlin 147 | plugins { 148 | id("com.revolut.jooq-docker") 149 | } 150 | 151 | repositories { 152 | mavenCentral() 153 | } 154 | 155 | tasks { 156 | generateJooqClasses { 157 | schemas = arrayOf("other") 158 | customizeGenerator { 159 | database.withExcludes("flyway_schema_history") 160 | } 161 | } 162 | } 163 | 164 | dependencies { 165 | implementation("org.jooq:jooq:3.14.15") 166 | jdbc("org.postgresql:postgresql:42.2.5") 167 | } 168 | ``` 169 | 170 | To enforce version of the plugin dependencies: 171 | ```kotlin 172 | plugins { 173 | id("com.revolut.jooq-docker") 174 | } 175 | 176 | buildscript { 177 | repositories { 178 | mavenCentral() 179 | } 180 | 181 | dependencies { 182 | classpath("org.jooq:jooq-codegen:3.12.0") { 183 | isForce = true 184 | } 185 | } 186 | } 187 | 188 | repositories { 189 | mavenCentral() 190 | } 191 | 192 | dependencies { 193 | implementation("org.jooq:jooq:3.12.0") 194 | jdbc("org.postgresql:postgresql:42.2.5") 195 | } 196 | ``` 197 | 198 | ### Remote docker setup 199 | 200 | The library plugin uses to communicate with docker daemon will pick up your environment variables like `DOCKER_HOST` 201 | and use them for connection ([all config options here](https://github.com/docker-java/docker-java#configuration)). 202 | Plugin then, based on this config, will try to figure out the host on which database is exposed, 203 | if it fail you can override it the following way: 204 | 205 | ```kotlin 206 | plugins { 207 | id("com.revolut.jooq-docker") 208 | } 209 | 210 | 211 | jooq { 212 | db { 213 | hostOverride = "localhost" 214 | } 215 | } 216 | ``` 217 | 218 | For the readiness probe plugin will always use localhost `127.0.0.1` as it's a command run within the database container. 219 | If for whatever reason you need to override this you can do that by specifying it as follows: 220 | 221 | ```kotlin 222 | plugins { 223 | id("com.revolut.jooq-docker") 224 | } 225 | 226 | 227 | jooq { 228 | image { 229 | readinessProbeHost = "someHost" 230 | } 231 | } 232 | ``` -------------------------------------------------------------------------------- /src/main/kotlin/com/revolut/jooq/GenerateJooqClassesTask.kt: -------------------------------------------------------------------------------- 1 | package com.revolut.jooq 2 | 3 | import groovy.lang.Closure 4 | import org.flywaydb.core.Flyway 5 | import org.flywaydb.core.api.Location.FILESYSTEM_PREFIX 6 | import org.flywaydb.core.internal.configuration.ConfigUtils.DEFAULT_SCHEMA 7 | import org.flywaydb.core.internal.configuration.ConfigUtils.TABLE 8 | import org.gradle.api.Action 9 | import org.gradle.api.DefaultTask 10 | import org.gradle.api.plugins.JavaPlugin 11 | import org.gradle.api.plugins.JavaPluginConvention 12 | import org.gradle.api.tasks.* 13 | import org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME 14 | import org.jooq.codegen.GenerationTool 15 | import org.jooq.codegen.JavaGenerator 16 | import org.jooq.meta.jaxb.* 17 | import org.jooq.meta.jaxb.Target 18 | import java.io.IOException 19 | import java.net.URL 20 | import java.net.URLClassLoader 21 | 22 | @CacheableTask 23 | open class GenerateJooqClassesTask : DefaultTask() { 24 | @Input 25 | var schemas = arrayOf("public") 26 | 27 | @Input 28 | var basePackageName = "org.jooq.generated" 29 | 30 | @Input 31 | var flywayProperties = emptyMap() 32 | 33 | @Input 34 | var outputSchemaToDefault = emptySet() 35 | 36 | @Input 37 | var schemaToPackageMapping = emptyMap() 38 | 39 | @Input 40 | var excludeFlywayTable = false 41 | 42 | @Internal 43 | var generatorConfig = project.provider(this::prepareGeneratorConfig) 44 | private set 45 | 46 | @InputFiles 47 | @PathSensitive(PathSensitivity.RELATIVE) 48 | val inputDirectory = project.objects.fileCollection().from("src/main/resources/db/migration") 49 | 50 | @OutputDirectory 51 | val outputDirectory = project.objects.directoryProperty().convention(project.layout.buildDirectory.dir("generated-jooq")) 52 | 53 | @Internal 54 | fun getDb() = getExtension().db 55 | 56 | @Internal 57 | fun getJdbc() = getExtension().jdbc 58 | 59 | @Internal 60 | fun getImage() = getExtension().image 61 | 62 | @Input 63 | fun getJdbcSchema() = getJdbc().schema 64 | 65 | @Input 66 | fun getJdbcDriverClassName() = getJdbc().driverClassName 67 | 68 | @Input 69 | fun getJooqMetaName() = getJdbc().jooqMetaName 70 | 71 | @Input 72 | fun getJdbcUrlQueryParams() = getJdbc().urlQueryParams 73 | 74 | @Input 75 | fun getDbUsername() = getDb().username 76 | 77 | @Input 78 | fun getDbPassword() = getDb().password 79 | 80 | @Input 81 | fun getDbPort() = getDb().port 82 | 83 | @Input 84 | @Optional 85 | fun getDbHostOverride() = getDb().hostOverride 86 | 87 | @Input 88 | fun getImageRepository() = getImage().repository 89 | 90 | @Input 91 | fun getImageTag() = getImage().tag 92 | 93 | @Input 94 | fun getImageEnvVars() = getImage().envVars 95 | 96 | @Input 97 | fun getContainerName() = getImage().containerName 98 | 99 | @Input 100 | fun getReadinessProbeHost() = getImage().readinessProbeHost 101 | 102 | @Input 103 | fun getReadinessCommand() = getImage().getReadinessCommand() 104 | 105 | @Input 106 | fun getCleanedGeneratorConfig() = generatorConfig.get().apply { 107 | target.withDirectory("ignored") 108 | } 109 | 110 | init { 111 | project.plugins.withType(JavaPlugin::class.java) { 112 | project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets.named(MAIN_SOURCE_SET_NAME) { 113 | java { 114 | srcDir(outputDirectory) 115 | } 116 | } 117 | } 118 | } 119 | 120 | private fun getExtension() = project.extensions.getByName("jooq") as JooqExtension 121 | 122 | 123 | @Suppress("unused") 124 | fun customizeGenerator(customizer: Action) { 125 | generatorConfig = generatorConfig.map { 126 | customizer.execute(it) 127 | it 128 | } 129 | } 130 | 131 | @Suppress("unused") 132 | fun customizeGenerator(closure: Closure) { 133 | generatorConfig = generatorConfig.map { 134 | closure.rehydrate(it, it, it).call(it) 135 | it 136 | } 137 | } 138 | 139 | @TaskAction 140 | fun generateClasses() { 141 | val image = getImage() 142 | val db = getDb() 143 | val jdbcAwareClassLoader = buildJdbcArtifactsAwareClassLoader() 144 | val docker = Docker( 145 | image.getImageName(), 146 | image.envVars, 147 | db.port to db.exposedPort, 148 | image.getReadinessCommand(), 149 | DatabaseHostResolver(db.hostOverride), 150 | image.containerName, 151 | project.gradle.startParameter.isOffline) 152 | docker.use { 153 | it.runInContainer { 154 | migrateDb(jdbcAwareClassLoader, this) 155 | generateJooqClasses(jdbcAwareClassLoader, this) 156 | } 157 | } 158 | } 159 | 160 | private fun migrateDb(jdbcAwareClassLoader: ClassLoader, dbHost: String) { 161 | val db = getDb() 162 | Flyway.configure(jdbcAwareClassLoader) 163 | .dataSource(db.getUrl(dbHost), db.username, db.password) 164 | .schemas(*schemas) 165 | .locations(*inputDirectory.map { "$FILESYSTEM_PREFIX${it.absolutePath}" }.toTypedArray()) 166 | .defaultSchema(defaultFlywaySchema()) 167 | .table(flywayTableName()) 168 | .configuration(flywayProperties) 169 | .load() 170 | .migrate() 171 | } 172 | 173 | private fun defaultFlywaySchema() = flywayProperties[DEFAULT_SCHEMA] ?: schemas.first() 174 | 175 | private fun flywayTableName() = flywayProperties[TABLE] ?: "flyway_schema_history" 176 | 177 | private fun generateJooqClasses(jdbcAwareClassLoader: ClassLoader, dbHost: String) { 178 | project.delete(outputDirectory) 179 | val db = getDb() 180 | val jdbc = getJdbc() 181 | FlywaySchemaVersionProvider.setup(defaultFlywaySchema(), flywayTableName()) 182 | SchemaPackageRenameGeneratorStrategy.schemaToPackageMapping.set(schemaToPackageMapping.toMap()) 183 | val generator = generatorConfig.get() 184 | excludeFlywaySchemaIfNeeded(generator) 185 | val tool = GenerationTool() 186 | tool.setClassLoader(jdbcAwareClassLoader) 187 | tool.run(Configuration() 188 | .withLogging(Logging.DEBUG) 189 | .withJdbc(Jdbc() 190 | .withDriver(jdbc.driverClassName) 191 | .withUrl(db.getUrl(dbHost)) 192 | .withUser(db.username) 193 | .withPassword(db.password)) 194 | .withGenerator(generator)) 195 | } 196 | 197 | private fun prepareGeneratorConfig(): Generator { 198 | return Generator() 199 | .withName(JavaGenerator::class.qualifiedName) 200 | .withStrategy(Strategy() 201 | .withName(SchemaPackageRenameGeneratorStrategy::class.qualifiedName)) 202 | .withDatabase(Database() 203 | .withName(getJdbc().jooqMetaName) 204 | .withSchemata(schemas.map(this::toSchemaMappingType)) 205 | .withSchemaVersionProvider(FlywaySchemaVersionProvider::class.qualifiedName) 206 | .withIncludes(".*") 207 | .withExcludes("")) 208 | .withTarget(Target() 209 | .withPackageName(basePackageName) 210 | .withDirectory(outputDirectory.asFile.get().toString()) 211 | .withClean(true)) 212 | .withGenerate(Generate()) 213 | } 214 | 215 | private fun toSchemaMappingType(schemaName: String): SchemaMappingType { 216 | return SchemaMappingType() 217 | .withInputSchema(schemaName) 218 | .withOutputSchemaToDefault(outputSchemaToDefault.contains(schemaName)) 219 | } 220 | 221 | private fun excludeFlywaySchemaIfNeeded(generator: Generator) { 222 | if (excludeFlywayTable) 223 | generator.database.withExcludes(addFlywaySchemaHistoryToExcludes(generator.database.excludes)) 224 | } 225 | 226 | private fun addFlywaySchemaHistoryToExcludes(currentExcludes: String?): String { 227 | return listOf(currentExcludes, flywayTableName()) 228 | .filterNot(String?::isNullOrEmpty) 229 | .joinToString("|") 230 | } 231 | 232 | private fun buildJdbcArtifactsAwareClassLoader(): ClassLoader { 233 | return URLClassLoader(resolveJdbcArtifacts(), project.buildscript.classLoader) 234 | } 235 | 236 | @Throws(IOException::class) 237 | private fun resolveJdbcArtifacts(): Array { 238 | return project.configurations.getByName("jdbc").resolvedConfiguration.resolvedArtifacts.map { 239 | it.file.toURI().toURL() 240 | }.toTypedArray() 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com/revolut/shaded/org/testcontainers/utility/RegistryAuthLocator.java: -------------------------------------------------------------------------------- 1 | package com.revolut.shaded.org.testcontainers.utility; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.dockerjava.api.model.AuthConfig; 6 | import com.google.common.annotations.VisibleForTesting; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.slf4j.Logger; 9 | import org.zeroturnaround.exec.InvalidResultException; 10 | import org.zeroturnaround.exec.ProcessExecutor; 11 | 12 | import javax.annotation.Generated; 13 | import java.io.ByteArrayInputStream; 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.util.*; 17 | import java.util.concurrent.ConcurrentHashMap; 18 | import java.util.concurrent.TimeUnit; 19 | import java.util.concurrent.TimeoutException; 20 | 21 | import static org.apache.commons.lang3.StringUtils.isBlank; 22 | import static org.slf4j.LoggerFactory.getLogger; 23 | import static com.revolut.shaded.org.testcontainers.utility.AuthConfigUtil.toSafeString; 24 | 25 | /** 26 | * Utility to look up registry authentication information for an image. 27 | */ 28 | @Generated("https://github.com/testcontainers/testcontainers-java/blob/7d5f4c9e35b5d671f24125395aed3f741f6c3d9e/core/src/main/java/org/testcontainers/utility/RegistryAuthLocator.java") 29 | public class RegistryAuthLocator { 30 | 31 | private static final Logger log = getLogger(RegistryAuthLocator.class); 32 | private static final String DEFAULT_REGISTRY_NAME = "index.docker.io"; 33 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 34 | 35 | private static RegistryAuthLocator instance; 36 | 37 | private final String commandPathPrefix; 38 | private final String commandExtension; 39 | private final File configFile; 40 | 41 | private final Map> cache = new ConcurrentHashMap<>(); 42 | 43 | /** 44 | * key - credential helper's name 45 | * value - helper's response for "credentials not found" use case 46 | */ 47 | private final Map CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE; 48 | 49 | @VisibleForTesting 50 | RegistryAuthLocator(File configFile, String commandPathPrefix, String commandExtension, 51 | Map notFoundMessageHolderReference) { 52 | this.configFile = configFile; 53 | this.commandPathPrefix = commandPathPrefix; 54 | this.commandExtension = commandExtension; 55 | 56 | this.CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE = notFoundMessageHolderReference; 57 | } 58 | 59 | /** 60 | */ 61 | protected RegistryAuthLocator() { 62 | final String dockerConfigLocation = System.getenv().getOrDefault("DOCKER_CONFIG", 63 | System.getProperty("user.home") + "/.docker"); 64 | this.configFile = new File(dockerConfigLocation + "/config.json"); 65 | this.commandPathPrefix = ""; 66 | this.commandExtension = ""; 67 | 68 | this.CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE = new HashMap<>(); 69 | } 70 | 71 | public synchronized static RegistryAuthLocator instance() { 72 | if (instance == null) { 73 | instance = new RegistryAuthLocator(); 74 | } 75 | 76 | return instance; 77 | } 78 | 79 | @VisibleForTesting 80 | static void setInstance(RegistryAuthLocator overrideInstance) { 81 | instance = overrideInstance; 82 | } 83 | 84 | /** 85 | * Looks up an AuthConfig for a given image name. 86 | *

87 | * Lookup is performed in following order, as per 88 | * https://docs.docker.com/engine/reference/commandline/cli/: 89 | *

    90 | *
  1. {@code credHelpers}
  2. 91 | *
  3. {@code credsStore}
  4. 92 | *
  5. Hard-coded Base64 encoded auth in {@code auths}
  6. 93 | *
  7. otherwise, if no credentials have been found then behaviour falls back to docker-java's 94 | * implementation
  8. 95 | *
96 | * 97 | * @param dockerImageName image name to be looked up (potentially including a registry URL part) 98 | * @param defaultAuthConfig an AuthConfig object that should be returned if there is no overriding authentication available for images that are looked up 99 | * @return an AuthConfig that is applicable to this specific image OR the defaultAuthConfig. 100 | */ 101 | public AuthConfig lookupAuthConfig(DockerImageName dockerImageName, AuthConfig defaultAuthConfig) { 102 | final String registryName = effectiveRegistryName(dockerImageName); 103 | log.debug("Looking up auth config for image: {} at registry: {}", dockerImageName, registryName); 104 | 105 | final Optional cachedAuth = cache.computeIfAbsent(registryName, __ -> lookupUncachedAuthConfig(registryName, dockerImageName)); 106 | 107 | if (cachedAuth.isPresent()) { 108 | log.debug("Cached auth found: [{}]", toSafeString(cachedAuth.get())); 109 | return cachedAuth.get(); 110 | } else { 111 | log.debug("No matching Auth Configs - falling back to defaultAuthConfig [{}]", toSafeString(defaultAuthConfig)); 112 | // otherwise, defaultAuthConfig should already contain any credentials available 113 | return defaultAuthConfig; 114 | } 115 | } 116 | 117 | private Optional lookupUncachedAuthConfig(String registryName, DockerImageName dockerImageName) { 118 | log.debug("RegistryAuthLocator has configFile: {} ({}) and commandPathPrefix: {}", 119 | configFile, 120 | configFile.exists() ? "exists" : "does not exist", 121 | commandPathPrefix); 122 | 123 | try { 124 | final JsonNode config = OBJECT_MAPPER.readTree(configFile); 125 | log.debug("registryName [{}] for dockerImageName [{}]", registryName, dockerImageName); 126 | 127 | // use helper preferentially (per https://docs.docker.com/engine/reference/commandline/cli/) 128 | final AuthConfig helperAuthConfig = authConfigUsingHelper(config, registryName); 129 | if (helperAuthConfig != null) { 130 | log.debug("found helper auth config [{}]", toSafeString(helperAuthConfig)); 131 | return Optional.of(helperAuthConfig); 132 | } 133 | // no credsHelper to use, using credsStore: 134 | final AuthConfig storeAuthConfig = authConfigUsingStore(config, registryName); 135 | if (storeAuthConfig != null) { 136 | log.debug("found creds store auth config [{}]", toSafeString(storeAuthConfig)); 137 | return Optional.of(storeAuthConfig); 138 | } 139 | // fall back to base64 encoded auth hardcoded in config file 140 | final AuthConfig existingAuthConfig = findExistingAuthConfig(config, registryName); 141 | if (existingAuthConfig != null) { 142 | log.debug("found existing auth config [{}]", toSafeString(existingAuthConfig)); 143 | return Optional.of(existingAuthConfig); 144 | } 145 | } catch (Exception e) { 146 | log.warn("Failure when attempting to lookup auth config (dockerImageName: {}, configFile: {}. Falling back to docker-java default behaviour. Exception message: {}", 147 | dockerImageName, 148 | configFile, 149 | e.getMessage()); 150 | } 151 | return Optional.empty(); 152 | } 153 | 154 | private AuthConfig findExistingAuthConfig(final JsonNode config, final String reposName) throws Exception { 155 | 156 | final Map.Entry entry = findAuthNode(config, reposName); 157 | 158 | if (entry != null && entry.getValue() != null && entry.getValue().size() > 0) { 159 | final AuthConfig deserializedAuth = OBJECT_MAPPER 160 | .treeToValue(entry.getValue(), AuthConfig.class) 161 | .withRegistryAddress(entry.getKey()); 162 | 163 | if (isBlank(deserializedAuth.getUsername()) && 164 | isBlank(deserializedAuth.getPassword()) && 165 | !isBlank(deserializedAuth.getAuth())) { 166 | 167 | final String rawAuth = new String(Base64.getDecoder().decode(deserializedAuth.getAuth())); 168 | final String[] splitRawAuth = rawAuth.split(":", 2); 169 | 170 | if (splitRawAuth.length == 2) { 171 | deserializedAuth.withUsername(splitRawAuth[0]); 172 | deserializedAuth.withPassword(splitRawAuth[1]); 173 | } 174 | } 175 | 176 | return deserializedAuth; 177 | } 178 | return null; 179 | } 180 | 181 | private AuthConfig authConfigUsingHelper(final JsonNode config, final String reposName) throws Exception { 182 | final JsonNode credHelpers = config.get("credHelpers"); 183 | if (credHelpers != null && credHelpers.size() > 0) { 184 | final JsonNode helperNode = credHelpers.get(reposName); 185 | if (helperNode != null && helperNode.isTextual()) { 186 | final String helper = helperNode.asText(); 187 | return runCredentialProvider(reposName, helper); 188 | } 189 | } 190 | return null; 191 | } 192 | 193 | private AuthConfig authConfigUsingStore(final JsonNode config, final String reposName) throws Exception { 194 | final JsonNode credsStoreNode = config.get("credsStore"); 195 | if (credsStoreNode != null && !credsStoreNode.isMissingNode() && credsStoreNode.isTextual()) { 196 | final String credsStore = credsStoreNode.asText(); 197 | if (isBlank(credsStore)) { 198 | log.warn("Docker auth config credsStore field will be ignored, because value is blank"); 199 | return null; 200 | } 201 | return runCredentialProvider(reposName, credsStore); 202 | } 203 | return null; 204 | } 205 | 206 | private Map.Entry findAuthNode(final JsonNode config, final String reposName) { 207 | final JsonNode auths = config.get("auths"); 208 | if (auths != null && auths.size() > 0) { 209 | final Iterator> fields = auths.fields(); 210 | while (fields.hasNext()) { 211 | final Map.Entry entry = fields.next(); 212 | if (entry.getKey().contains("://" + reposName) || entry.getKey().equals(reposName)) { 213 | return entry; 214 | } 215 | } 216 | } 217 | return null; 218 | } 219 | 220 | private AuthConfig runCredentialProvider(String hostName, String helperOrStoreName) throws Exception { 221 | 222 | if (isBlank(hostName)) { 223 | log.debug("There is no point in locating AuthConfig for blank hostName. Returning NULL to allow fallback"); 224 | return null; 225 | } 226 | 227 | final String credentialProgramName = getCredentialProgramName(helperOrStoreName); 228 | final String data; 229 | 230 | log.debug("Executing docker credential provider: {} to locate auth config for: {}", 231 | credentialProgramName, hostName); 232 | 233 | try { 234 | data = runCredentialProgram(hostName, credentialProgramName); 235 | } catch (InvalidResultException e) { 236 | 237 | final String responseErrorMsg = extractCredentialProviderErrorMessage(e); 238 | 239 | if (!isBlank(responseErrorMsg)) { 240 | String credentialsNotFoundMsg = getGenericCredentialsNotFoundMsg(credentialProgramName); 241 | if (credentialsNotFoundMsg != null && credentialsNotFoundMsg.equals(responseErrorMsg)) { 242 | log.info("Credential helper/store ({}) does not have credentials for {}", 243 | credentialProgramName, 244 | hostName); 245 | 246 | return null; 247 | } 248 | 249 | log.debug("Failure running docker credential helper/store ({}) with output '{}'", 250 | credentialProgramName, responseErrorMsg); 251 | 252 | } else { 253 | log.debug("Failure running docker credential helper/store ({})", credentialProgramName); 254 | } 255 | 256 | throw e; 257 | } catch (Exception e) { 258 | log.debug("Failure running docker credential helper/store ({})", credentialProgramName); 259 | throw e; 260 | } 261 | 262 | final JsonNode helperResponse = OBJECT_MAPPER.readTree(data); 263 | log.debug("Credential helper/store provided auth config for: {}", hostName); 264 | 265 | final String username = helperResponse.at("/Username").asText(); 266 | final String password = helperResponse.at("/Secret").asText(); 267 | if ("".equals(username)) { 268 | return new AuthConfig().withIdentityToken(password); 269 | } else { 270 | return new AuthConfig() 271 | .withRegistryAddress(helperResponse.at("/ServerURL").asText()) 272 | .withUsername(username) 273 | .withPassword(password); 274 | } 275 | } 276 | 277 | private String getCredentialProgramName(String credHelper) { 278 | return commandPathPrefix + "docker-credential-" + credHelper + commandExtension; 279 | } 280 | 281 | private String effectiveRegistryName(DockerImageName dockerImageName) { 282 | return StringUtils.defaultIfEmpty(dockerImageName.getRegistry(), DEFAULT_REGISTRY_NAME); 283 | } 284 | 285 | private String getGenericCredentialsNotFoundMsg(String credentialHelperName) { 286 | if (!CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE.containsKey(credentialHelperName)) { 287 | String credentialsNotFoundMsg = discoverCredentialsHelperNotFoundMessage(credentialHelperName); 288 | if (!isBlank(credentialsNotFoundMsg)) { 289 | CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE.put(credentialHelperName, credentialsNotFoundMsg); 290 | } 291 | } 292 | 293 | return CREDENTIALS_HELPERS_NOT_FOUND_MESSAGE_CACHE.get(credentialHelperName); 294 | } 295 | 296 | private String discoverCredentialsHelperNotFoundMessage(String credentialHelperName) { 297 | // will do fake call to given credential helper to find out with which message 298 | // it response when there are no credentials for given hostName 299 | 300 | // hostName should be valid, but most probably not existing 301 | // IF its not enough, then should probably run 'list' command first to be sure... 302 | final String notExistentFakeHostName = "https://not.a.real.registry/url"; 303 | 304 | String credentialsNotFoundMsg = null; 305 | try { 306 | runCredentialProgram(notExistentFakeHostName, credentialHelperName); 307 | 308 | // should not reach here 309 | log.warn("Failure running docker credential helper ({}) with fake call, expected 'credentials not found' response", 310 | credentialHelperName); 311 | } catch(Exception e) { 312 | if (e instanceof InvalidResultException) { 313 | credentialsNotFoundMsg = extractCredentialProviderErrorMessage((InvalidResultException)e); 314 | } 315 | 316 | if (isBlank(credentialsNotFoundMsg)) { 317 | log.warn("Failure running docker credential helper ({}) with fake call, expected 'credentials not found' response. Exception message: {}", 318 | credentialHelperName, 319 | e.getMessage()); 320 | } else { 321 | log.debug("Got credentials not found error message from docker credential helper - {}", credentialsNotFoundMsg); 322 | } 323 | } 324 | 325 | return credentialsNotFoundMsg; 326 | } 327 | 328 | private String extractCredentialProviderErrorMessage(InvalidResultException invalidResultEx) { 329 | if (invalidResultEx.getResult() != null && invalidResultEx.getResult().hasOutput()) { 330 | return invalidResultEx.getResult().outputString().trim(); 331 | } 332 | return null; 333 | } 334 | 335 | private String runCredentialProgram(String hostName, String credentialHelperName) 336 | throws InvalidResultException, InterruptedException, TimeoutException, IOException { 337 | 338 | return new ProcessExecutor() 339 | .command(credentialHelperName, "get") 340 | .redirectInput(new ByteArrayInputStream(hostName.getBytes())) 341 | .readOutput(true) 342 | .exitValueNormal() 343 | .timeout(30, TimeUnit.SECONDS) 344 | .execute() 345 | .outputUTF8() 346 | .trim(); 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/test/groovy/JooqDockerPluginSpec.groovy: -------------------------------------------------------------------------------- 1 | import org.gradle.testkit.runner.GradleRunner 2 | import spock.lang.Specification 3 | import spock.lang.TempDir 4 | import spock.util.io.FileSystemFixture 5 | 6 | import java.nio.file.Files 7 | import java.nio.file.Paths 8 | 9 | import static org.gradle.testkit.runner.TaskOutcome.FROM_CACHE 10 | import static org.gradle.testkit.runner.TaskOutcome.SUCCESS 11 | import static org.gradle.testkit.runner.TaskOutcome.UP_TO_DATE 12 | 13 | class JooqDockerPluginSpec extends Specification { 14 | 15 | @TempDir 16 | FileSystemFixture fileSystem 17 | File projectDir 18 | 19 | def setup() { 20 | projectDir = fileSystem.dir("project").toFile() 21 | if (getClass().getResourceAsStream("testkit-gradle.properties")) { 22 | copyResource("testkit-gradle.properties", "gradle.properties") 23 | } 24 | } 25 | 26 | def "plugin is applicable"() { 27 | given: 28 | prepareBuildGradleFile(""" 29 | plugins { 30 | id("com.revolut.jooq-docker") 31 | } 32 | """) 33 | 34 | when: 35 | def result = GradleRunner.create() 36 | .withProjectDir(projectDir) 37 | .withPluginClasspath() 38 | .build() 39 | 40 | then: 41 | result != null 42 | } 43 | 44 | def "generates jooq classes for PostgreSQL db with default config"() { 45 | given: 46 | prepareBuildGradleFile(""" 47 | plugins { 48 | id("com.revolut.jooq-docker") 49 | } 50 | 51 | repositories { 52 | mavenCentral() 53 | } 54 | 55 | dependencies { 56 | jdbc("org.postgresql:postgresql:42.2.5") 57 | } 58 | """) 59 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 60 | 61 | when: 62 | def result = GradleRunner.create() 63 | .withProjectDir(projectDir) 64 | .withPluginClasspath() 65 | .withArguments("generateJooqClasses") 66 | .build() 67 | 68 | then: 69 | result.task(":generateJooqClasses").outcome == SUCCESS 70 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 71 | def generatedFlywayClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/FlywaySchemaHistory.java") 72 | Files.exists(generatedFooClass) 73 | Files.exists(generatedFlywayClass) 74 | } 75 | 76 | def "generates jooq classes for PostgreSQL db with default config for multiple schemas"() { 77 | given: 78 | prepareBuildGradleFile(""" 79 | plugins { 80 | id("com.revolut.jooq-docker") 81 | } 82 | 83 | repositories { 84 | mavenCentral() 85 | } 86 | 87 | tasks { 88 | generateJooqClasses { 89 | schemas = arrayOf("public", "other") 90 | } 91 | } 92 | 93 | dependencies { 94 | jdbc("org.postgresql:postgresql:42.2.5") 95 | } 96 | """) 97 | copyResource("/V01__init_multiple_schemas.sql", "src/main/resources/db/migration/V01__init_multiple_schemas.sql") 98 | 99 | when: 100 | def result = GradleRunner.create() 101 | .withProjectDir(projectDir) 102 | .withPluginClasspath() 103 | .withArguments("generateJooqClasses") 104 | .build() 105 | 106 | then: 107 | result.task(":generateJooqClasses").outcome == SUCCESS 108 | def generatedPublic = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/public_/tables/Foo.java") 109 | def generatedOther = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/other/tables/Bar.java") 110 | def generatedFlywaySchemaClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/public_/tables/FlywaySchemaHistory.java") 111 | Files.exists(generatedPublic) 112 | Files.exists(generatedOther) 113 | Files.exists(generatedFlywaySchemaClass) 114 | } 115 | 116 | def "generates jooq classes for PostgreSQL db with default config for multiple schemas and renames package"() { 117 | given: 118 | prepareBuildGradleFile(""" 119 | plugins { 120 | id("com.revolut.jooq-docker") 121 | } 122 | 123 | repositories { 124 | mavenCentral() 125 | } 126 | 127 | tasks { 128 | generateJooqClasses { 129 | schemas = arrayOf("public", "other") 130 | schemaToPackageMapping = mapOf("public" to "fancy_name") 131 | } 132 | } 133 | 134 | dependencies { 135 | jdbc("org.postgresql:postgresql:42.2.5") 136 | } 137 | """) 138 | copyResource("/V01__init_multiple_schemas.sql", "src/main/resources/db/migration/V01__init_multiple_schemas.sql") 139 | 140 | when: 141 | def result = GradleRunner.create() 142 | .withProjectDir(projectDir) 143 | .withPluginClasspath() 144 | .withArguments("generateJooqClasses") 145 | .build() 146 | 147 | then: 148 | result.task(":generateJooqClasses").outcome == SUCCESS 149 | def generatedPublic = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/fancy_name/tables/Foo.java") 150 | def generatedOther = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/other/tables/Bar.java") 151 | Files.exists(generatedPublic) 152 | Files.exists(generatedOther) 153 | } 154 | 155 | def "respects the generator customizations"() { 156 | given: 157 | prepareBuildGradleFile(""" 158 | plugins { 159 | id("com.revolut.jooq-docker") 160 | } 161 | 162 | repositories { 163 | mavenCentral() 164 | } 165 | 166 | tasks { 167 | generateJooqClasses { 168 | schemas = arrayOf("public", "other") 169 | customizeGenerator { 170 | database.withExcludes("BAR") 171 | } 172 | } 173 | } 174 | 175 | dependencies { 176 | jdbc("org.postgresql:postgresql:42.2.5") 177 | } 178 | """) 179 | copyResource("/V01__init_multiple_schemas.sql", "src/main/resources/db/migration/V01__init_multiple_schemas.sql") 180 | 181 | when: 182 | def result = GradleRunner.create() 183 | .withProjectDir(projectDir) 184 | .withPluginClasspath() 185 | .withArguments("generateJooqClasses") 186 | .build() 187 | 188 | then: 189 | result.task(":generateJooqClasses").outcome == SUCCESS 190 | def generatedPublic = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/public_/tables/Foo.java") 191 | def generatedOther = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/other/tables/Bar.java") 192 | Files.exists(generatedPublic) 193 | !Files.exists(generatedOther) 194 | } 195 | 196 | def "up to date check works for output dir"() { 197 | given: 198 | prepareBuildGradleFile(""" 199 | plugins { 200 | id("com.revolut.jooq-docker") 201 | } 202 | 203 | repositories { 204 | mavenCentral() 205 | } 206 | 207 | dependencies { 208 | jdbc("org.postgresql:postgresql:42.2.5") 209 | } 210 | """) 211 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 212 | 213 | when: 214 | def firstRun = GradleRunner.create() 215 | .withProjectDir(projectDir) 216 | .withPluginClasspath() 217 | .withArguments("generateJooqClasses") 218 | .build() 219 | def secondRun = GradleRunner.create() 220 | .withProjectDir(projectDir) 221 | .withPluginClasspath() 222 | .withArguments("generateJooqClasses") 223 | .build() 224 | Paths.get(projectDir.path, "build/generated-jooq").toFile().deleteDir() 225 | def runAfterDeletion = GradleRunner.create() 226 | .withProjectDir(projectDir) 227 | .withPluginClasspath() 228 | .withArguments("generateJooqClasses") 229 | .build() 230 | 231 | then: 232 | firstRun.task(":generateJooqClasses").outcome == SUCCESS 233 | secondRun.task(":generateJooqClasses").outcome == UP_TO_DATE 234 | runAfterDeletion.task(":generateJooqClasses").outcome == SUCCESS 235 | } 236 | 237 | def "up to date check works for input dir"() { 238 | given: 239 | prepareBuildGradleFile(""" 240 | plugins { 241 | id("com.revolut.jooq-docker") 242 | } 243 | 244 | repositories { 245 | mavenCentral() 246 | } 247 | 248 | dependencies { 249 | jdbc("org.postgresql:postgresql:42.2.5") 250 | } 251 | """) 252 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 253 | 254 | when: 255 | def firstRun = GradleRunner.create() 256 | .withProjectDir(projectDir) 257 | .withPluginClasspath() 258 | .withArguments("generateJooqClasses") 259 | .build() 260 | def secondRun = GradleRunner.create() 261 | .withProjectDir(projectDir) 262 | .withPluginClasspath() 263 | .withArguments("generateJooqClasses") 264 | .build() 265 | copyResource("/V02__add_bar.sql", "src/main/resources/db/migration/V02__add_bar.sql") 266 | def runAfterDeletion = GradleRunner.create() 267 | .withProjectDir(projectDir) 268 | .withPluginClasspath() 269 | .withArguments("generateJooqClasses") 270 | .build() 271 | 272 | then: 273 | firstRun.task(":generateJooqClasses").outcome == SUCCESS 274 | secondRun.task(":generateJooqClasses").outcome == UP_TO_DATE 275 | runAfterDeletion.task(":generateJooqClasses").outcome == SUCCESS 276 | } 277 | 278 | def "up to date check works for extension changes"() { 279 | given: 280 | def initialBuildGradle = 281 | """ 282 | plugins { 283 | id("com.revolut.jooq-docker") 284 | } 285 | 286 | jooq { 287 | image { 288 | tag = "11.2-alpine" 289 | } 290 | } 291 | 292 | repositories { 293 | mavenCentral() 294 | } 295 | 296 | dependencies { 297 | jdbc("org.postgresql:postgresql:42.2.5") 298 | } 299 | """ 300 | def extensionUpdatedBuildGradle = 301 | """ 302 | plugins { 303 | id("com.revolut.jooq-docker") 304 | } 305 | 306 | jooq { 307 | image { 308 | tag = "11.3-alpine" 309 | } 310 | } 311 | 312 | repositories { 313 | mavenCentral() 314 | } 315 | 316 | dependencies { 317 | jdbc("org.postgresql:postgresql:42.2.5") 318 | } 319 | """ 320 | prepareBuildGradleFile(initialBuildGradle) 321 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 322 | 323 | when: 324 | def initialResult = GradleRunner.create() 325 | .withProjectDir(projectDir) 326 | .withPluginClasspath() 327 | .withArguments("generateJooqClasses") 328 | .build() 329 | prepareBuildGradleFile(extensionUpdatedBuildGradle) 330 | def resultAfterChangeToExtension = GradleRunner.create() 331 | .withProjectDir(projectDir) 332 | .withPluginClasspath() 333 | .withArguments("generateJooqClasses") 334 | .build() 335 | def finalRunNoChanges = GradleRunner.create() 336 | .withProjectDir(projectDir) 337 | .withPluginClasspath() 338 | .withArguments("generateJooqClasses") 339 | .build() 340 | 341 | then: 342 | initialResult.task(":generateJooqClasses").outcome == SUCCESS 343 | resultAfterChangeToExtension.task(":generateJooqClasses").outcome == SUCCESS 344 | finalRunNoChanges.task(":generateJooqClasses").outcome == UP_TO_DATE 345 | } 346 | 347 | def "up to date check works for generator customizations"() { 348 | given: 349 | def initialBuildGradle = 350 | """ 351 | plugins { 352 | id("com.revolut.jooq-docker") 353 | } 354 | 355 | repositories { 356 | mavenCentral() 357 | } 358 | 359 | tasks { 360 | generateJooqClasses { 361 | schemas = arrayOf("public", "other") 362 | customizeGenerator { 363 | database.withExcludes("BAR") 364 | } 365 | } 366 | } 367 | 368 | dependencies { 369 | jdbc("org.postgresql:postgresql:42.2.5") 370 | } 371 | """ 372 | def updatedBuildGradle = 373 | """ 374 | plugins { 375 | id("com.revolut.jooq-docker") 376 | } 377 | 378 | repositories { 379 | mavenCentral() 380 | } 381 | 382 | tasks { 383 | generateJooqClasses { 384 | schemas = arrayOf("public", "other") 385 | customizeGenerator { 386 | database.withExcludes(".*") 387 | } 388 | } 389 | } 390 | 391 | dependencies { 392 | jdbc("org.postgresql:postgresql:42.2.5") 393 | } 394 | """ 395 | copyResource("/V01__init_multiple_schemas.sql", "src/main/resources/db/migration/V01__init_multiple_schemas.sql") 396 | prepareBuildGradleFile(initialBuildGradle) 397 | 398 | when: 399 | def initialRun = GradleRunner.create() 400 | .withProjectDir(projectDir) 401 | .withPluginClasspath() 402 | .withArguments("generateJooqClasses") 403 | .build() 404 | prepareBuildGradleFile(updatedBuildGradle) 405 | def runAfterUpdate = GradleRunner.create() 406 | .withProjectDir(projectDir) 407 | .withPluginClasspath() 408 | .withArguments("generateJooqClasses") 409 | .build() 410 | def finalRunNoChanges = GradleRunner.create() 411 | .withProjectDir(projectDir) 412 | .withPluginClasspath() 413 | .withArguments("generateJooqClasses") 414 | .build() 415 | 416 | then: 417 | initialRun.task(":generateJooqClasses").outcome == SUCCESS 418 | runAfterUpdate.task(":generateJooqClasses").outcome == SUCCESS 419 | finalRunNoChanges.task(":generateJooqClasses").outcome == UP_TO_DATE 420 | def generatedPublic = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/public_/tables/Foo.java") 421 | def generatedOther = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/other/tables/Bar.java") 422 | !Files.exists(generatedPublic) 423 | !Files.exists(generatedOther) 424 | } 425 | 426 | def "generates jooq classes in a given package"() { 427 | given: 428 | prepareBuildGradleFile(""" 429 | plugins { 430 | id("com.revolut.jooq-docker") 431 | } 432 | 433 | repositories { 434 | mavenCentral() 435 | } 436 | 437 | tasks { 438 | generateJooqClasses { 439 | basePackageName = "com.example" 440 | } 441 | } 442 | 443 | dependencies { 444 | jdbc("org.postgresql:postgresql:42.2.5") 445 | } 446 | """) 447 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 448 | 449 | when: 450 | def result = GradleRunner.create() 451 | .withProjectDir(projectDir) 452 | .withPluginClasspath() 453 | .withArguments("generateJooqClasses") 454 | .build() 455 | 456 | then: 457 | result.task(":generateJooqClasses").outcome == SUCCESS 458 | def generatedClass = Paths.get(projectDir.getPath(), "build/generated-jooq/com/example/tables/Foo.java") 459 | Files.exists(generatedClass) 460 | } 461 | 462 | def "plugin is configurable"() { 463 | given: 464 | prepareBuildGradleFile(""" 465 | plugins { 466 | id("com.revolut.jooq-docker") 467 | } 468 | 469 | repositories { 470 | mavenCentral() 471 | } 472 | 473 | jooq { 474 | image { 475 | repository = "mysql" 476 | tag = "8.3.0" 477 | envVars = mapOf( 478 | "MYSQL_ROOT_PASSWORD" to "mysql", 479 | "MYSQL_DATABASE" to "mysql") 480 | containerName = "uniqueMySqlContainerName" 481 | readinessProbe = { host: String, port: Int -> 482 | arrayOf("sh", "-c", "until mysqladmin -h\$host -P\$port -uroot -pmysql ping; do echo wait; sleep 1; done;") 483 | } 484 | } 485 | 486 | db { 487 | username = "root" 488 | password = "mysql" 489 | name = "mysql" 490 | port = 3306 491 | } 492 | 493 | jdbc { 494 | schema = "jdbc:mysql" 495 | driverClassName = "com.mysql.cj.jdbc.Driver" 496 | jooqMetaName = "org.jooq.meta.mysql.MySQLDatabase" 497 | urlQueryParams = "?useSSL=false" 498 | } 499 | } 500 | 501 | dependencies { 502 | jdbc("mysql:mysql-connector-java:8.0.15") 503 | } 504 | """) 505 | copyResource("/V01__init_mysql.sql", "src/main/resources/db/migration/V01__init_mysql.sql") 506 | 507 | when: 508 | def result = GradleRunner.create() 509 | .withProjectDir(projectDir) 510 | .withPluginClasspath() 511 | .withArguments("generateJooqClasses") 512 | .build() 513 | 514 | then: 515 | result.task(":generateJooqClasses").outcome == SUCCESS 516 | def generatedClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 517 | Files.exists(generatedClass) 518 | } 519 | 520 | def "flyway configuration overridden with flywayProperties task input"() { 521 | given: 522 | prepareBuildGradleFile(""" 523 | plugins { 524 | id("com.revolut.jooq-docker") 525 | } 526 | 527 | repositories { 528 | mavenCentral() 529 | } 530 | 531 | tasks { 532 | generateJooqClasses { 533 | flywayProperties = mapOf("flyway.placeholderReplacement" to "false") 534 | } 535 | } 536 | 537 | dependencies { 538 | jdbc("org.postgresql:postgresql:42.2.5") 539 | } 540 | """) 541 | copyResource("/V01__init_with_placeholders.sql", "src/main/resources/db/migration/V01__init_with_placeholders.sql") 542 | 543 | when: 544 | def result = GradleRunner.create() 545 | .withProjectDir(projectDir) 546 | .withPluginClasspath() 547 | .withArguments("generateJooqClasses") 548 | .build() 549 | 550 | then: 551 | result.task(":generateJooqClasses").outcome == SUCCESS 552 | def generatedClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 553 | Files.exists(generatedClass) 554 | } 555 | 556 | def "schema version provider is aware of flyway table name override"() { 557 | given: 558 | prepareBuildGradleFile(""" 559 | plugins { 560 | id("com.revolut.jooq-docker") 561 | } 562 | 563 | repositories { 564 | mavenCentral() 565 | } 566 | 567 | tasks { 568 | generateJooqClasses { 569 | flywayProperties = mapOf("flyway.table" to "some_schema_table") 570 | } 571 | } 572 | 573 | dependencies { 574 | jdbc("org.postgresql:postgresql:42.2.5") 575 | } 576 | """) 577 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 578 | 579 | when: 580 | def result = GradleRunner.create() 581 | .withProjectDir(projectDir) 582 | .withPluginClasspath() 583 | .withArguments("generateJooqClasses") 584 | .build() 585 | 586 | then: 587 | result.task(":generateJooqClasses").outcome == SUCCESS 588 | def generatedFlywayClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/SomeSchemaTable.java") 589 | Files.exists(generatedFlywayClass) 590 | } 591 | 592 | def "plugin works in Groovy gradle file"() { 593 | given: 594 | def buildGradleFile = new File(projectDir, "build.gradle") 595 | buildGradleFile.write( 596 | """ 597 | plugins { 598 | id "com.revolut.jooq-docker" 599 | } 600 | 601 | repositories { 602 | mavenCentral() 603 | } 604 | 605 | tasks { 606 | generateJooqClasses { 607 | flywayProperties = ["flyway.placeholderReplacement": "false"] 608 | customizeGenerator { 609 | database.withExcludes("BAR") 610 | } 611 | } 612 | } 613 | 614 | dependencies { 615 | jdbc "org.postgresql:postgresql:42.2.5" 616 | } 617 | """) 618 | copyResource("/V01__init_with_placeholders.sql", "src/main/resources/db/migration/V01__init_with_placeholders.sql") 619 | copyResource("/V02__add_bar.sql", "src/main/resources/db/migration/V02__add_bar.sql") 620 | 621 | when: 622 | def result = GradleRunner.create() 623 | .withProjectDir(projectDir) 624 | .withPluginClasspath() 625 | .withArguments("generateJooqClasses", "--stacktrace") 626 | .build() 627 | 628 | then: 629 | result.task(":generateJooqClasses").outcome == SUCCESS 630 | def generatedFoo = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 631 | def generatedBar = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Bar.java") 632 | Files.exists(generatedFoo) 633 | !Files.exists(generatedBar) 634 | } 635 | 636 | def "output schema to default properly passed to jOOQ generator"() { 637 | given: 638 | prepareBuildGradleFile(""" 639 | plugins { 640 | id("com.revolut.jooq-docker") 641 | } 642 | 643 | repositories { 644 | mavenCentral() 645 | } 646 | 647 | tasks { 648 | generateJooqClasses { 649 | outputSchemaToDefault = setOf("public") 650 | } 651 | } 652 | 653 | dependencies { 654 | jdbc("org.postgresql:postgresql:42.2.5") 655 | } 656 | """) 657 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 658 | 659 | when: 660 | def result = GradleRunner.create() 661 | .withProjectDir(projectDir) 662 | .withPluginClasspath() 663 | .withArguments("generateJooqClasses") 664 | .build() 665 | 666 | then: 667 | result.task(":generateJooqClasses").outcome == SUCCESS 668 | def generatedTableClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 669 | def generatedSchemaClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/DefaultSchema.java") 670 | Files.exists(generatedTableClass) 671 | Files.exists(generatedSchemaClass) 672 | } 673 | 674 | def "exclude flyway schema history"() { 675 | given: 676 | prepareBuildGradleFile(""" 677 | plugins { 678 | id("com.revolut.jooq-docker") 679 | } 680 | 681 | repositories { 682 | mavenCentral() 683 | } 684 | 685 | tasks { 686 | generateJooqClasses { 687 | excludeFlywayTable = true 688 | } 689 | } 690 | 691 | dependencies { 692 | jdbc("org.postgresql:postgresql:42.2.5") 693 | } 694 | """) 695 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 696 | 697 | when: 698 | def result = GradleRunner.create() 699 | .withProjectDir(projectDir) 700 | .withPluginClasspath() 701 | .withArguments("generateJooqClasses") 702 | .build() 703 | 704 | then: 705 | result.task(":generateJooqClasses").outcome == SUCCESS 706 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 707 | def generatedFlywayClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/FlywaySchemaHistory.java") 708 | Files.exists(generatedFooClass) 709 | Files.notExists(generatedFlywayClass) 710 | } 711 | 712 | 713 | def "exclude flyway schema history given custom Flyway table name"() { 714 | given: 715 | prepareBuildGradleFile(""" 716 | plugins { 717 | id("com.revolut.jooq-docker") 718 | } 719 | 720 | repositories { 721 | mavenCentral() 722 | } 723 | 724 | tasks { 725 | generateJooqClasses { 726 | excludeFlywayTable = true 727 | flywayProperties = mapOf("flyway.table" to "some_schema_table") 728 | } 729 | } 730 | 731 | dependencies { 732 | jdbc("org.postgresql:postgresql:42.2.5") 733 | } 734 | """) 735 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 736 | 737 | when: 738 | def result = GradleRunner.create() 739 | .withProjectDir(projectDir) 740 | .withPluginClasspath() 741 | .withArguments("generateJooqClasses") 742 | .build() 743 | 744 | then: 745 | result.task(":generateJooqClasses").outcome == SUCCESS 746 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 747 | def generatedCustomFlywayClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/SomeSchemaTable.java") 748 | Files.exists(generatedFooClass) 749 | Files.notExists(generatedCustomFlywayClass) 750 | } 751 | 752 | 753 | def "exclude flyway schema history without overriding existing excludes"() { 754 | given: 755 | prepareBuildGradleFile(""" 756 | plugins { 757 | id("com.revolut.jooq-docker") 758 | } 759 | 760 | repositories { 761 | mavenCentral() 762 | } 763 | 764 | tasks { 765 | generateJooqClasses { 766 | excludeFlywayTable = true 767 | schemas = arrayOf("public", "other") 768 | customizeGenerator { 769 | database.withExcludes("BAR") 770 | } 771 | } 772 | } 773 | 774 | dependencies { 775 | jdbc("org.postgresql:postgresql:42.2.5") 776 | } 777 | """) 778 | copyResource("/V01__init_multiple_schemas.sql", "src/main/resources/db/migration/V01__init_multiple_schemas.sql") 779 | 780 | when: 781 | def result = GradleRunner.create() 782 | .withProjectDir(projectDir) 783 | .withPluginClasspath() 784 | .withArguments("generateJooqClasses") 785 | .build() 786 | 787 | then: 788 | result.task(":generateJooqClasses").outcome == SUCCESS 789 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/public_/tables/Foo.java") 790 | def generatedBarClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/other/tables/Bar.java") 791 | def generatedFlywaySchemaClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/public_/tables/FlywaySchemaHistory.java") 792 | Files.exists(generatedFooClass) 793 | Files.notExists(generatedBarClass) 794 | Files.notExists(generatedFlywaySchemaClass) 795 | } 796 | 797 | def "outputDirectory task property is respected"() { 798 | given: 799 | prepareBuildGradleFile(""" 800 | plugins { 801 | id("com.revolut.jooq-docker") 802 | } 803 | 804 | repositories { 805 | mavenCentral() 806 | } 807 | 808 | tasks { 809 | generateJooqClasses { 810 | outputDirectory.set(project.layout.buildDirectory.dir("gen")) 811 | } 812 | } 813 | 814 | dependencies { 815 | jdbc("org.postgresql:postgresql:42.2.5") 816 | } 817 | """) 818 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 819 | 820 | when: 821 | def result = GradleRunner.create() 822 | .withProjectDir(projectDir) 823 | .withPluginClasspath() 824 | .withArguments("generateJooqClasses") 825 | .build() 826 | 827 | then: 828 | result.task(":generateJooqClasses").outcome == SUCCESS 829 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/gen/org/jooq/generated/tables/Foo.java") 830 | def generatedFlywayClass = Paths.get(projectDir.getPath(), "build/gen/org/jooq/generated/tables/FlywaySchemaHistory.java") 831 | Files.exists(generatedFooClass) 832 | Files.exists(generatedFlywayClass) 833 | } 834 | 835 | def "source sets and tasks are configured for java project"() { 836 | given: 837 | prepareBuildGradleFile(""" 838 | plugins { 839 | java 840 | id("com.revolut.jooq-docker") 841 | } 842 | 843 | repositories { 844 | mavenCentral() 845 | } 846 | 847 | dependencies { 848 | jdbc("org.postgresql:postgresql:42.2.5") 849 | implementation("org.jooq:jooq:3.14.15") 850 | implementation("javax.annotation:javax.annotation-api:1.3.2") 851 | } 852 | """) 853 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 854 | writeProjectFile("src/main/java/com/test/Main.java", 855 | """ 856 | package com.test; 857 | 858 | import static org.jooq.generated.Tables.FOO; 859 | 860 | public class Main { 861 | public static void main(String[] args) { 862 | System.out.println(FOO.ID.getName()); 863 | } 864 | } 865 | """); 866 | 867 | when: 868 | def result = GradleRunner.create() 869 | .withProjectDir(projectDir) 870 | .withPluginClasspath() 871 | .withArguments("classes") 872 | .build() 873 | 874 | then: 875 | result.task(":generateJooqClasses").outcome == SUCCESS 876 | result.task(":classes").outcome == SUCCESS 877 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 878 | def mainClass = Paths.get(projectDir.getPath(), "build/classes/java/main/com/test/Main.class") 879 | Files.exists(generatedFooClass) 880 | Files.exists(mainClass) 881 | } 882 | 883 | def "source sets and tasks are configured for kotlin project"() { 884 | given: 885 | prepareBuildGradleFile(""" 886 | plugins { 887 | id("com.revolut.jooq-docker") 888 | kotlin("jvm").version("1.9.22") 889 | } 890 | 891 | repositories { 892 | mavenCentral() 893 | } 894 | 895 | dependencies { 896 | implementation(kotlin("stdlib")) 897 | jdbc("org.postgresql:postgresql:42.2.5") 898 | implementation("org.jooq:jooq:3.14.15") 899 | implementation("javax.annotation:javax.annotation-api:1.3.2") 900 | } 901 | """) 902 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 903 | writeProjectFile("src/main/kotlin/com/test/Main.kt", 904 | """ 905 | package com.test 906 | 907 | import org.jooq.generated.Tables.FOO 908 | 909 | fun main() = println(FOO.ID.name) 910 | """); 911 | 912 | when: 913 | def result = GradleRunner.create() 914 | .withProjectDir(projectDir) 915 | .withPluginClasspath() 916 | .withArguments("classes") 917 | .build() 918 | 919 | then: 920 | result.task(":generateJooqClasses").outcome == SUCCESS 921 | result.task(":classes").outcome == SUCCESS 922 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 923 | def mainClass = Paths.get(projectDir.getPath(), "build/classes/kotlin/main/com/test/MainKt.class") 924 | Files.exists(generatedFooClass) 925 | Files.exists(mainClass) 926 | } 927 | 928 | def "source sets and tasks are configured for java project when jooq plugin is applied before java plugin"() { 929 | given: 930 | prepareBuildGradleFile(""" 931 | plugins { 932 | id("com.revolut.jooq-docker") 933 | } 934 | apply(plugin = "java") 935 | 936 | repositories { 937 | mavenCentral() 938 | } 939 | 940 | dependencies { 941 | jdbc("org.postgresql:postgresql:42.2.5") 942 | "implementation"("org.jooq:jooq:3.14.15") 943 | "implementation"("javax.annotation:javax.annotation-api:1.3.2") 944 | } 945 | """) 946 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 947 | writeProjectFile("src/main/java/com/test/Main.java", 948 | """ 949 | package com.test; 950 | 951 | import static org.jooq.generated.Tables.FOO; 952 | 953 | public class Main { 954 | public static void main(String[] args) { 955 | System.out.println(FOO.ID.getName()); 956 | } 957 | } 958 | """); 959 | 960 | when: 961 | def result = GradleRunner.create() 962 | .withProjectDir(projectDir) 963 | .withPluginClasspath() 964 | .withArguments("classes") 965 | .build() 966 | 967 | then: 968 | result.task(":generateJooqClasses").outcome == SUCCESS 969 | result.task(":classes").outcome == SUCCESS 970 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 971 | def mainClass = Paths.get(projectDir.getPath(), "build/classes/java/main/com/test/Main.class") 972 | Files.exists(generatedFooClass) 973 | Files.exists(mainClass) 974 | } 975 | 976 | def "source sets and tasks are configured for java project when jooq plugin is applied before java-library plugin"() { 977 | given: 978 | prepareBuildGradleFile(""" 979 | plugins { 980 | id("com.revolut.jooq-docker") 981 | } 982 | apply(plugin = "java-library") 983 | 984 | repositories { 985 | mavenCentral() 986 | } 987 | 988 | dependencies { 989 | jdbc("org.postgresql:postgresql:42.2.5") 990 | "implementation"("org.jooq:jooq:3.14.15") 991 | "implementation"("javax.annotation:javax.annotation-api:1.3.2") 992 | } 993 | """) 994 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 995 | writeProjectFile("src/main/java/com/test/Main.java", 996 | """ 997 | package com.test; 998 | 999 | import static org.jooq.generated.Tables.FOO; 1000 | 1001 | public class Main { 1002 | public static void main(String[] args) { 1003 | System.out.println(FOO.ID.getName()); 1004 | } 1005 | } 1006 | """); 1007 | 1008 | when: 1009 | def result = GradleRunner.create() 1010 | .withProjectDir(projectDir) 1011 | .withPluginClasspath() 1012 | .withArguments("classes") 1013 | .build() 1014 | 1015 | then: 1016 | result.task(":generateJooqClasses").outcome == SUCCESS 1017 | result.task(":classes").outcome == SUCCESS 1018 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 1019 | def mainClass = Paths.get(projectDir.getPath(), "build/classes/java/main/com/test/Main.class") 1020 | Files.exists(generatedFooClass) 1021 | Files.exists(mainClass) 1022 | } 1023 | 1024 | def "source sets and tasks are configured for kotlin project when jooq plugin is applied before kotlin plugin"() { 1025 | given: 1026 | prepareBuildGradleFile(""" 1027 | buildscript { 1028 | dependencies { 1029 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") 1030 | } 1031 | } 1032 | 1033 | plugins { 1034 | id("com.revolut.jooq-docker") 1035 | } 1036 | apply(plugin = "org.jetbrains.kotlin.jvm") 1037 | 1038 | repositories { 1039 | mavenCentral() 1040 | } 1041 | 1042 | dependencies { 1043 | "implementation"(kotlin("stdlib")) 1044 | jdbc("org.postgresql:postgresql:42.2.5") 1045 | "implementation"("org.jooq:jooq:3.14.15") 1046 | "implementation"("javax.annotation:javax.annotation-api:1.3.2") 1047 | } 1048 | """) 1049 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 1050 | writeProjectFile("src/main/kotlin/com/test/Main.kt", 1051 | """ 1052 | package com.test 1053 | 1054 | import org.jooq.generated.Tables.FOO 1055 | 1056 | fun main() = println(FOO.ID.name) 1057 | """); 1058 | 1059 | when: 1060 | def result = GradleRunner.create() 1061 | .withProjectDir(projectDir) 1062 | .withPluginClasspath() 1063 | .withArguments("classes") 1064 | .build() 1065 | 1066 | then: 1067 | result.task(":generateJooqClasses").outcome == SUCCESS 1068 | result.task(":classes").outcome == SUCCESS 1069 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 1070 | def mainClass = Paths.get(projectDir.getPath(), "build/classes/kotlin/main/com/test/MainKt.class") 1071 | Files.exists(generatedFooClass) 1072 | Files.exists(mainClass) 1073 | } 1074 | 1075 | def "generateJooqClasses task output is loaded from cache"() { 1076 | given: 1077 | configureLocalGradleCache(); 1078 | prepareBuildGradleFile(""" 1079 | plugins { 1080 | id("com.revolut.jooq-docker") 1081 | } 1082 | 1083 | repositories { 1084 | mavenCentral() 1085 | } 1086 | 1087 | dependencies { 1088 | jdbc("org.postgresql:postgresql:42.2.5") 1089 | } 1090 | """) 1091 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 1092 | 1093 | when: 1094 | //first run loads to cache 1095 | def firstRun = GradleRunner.create() 1096 | .withProjectDir(projectDir) 1097 | .withPluginClasspath() 1098 | .withArguments("generateJooqClasses", "--build-cache") 1099 | .build() 1100 | //second run uses from cache 1101 | new File(projectDir, 'build').deleteDir() 1102 | def secondRun = GradleRunner.create() 1103 | .withProjectDir(projectDir) 1104 | .withPluginClasspath() 1105 | .withArguments("generateJooqClasses", "--build-cache") 1106 | .build() 1107 | //third run got changes and can't use cached output 1108 | new File(projectDir, 'build').deleteDir() 1109 | copyResource("/V02__add_bar.sql", "src/main/resources/db/migration/V02__add_bar.sql") 1110 | def thirdRun = GradleRunner.create() 1111 | .withProjectDir(projectDir) 1112 | .withPluginClasspath() 1113 | .withArguments("generateJooqClasses", "--build-cache") 1114 | .build() 1115 | 1116 | then: 1117 | firstRun.task(":generateJooqClasses").outcome == SUCCESS 1118 | secondRun.task(":generateJooqClasses").outcome == FROM_CACHE 1119 | thirdRun.task(":generateJooqClasses").outcome == SUCCESS 1120 | 1121 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 1122 | def generatedFlywayClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/FlywaySchemaHistory.java") 1123 | def generatedBarClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Bar.java") 1124 | Files.exists(generatedFooClass) 1125 | Files.exists(generatedBarClass) 1126 | Files.exists(generatedFlywayClass) 1127 | } 1128 | 1129 | def "regenerates jooq classes when out of date even though output directory already has classes generated"() { 1130 | given: 1131 | def initialBuildGradle = 1132 | """ 1133 | import org.jooq.meta.jaxb.ForcedType 1134 | 1135 | plugins { 1136 | id("com.revolut.jooq-docker") 1137 | } 1138 | 1139 | repositories { 1140 | mavenCentral() 1141 | } 1142 | 1143 | tasks { 1144 | generateJooqClasses { 1145 | customizeGenerator { 1146 | database.withForcedTypes(ForcedType() 1147 | .withUserType("com.example.UniqueClassForFirstGeneration") 1148 | .withBinding("com.example.PostgresJSONGsonBinding") 1149 | .withTypes("JSONB")) 1150 | } 1151 | } 1152 | } 1153 | 1154 | dependencies { 1155 | jdbc("org.postgresql:postgresql:42.2.5") 1156 | } 1157 | """ 1158 | def updatedBuildFile = 1159 | """ 1160 | import org.jooq.meta.jaxb.ForcedType 1161 | 1162 | plugins { 1163 | id("com.revolut.jooq-docker") 1164 | } 1165 | 1166 | repositories { 1167 | mavenCentral() 1168 | } 1169 | 1170 | tasks { 1171 | generateJooqClasses { 1172 | customizeGenerator { 1173 | database.withForcedTypes(ForcedType() 1174 | .withUserType("com.example.UniqueClassForSecondGeneration") 1175 | .withBinding("com.example.PostgresJSONGsonBinding") 1176 | .withTypes("JSONB")) 1177 | } 1178 | } 1179 | } 1180 | 1181 | dependencies { 1182 | jdbc("org.postgresql:postgresql:42.2.5") 1183 | } 1184 | """ 1185 | prepareBuildGradleFile(initialBuildGradle) 1186 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 1187 | 1188 | when: 1189 | def firstRun = GradleRunner.create() 1190 | .withProjectDir(projectDir) 1191 | .withPluginClasspath() 1192 | .withArguments("generateJooqClasses") 1193 | .build() 1194 | 1195 | then: 1196 | firstRun.task(":generateJooqClasses").outcome == SUCCESS 1197 | with(Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java")) { 1198 | Files.exists(it) 1199 | Files.readAllLines(it).any { it.contains("com.example.UniqueClassForFirstGeneration") } 1200 | } 1201 | 1202 | when: 1203 | prepareBuildGradleFile(updatedBuildFile) 1204 | def secondRun = GradleRunner.create() 1205 | .withProjectDir(projectDir) 1206 | .withPluginClasspath() 1207 | .withArguments("generateJooqClasses") 1208 | .build() 1209 | 1210 | then: 1211 | secondRun.task(":generateJooqClasses").outcome == SUCCESS 1212 | with(Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java")) { 1213 | Files.exists(it) 1214 | Files.readAllLines(it).any { it.contains("com.example.UniqueClassForSecondGeneration") } 1215 | } 1216 | } 1217 | 1218 | def "generates flyway table in first schema by default"() { 1219 | given: 1220 | prepareBuildGradleFile(""" 1221 | plugins { 1222 | id("com.revolut.jooq-docker") 1223 | } 1224 | 1225 | repositories { 1226 | mavenCentral() 1227 | } 1228 | 1229 | tasks { 1230 | generateJooqClasses { 1231 | schemas = arrayOf("other", "public") 1232 | } 1233 | } 1234 | 1235 | dependencies { 1236 | jdbc("org.postgresql:postgresql:42.2.5") 1237 | } 1238 | """) 1239 | copyResource("/V01__init_multiple_schemas.sql", "src/main/resources/db/migration/V01__init_multiple_schemas.sql") 1240 | 1241 | when: 1242 | def result = GradleRunner.create() 1243 | .withProjectDir(projectDir) 1244 | .withPluginClasspath() 1245 | .withArguments("generateJooqClasses") 1246 | .build() 1247 | 1248 | then: 1249 | result.task(":generateJooqClasses").outcome == SUCCESS 1250 | def generatedPublic = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/public_/tables/Foo.java") 1251 | def generatedOther = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/other/tables/Bar.java") 1252 | def generatedFlywaySchemaClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/other/tables/FlywaySchemaHistory.java") 1253 | Files.exists(generatedPublic) 1254 | Files.exists(generatedOther) 1255 | Files.exists(generatedFlywaySchemaClass) 1256 | } 1257 | 1258 | def "customizer has default generate object defined"() { 1259 | given: 1260 | prepareBuildGradleFile(""" 1261 | plugins { 1262 | id("com.revolut.jooq-docker") 1263 | } 1264 | 1265 | repositories { 1266 | mavenCentral() 1267 | } 1268 | 1269 | tasks { 1270 | generateJooqClasses { 1271 | customizeGenerator { 1272 | generate.setDeprecated(true) 1273 | } 1274 | } 1275 | } 1276 | 1277 | dependencies { 1278 | jdbc("org.postgresql:postgresql:42.2.5") 1279 | } 1280 | """) 1281 | copyResource("/V01__init.sql", "src/main/resources/db/migration/V01__init.sql") 1282 | 1283 | when: 1284 | def result = GradleRunner.create() 1285 | .withProjectDir(projectDir) 1286 | .withPluginClasspath() 1287 | .withArguments("generateJooqClasses") 1288 | .build() 1289 | 1290 | then: 1291 | result.task(":generateJooqClasses").outcome == SUCCESS 1292 | def generatedFooClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/Foo.java") 1293 | def generatedFlywayClass = Paths.get(projectDir.getPath(), "build/generated-jooq/org/jooq/generated/tables/FlywaySchemaHistory.java") 1294 | Files.exists(generatedFooClass) 1295 | Files.exists(generatedFlywayClass) 1296 | } 1297 | 1298 | def configureLocalGradleCache() { 1299 | File localBuildCacheDirectory = fileSystem.dir("cache").toFile() 1300 | def settingsGradleFile = new File(projectDir, "settings.gradle.kts") 1301 | settingsGradleFile.write(""" 1302 | buildCache { 1303 | local { 1304 | directory = "${localBuildCacheDirectory.path}" 1305 | } 1306 | } 1307 | """) 1308 | } 1309 | 1310 | private void prepareBuildGradleFile(String script) { 1311 | def buildGradleFile = new File(projectDir, "build.gradle.kts") 1312 | buildGradleFile.write(script) 1313 | } 1314 | 1315 | private void copyResource(String resource, String relativePath) { 1316 | def file = new File(projectDir, relativePath) 1317 | file.parentFile.mkdirs() 1318 | getClass().getResourceAsStream(resource).transferTo(new FileOutputStream(file)) 1319 | } 1320 | 1321 | private void writeProjectFile(String relativePath, String content) { 1322 | def file = new File(projectDir, relativePath) 1323 | file.parentFile.mkdirs() 1324 | file.write(content) 1325 | } 1326 | } 1327 | --------------------------------------------------------------------------------