Skip to content

Commit

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

Closes #882
  • Loading branch information
lhazlewood committed Jan 10, 2024
1 parent eae68cd commit 584d91c
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 4 deletions.
Expand Up @@ -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;
Expand All @@ -33,14 +36,25 @@
*/
public class OrgJsonDeserializer extends AbstractDeserializer<Object> {

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
Expand Down Expand Up @@ -94,4 +108,67 @@ private Object convertIfNecessary(Object v) {
}
return value;
}

/**
* A factory to create {@link JSONTokener} instances from {@link Reader}s.
*
* @see <a href="https://github.com/jwtk/jjwt/issues/882">JJWT Issue 882</a>.
* @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);
}
}
}
Expand Up @@ -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
Expand All @@ -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)
}

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

}

0 comments on commit 584d91c

Please sign in to comment.