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 11413fd9a4..789fa41efc 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 9345403b33..ba24455df0 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 e772892e0b..e43ce9cc63 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 bd9cbcd32c..ee32006ad7 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 1e13029ac3..ad4cdbfbec 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 3f3df6d81d..bb562a8f42 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 3261ac402c..0c557b6df7 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 81166ac816..5901be9952 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 dce1e913d0..c7c5a23692 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 e8cfda33c8..3dbc18fb4a 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 bc2cf3b71e..452a920fe4 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 0000000000..61d3ac4487 --- /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 4fbbc4c42e..8ad92dea3f 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 808f45ea15..4c1f063638 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 816d66be3e..a3a3e63971 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 ae9b4c4689..559a5ddb11 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 434897f5e6..dd7684e068 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 cdb1e14a07..c305c46841 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