Skip to content

Commit

Permalink
OrgJsonDeserializer: Added fallback implementation for Android when J…
Browse files Browse the repository at this point in the history
…SONTokener(Reader) constructor is not available.

Closes #882
  • Loading branch information
lhazlewood committed Jan 10, 2024
1 parent eae68cd commit ad1ffe0
Show file tree
Hide file tree
Showing 2 changed files with 139 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 <a href="https://github.com/jwtk/jjwt/issues/882">JJWT Issue 882</a>.
* @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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
}

Expand Down Expand Up @@ -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()
}
}

}

0 comments on commit ad1ffe0

Please sign in to comment.