diff --git a/.github_changelog_generator b/.github_changelog_generator index 458015c6..7442d8ac 100644 --- a/.github_changelog_generator +++ b/.github_changelog_generator @@ -1,7 +1,7 @@ base=CHANGELOG.md breaking-labels=breaking-changes bug-labels=T: Bug -exclude-tags-regex=(jpa|rsql|docs\/.+)\/v.+ +exclude-tags-regex=(jpa|rsql|(docs\/.+))\/v.+ exclude-labels=R: Duplicate,R: Invalid,R: Wontfix,T: Question,!release-note,Project: jpa,Project: rsql enhancement-labels=T: Improvement,T: Feature exclude-tags=v2.0.0-rc2,v2.0.0-rc2+2 diff --git a/build.gradle.kts b/build.gradle.kts index 1e6c1d61..93c365ce 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -71,7 +71,6 @@ subprojects { } } } - val buildVersion = when (jvmRuntime) { "8" -> if (jvmRelease == "8") "" else "+jvm8" "11" -> if (jvmRelease == "11") "" else "+jvm11" @@ -82,8 +81,7 @@ subprojects { val semanticVersion = prop(project, "semanticVersion", "") version = when (semanticVersion) { "-SNAPSHOT" -> project.version.toString().replace(semanticVersion, buildVersion + semanticVersion) - "" -> project.version.toString() + buildVersion - else -> project.version.toString().replace(semanticVersion, semanticVersion + buildVersion) + else -> "${project.version}$buildVersion" } dependencies { diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 273fbc08..28079e21 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -60,6 +60,17 @@ val bridges = setOf( setOf("**/datatype/BridgeConverter.java") ) ) +val createVersionFile = tasks.register("createVersionFile") { + group = "build" + // Register the project version as a task input, so Gradle can cache the task + inputs.property("projectVersion", project.version) + // tell Gradle about the output directory + outputs.dir(temporaryDir) + + doLast { + File(temporaryDir, "jooqx-version.txt").writeText(project.version.toString(), Charsets.UTF_8) + } +} sourceSets { bridges.forEach { @@ -73,6 +84,9 @@ sourceSets { java { bridges.filter { it.versionConstraint }.forEach { exclude(it.excludes) } } + resources { + srcDir(createVersionFile.map { it.outputs }) + } } } @@ -103,4 +117,7 @@ tasks { title = "jOOQx Testing ${project.version} API" } + test { + systemProperty("jooqx-build-version", project.version) + } } diff --git a/core/src/main/java/io/github/zero88/jooqx/JooqxVersion.java b/core/src/main/java/io/github/zero88/jooqx/JooqxVersion.java new file mode 100644 index 00000000..a3f2a848 --- /dev/null +++ b/core/src/main/java/io/github/zero88/jooqx/JooqxVersion.java @@ -0,0 +1,126 @@ +package io.github.zero88.jooqx; + +import static io.github.zero88.jooqx.Utils.brackets; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.Objects; +import java.util.Scanner; + +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jooq.Constants; + +import io.vertx.core.impl.launcher.commands.VersionCommand; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; + +/** + * JOOQ.X version + * + * @since 2.0.0 + */ +public final class JooqxVersion { + + private static final Logger LOGGER = LoggerFactory.getLogger(JooqxVersion.class); + private static String version; + private static String jooqVersion; + + private JooqxVersion() { } + + /** + * @return {@code jooq.x} version + */ + @Internal + public static String getVersion() { + if (version == null) { + version = loadFromFile(); + } + return version; + } + + /** + * Query version from the direct dependent libraries: {@code jOOQ}, {@code vert.x}, and combine with {@code jooq.x} + * version + * + * @return versions in JSON format + * @see #getVersion() + */ + public static JsonObject versionComponents() { + return JsonObject.of("jOOQ", getJooqVersion(), "Vert.x", VersionCommand.getVersion(), "jooq.x", getVersion()); + } + + /** + * Validate the version compatibility between {@code jooq.x} and the direct dependent libraries + */ + public static void validate() { + try { + validateJooq(getVersion(), getJooqVersion()); + } catch (IncompatibleVersion ex) { + LOGGER.warn(ex.getMessage()); + } catch (Exception ex) { + LOGGER.trace("Validate compatibility version failed", ex); + } + } + + static void validateJooq(String jooqxVer, String jooqVersion) { + boolean isJvm8 = jooqxVer.contains("+jvm8"); + String[] versions = jooqVersion.split("\\.", 3); + boolean isMajor3 = Objects.equals(versions[0], "3"); + int jooqMinor = Integer.parseInt(versions[1]); + if (isMajor3 && isJvm8 == (jooqMinor >= 18)) { + throw new IncompatibleVersion( + "jOOQ.x version" + brackets(jooqxVer) + " is not compatible with jOOQ version" + brackets(jooqVersion) + + ". Please refer the documentation: " + docUrl(jooqxVer) + " for more details."); + } + } + + private static String docUrl(String jooqxVer) { + String releaseVer = jooqxVer.replace("+jvm8", ""); + releaseVer = releaseVer.contains("-SNAPSHOT") ? "main" : releaseVer; + return "https://zero88.github.io/webdocs/jooqx/" + releaseVer + "/core-usage.html#_compatibility_matrix"; + } + + private static String getJooqVersion() { + if (jooqVersion == null) { + try { + final Class aClass = JooqxVersion.class.getClassLoader().loadClass("org.jooq.Constants"); + final Field versionField = aClass.getDeclaredField("VERSION"); + jooqVersion = (String) versionField.get(null); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException ex) { + LOGGER.debug("Unable to load runtime jOOQ version, fallback to build version", ex); + jooqVersion = Constants.VERSION; + } + } + return jooqVersion; + } + + private static String loadFromFile() { + // in build process, version will be auto-inject to manifest by + // `JooqxVersion.class.getPackage().getImplementationVersion()` + // however, fat-jar will override the package version by itself application version + // so load from file is safe choice + LOGGER.debug("Loading `jooq.x` version from file `jooqx-version.txt`..."); + try (InputStream is = JooqxVersion.class.getClassLoader().getResourceAsStream("jooqx-version.txt")) { + if (is == null) { + LOGGER.warn("Cannot find jooqx-version.txt on classpath"); + return null; + } + try (Scanner scanner = new Scanner(is, StandardCharsets.UTF_8.name()).useDelimiter("\\A")) { + return scanner.hasNext() ? scanner.next().trim() : ""; + } + } catch (IOException ex) { + LOGGER.warn("Cannot read jooqx-version.txt on classpath", ex); + return null; + } + } + + static class IncompatibleVersion extends RuntimeException { + + public IncompatibleVersion(String msg) { super(msg); } + + } + +} diff --git a/core/src/main/java/io/github/zero88/jooqx/SQLImpl.java b/core/src/main/java/io/github/zero88/jooqx/SQLImpl.java index 86a14d43..ac10e5ea 100644 --- a/core/src/main/java/io/github/zero88/jooqx/SQLImpl.java +++ b/core/src/main/java/io/github/zero88/jooqx/SQLImpl.java @@ -40,6 +40,7 @@ abstract static class SQLEI, RC extends SQL protected SQLEI(Vertx vertx, DSLContext dsl, S sqlClient, PQ preparedQuery, RC resultCollector, SQLErrorConverter errorConverter, DataTypeMapperRegistry typeMapperRegistry) { + JooqxVersion.validate(); this.vertx = vertx; this.dsl = dsl; this.sqlClient = sqlClient; diff --git a/core/src/test/java/io/github/zero88/jooqx/JooqxVersionTest.java b/core/src/test/java/io/github/zero88/jooqx/JooqxVersionTest.java new file mode 100644 index 00000000..924f3a29 --- /dev/null +++ b/core/src/test/java/io/github/zero88/jooqx/JooqxVersionTest.java @@ -0,0 +1,55 @@ +package io.github.zero88.jooqx; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import io.github.zero88.jooqx.JooqxVersion.IncompatibleVersion; +import io.vertx.core.json.JsonObject; + +class JooqxVersionTest { + + @Test + void test_version_should_be_available() { + final String buildVersion = System.getProperty("jooqx-build-version"); + Assertions.assertNotNull(buildVersion); + Assertions.assertEquals(buildVersion, JooqxVersion.getVersion()); + } + + @Test + void test_version_json_should_be_available() { + final String buildVersion = System.getProperty("jooqx-build-version"); + Assertions.assertNotNull(buildVersion); + final JsonObject jsonObject = JooqxVersion.versionComponents(); + Assertions.assertEquals(buildVersion, jsonObject.getString("jooq.x")); + Assertions.assertTrue(jsonObject.containsKey("jOOQ")); + Assertions.assertTrue(jsonObject.containsKey("Vert.x")); + } + + @ParameterizedTest + //@formatter:off + @CsvSource({ + // jooq.x `+jvm8` + "2.0.0+jvm8-SNAPSHOT,3.14.9,true", "2.0.0+jvm8-SNAPSHOT,3.17.26,true", "2.0.0+jvm8-SNAPSHOT,3.18.5,false", + "2.0.0+jvm8,3.14.9,true", "2.0.0+jvm8,3.17.26,true", "2.0.0+jvm8,3.18.5,false", + "2.0.0-rc3+jvm8,3.14.9,true", "2.0.0-rc3+jvm8,3.17.26,true", "2.0.0-rc3+jvm8,3.18.5,false", + "2.0.0+hf1+jvm8,3.14.9,true", "2.0.0+hf1+jvm8,3.17.26,true", "2.0.0+hf1+jvm8,3.18.5,false", + // jooq.x + "2.0.0-SNAPSHOT,3.18.5,true", "2.0.0-SNAPSHOT,3.19.6,true", "2.0.0-SNAPSHOT,3.17.26,false", + "2.0.0,3.18.5,true", "2.0.0,3.19.6,true", "2.0.0,3.17.26,false", + "2.0.0-rc3,3.18.5,true", "2.0.0-rc3,3.19.6,true", "2.0.0-rc3,3.17.26,false", + "2.0.0+hf1,3.18.5,true", "2.0.0+hf1,3.19.6,true", "2.0.0+hf1,3.17.26,false", + }) + //@formatter:on + void test_validate_jooq_compatibility_version(String jooqxVersion, String jooqVersion, boolean expected) { + if (expected) { + JooqxVersion.validateJooq(jooqxVersion, jooqVersion); + } else { + Assertions.assertThrows(IncompatibleVersion.class, + () -> JooqxVersion.validateJooq(jooqxVersion, jooqVersion), + "Expected IncompatibleVersion exception"); + } + } + +} diff --git a/docs/asciidoc/src/antora/pages/core-usage.adoc b/docs/asciidoc/src/antora/pages/core-usage.adoc index 50af9924..d1e6a638 100644 --- a/docs/asciidoc/src/antora/pages/core-usage.adoc +++ b/docs/asciidoc/src/antora/pages/core-usage.adoc @@ -50,25 +50,27 @@ dependencies { } ---- -== Matrix support +== Compatibility Matrix + +The table below describe `jooq.x` compatibility with several integrations. |=== |Java |jOOQ |Vert.x |jooq.x |`8` -|`<= 3.14.16` +|`\<= 3.14.16` |`^4.2` -|{jooqx-version}-jvm8 +|{jooqx-jvm8-artifact} |`11` -|`<= 3.16.23` +|`\<= 3.16.23` |`^4.2` -|{jooqx-version}-jvm8 +|{jooqx-jvm8-artifact} |`17` |`~3.17.0` |`^4.2` -|{jooqx-version}-jvm8 +|{jooqx-jvm8-artifact} |`17` |`^3.18` diff --git a/docs/build.gradle.kts b/docs/build.gradle.kts index 2a413b6d..53f273c6 100644 --- a/docs/build.gradle.kts +++ b/docs/build.gradle.kts @@ -1,3 +1,5 @@ +import cloud.playio.gradle.shared.prop + @Suppress("DSL_SCOPE_VIOLATION") plugins { alias(libs.plugins.antora) @@ -10,11 +12,18 @@ subprojects { } } +val semanticVersion = prop(project, "semanticVersion", "") +val jvm8Version = when (semanticVersion) { + "-SNAPSHOT" -> project.version.toString().replace(semanticVersion, "+jvm8$semanticVersion") + else -> "${project.version}+jvm8" +} + documentation { antora { asciiAttributes.set( mapOf( "jooqx-version" to project.version, + "jooqx-jvm8-artifact" to jvm8Version, "jooq-version" to libs.jooq.get().version, "vertx-version" to libs.vertxCore.get().version, "mutiny-version" to libs.mutinyCore.get().version,