From 7ead72ecdb3f33308101934ffe1d04c24944336d 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/2] 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 390e5f763..516bf0ea3 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) { @@ -215,7 +215,7 @@ tasks.named("javadoc", Javadoc) { if (JavaVersion.current().isJava8Compatible()) { options.addBooleanOption "Xdoclint:all,-missing", 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 ea7bbb273f63373af36d7043f8c98a9e41fff80d 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/2] 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 936d5387a..946f5fa73 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.