diff --git a/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/AsyncJSON.java b/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/AsyncJSON.java index a93de93d5016..b19842084b02 100644 --- a/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/AsyncJSON.java +++ b/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/AsyncJSON.java @@ -19,7 +19,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.Index; @@ -62,6 +64,8 @@ * *

Class {@code com.acme.Person} must either implement {@link Convertible}, * or be mapped with a {@link Convertor} via {@link Factory#putConvertor(String, Convertor)}.

+ *

JSON arrays are by default represented with a {@code List}, but the + * Java representation can be customized via {@link Factory#setArrayConverter(Function)}.

*/ public class AsyncJSON { @@ -75,8 +79,31 @@ public static class Factory { private Index.Mutable cache; private Map convertors; + private Function, Object> arrayConverter = list -> list; private boolean detailedParseException; + /** + * @return the function to customize the Java representation of JSON arrays + * @see #setArrayConverter(Function) + */ + public Function, Object> getArrayConverter() + { + return arrayConverter; + } + + /** + *

Sets the function to convert JSON arrays from their default Java + * representation, a {@code List}, to another Java data structure + * such as an {@code Object[]}.

+ * + * @param arrayConverter the function to customize the Java representation of JSON arrays + * @see #getArrayConverter() + */ + public void setArrayConverter(Function, Object> arrayConverter) + { + this.arrayConverter = Objects.requireNonNull(arrayConverter); + } + /** * @return whether a parse failure should report the whole JSON string or just the last chunk */ @@ -870,9 +897,10 @@ private boolean parseArray(ByteBuffer buffer) case ']': { buffer.get(); - Object array = stack.peek().value; + @SuppressWarnings("unchecked") + List array = (List)stack.peek().value; stack.pop(); - stack.peek().value(array); + stack.peek().value(convertArray(array)); return true; } case ',': @@ -1067,6 +1095,11 @@ private boolean parseObjectFieldValue(ByteBuffer buffer) return true; } + private Object convertArray(List array) + { + return factory.getArrayConverter().apply(array); + } + private Object convertObject(Map object) { Object result = convertObject("x-class", object); diff --git a/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/JSON.java b/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/JSON.java index 6407c8f3d9b8..719745c27369 100644 --- a/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/JSON.java +++ b/jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/JSON.java @@ -19,11 +19,14 @@ import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import org.eclipse.jetty.util.Loader; import org.eclipse.jetty.util.TypeUtil; @@ -81,6 +84,7 @@ public class JSON private final Map _convertors = new ConcurrentHashMap<>(); private int _stringBufferSize = 1024; + private Function, Object> _arrayConverter = List::toArray; /** * @return the initial stringBuffer size to use when creating JSON strings @@ -461,7 +465,9 @@ protected Map newMap() * * @param size the size of the array * @return a new array representing the JSON array + * @deprecated use {@link #setArrayConverter(Function)} instead. */ + @Deprecated protected Object[] newArray(int size) { return new Object[size]; @@ -601,6 +607,28 @@ public Convertor getConvertorFor(String name) return _convertors.get(name); } + /** + * @return the function to customize the Java representation of JSON arrays + * @see #setArrayConverter(Function) + */ + public Function, Object> getArrayConverter() + { + return _arrayConverter; + } + + /** + *

Sets the function to convert JSON arrays from their default Java + * representation, a {@code List}, to another Java data structure + * such as an {@code Object[]}.

+ * + * @param arrayConverter the function to customize the Java representation of JSON arrays + * @see #getArrayConverter() + */ + public void setArrayConverter(Function, Object> arrayConverter) + { + _arrayConverter = Objects.requireNonNull(arrayConverter); + } + /** *

Parses the given JSON source into an object.

*

Although the JSON specification does not allow comments (of any kind) @@ -928,14 +956,16 @@ protected Object parseArray(Source source) switch (size) { case 0: - return newArray(0); + list = Collections.emptyList(); + break; case 1: - Object array = newArray(1); - Array.set(array, 0, item); - return array; + list = Collections.singletonList(item); + break; default: - return list.toArray(newArray(list.size())); + break; } + return getArrayConverter().apply(list); + case ',': if (comma) throw new IllegalStateException(); @@ -970,6 +1000,7 @@ else if (list == null) item = null; } } + break; } } @@ -1199,7 +1230,7 @@ public Number parseNumber(Source source) break doubleLoop; } } - return Double.parseDouble(buffer.toString()); + return Double.valueOf(buffer.toString()); } protected void seekTo(char seek, Source source) @@ -1585,7 +1616,7 @@ public interface Generator */ public static class Literal implements Generator { - private String _json; + private final String _json; /** * Constructs a literal JSON instance. diff --git a/jetty-util-ajax/src/test/java/org/eclipse/jetty/util/ajax/AsyncJSONTest.java b/jetty-util-ajax/src/test/java/org/eclipse/jetty/util/ajax/AsyncJSONTest.java index b9a2f82ac5d9..2c8c8ebbdccf 100644 --- a/jetty-util-ajax/src/test/java/org/eclipse/jetty/util/ajax/AsyncJSONTest.java +++ b/jetty-util-ajax/src/test/java/org/eclipse/jetty/util/ajax/AsyncJSONTest.java @@ -19,15 +19,19 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; 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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -520,4 +524,47 @@ public void testEncodedCaching() 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 map = (Map)object; + return map.get("array"); + }); + } + + private void testArrayConverter(String json, Function 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)); + } }