diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b067b0a..dd0ee30 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - java-version: [ '17', '18', '19', '20', '21', '22', '23', '24', '25','26' ] + java-version: [ '17', '18', '19', '20', '21', '22', '23', '24', '25','26','27-ea' ] runs-on: ubuntu-latest @@ -30,28 +30,34 @@ jobs: run: | echo "java-version=${JAVA_VERSION%-ea}" >> $GITHUB_OUTPUT - # Gradle 8.X no longer uses a default repository for toolchains. - # Ref: https://docs.gradle.org/current/userguide/upgrading_version_7.html#using_automatic_toolchain_downloading_without_having_a_repository_configured - # Always preload toolchain JDK + # Preload the target JDK. setup-java sets JAVA_HOME to the installed JDK; + # build.gradle for versions not yet in Gradle's toolchain matrix (e.g. 27) + # forks javac directly from JAVA_HOME, so the order of the two setup steps + # below matters: preload the target JDK first, then overwrite JAVA_HOME with + # JDK 21 so that Gradle itself runs on a supported version. - name: Preload JDK ${{ matrix.java-version }} + id: preload uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: ${{ matrix.java-version }} - distribution: 'oracle' + distribution: 'temurin' - # This is the JDK gradle will use since gradle does not always support the -ea version - - name: Set up JDK 17 + # Gradle itself runs on JDK 21; set it up last so JAVA_HOME points here. + # build.gradle forks javac from the preloaded JDK path captured above. + - name: Set up JDK 21 (Gradle runtime) uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: '21' - distribution: 'oracle' - cache: gradle + distribution: 'temurin' + cache: gradle - name: Toolchain debug run: | ./gradlew -q javaToolchains - name: Build application + env: + JDK27: ${{ steps.preload.outputs.path }} run: | ./gradlew io.openliberty.java.internal_fat_${{ steps.strip.outputs.java-version }}:build @@ -63,7 +69,7 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | ./.github/workflows/scripts/repackageApplication.sh io.openliberty.java.internal_fat_${{ steps.strip.outputs.java-version }}/build/libs/io.openliberty.java.internal_fat_${{ steps.strip.outputs.java-version }}.war .jar - + - name: Upload application if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 diff --git a/.github/workflows/scripts/ClassVersionChecker.java b/.github/workflows/scripts/ClassVersionChecker.java index ec08cf4..75b471a 100755 --- a/.github/workflows/scripts/ClassVersionChecker.java +++ b/.github/workflows/scripts/ClassVersionChecker.java @@ -10,8 +10,10 @@ * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ -import java.io.*; -import java.util.*; +import java.io.DataInputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; public class ClassVersionChecker { @@ -50,6 +52,7 @@ public static void main(String[] args) throws IOException { majorCodeMap.put("24", 68); majorCodeMap.put("25", 69); majorCodeMap.put("26", 70); + majorCodeMap.put("27", 71); String filename = args[0]; int expected = majorCodeMap.get(args[1]); diff --git a/io.openliberty.java.internal_fat_27/README.md b/io.openliberty.java.internal_fat_27/README.md new file mode 100644 index 0000000..9980464 --- /dev/null +++ b/io.openliberty.java.internal_fat_27/README.md @@ -0,0 +1,101 @@ +# Repo for quick testing of Liberty apps +Especially useful for new Java versions + + +### Build locally + +Requires a JDK 27 installation. From the root of the repository: + +``` +./gradlew io.openliberty.java.internal_fat_27:build +``` + +Gradle resolves the JDK 27 toolchain automatically via its toolchain support. If Gradle cannot find a local JDK 27, point it at one explicitly: + +``` +./gradlew io.openliberty.java.internal_fat_27:build -P"org.gradle.java.installations.fromEnv=JDK27" +``` + +where `JDK27` is an environment variable set to your JDK 27 home: + +``` +(Mac/Unix) export JDK27="/path/to/jdk-27" +(Win DOS) set JDK27="C:\path\to\jdk-27" +(Win PS) $env:JDK27="C:\path\to\jdk-27" +``` + +### When moving to a new release of Java + +Update the `languageVersion` in **build.gradle**, the `appUrl` in the `ext` block, and add new test coverage to **TestService.java**. Make sure **TestApp.java** is in the same directory. + +--- + +## Java 27 JEP Coverage + +This FAT covers only **finalized (non-preview, non-incubator)** JEPs that ship as standard Java API in Java 27. + +--- + +### JEP 527 — Post-Quantum Hybrid Key Exchange for TLS 1.3 +**Link:** https://openjdk.org/jeps/527 + +Java 27 adds three ML-KEM/ECDHE hybrid named groups to the TLS 1.3 stack: `X25519MLKEM768`, `SecP256r1MLKEM768`, and `SecP384r1MLKEM1024`. `X25519MLKEM768` is placed first in the JDK default preference list so existing code benefits automatically without any change. + +**Test:** `testPostQuantumTLS()` +- Obtains `SSLParameters` from `SSLContext.getDefault().getDefaultSSLParameters()` (not `new SSLParameters()`, which returns `null` for `getNamedGroups()`). +- Asserts `X25519MLKEM768` is the first (most preferred) named group. +- Asserts all three hybrid groups are present in the supported set. + +--- + +### JEP 534 — Compact Object Headers by Default +**Link:** https://openjdk.org/jeps/534 + +Java 27 makes compact object headers the default in HotSpot, reducing object header size from 96 bits (12 bytes) to 64 bits (8 bytes) on 64-bit architectures. Controlled by `-XX:+/-UseCompactObjectHeaders`. + +**Test:** `testCompactObjectHeaders()` +- Reads the `UseCompactObjectHeaders` JVM flag via `HotSpotDiagnosticMXBean`. +- Logs `SUCCESS` if the flag is `true` (the Java 27 default). +- Logs a `NOTICE` (non-fatal) if the flag was explicitly disabled — e.g. Liberty sets `-XX:-UseCompactObjectHeaders` for compatibility. +- Skips gracefully on non-HotSpot JVMs where the MBean is absent. + +--- + +### JEP 536 — JFR In-Process Data Redaction +**Link:** https://openjdk.org/jeps/536 + +JFR now redacts sensitive values from built-in startup events (`jdk.InitialSystemProperty`, `jdk.InitialEnvironmentVariable`, `jdk.JVMInformation`) before writing them to a recording. Redaction is controlled by glob filters via `-XX:FlightRecorderOptions:redact-key/redact-argument`; default filters cover common patterns including `*password*`, `*token*`, `*secret*`. This is a JVM-engine feature — there is no Java annotation API. + +**Test:** `testJFRDataRedaction()` +- Starts a JFR recording with `jdk.InitialSystemProperty` enabled, then reads it back via `RecordingFile`. +- Looks for a property key (`jep536.test.password`) that matches the default `*password*` filter. +- Asserts the recorded value is `[REDACTED]`, not the original plaintext — fails hard if plaintext is found. +- Logs a skip notice if the property was not present at JVM startup (the snapshot is taken once at JVM init; runtime `System.setProperty()` is too late). + +> **To exercise the redaction path**, add the following to `run/jvm.options`: +> ``` +> -Djep536.test.password=sup3rS3cr3t! +> ``` + +--- + +## JEPs Excluded (Preview / Incubator) + +| JEP | Title | Status | +|-----|-------|--------| +| 531 | Lazy Constants | Third Preview — excluded | +| 532 | Primitive Types in Patterns, instanceof, and switch | Fifth Preview — excluded | +| 533 | Structured Concurrency | Seventh Preview — excluded | +| 538 | PEM Encodings of Cryptographic Objects | Third Preview — excluded | +| 537 | Vector API | Twelfth Incubator — excluded | +| 523 | Make G1 the Default GC in All Environments | Final but JVM-internal; no testable API surface | + +--- + +## JVM Options (`run/jvm.options`) + +No special flags are required for JEP 527 or JEP 534. To fully exercise JEP 536 redaction, add: + +``` +-Djep536.test.password=sup3rS3cr3t! +``` diff --git a/io.openliberty.java.internal_fat_27/build.gradle b/io.openliberty.java.internal_fat_27/build.gradle new file mode 100644 index 0000000..3b58b40 --- /dev/null +++ b/io.openliberty.java.internal_fat_27/build.gradle @@ -0,0 +1,52 @@ +/******************************************************************************* + * Copyright (c) 2026 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +apply plugin: 'war' + +description = "Basic Liberty repo" + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} + +// TODO revert to using toolchain once gradle supports JDK 27 +def jdk27Home = System.getenv('JDK27') ?: System.getenv('JAVA_HOME') + +compileJava { + javaCompiler.set(null as JavaCompiler) + options.fork = true + options.forkOptions.executable = "${jdk27Home}/bin/javac" + + doFirst { + options.compilerArgs = [ + '--module-path', classpath.asPath, + '--release', '27' + ] + classpath = files() + options.warnings = true + options.deprecation = true + options.debug = true + options.incremental = false + } +} + +repositories { + mavenCentral() +} + +dependencies { + compileOnly group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.1.1' + compileOnly group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1' + compileOnly group: 'javax.enterprise', name: 'cdi-api', version: '2.0' +} + +// This is the URL the test application will be available at +ext { + appUrl = 'http://localhost:9080/io.openliberty.java.internal_fat_27/' +} diff --git a/io.openliberty.java.internal_fat_27/run/jvm.options b/io.openliberty.java.internal_fat_27/run/jvm.options new file mode 100644 index 0000000..1c358ec --- /dev/null +++ b/io.openliberty.java.internal_fat_27/run/jvm.options @@ -0,0 +1,11 @@ +# No preview features are used in this FAT (all tested JEPs are finalized). +# Uncomment the line below if preview features are needed in future: +# --enable-preview + +# JEP 534: It is the Java 27 default, but setting it here makes the FAT +# self-contained and immune to any container-level override. +-XX:+UseCompactObjectHeaders + +# JEP 536: required so jdk.InitialSystemProperty captures this key at JVM startup. +# Value is a clearly-labelled test fixture — not a real credential. +-Djep536.test.password=test-fixture-value diff --git a/io.openliberty.java.internal_fat_27/run/server.env b/io.openliberty.java.internal_fat_27/run/server.env new file mode 100644 index 0000000..aa4e019 --- /dev/null +++ b/io.openliberty.java.internal_fat_27/run/server.env @@ -0,0 +1,2 @@ +# Need to update the JAVA_HOME environment variable to point to your Java 27 JDK +JAVA_HOME=/jdk/temurin/jdk27 \ No newline at end of file diff --git a/io.openliberty.java.internal_fat_27/src/main/java/io/openliberty/java/internal/TestApp.java b/io.openliberty.java.internal_fat_27/src/main/java/io/openliberty/java/internal/TestApp.java new file mode 100644 index 0000000..b86df30 --- /dev/null +++ b/io.openliberty.java.internal_fat_27/src/main/java/io/openliberty/java/internal/TestApp.java @@ -0,0 +1,17 @@ +/******************************************************************************* + * Copyright (c) 2026 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package io.openliberty.java.internal; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +@ApplicationPath("/") +public class TestApp extends Application { +} diff --git a/io.openliberty.java.internal_fat_27/src/main/java/io/openliberty/java/internal/TestService.java b/io.openliberty.java.internal_fat_27/src/main/java/io/openliberty/java/internal/TestService.java new file mode 100644 index 0000000..233aa26 --- /dev/null +++ b/io.openliberty.java.internal_fat_27/src/main/java/io/openliberty/java/internal/TestService.java @@ -0,0 +1,174 @@ +/******************************************************************************* + * Copyright (c) 2026 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package io.openliberty.java.internal; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.management.ManagementFactory; + +import javax.enterprise.context.ApplicationScoped; +import javax.management.MBeanServer; +import javax.net.ssl.SSLParameters; +import javax.ws.rs.GET; +import javax.ws.rs.Path; + +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; + +@Path("/") +@ApplicationScoped +public class TestService { + + private StringWriter sw = new StringWriter(); + + @GET + public String test() { + try { + log(">>> ENTER"); + doTest(); + log("<<< EXIT SUCCESSFUL"); + } catch (Exception e) { + e.printStackTrace(System.out); + e.printStackTrace(new PrintWriter(sw)); + log("<<< EXIT FAILED"); + } + String result = sw.toString(); + sw = new StringWriter(); + return result; + } + + private void doTest() throws Exception { + log("Beginning Java 27 testing"); + testPostQuantumTLS(); // JEP 527 + testCompactObjectHeaders(); // JEP 534 + testJFRDataRedaction(); // JEP 536 + log("Leaving Java 27 testing"); + } + + // JEP 527: Post-Quantum Hybrid Key Exchange for TLS 1.3 + // https://openjdk.org/jeps/527 + + private void testPostQuantumTLS() throws Exception { + log("Beginning JEP 527 testing: Post-Quantum Hybrid Key Exchange for TLS 1.3"); + + // Must use SSLContext to get the populated default list; + // new SSLParameters() is blank and returns null for getNamedGroups(). + SSLParameters params = javax.net.ssl.SSLContext.getDefault().getDefaultSSLParameters(); + String[] namedGroups = params.getNamedGroups(); + + if (namedGroups == null || namedGroups.length == 0) { + throw new Exception("JEP 527 FAILED: default SSLParameters returned no named groups"); + } + log("Default TLS named groups (" + namedGroups.length + "): " + java.util.Arrays.toString(namedGroups)); + + // All three hybrid groups must be present; ordering is vendor-dependent + java.util.Set groups = new java.util.HashSet<>(java.util.Arrays.asList(namedGroups)); + for (String hybrid : new String[]{"X25519MLKEM768", "SecP256r1MLKEM768", "SecP384r1MLKEM1024"}) { + if (!groups.contains(hybrid)) { + throw new Exception("JEP 527 FAILED: hybrid group missing from supported set: " + hybrid); + } + log("Hybrid group present: " + hybrid); + } + + log("Leaving JEP 527 testing"); + } + + // JEP 534: Compact Object Headers by Default + // https://openjdk.org/jeps/534 + // + // Reduces object header size from 96 bits (12 bytes) to 64 bits (8 bytes) on + // 64-bit HotSpot. -XX:+UseCompactObjectHeaders is set in jvm.options, so this + // test asserts it as a hard failure rather than a soft notice. + // Skips gracefully on non-HotSpot JVMs where HotSpotDiagnosticMXBean is absent. + private void testCompactObjectHeaders() throws Exception { + log("Beginning JEP 534 testing: Compact Object Headers by Default"); + + com.sun.management.HotSpotDiagnosticMXBean diagBean = + ManagementFactory.getPlatformMXBean(com.sun.management.HotSpotDiagnosticMXBean.class); + if (diagBean == null) { + log("HotSpotDiagnosticMXBean not available — skipping JEP 534 check (non-HotSpot JVM)"); + return; + } + + // Ensure the MBean is registered before querying + MBeanServer server = ManagementFactory.getPlatformMBeanServer(); + javax.management.ObjectName diagName = + new javax.management.ObjectName("com.sun.management:type=HotSpotDiagnostic"); + if (!server.isRegistered(diagName)) { + log("HotSpotDiagnostic MBean not registered — skipping JEP 534 check"); + return; + } + + String flagValue = diagBean.getVMOption("UseCompactObjectHeaders").getValue(); + log("UseCompactObjectHeaders = " + flagValue); + + if (!"true".equalsIgnoreCase(flagValue)) { + throw new Exception("JEP 534 FAILED: UseCompactObjectHeaders is not 'true' — " + + "expected it to be enabled via -XX:+UseCompactObjectHeaders in jvm.options"); + } + log("SUCCESS: Compact object headers are active (UseCompactObjectHeaders=true)"); + + log("Leaving JEP 534 testing"); + } + + // JEP 536: JFR In-Process Data Redaction + // https://openjdk.org/jeps/536 + + private void testJFRDataRedaction() throws Exception { + log("Beginning JEP 536 testing: JFR In-Process Data Redaction"); + + final String sensitiveKey = "jep536.test.password"; // matches default filter *password* + final String sensitiveValue = "test-fixture-value"; + + java.util.List events = new java.util.ArrayList<>(); + try (Recording rec = new Recording()) { + rec.enable("jdk.InitialSystemProperty"); + rec.start(); + rec.stop(); + java.nio.file.Path tmp = java.nio.file.Files.createTempFile("jep536-", ".jfr"); + try { + rec.dump(tmp); + events.addAll(RecordingFile.readAllEvents(tmp)); + } finally { + java.nio.file.Files.deleteIfExists(tmp); + } + } + + // Find the recorded value for our sensitive key + String recordedValue = null; + for (RecordedEvent e : events) { + if ("jdk.InitialSystemProperty".equals(e.getEventType().getName()) + && sensitiveKey.equals(e.getString("key"))) { + recordedValue = e.getString("value"); + log("jdk.InitialSystemProperty key=" + sensitiveKey + " value=" + recordedValue); + break; + } + } + + if (recordedValue == null) { + log("NOTE: '" + sensitiveKey + "' not in startup snapshot — re-run with -D" + sensitiveKey + "="); + } else if ("[REDACTED]".equals(recordedValue)) { + log("SUCCESS: JEP 536 redacted '" + sensitiveKey + "' in the JFR recording"); + } else if (sensitiveValue.equals(recordedValue)) { + throw new Exception("JEP 536 FAILED: '" + sensitiveKey + "' was recorded in plain text — expected [REDACTED]"); + } else { + log("NOTE: unexpected recorded value for '" + sensitiveKey + "': " + recordedValue); + } + + log("Leaving JEP 536 testing"); + } + + public void log(String msg) { + System.out.println(msg); + sw.append(msg); + sw.append("
"); + } +} diff --git a/io.openliberty.java.internal_fat_27/src/main/java/io/openliberty/java/internal/package-info.java b/io.openliberty.java.internal_fat_27/src/main/java/io/openliberty/java/internal/package-info.java new file mode 100644 index 0000000..9c06698 --- /dev/null +++ b/io.openliberty.java.internal_fat_27/src/main/java/io/openliberty/java/internal/package-info.java @@ -0,0 +1,10 @@ +/******************************************************************************* + * Copyright (c) 2026 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package io.openliberty.java.internal; diff --git a/io.openliberty.java.internal_fat_27/src/main/java/module-info.java b/io.openliberty.java.internal_fat_27/src/main/java/module-info.java new file mode 100644 index 0000000..b8874ad --- /dev/null +++ b/io.openliberty.java.internal_fat_27/src/main/java/module-info.java @@ -0,0 +1,18 @@ +/******************************************************************************* + * Copyright (c) 2026 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +open module io.openliberty.java.internal.basic_app { + + requires java.ws.rs; + requires java.net.http; + requires java.management; // JEP 534: Compact Object Headers — HotSpotDiagnosticMXBean + requires jdk.management; // JEP 534: com.sun.management.HotSpotDiagnosticMXBean + requires jdk.jfr; // JEP 536: JFR In-Process Data Redaction + +} diff --git a/io.openliberty.java.internal_fat_27/src/main/liberty/config/server.xml b/io.openliberty.java.internal_fat_27/src/main/liberty/config/server.xml new file mode 100644 index 0000000..269f129 --- /dev/null +++ b/io.openliberty.java.internal_fat_27/src/main/liberty/config/server.xml @@ -0,0 +1,6 @@ + + + servlet-4.0 + jaxrs-2.1 + + diff --git a/settings.gradle b/settings.gradle index bf18884..a95a84b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,3 +22,4 @@ include 'io.openliberty.java.internal_fat_23' include 'io.openliberty.java.internal_fat_24' include 'io.openliberty.java.internal_fat_25' include 'io.openliberty.java.internal_fat_26' +include 'io.openliberty.java.internal_fat_27'