JSONTokener constructor fallback (#888)

OrgJsonDeserializer: Added fallback implementation for Android when JSONTokener(Reader) constructor is not available.

Closes #882
This commit is contained in:
lhazlewood 2024-01-09 18:50:13 -08:00 committed by GitHub
parent eae68cddc9
commit 584d91c2b4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 150 additions and 4 deletions

View File

@ -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<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
@ -94,4 +108,67 @@ public class OrgJsonDeserializer extends AbstractDeserializer<Object> {
}
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);
}
}
}

View File

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