From ad1ffe00c1f539a22e75ec2bad0872e120262b98 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Tue, 9 Jan 2024 17:14:32 -0800 Subject: [PATCH] OrgJsonDeserializer: Added fallback implementation for Android when JSONTokener(Reader) constructor is not available. Closes #882 --- .../orgjson/io/OrgJsonDeserializer.java | 67 +++++++++++++++- .../orgjson/io/OrgJsonDeserializerTest.groovy | 76 ++++++++++++++++++- 2 files changed, 139 insertions(+), 4 deletions(-) diff --git a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java index 49be0675c..8e04d0911 100644 --- a/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java +++ b/extensions/orgjson/src/main/java/io/jsonwebtoken/orgjson/io/OrgJsonDeserializer.java @@ -21,6 +21,7 @@ import org.json.JSONObject; import org.json.JSONTokener; +import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.Iterator; @@ -38,9 +39,71 @@ protected Object doDeserialize(Reader reader) { return parse(reader); } - private Object parse(java.io.Reader reader) throws JSONException { - JSONTokener tokener = new JSONTokener(reader); + // protected for testing override only + + /** + * Returns a new {@link JSONTokener} instance using the {@link JSONTokener#JSONTokener(Reader) JSONTokener(Reader)} + * constructor, primarily exposed for unit testing. + * + * @param reader the reader to wrap + * @return a new JSONTokener + * @throws NoSuchMethodError if the {@link JSONTokener#JSONTokener(Reader) JSONTokener(Reader)} constructor is not + * available, for example, on Android. + * @since 0.12.4 + */ + protected JSONTokener newTokener(Reader reader) throws NoSuchMethodError { + return new JSONTokener(reader); + } + + /** + * Reads all content from the specified reader and returns it as a single String. + * + * @param reader the reader to read characters from + * @return the reader content as a single string + * @since 0.12.4 + */ + private static String toString(Reader reader) throws IOException { + StringBuilder sb = new StringBuilder(4096); + char[] buf = new char[4096]; + int n = 0; + while (EOF != n) { + n = reader.read(buf); + if (n > 0) sb.append(buf, 0, n); + } + return sb.toString(); + } + + /** + * Attempts to create a {@link JSONTokener} using the more efficient + * {@link JSONTokener#JSONTokener(Reader) JSONTokener(Reader)} constructor. If that constructor is not available + * (for example, on Android), this method reads the {@code Reader} argument in its entirety to a {@code String} + * first, then falls back to using the {@link JSONTokener#JSONTokener(String) JSONTokener(String)} constructor + * (which is available on Android). + * + * @param reader the reader to convert to use when constructing a {@link JSONTokener}. + * @return the JSONTokener to use for parsing. + * @see JJWT Issue 882. + * @since 0.12.4 + */ + private JSONTokener toTokener(Reader reader) { + try { + return newTokener(reader); + } catch (NoSuchMethodError e) { // Reader constructor not available (Android), fall back to String ctor: + String s; + try { + s = toString(reader); + } catch (IOException ex) { + String msg = "Unable to obtain JSON String from Reader: " + ex.getMessage(); + throw new JSONException(msg, ex); + } + return new JSONTokener(s); + } + } + + private Object parse(Reader reader) throws JSONException { + + JSONTokener tokener = toTokener(reader); char c = tokener.nextClean(); //peak ahead tokener.back(); //revert diff --git a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy index 5f27dcbef..014a54d50 100644 --- a/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy +++ b/extensions/orgjson/src/test/groovy/io/jsonwebtoken/orgjson/io/OrgJsonDeserializerTest.groovy @@ -18,7 +18,9 @@ package io.jsonwebtoken.orgjson.io import io.jsonwebtoken.io.DeserializationException import io.jsonwebtoken.io.Deserializer +import io.jsonwebtoken.io.IOException import io.jsonwebtoken.lang.Strings +import org.json.JSONTokener import org.junit.Before import org.junit.Test @@ -28,9 +30,17 @@ class OrgJsonDeserializerTest { private OrgJsonDeserializer des - private Object fromBytes(byte[] data) { + private static Reader reader(byte[] data) { def ins = new ByteArrayInputStream(data) - def reader = new InputStreamReader(ins, Strings.UTF_8) + return new InputStreamReader(ins, Strings.UTF_8) + } + + private static Reader reader(String s) { + return reader(Strings.utf8(s)) + } + + private Object fromBytes(byte[] data) { + def reader = reader(data) return des.deserialize(reader) } @@ -188,4 +198,66 @@ class OrgJsonDeserializerTest { } } + /** + * Asserts that, when the JSONTokener(Reader) constructor isn't available (e.g. on Android), that the Reader is + * converted to a String and the JSONTokener(String) constructor is used instead. + * @since 0.12.4 + */ + @Test + void jsonTokenerMissingReaderConstructor() { + + def json = '{"hello": "世界", "test": [1, 2]}' + def expected = read(json) // 'normal' reading + + des = new OrgJsonDeserializer() { + @Override + protected JSONTokener newTokener(Reader reader) throws NoSuchMethodError { + throw new NoSuchMethodError('Android says nope!') + } + } + + def reader = reader('{"hello": "世界", "test": [1, 2]}') + + def result = des.deserialize(reader) // should still work + + assertEquals expected, result + } + + /** + * Asserts that, when the JSONTokener(Reader) constructor isn't available, and conversion of the Reader to a String + * fails, that a JSONException is thrown + * @since 0.12.4 + */ + @Test + void readerFallbackToStringFails() { + def causeMsg = 'Reader failed.' + def cause = new java.io.IOException(causeMsg) + def reader = new Reader() { + @Override + int read(char[] cbuf, int off, int len) throws IOException { + throw cause + } + + @Override + void close() throws IOException { + } + } + des = new OrgJsonDeserializer() { + @Override + protected JSONTokener newTokener(Reader r) throws NoSuchMethodError { + throw new NoSuchMethodError('Android says nope!') + } + } + + try { + des.deserialize(reader) + fail() + } catch (DeserializationException expected) { + def jsonEx = expected.getCause() + String msg = "Unable to obtain JSON String from Reader: $causeMsg" + assertEquals msg, jsonEx.getMessage() + assertSame cause, jsonEx.getCause() + } + } + }