From f2594e8402bdb477847dca7b504ec34711a28fc1 Mon Sep 17 00:00:00 2001 From: Tony Germano Date: Thu, 22 Oct 2026 17:35:15 -0400 Subject: [PATCH 1/3] Upgrade JaCoCo to 0.8.15 JaCoCo 0.8.8 cannot read class files newer than Java 18. On a newer JDK it logs a stack trace for each JDK class it fails to instrument, such as CLDRLocaleDataMetaInfo on Java 21. 0.8.15 officially supports up to Java 26. There are no API changes since 0.8.8. JaCoCo now depends on ASM 9.10.1 instead of 9.2. That ASM is only on JaCoCo's own classpath and does not change the ASM version the distribution ships. The verification metadata was regenerated with a cold cache, and the entries for the old versions were removed. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Tony Germano --- build.gradle | 2 +- gradle/verification-metadata.xml | 89 +++++++++++++++----------------- 2 files changed, 44 insertions(+), 47 deletions(-) diff --git a/build.gradle b/build.gradle index 83d7e13a6..0c993bd9e 100644 --- a/build.gradle +++ b/build.gradle @@ -275,7 +275,7 @@ subprojects { if (rootProject.coverageEnabled) { apply plugin: 'jacoco' - jacoco { toolVersion = '0.8.8' } + jacoco { toolVersion = '0.8.15' } tasks.withType(Test).configureEach { finalizedBy tasks.named('jacocoTestReport') } tasks.named('jacocoTestReport') { reports.xml.required = true diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a55f913c9..f954bcabb 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2533,41 +2533,41 @@ - - - + + + - - + + - - - + + + - - + + - - - + + + - - - + + + - - + + - - - + + + - - + + @@ -2858,12 +2858,12 @@ - - - + + + - - + + @@ -2874,14 +2874,6 @@ - - - - - - - - @@ -2890,12 +2882,17 @@ - - - + + + + + + + + - - + + @@ -2906,12 +2903,12 @@ - - - + + + - - + + From 52eb08df04413e73eb959529ba0fd8ac97814b3a Mon Sep 17 00:00:00 2001 From: Tony Germano Date: Thu, 22 Oct 2026 22:49:03 -0400 Subject: [PATCH 2/3] Compile server tests without JDK internal exports KeyEncryptorTest only referenced com.sun.crypto.provider.SunJCE to get its class name, which the encryption settings take as a string. Use the name directly. The other packages exported to the server test compile only appear in string literals, so the test compile needs no --add-modules or --add-exports. This prepares for compiling with --release, which javac does not allow together with --add-exports of system modules. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Tony Germano --- server/build.gradle | 10 ---------- .../commons/encryption/test/KeyEncryptorTest.java | 12 +++++++----- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index d928023bb..049b02a1f 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -40,16 +40,6 @@ def syncDonkeyLibs = tasks.register('syncDonkeyLibs', Sync) { compileJava.dependsOn syncDonkeyLibs -compileTestJava { - options.compilerArgs += [ - '--add-modules=java.sql.rowset', - '--add-exports=java.sql.rowset/com.sun.rowset=ALL-UNNAMED', - '--add-exports=java.base/com.sun.crypto.provider=ALL-UNNAMED', - '--add-exports=java.base/sun.security.provider=ALL-UNNAMED', - ] -} - - def generateVersionProperties = tasks.register('generateVersionProperties') { description = 'Generates version.properties (embedded in mirth-client-core.jar).' def outFile = file('version.properties') diff --git a/server/src/test/java/com/mirth/commons/encryption/test/KeyEncryptorTest.java b/server/src/test/java/com/mirth/commons/encryption/test/KeyEncryptorTest.java index a2d0f1276..28bc97bd9 100644 --- a/server/src/test/java/com/mirth/commons/encryption/test/KeyEncryptorTest.java +++ b/server/src/test/java/com/mirth/commons/encryption/test/KeyEncryptorTest.java @@ -35,10 +35,12 @@ import com.mirth.commons.encryption.Output; import com.mirth.commons.encryption.util.EncryptionUtil; import com.mirth.connect.model.EncryptionSettings; -import com.sun.crypto.provider.SunJCE; public class KeyEncryptorTest { + // a name, not a class literal: java.base does not export this package + private static final String SUN_JCE_PROVIDER = "com.sun.crypto.provider.SunJCE"; + @Test public void testAESCBC128BC() throws Exception { EncryptionSettings encryptionSettings = new EncryptionSettings(); @@ -65,7 +67,7 @@ public void testAESCBC128SunJCE() throws Exception { EncryptionSettings encryptionSettings = new EncryptionSettings(); encryptionSettings.setEncryptionAlgorithm("AES/CBC/PKCS5Padding"); encryptionSettings.setEncryptionKeyLength(128); - encryptionSettings.setSecurityProvider(SunJCE.class.getName()); + encryptionSettings.setSecurityProvider(SUN_JCE_PROVIDER); encryptionSettings.setEncryptionCharset(StandardCharsets.UTF_8.name()); testEncryptAndDecrypt(encryptionSettings); } @@ -76,7 +78,7 @@ public void testAESCBC128SunJCE() throws Exception { // EncryptionSettings encryptionSettings = new EncryptionSettings(); // encryptionSettings.setEncryptionAlgorithm("AES/CBC/PKCS5Padding"); // encryptionSettings.setEncryptionKeyLength(256); -// encryptionSettings.setSecurityProvider(SunJCE.class.getName()); +// encryptionSettings.setSecurityProvider(SUN_JCE_PROVIDER); // encryptionSettings.setEncryptionCharset(StandardCharsets.UTF_8.name()); // testEncryptAndDecrypt(encryptionSettings); // } @@ -161,7 +163,7 @@ public void testAESCBC128SunJCE_AESGCM128BC() throws Exception { EncryptionSettings oldEncryptionSettings = new EncryptionSettings(); oldEncryptionSettings.setEncryptionAlgorithm("AES/CBC/PKCS5Padding"); oldEncryptionSettings.setEncryptionKeyLength(128); - oldEncryptionSettings.setSecurityProvider(SunJCE.class.getName()); + oldEncryptionSettings.setSecurityProvider(SUN_JCE_PROVIDER); oldEncryptionSettings.setEncryptionCharset(StandardCharsets.UTF_8.name()); EncryptionSettings encryptionSettings = new EncryptionSettings(); @@ -225,7 +227,7 @@ public void testDESCBC56SunJCE() throws Exception { EncryptionSettings encryptionSettings = new EncryptionSettings(); encryptionSettings.setEncryptionAlgorithm("DES/CBC/PKCS5Padding"); encryptionSettings.setEncryptionKeyLength(56); - encryptionSettings.setSecurityProvider(SunJCE.class.getName()); + encryptionSettings.setSecurityProvider(SUN_JCE_PROVIDER); encryptionSettings.setEncryptionCharset(StandardCharsets.UTF_8.name()); testEncryptAndDecrypt(encryptionSettings); } From de69da84bba3ad62bfcfe786a15198f024cd3a74 Mon Sep 17 00:00:00 2001 From: Tony Germano Date: Sun, 18 Oct 2026 13:18:13 -0400 Subject: [PATCH 3/3] Allow building with a newer JDK while targeting Java 17 Compile with --release 17, so the output keeps Java 17 bytecode and API usage whichever JDK compiles it. The toolchain still defaults to 17 and can be overridden with -PjavaToolchain=21, which also selects the JDK the tests run on. Java 21 works with the current Gradle and test libraries. Newer JDKs need a Gradle upgrade. userApiJavadoc now runs the toolchain's javadoc instead of the one from the JDK running Gradle, and its version is a task input, so switching toolchains regenerates the docs. CONTRIBUTING.md describes the new option. Compared with the JDK 17 toolchain, a build with the JDK 21 toolchain produces only Java 17 (major version 61) class files. Of the 3,640 classes, 1,487 are byte-identical. The vocab classes, which build does not compile, are all byte-identical. The rest differ only in ways that come from newer javac versions and do not change behavior: - Metadata: constant pool order, MethodParameters attributes for the mandated and synthetic parameters of inner class constructors, and argN parameter names in the LocalVariableTable of anonymous class constructors. - Object methods called on an interface type (toString, equals, getClass) are emitted as invokeinterface on the interface instead of invokevirtual on Object (25 classes). The JVM resolves both to the same method. - A switch on an enum inside that enum's own class uses ordinal() directly instead of a $SwitchMap. This drops two synthetic classes, MetaDataColumnType$1 and ContextType$1, and three switch map fields from other synthetic classes. Closes #215. Issue: https://github.com/OpenIntegrationEngine/engine/issues/215 Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Tony Germano --- CONTRIBUTING.md | 2 ++ build.gradle | 8 +++++++- server/build.gradle | 5 ++++- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17ec55d4c..c3cc6936d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,6 +38,8 @@ The build is driven by Gradle through the included wrapper; no separate Gradle i ``` On Windows use `gradlew.bat` instead of `./gradlew`. The assembled distribution lands in `server/setup`, the same location the previous Ant build used. For release artifacts, run a clean build: `./gradlew clean build dist`. Windows with SDKMan may generate an error about the file path being too long in the javadoc step, skip this by adding `-x :server:userApiJavadoc` to your Gradle command. +By default the build compiles and runs the tests with any Java 17 JDK Gradle finds installed, including those installed by SDKMAN. To use Java 21 instead, add `-PjavaToolchain=21`, or put `javaToolchain=21` in `~/.gradle/gradle.properties`. The compiled output still targets Java 17. Gradle does not download JDKs, so the version you name must already be installed. + Dependencies are pinned and checksum-verified. To change a dependency version: edit `gradle/libs.versions.toml`, then refresh the checksum metadata **with a cold dependency cache and CI's flags**: ```bash GRADLE_USER_HOME=$(mktemp -d) ./gradlew --write-verification-metadata sha256 build dist -PdisableSigning=true -Pcoverage=true diff --git a/build.gradle b/build.gradle index 0c993bd9e..6f363be8d 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,8 @@ * ./gradlew test [-Pcoverage=true] tests only (JaCoCo optional) * ./gradlew dist distribution extension zips * ./gradlew clean clean all modules + * ./gradlew build -PjavaToolchain=21 compile/test with another JDK + * (output still targets Java 17) * * For release artifacts run a clean build (./gradlew clean build dist): * the staging into server/setup removes stale files where it owns the @@ -71,6 +73,9 @@ ext { disableTests = flag('disableTests') disableSigning = flag('disableSigning') coverageEnabled = flag('coverage') + // the shipped bytecode and API level; the JDK used to build may be newer + javaRelease = 17 + javaToolchain = (providers.gradleProperty('javaToolchain').getOrNull() ?: javaRelease) as int } allprojects { @@ -158,7 +163,7 @@ subprojects { java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(rootProject.javaToolchain) } } @@ -177,6 +182,7 @@ subprojects { tasks.withType(JavaCompile).configureEach { options.encoding = 'UTF-8' + options.release = rootProject.javaRelease options.debug = true } // Match Ant's javac invocation: -sourcepath defaults to srcdir, and diff --git a/server/build.gradle b/server/build.gradle index 049b02a1f..f9855c247 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -492,9 +492,12 @@ def userApiJavadoc = tasks.register('userApiJavadoc') { }) outputs.dir outDir dependsOn syncDonkeyLibs + def javadocTool = javaToolchains.javadocToolFor(java.toolchain) + // the output depends on the javadoc version, which is not otherwise an input + inputs.property('javadocVersion', javadocTool.map { it.metadata.languageVersion.asInt() }) doLast { outDir.mkdirs() - def cmd = [new File(System.getProperty('java.home'), 'bin/javadoc').absolutePath, + def cmd = [javadocTool.get().executablePath.asFile.absolutePath, '-d', outDir.absolutePath, '-notimestamp', '-classpath', (configurations.compileClasspath + fileTree(dir: 'lib', include: '**/*.jar')).asPath,