Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
strategy:
fail-fast: false
matrix:
jre: [17]
jre: [21]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
### Added
- `webtools.Env.isHerokuBuild()` and `isGitHubAction()`
- `com.diffplug.webtools.jte` plugin ([#10](https://github.com/diffplug/webtools/pull/10))
- `com.diffplug.webtools.flywayjooq` plugin ([#11](https://github.com/diffplug/webtools/pull/11))

## [1.2.6] - 2025-08-22
### Fixed
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [node](#node) - hassle-free `npm install` and `npm run blah`
- [static server](#static-server) - a simple static file server
- [jte](#jte) - creates idiomatic Kotlin model classes for `jte` templates (strict nullability & idiomatic collections and generics)
- [flywayjooq](#flywayjooq) - coordinates docker, flyway, and jOOQ for fast testing

## Node

Expand Down Expand Up @@ -66,3 +67,24 @@ class header(
```

We also translate Java collections and generics to their Kotlin equivalents. See `JteRenderer.convertJavaToKotlin` for details.

### flywayjooq

Compile tasks just need to depend on the `jooq` task. It will keep a live database running to test against.

```gradle
flywayJooq {
// starts this docker container which needs to have postgres
setup.dockerComposeFile = file('src/test/resources/docker-compose.yml')
// writes out connection data to this file
setup.dockerConnectionParams = file('build/pgConnection.properties')
// migrates a template database to this
setup.flywayMigrations = file('src/main/resources/db/migration')
// dumps the final schema out to this
setup.flywaySchemaDump = file('src/test/resources/schema.sql')
// sets up jOOQ
configuration {
// jOOQ setup same as the official jOOQ plugin
}
}
```
18 changes: 18 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,22 @@ dependencies {
jteCompileOnly gradleApi()
jteCompileOnly "gg.jte:jte-runtime:${VER_JTE}"
jteCompileOnly "gg.jte:jte:${VER_JTE}"
// flyway and jooq
String VER_FLYWAY='11.11.1'
String VER_JOOQ='3.20.6'
String VER_PALANTIR_DOCKER_COMPOSE='2.3.0'
String VER_POSTGRESQL_DRIVER='42.7.7'
// https://github.com/palantir/docker-compose-rule
api "com.palantir.docker.compose:docker-compose-rule-core:${VER_PALANTIR_DOCKER_COMPOSE}"
api "com.palantir.docker.compose:docker-compose-rule-junit4:${VER_PALANTIR_DOCKER_COMPOSE}"
// https://jdbc.postgresql.org/documentation/changelog.html
implementation "org.postgresql:postgresql:${VER_POSTGRESQL_DRIVER}"
// jooq codegen
api "org.jooq:jooq-codegen:${VER_JOOQ}"
api "org.jooq.jooq-codegen-gradle:org.jooq.jooq-codegen-gradle.gradle.plugin:${VER_JOOQ}"
// db migration
api "org.flywaydb:flyway-core:${VER_FLYWAY}"
api "org.flywaydb:flyway-database-postgresql:${VER_FLYWAY}"
// java8 utilities
implementation 'com.diffplug.durian:durian-core:1.2.0'
}
9 changes: 7 additions & 2 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ org=diffplug
license=apache
git_url=github.com/diffplug/webtools
plugin_tags=node
plugin_list=node jte
plugin_list=node jte flywayjooq

ver_java=17
ver_java=21

javadoc_links=

Expand All @@ -20,3 +20,8 @@ plugin_jte_id=com.diffplug.webtools.jte
plugin_jte_impl=com.diffplug.webtools.jte.JtePlugin
plugin_jte_name=DiffPlug JTE
plugin_jte_desc=Runs the JTE plugin and adds typesafe model classes

plugin_flywayjooq_id=com.diffplug.webtools.flywayjooq
plugin_flywayjooq_impl=com.diffplug.webtools.flywayjooq.FlywayJooqPlugin
plugin_flywayjooq_name=DiffPlug Flyway jOOQ
plugin_flywayjooq_desc=Coordinates Docker Compose, Flyway, and jOOQ for fast testing and easy deployment.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.webtools.node;
package com.diffplug.webtools;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
Expand All @@ -24,7 +24,7 @@
import java.nio.file.Files;
import java.util.Arrays;

abstract class SetupCleanup<K> {
public abstract class SetupCleanup<K> {
public void start(File keyFile, K key) throws Exception {
synchronized (key.getClass()) {
byte[] required = toBytes(key);
Expand Down
159 changes: 159 additions & 0 deletions src/main/java/com/diffplug/webtools/flywayjooq/FlywayJooqPlugin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* Copyright (C) 2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.webtools.flywayjooq;

import java.net.URL;
import java.net.URLClassLoader;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.inject.Inject;
import org.gradle.api.DefaultTask;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.ProjectLayout;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.TaskAction;
import org.gradle.api.tasks.TaskProvider;
import org.jooq.codegen.gradle.CodegenPluginExtension;

/**
* This plugin spools up a fresh postgres session,
* runs flyway on it, then runs jooq on the result.
* The postgres stays up so that the flyway result
* can be used as a template database for testing.
*/
public class FlywayJooqPlugin implements Plugin<Project> {
private static final String EXTENSION_NAME = "flywayJooq";

public static class Extension extends CodegenPluginExtension {
@Inject
public Extension(
ObjectFactory objects,
ProjectLayout layout) {
super(objects, layout);
}

public final SetupCleanupDockerFlyway setup = new SetupCleanupDockerFlyway();

/** Ensures a database with a template prepared by Flyway is available. */
public void neededBy(TaskProvider<?> taskProvider) {
taskProvider.configure(this::neededBy);
}

/** Ensures a database with a template prepared by Flyway is available. */
public void neededBy(Task task) {
task.dependsOn(DockerUp.TASK_NAME);
task.getInputs().file(setup.dockerComposeFile).withPathSensitivity(PathSensitivity.RELATIVE);
task.getInputs().dir(setup.flywayMigrations).withPathSensitivity(PathSensitivity.RELATIVE);
}
}

@Override
public void apply(Project project) {
// FlywayPlugin needs to be applied first
project.getPlugins().apply(JavaBasePlugin.class);

Extension extension = project.getExtensions().create(EXTENSION_NAME, Extension.class);
extension.configuration(a -> {});
extension.setup.dockerPullOnStartup = !project.getGradle().getStartParameter().isOffline();

// force all jooq versions to match
String jooqVersion = detectJooqVersion();
project.getConfigurations().all(config -> {
config.resolutionStrategy(strategy -> {
strategy.eachDependency(details -> {
String group = details.getRequested().getGroup();
String name = details.getRequested().getName();
if (group.equals("org.jooq") && name.startsWith("jooq")) {
details.useTarget(group + ":" + name + ":" + jooqVersion);
}
});
});
});

// create a jooq task, which will be needed by all compilation tasks
TaskProvider<JooqTask> jooqTask = project.getTasks().register("jooq", JooqTask.class, task -> {
task.setup = extension.setup;
var generator = extension.getExecutions().maybeCreate("").getConfiguration().getGenerator();
task.generatorConfig = generator;
task.getGeneratedSource().set(project.file(generator.getTarget().getDirectory()));
});
extension.neededBy(jooqTask);
project.getTasks().named(JavaPlugin.COMPILE_JAVA_TASK_NAME).configure(task -> {
task.dependsOn(jooqTask);
});

project.getTasks().register(DockerDown.TASK_NAME, DockerDown.class, task -> {
task.getSetupCleanup().set(extension.setup);
task.getProjectDir().set(project.getProjectDir());
});
project.getTasks().register(DockerUp.TASK_NAME, DockerUp.class, task -> {
task.getSetupCleanup().set(extension.setup);
task.getProjectDir().set(project.getProjectDir());
task.mustRunAfter(DockerDown.TASK_NAME);
});
}

public abstract static class DockerUp extends DefaultTask {
private static final String TASK_NAME = "dockerUp";

@Internal
public abstract DirectoryProperty getProjectDir();

@Internal
public abstract Property<SetupCleanupDockerFlyway> getSetupCleanup();

@TaskAction
public void dockerUp() throws Exception {
getSetupCleanup().get().start(getProjectDir().get().getAsFile());
}
}

public abstract static class DockerDown extends DefaultTask {
private static final String TASK_NAME = "dockerDown";

@Internal
public abstract DirectoryProperty getProjectDir();

@Internal
public abstract Property<SetupCleanupDockerFlyway> getSetupCleanup();

@TaskAction
public void dockerDown() throws Exception {
getSetupCleanup().get().forceStop(getProjectDir().get().getAsFile());
}
}

/** Detects the jooq version. */
private static String detectJooqVersion() {
URLClassLoader loader = (URLClassLoader) FlywayJooqPlugin.class.getClassLoader();
for (URL url : loader.getURLs()) {
Pattern pattern = Pattern.compile("(.*)/jooq-([0-9,.]*?).jar$");
Matcher matcher = pattern.matcher(url.getPath());
if (matcher.matches()) {
return matcher.group(2);
}
}
throw new IllegalStateException("Unable to detect jooq version.");
}
}
75 changes: 75 additions & 0 deletions src/main/java/com/diffplug/webtools/flywayjooq/JooqTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2025 DiffPlug
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.diffplug.webtools.flywayjooq;

import com.diffplug.common.base.Preconditions;
import com.diffplug.common.base.Throwables;
import java.io.File;
import java.net.ConnectException;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.tasks.*;
import org.jooq.codegen.GenerationTool;
import org.jooq.meta.jaxb.Configuration;
import org.jooq.meta.jaxb.Generator;
import org.jooq.meta.jaxb.Logging;

@CacheableTask
public abstract class JooqTask extends DefaultTask {
SetupCleanupDockerFlyway setup;
Generator generatorConfig;

@Internal
public SetupCleanupDockerFlyway getSetup() {
return setup;
}

@OutputDirectory
public abstract DirectoryProperty getGeneratedSource();

@Input
public Generator getGeneratorConfig() {
return generatorConfig;
}

@TaskAction
public void generate() throws Exception {
String targetDir = generatorConfig.getTarget().getDirectory();
Preconditions.checkArgument(!(new File(targetDir).isAbsolute()), "`generator.target.directory` must not be absolute, was `%s`", targetDir);
// configure jooq to run against the db
try {
generatorConfig.getTarget().setDirectory(getGeneratedSource().get().getAsFile().getAbsolutePath());
Configuration jooqConfig = new Configuration();
jooqConfig.setGenerator(generatorConfig);
jooqConfig.setLogging(Logging.TRACE);

// write the config out to file
GenerationTool tool = new GenerationTool();
tool.setDataSource(setup.getConnection());
tool.run(jooqConfig);
} catch (Exception e) {
var rootCause = Throwables.getRootCause(e);
if (rootCause instanceof ConnectException) {
throw new GradleException("Unable to connect to the database. Is the docker container running?", e);
} else {
throw e;
}
} finally {
generatorConfig.getTarget().setDirectory(targetDir);
}
}
}
Loading
Loading