Fixes #6558 - Allow configuring return type in JSON array parsing.

Introduced `arrayConverter` in both JSON and AsyncJSON.Factory.

Signed-off-by: Simone Bordet <simone.bordet@gmail.com>
(cherry picked from commit 342396c7ee)
This commit is contained in:
Simone Bordet 2021-08-03 17:09:28 +02:00
parent 4e3e99c5c5
commit 266d8f0dca
3 changed files with 120 additions and 9 deletions

View File

@ -19,7 +19,9 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Index; import org.eclipse.jetty.util.Index;
@ -62,6 +64,8 @@ import org.eclipse.jetty.util.ajax.JSON.Convertor;
* </pre> * </pre>
* <p>Class {@code com.acme.Person} must either implement {@link Convertible}, * <p>Class {@code com.acme.Person} must either implement {@link Convertible},
* or be mapped with a {@link Convertor} via {@link Factory#putConvertor(String, Convertor)}.</p> * or be mapped with a {@link Convertor} via {@link Factory#putConvertor(String, Convertor)}.</p>
* <p>JSON arrays are by default represented with a {@code List<Object>}, but the
* Java representation can be customized via {@link Factory#setArrayConverter(Function)}.</p>
*/ */
public class AsyncJSON public class AsyncJSON
{ {
@ -75,8 +79,31 @@ public class AsyncJSON
{ {
private Index.Mutable<CachedString> cache; private Index.Mutable<CachedString> cache;
private Map<String, Convertor> convertors; private Map<String, Convertor> convertors;
private Function<List<?>, Object> arrayConverter = list -> list;
private boolean detailedParseException; private boolean detailedParseException;
/**
* @return the function to customize the Java representation of JSON arrays
* @see #setArrayConverter(Function)
*/
public Function<List<?>, Object> getArrayConverter()
{
return arrayConverter;
}
/**
* <p>Sets the function to convert JSON arrays from their default Java
* representation, a {@code List<Object>}, to another Java data structure
* such as an {@code Object[]}.</p>
*
* @param arrayConverter the function to customize the Java representation of JSON arrays
* @see #getArrayConverter()
*/
public void setArrayConverter(Function<List<?>, Object> arrayConverter)
{
this.arrayConverter = Objects.requireNonNull(arrayConverter);
}
/** /**
* @return whether a parse failure should report the whole JSON string or just the last chunk * @return whether a parse failure should report the whole JSON string or just the last chunk
*/ */
@ -870,9 +897,10 @@ public class AsyncJSON
case ']': case ']':
{ {
buffer.get(); buffer.get();
Object array = stack.peek().value; @SuppressWarnings("unchecked")
List<Object> array = (List<Object>)stack.peek().value;
stack.pop(); stack.pop();
stack.peek().value(array); stack.peek().value(convertArray(array));
return true; return true;
} }
case ',': case ',':
@ -1067,6 +1095,11 @@ public class AsyncJSON
return true; return true;
} }
private Object convertArray(List<?> array)
{
return factory.getArrayConverter().apply(array);
}
private Object convertObject(Map<String, Object> object) private Object convertObject(Map<String, Object> object)
{ {
Object result = convertObject("x-class", object); Object result = convertObject("x-class", object);

View File

@ -19,11 +19,14 @@ import java.io.Reader;
import java.lang.reflect.Array; import java.lang.reflect.Array;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.Loader;
import org.eclipse.jetty.util.TypeUtil; import org.eclipse.jetty.util.TypeUtil;
@ -81,6 +84,7 @@ public class JSON
private final Map<String, Convertor> _convertors = new ConcurrentHashMap<>(); private final Map<String, Convertor> _convertors = new ConcurrentHashMap<>();
private int _stringBufferSize = 1024; private int _stringBufferSize = 1024;
private Function<List<?>, Object> _arrayConverter = List::toArray;
/** /**
* @return the initial stringBuffer size to use when creating JSON strings * @return the initial stringBuffer size to use when creating JSON strings
@ -461,7 +465,9 @@ public class JSON
* *
* @param size the size of the array * @param size the size of the array
* @return a new array representing the JSON array * @return a new array representing the JSON array
* @deprecated use {@link #setArrayConverter(Function)} instead.
*/ */
@Deprecated
protected Object[] newArray(int size) protected Object[] newArray(int size)
{ {
return new Object[size]; return new Object[size];
@ -601,6 +607,28 @@ public class JSON
return _convertors.get(name); return _convertors.get(name);
} }
/**
* @return the function to customize the Java representation of JSON arrays
* @see #setArrayConverter(Function)
*/
public Function<List<?>, Object> getArrayConverter()
{
return _arrayConverter;
}
/**
* <p>Sets the function to convert JSON arrays from their default Java
* representation, a {@code List<Object>}, to another Java data structure
* such as an {@code Object[]}.</p>
*
* @param arrayConverter the function to customize the Java representation of JSON arrays
* @see #getArrayConverter()
*/
public void setArrayConverter(Function<List<?>, Object> arrayConverter)
{
_arrayConverter = Objects.requireNonNull(arrayConverter);
}
/** /**
* <p>Parses the given JSON source into an object.</p> * <p>Parses the given JSON source into an object.</p>
* <p>Although the JSON specification does not allow comments (of any kind) * <p>Although the JSON specification does not allow comments (of any kind)
@ -928,14 +956,16 @@ public class JSON
switch (size) switch (size)
{ {
case 0: case 0:
return newArray(0); list = Collections.emptyList();
break;
case 1: case 1:
Object array = newArray(1); list = Collections.singletonList(item);
Array.set(array, 0, item); break;
return array;
default: default:
return list.toArray(newArray(list.size())); break;
} }
return getArrayConverter().apply(list);
case ',': case ',':
if (comma) if (comma)
throw new IllegalStateException(); throw new IllegalStateException();
@ -970,6 +1000,7 @@ public class JSON
item = null; item = null;
} }
} }
break;
} }
} }
@ -1199,7 +1230,7 @@ public class JSON
break doubleLoop; break doubleLoop;
} }
} }
return Double.parseDouble(buffer.toString()); return Double.valueOf(buffer.toString());
} }
protected void seekTo(char seek, Source source) protected void seekTo(char seek, Source source)
@ -1585,7 +1616,7 @@ public class JSON
*/ */
public static class Literal implements Generator public static class Literal implements Generator
{ {
private String _json; private final String _json;
/** /**
* Constructs a literal JSON instance. * Constructs a literal JSON instance.

View File

@ -19,15 +19,19 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -520,4 +524,47 @@ public class AsyncJSONTest
assertSame(foo, item); assertSame(foo, item);
} }
} }
@Test
public void testArrayConverter()
{
// Test root arrays.
testArrayConverter("[1]", Function.identity());
// Test non-root arrays.
testArrayConverter("{\"array\": [1]}", object ->
{
@SuppressWarnings("unchecked")
Map<String, Object> map = (Map<String, Object>)object;
return map.get("array");
});
}
private void testArrayConverter(String json, Function<Object, Object> extractor)
{
AsyncJSON.Factory factory = new AsyncJSON.Factory();
AsyncJSON async = factory.newAsyncJSON();
JSON sync = new JSON();
async.parse(UTF_8.encode(json));
Object result = extractor.apply(async.complete());
// AsyncJSON historically defaults to list.
assertThat(result, Matchers.instanceOf(List.class));
// JSON historically defaults to array.
result = extractor.apply(sync.parse(new JSON.StringSource(json)));
assertNotNull(result);
assertTrue(result.getClass().isArray(), json + " -> " + result);
// Configure AsyncJSON to return arrays.
factory.setArrayConverter(List::toArray);
async.parse(UTF_8.encode(json));
result = extractor.apply(async.complete());
assertNotNull(result);
assertTrue(result.getClass().isArray(), json + " -> " + result);
// Configure JSON to return lists.
sync.setArrayConverter(list -> list);
result = extractor.apply(sync.parse(new JSON.StringSource(json)));
assertThat(result, Matchers.instanceOf(List.class));
}
} }