From 584d91c2b4aa3370bae345e2f4fb788fba54be38 Mon Sep 17 00:00:00 2001 From: lhazlewood <121180+lhazlewood@users.noreply.github.com> Date: Tue, 9 Jan 2024 18:50:13 -0800 Subject: [PATCH] JSONTokener constructor fallback (#888) OrgJsonDeserializer: Added fallback implementation for Android when JSONTokener(Reader) constructor is not available. Closes #882 --- .../orgjson/io/OrgJsonDeserializer.java | 81 ++++++++++++++++++- .../orgjson/io/OrgJsonDeserializerTest.groovy | 73 ++++++++++++++++- 2 files changed, 150 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 49be0675..0367eebc 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 @@ -16,11 +16,14 @@ package io.jsonwebtoken.orgjson.io; import io.jsonwebtoken.io.AbstractDeserializer; +import io.jsonwebtoken.lang.Assert; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; +import java.io.CharArrayReader; +import java.io.IOException; import java.io.Reader; import java.util.ArrayList; import java.util.Iterator; @@ -33,14 +36,25 @@ import java.util.Map; */ public class OrgJsonDeserializer extends AbstractDeserializer { + private final JSONTokenerFactory TOKENER_FACTORY; + + public OrgJsonDeserializer() { + this(JSONTokenerFactory.INSTANCE); + } + + private OrgJsonDeserializer(JSONTokenerFactory factory) { + this.TOKENER_FACTORY = Assert.notNull(factory, "JSONTokenerFactory cannot be null."); + } + @Override protected Object doDeserialize(Reader reader) { return parse(reader); } - private Object parse(java.io.Reader reader) throws JSONException { + private Object parse(Reader reader) throws JSONException { - JSONTokener tokener = new JSONTokener(reader); + JSONTokener tokener = this.TOKENER_FACTORY.newTokener(reader); + Assert.notNull(tokener, "JSONTokener cannot be null."); char c = tokener.nextClean(); //peak ahead tokener.back(); //revert @@ -94,4 +108,67 @@ public class OrgJsonDeserializer extends AbstractDeserializer { } return value; } + + /** + * A factory to create {@link JSONTokener} instances from {@link Reader}s. + * + * @see JJWT Issue 882. + * @since 0.12.4 + */ + static class JSONTokenerFactory { // package-protected on purpose. Not to be exposed as part of public API + + private static final Reader TEST_READER = new CharArrayReader("{}".toCharArray()); + + private static final JSONTokenerFactory INSTANCE = new JSONTokenerFactory(); + + private final boolean readerCtorAvailable; + + // package protected visibility for testing only: + JSONTokenerFactory() { + boolean avail = true; + try { + testTokener(TEST_READER); + } catch (NoSuchMethodError err) { + avail = false; + } + this.readerCtorAvailable = avail; + } + + // visible for testing only + protected void testTokener(@SuppressWarnings("SameParameterValue") Reader reader) throws NoSuchMethodError { + 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 + */ + 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(); + } + + private JSONTokener newTokener(Reader reader) { + if (this.readerCtorAvailable) { + return new JSONTokener(reader); + } + // otherwise not available, likely Android or earlier org.json version, 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); + } + } } 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 5f27dcbe..df780b08 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,6 +18,7 @@ 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.junit.Before import org.junit.Test @@ -28,9 +29,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 +197,64 @@ 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(new NoReaderCtorTokenerFactory()) + + 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(new NoReaderCtorTokenerFactory()) + + 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() + } + } + + private static class NoReaderCtorTokenerFactory extends OrgJsonDeserializer.JSONTokenerFactory { + @Override + protected void testTokener(Reader reader) throws NoSuchMethodError { + throw new NoSuchMethodError('Android says nope!') + } + } + }