From 9710c4d04d9d957f7fc46873d5c49691f49704ca Mon Sep 17 00:00:00 2001 From: Josh Feinberg <15068619+joshafeinberg@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:30:10 -0500 Subject: [PATCH 1/3] Upgrade build to Java 21 --- android/build.gradle | 5 +-- core/build.gradle | 8 ++--- .../com/dropbox/core/DbxOAuth1Upgrader.java | 10 ++---- .../java/com/dropbox/core/DbxPKCEManager.java | 14 +++----- .../java/com/dropbox/core/DbxPKCEWebAuth.java | 7 ---- .../java/com/dropbox/core/DbxRequestUtil.java | 14 +++----- .../com/dropbox/core/json/JsonDateReader.java | 33 ++++--------------- .../java/com/dropbox/core/util/IOUtil.java | 12 +++++-- .../com/dropbox/core/util/StringUtil.java | 27 ++++++++------- .../core/v2/DbxDownloadStyleBuilder.java | 2 +- .../dropbox/core/json/JsonDateReaderTest.java | 18 ++++++++++ .../com/dropbox/core/util/IOUtilTest.java | 32 ++++++++++++++++++ examples/android/build.gradle | 10 ++++-- examples/examples/build.gradle | 8 ++--- examples/java/build.gradle | 6 ++-- proguard/build.gradle | 6 ++-- 16 files changed, 117 insertions(+), 95 deletions(-) create mode 100644 core/src/test/java/com/dropbox/core/util/IOUtilTest.java diff --git a/android/build.gradle b/android/build.gradle index 11413fd9a..789fa41ef 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,13 +25,14 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } } kotlin { compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) freeCompilerArgs.add('-Xexplicit-api=strict') } } diff --git a/core/build.gradle b/core/build.gradle index 9345403b3..ba24455df 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -18,8 +18,8 @@ dependencyGuard { } java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } ext { @@ -105,7 +105,7 @@ configurations { } tasks.withType(JavaCompile).configureEach { - options.release.set(17) + options.release.set(21) } tasks.named("compileJava", JavaCompile) { @@ -216,7 +216,7 @@ tasks.named("javadoc", Javadoc) { if (JavaVersion.current().isJava8Compatible()) { options.addBooleanOption "Xdoclint:all,-missing,-reference", true } - options.addStringOption "link", "https://docs.oracle.com/en/java/javase/17/docs/api/" + options.addStringOption "link", "https://docs.oracle.com/en/java/javase/21/docs/api/" } tasks.named("jar", Jar) { diff --git a/core/src/main/java/com/dropbox/core/DbxOAuth1Upgrader.java b/core/src/main/java/com/dropbox/core/DbxOAuth1Upgrader.java index e772892e0..e43ce9cc6 100644 --- a/core/src/main/java/com/dropbox/core/DbxOAuth1Upgrader.java +++ b/core/src/main/java/com/dropbox/core/DbxOAuth1Upgrader.java @@ -4,15 +4,14 @@ import com.dropbox.core.json.JsonReadException; import com.dropbox.core.json.JsonReader; import com.dropbox.core.v1.DbxClientV1; -import static com.dropbox.core.util.LangUtil.mkAssert; import com.fasterxml.jackson.core.JsonLocation; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; /** @@ -106,12 +105,7 @@ private String buildOAuth1Header(DbxOAuth1AccessToken token) private static String encode(String s) { - try { - return URLEncoder.encode(s, "UTF-8"); - } - catch (UnsupportedEncodingException ex) { - throw mkAssert("UTF-8 should always be supported", ex); - } + return URLEncoder.encode(s, StandardCharsets.UTF_8); } /** diff --git a/core/src/main/java/com/dropbox/core/DbxPKCEManager.java b/core/src/main/java/com/dropbox/core/DbxPKCEManager.java index bd9cbcd32..ee32006ad 100644 --- a/core/src/main/java/com/dropbox/core/DbxPKCEManager.java +++ b/core/src/main/java/com/dropbox/core/DbxPKCEManager.java @@ -4,17 +4,14 @@ import com.dropbox.core.util.LangUtil; import com.dropbox.core.v2.DbxRawClientV2; -import java.io.IOException; -import java.io.Serializable; -import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; +import java.util.Base64; import java.util.HashMap; import java.util.Map; -import static com.dropbox.core.util.StringUtil.urlSafeBase64Encode; - /** * This class should be lib/jar private. We make it public so that Android related code can use it. * @@ -29,6 +26,7 @@ public class DbxPKCEManager { private static final SecureRandom RAND = new SecureRandom(); private static final String CODE_VERIFIER_CHAR_SET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"; + private static final Base64.Encoder UrlSafeBase64Encoder = Base64.getUrlEncoder().withoutPadding(); private String codeVerifier; private String codeChallenge; @@ -61,12 +59,10 @@ String generateCodeVerifier() { static String generateCodeChallenge(String codeVerifier) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] signiture = digest.digest(codeVerifier.getBytes("US-ASCII")); - return urlSafeBase64Encode(signiture).replaceAll("=+$", ""); // remove trailing equal + byte[] signature = digest.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)); + return UrlSafeBase64Encoder.encodeToString(signature); } catch (NoSuchAlgorithmException e) { throw LangUtil.mkAssert("Impossible", e); - } catch (UnsupportedEncodingException e) { - throw LangUtil.mkAssert("Impossible", e); } } diff --git a/core/src/main/java/com/dropbox/core/DbxPKCEWebAuth.java b/core/src/main/java/com/dropbox/core/DbxPKCEWebAuth.java index 1e13029ac..ad4cdbfbe 100644 --- a/core/src/main/java/com/dropbox/core/DbxPKCEWebAuth.java +++ b/core/src/main/java/com/dropbox/core/DbxPKCEWebAuth.java @@ -1,18 +1,11 @@ package com.dropbox.core; import com.dropbox.core.http.HttpRequestor; -import com.dropbox.core.util.LangUtil; import com.dropbox.core.v2.DbxRawClientV2; -import java.io.UnsupportedEncodingException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; import java.util.HashMap; import java.util.Map; -import static com.dropbox.core.util.StringUtil.urlSafeBase64Encode; - /** * * diff --git a/core/src/main/java/com/dropbox/core/DbxRequestUtil.java b/core/src/main/java/com/dropbox/core/DbxRequestUtil.java index 3f3df6d81..bb562a8f4 100644 --- a/core/src/main/java/com/dropbox/core/DbxRequestUtil.java +++ b/core/src/main/java/com/dropbox/core/DbxRequestUtil.java @@ -1,16 +1,16 @@ package com.dropbox.core; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.CharacterCodingException; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; import com.dropbox.core.stone.StoneSerializer; import com.dropbox.core.v2.auth.AuthError; @@ -35,16 +35,10 @@ /*>>> import checkers.nullness.quals.Nullable; */ public final class DbxRequestUtil { - private static final Random RAND = new Random(); - public static DbxGlobalCallbackFactory sharedCallbackFactory; public static String encodeUrlParam(String s) { - try { - return URLEncoder.encode(s, "UTF-8"); - } catch (UnsupportedEncodingException ex) { - throw mkAssert("UTF-8 should always be supported", ex); - } + return URLEncoder.encode(s, StandardCharsets.UTF_8); } public static String buildUrlWithParams(/*@Nullable*/String userLocale, @@ -572,7 +566,7 @@ public static T runAndRetry(int maxRetries, RequestMake // add a random jitter to the backoff to avoid stampeding herd. This is especially // useful for ServerExceptions, where backoff is 0. - backoff += RAND.nextInt(1000); + backoff += ThreadLocalRandom.current().nextInt(1000); if (backoff > 0L) { try { diff --git a/core/src/main/java/com/dropbox/core/json/JsonDateReader.java b/core/src/main/java/com/dropbox/core/json/JsonDateReader.java index 3261ac402..0c557b6df 100644 --- a/core/src/main/java/com/dropbox/core/json/JsonDateReader.java +++ b/core/src/main/java/com/dropbox/core/json/JsonDateReader.java @@ -5,11 +5,11 @@ import com.fasterxml.jackson.core.JsonParser; import java.io.IOException; +import java.time.Instant; +import java.time.format.DateTimeParseException; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; -import java.text.DateFormat; -import java.text.SimpleDateFormat; public class JsonDateReader { @@ -261,32 +261,13 @@ public static Date parseDropbox8601Date(char[] buffer, int offset, int length) throw new java.text.ParseException("expecting date to be 20 or 24 characters, got " + length, 0); } - // TODO: This needs to be looked at further. - // Does this need to handle arbitrary timezones? String s = new String(b, i, length); - final DateFormat format; - if (length == 20) { - // Assume this is an ISO 8601 date with a trailing Z to indicate UTC: - // e.g. "2015-04-01T12:01:12Z", - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - } else { - // Assume this is an ISO 8601 date with a trailing Z to indicate UTC: - // plus milliseconds, e.g. "2012-04-23T18:25:43.511Z". - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); - } - format.setTimeZone(TimeZone.getTimeZone("UTC")); - - Date result; try { - result = format.parse(s); - } catch (IllegalArgumentException ex) { - throw new java.text.ParseException("invalid characters in date" + s, 0); + return Date.from(Instant.parse(s)); + } catch (DateTimeParseException | IllegalArgumentException ex) { + java.text.ParseException parseException = new java.text.ParseException("invalid date" + s, 0); + parseException.initCause(ex); + throw parseException; } - - if (result == null) { - throw new java.text.ParseException("invalid date" + s, 0); - } - - return result; } } diff --git a/core/src/main/java/com/dropbox/core/util/IOUtil.java b/core/src/main/java/com/dropbox/core/util/IOUtil.java index 81166ac81..5901be995 100644 --- a/core/src/main/java/com/dropbox/core/util/IOUtil.java +++ b/core/src/main/java/com/dropbox/core/util/IOUtil.java @@ -74,9 +74,17 @@ public static byte[] slurp(InputStream in, int byteLimit) throws IOException { public static byte[] slurp(InputStream in, int byteLimit, byte[] slurpBuffer) throws IOException { if (byteLimit < 0) throw new RuntimeException("'byteLimit' must be non-negative: " + byteLimit); + if (slurpBuffer.length == 0) throw new IllegalArgumentException("'slurpBuffer' must not be empty"); - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - copyStreamToStream(in, baos, slurpBuffer); + int remaining = byteLimit; + ByteArrayOutputStream baos = new ByteArrayOutputStream(Math.min(byteLimit, slurpBuffer.length)); + while (remaining > 0) { + int count = in.read(slurpBuffer, 0, Math.min(slurpBuffer.length, remaining)); + if (count == -1) break; + + baos.write(slurpBuffer, 0, count); + remaining -= count; + } return baos.toByteArray(); } diff --git a/core/src/main/java/com/dropbox/core/util/StringUtil.java b/core/src/main/java/com/dropbox/core/util/StringUtil.java index dce1e913d..c7c5a2369 100644 --- a/core/src/main/java/com/dropbox/core/util/StringUtil.java +++ b/core/src/main/java/com/dropbox/core/util/StringUtil.java @@ -1,21 +1,23 @@ package com.dropbox.core.util; -import static com.dropbox.core.util.LangUtil.mkAssert; - -import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; +import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; public class StringUtil { - public static final Charset UTF8 = Charset.forName("UTF-8"); + public static final Charset UTF8 = StandardCharsets.UTF_8; private static final char[] HexDigits = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f',}; + private static final Base64.Encoder Base64Encoder = Base64.getEncoder(); + private static final Base64.Encoder UrlSafeBase64Encoder = Base64.getUrlEncoder(); + public static char hexDigit(int i) { return HexDigits[i]; } public static String utf8ToString(byte[] utf8Data) @@ -36,14 +38,7 @@ public static String utf8ToString(byte[] utf8Data, int offset, int length) public static byte[] stringToUtf8(String s) { - try { - // Java 1.5 doesn't have the version of getBytes that takes a Charset object, so we - // just use this one and catch the exception. - return s.getBytes("UTF-8"); - } - catch (UnsupportedEncodingException ex) { - throw mkAssert("UTF-8 should always be supported", ex); - } + return s.getBytes(UTF8); } /** @@ -163,12 +158,14 @@ public static boolean secureStringEquals(String a, String b) public static String base64Encode(byte[] data) { - return base64EncodeGeneric(Base64Digits, data); + if (data == null) throw new IllegalArgumentException("'data' can't be null"); + return Base64Encoder.encodeToString(data); } public static String urlSafeBase64Encode(byte[] data) { - return base64EncodeGeneric(UrlSafeBase64Digits, data); + if (data == null) throw new IllegalArgumentException("'data' can't be null"); + return UrlSafeBase64Encoder.encodeToString(data); } public static String base64EncodeGeneric(String digits, byte[] data) @@ -176,6 +173,8 @@ public static String base64EncodeGeneric(String digits, byte[] data) if (data == null) throw new IllegalArgumentException("'data' can't be null"); if (digits == null) throw new IllegalArgumentException("'digits' can't be null"); if (digits.length() != 64) throw new IllegalArgumentException("'digits' must be 64 characters long: " + jq(digits)); + if (Base64Digits.equals(digits)) return Base64Encoder.encodeToString(data); + if (UrlSafeBase64Digits.equals(digits)) return UrlSafeBase64Encoder.encodeToString(data); int numGroupsOfThreeInputBytes = (data.length + 2) / 3; int numOutputChars = numGroupsOfThreeInputBytes * 4; diff --git a/core/src/main/java/com/dropbox/core/v2/DbxDownloadStyleBuilder.java b/core/src/main/java/com/dropbox/core/v2/DbxDownloadStyleBuilder.java index e8cfda33c..3dbc18fb4 100644 --- a/core/src/main/java/com/dropbox/core/v2/DbxDownloadStyleBuilder.java +++ b/core/src/main/java/com/dropbox/core/v2/DbxDownloadStyleBuilder.java @@ -71,7 +71,7 @@ protected List getHeaders() { } List headers = new ArrayList(); - String rangeValue = String.format("bytes=%d-", start.longValue()); + String rangeValue = "bytes=" + start.longValue() + "-"; if (length != null) { // Range header is inclusive (e.g. bytes=0-499 means first 500 bytes) rangeValue += Long.toString(start.longValue() + length.longValue() - 1); diff --git a/core/src/test/java/com/dropbox/core/json/JsonDateReaderTest.java b/core/src/test/java/com/dropbox/core/json/JsonDateReaderTest.java index bc2cf3b71..452a920fe 100644 --- a/core/src/test/java/com/dropbox/core/json/JsonDateReaderTest.java +++ b/core/src/test/java/com/dropbox/core/json/JsonDateReaderTest.java @@ -8,6 +8,8 @@ import java.util.GregorianCalendar; import java.util.Locale; +import static org.testng.Assert.assertEquals; + public class JsonDateReaderTest { @Test @@ -43,6 +45,14 @@ public void parseDropboxDateTestMany() if (count < 1000) throw new AssertionError("Loop didn't run enough: " + count); } + @Test + public void parseDropbox8601DateTest() + throws java.text.ParseException + { + validateDropbox8601DateParser("2015-04-01T12:01:12Z", 1427889672000L); + validateDropbox8601DateParser("2012-04-23T18:25:43.511Z", 1335205543511L); + } + private static final ThreadLocal dateFormatHolder = new ThreadLocal() { protected SimpleDateFormat initialValue() { @@ -93,4 +103,12 @@ private static void validateDropboxDateParser(String date) throw new AssertionError(jq(date) + ": us=Date(" + preciseDateFormatHolder.get().format(ourResult) + "), lib=Date(" + preciseDateFormatHolder.get().format(libResult) + ")"); } } + + private static void validateDropbox8601DateParser(String date, long expectedMillis) + throws java.text.ParseException + { + char[] buf = date.toCharArray(); + Date result = JsonDateReader.parseDropbox8601Date(buf, 0, buf.length); + assertEquals(result.getTime(), expectedMillis); + } } diff --git a/core/src/test/java/com/dropbox/core/util/IOUtilTest.java b/core/src/test/java/com/dropbox/core/util/IOUtilTest.java new file mode 100644 index 000000000..61d3ac448 --- /dev/null +++ b/core/src/test/java/com/dropbox/core/util/IOUtilTest.java @@ -0,0 +1,32 @@ +package com.dropbox.core.util; + +import org.testng.annotations.Test; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +import static org.testng.Assert.assertEquals; + +public class IOUtilTest { + @Test + public void slurpHonorsByteLimit() throws Exception { + byte[] result = IOUtil.slurp( + new ByteArrayInputStream("abcdef".getBytes(StandardCharsets.UTF_8)), + 3, + new byte[2] + ); + + assertEquals(new String(result, StandardCharsets.UTF_8), "abc"); + } + + @Test + public void slurpReturnsShortStreamWithoutPadding() throws Exception { + byte[] result = IOUtil.slurp( + new ByteArrayInputStream("ab".getBytes(StandardCharsets.UTF_8)), + 3, + new byte[2] + ); + + assertEquals(new String(result, StandardCharsets.UTF_8), "ab"); + } +} diff --git a/examples/android/build.gradle b/examples/android/build.gradle index 4fbbc4c42..8ad92dea3 100644 --- a/examples/android/build.gradle +++ b/examples/android/build.gradle @@ -57,8 +57,14 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } +} + +kotlin { + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) } } diff --git a/examples/examples/build.gradle b/examples/examples/build.gradle index 808f45ea1..4c1f06363 100644 --- a/examples/examples/build.gradle +++ b/examples/examples/build.gradle @@ -6,18 +6,18 @@ plugins { description = 'Consolidated Examples' java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } kotlin { compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21) } } tasks.withType(JavaCompile).configureEach { - options.release.set(17) + options.release.set(21) } dependencies { diff --git a/examples/java/build.gradle b/examples/java/build.gradle index 816d66be3..a3a3e6397 100644 --- a/examples/java/build.gradle +++ b/examples/java/build.gradle @@ -11,12 +11,12 @@ dependencyGuard { description = 'Java Examples' java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } tasks.withType(JavaCompile).configureEach { - options.release.set(17) + options.release.set(21) } dependencies { diff --git a/proguard/build.gradle b/proguard/build.gradle index ae9b4c468..559a5ddb1 100644 --- a/proguard/build.gradle +++ b/proguard/build.gradle @@ -8,12 +8,12 @@ base { } java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } tasks.withType(JavaCompile).configureEach { - options.release.set(17) + options.release.set(21) } ext { From cf0f8fcf4ac3e4b14dc8f811c4230dea4afd79d6 Mon Sep 17 00:00:00 2001 From: Josh Feinberg <15068619+joshafeinberg@users.noreply.github.com> Date: Tue, 23 Jun 2026 12:28:04 -0500 Subject: [PATCH 2/3] Document Java 21 requirement for v8 --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 434897f5e..dd7684e06 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,10 @@ Documentation: [Javadocs](https://dropbox.github.io/dropbox-sdk-java/) ### Java Version -The current release of Dropbox SDK Java supports Java 17+. +Starting with version 8.0.0, Dropbox SDK Java requires Java 21+. + +If your project needs Java 8 through Java 20 support, use the latest 7.x release +of the SDK. The 7.x line remains available for Java 8+. ### Android Version @@ -370,7 +373,7 @@ dependencies { The JAR's manifest has the following line: ``` -Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=17))" +Require-Capability: osgi.ee;filter:="(&(osgi.ee=JavaSE)(version=21))" ``` Most OSGi containers should provide this capability. Unfortunately, some OSGi containers don't do this correctly and will reject the bundle JAR in the OSGi subsystem context. From 34faf2c65878c0a4297cda668558a49fcf316500 Mon Sep 17 00:00:00 2001 From: Josh Feinberg <15068619+joshafeinberg@users.noreply.github.com> Date: Fri, 26 Jun 2026 15:31:15 -0500 Subject: [PATCH 3/3] Update JDK version from 17 to 21 in workflow --- .github/workflows/check.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index cdb1e14a0..c305c4684 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -18,10 +18,10 @@ jobs: - name: Gradle Wrapper Validation uses: gradle/actions/wrapper-validation@v6 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v5 with: - java-version: '17' + java-version: '21' distribution: 'zulu' - name: Set up Python @@ -140,11 +140,11 @@ jobs: - name: Gradle Wrapper Validation uses: gradle/actions/wrapper-validation@v6 - - name: Install JDK 17 + - name: Install JDK 21 uses: actions/setup-java@v5 with: distribution: 'zulu' - java-version: 17 + java-version: 21 - name: Set up Python uses: actions/setup-python@v6