From 241f42c26e74f5d317dac4b10461d99a892a0564 Mon Sep 17 00:00:00 2001 From: Simone Bordet Date: Mon, 2 Aug 2021 11:44:09 +0200 Subject: [PATCH] Fixes #6558 - Allow configuring return type in JSON array parsing. Introduced `arrayConverter` in both JSON and AsyncJSON.Factory. Signed-off-by: Simone Bordet --- .../eclipse/jetty/util/ajax/AsyncJSON.java | 37 ++++++++++++- .../org/eclipse/jetty/util/ajax/JSON.java | 54 +++++++++++++++++-- .../jetty/util/ajax/AsyncJSONTest.java | 47 ++++++++++++++++ 3 files changed, 131 insertions(+), 7 deletions(-) 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 11c815487762..b1126d5331f0 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 @@ -24,7 +24,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.ArrayTernaryTrie; import org.eclipse.jetty.util.BufferUtil; @@ -68,6 +70,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 { @@ -81,8 +85,31 @@ public static class Factory { private Trie 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 */ @@ -873,9 +900,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 ',': @@ -1070,6 +1098,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 e53bd1442e6b..483494ca6345 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 @@ -25,11 +25,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.IO; import org.eclipse.jetty.util.Loader; @@ -94,6 +97,7 @@ public class JSON private final Map _convertors = new ConcurrentHashMap<>(); private int _stringBufferSize = 1024; + private Function, Object> _arrayConverter = List::toArray; public JSON() { @@ -669,6 +673,12 @@ protected Map newMap() return new HashMap<>(); } + /** + * @param size the size of the array + * @return a new array + * @deprecated use {@link #setArrayConverter(Function)} instead. + */ + @Deprecated protected Object[] newArray(int size) { return new Object[size]; @@ -764,6 +774,39 @@ public void addConvertorFor(String name, Convertor convertor) _convertors.put(name, convertor); } + /** + * Removes a registered {@link JSON.Convertor} for the given named class or interface. + * + * @param name name of a class or an interface for a registered {@link JSON.Convertor} + * @return the {@link JSON.Convertor} that was removed, or null + */ + public Convertor removeConvertorFor(String name) + { + return _convertors.remove(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); + } + /** * Lookup a convertor for a named class. * @@ -1047,14 +1090,15 @@ 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) 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 47f550c75bc5..08075559d342 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 @@ -24,15 +24,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; @@ -525,4 +529,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)); + } }