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>
This commit is contained in:
parent
51e6335c7a
commit
342396c7ee
|
@ -24,7 +24,9 @@ import java.util.Arrays;
|
|||
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 @@ import org.eclipse.jetty.util.ajax.JSON.Convertor;
|
|||
* </pre>
|
||||
* <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>
|
||||
* <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
|
||||
{
|
||||
|
@ -81,8 +85,31 @@ public class AsyncJSON
|
|||
{
|
||||
private Trie<CachedString> cache;
|
||||
private Map<String, Convertor> convertors;
|
||||
private Function<List<?>, Object> arrayConverter = list -> list;
|
||||
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
|
||||
*/
|
||||
|
@ -873,9 +900,10 @@ public class AsyncJSON
|
|||
case ']':
|
||||
{
|
||||
buffer.get();
|
||||
Object array = stack.peek().value;
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Object> array = (List<Object>)stack.peek().value;
|
||||
stack.pop();
|
||||
stack.peek().value(array);
|
||||
stack.peek().value(convertArray(array));
|
||||
return true;
|
||||
}
|
||||
case ',':
|
||||
|
@ -1070,6 +1098,11 @@ public class AsyncJSON
|
|||
return true;
|
||||
}
|
||||
|
||||
private Object convertArray(List<?> array)
|
||||
{
|
||||
return factory.getArrayConverter().apply(array);
|
||||
}
|
||||
|
||||
private Object convertObject(Map<String, Object> object)
|
||||
{
|
||||
Object result = convertObject("x-class", object);
|
||||
|
|
|
@ -25,10 +25,14 @@ import java.io.Reader;
|
|||
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;
|
||||
|
@ -91,8 +95,9 @@ public class JSON
|
|||
static final Logger LOG = Log.getLogger(JSON.class);
|
||||
public static final JSON DEFAULT = new JSON();
|
||||
|
||||
private Map<String, Convertor> _convertors = new ConcurrentHashMap<String, Convertor>();
|
||||
private final Map<String, Convertor> _convertors = new ConcurrentHashMap<>();
|
||||
private int _stringBufferSize = 1024;
|
||||
private Function<List<?>, Object> _arrayConverter = List::toArray;
|
||||
|
||||
public JSON()
|
||||
{
|
||||
|
@ -307,13 +312,13 @@ public class JSON
|
|||
* This overridable allows for alternate behavior to escape those with your choice
|
||||
* of encoding.
|
||||
*
|
||||
* <code>
|
||||
* <pre>
|
||||
* protected void escapeUnicode(Appendable buffer, char c) throws IOException
|
||||
* {
|
||||
* // Unicode is slash-u escaped
|
||||
* buffer.append(String.format("\\u%04x", (int)c));
|
||||
* // Unicode is slash-u escaped
|
||||
* buffer.append(String.format("\\u%04x", (int)c));
|
||||
* }
|
||||
* </code>
|
||||
* </pre>
|
||||
*/
|
||||
protected void escapeUnicode(Appendable buffer, char c) throws IOException
|
||||
{
|
||||
|
@ -665,9 +670,15 @@ public class JSON
|
|||
|
||||
protected Map<String, Object> newMap()
|
||||
{
|
||||
return new HashMap<String, Object>();
|
||||
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];
|
||||
|
@ -739,7 +750,7 @@ public class JSON
|
|||
{
|
||||
Class[] ifs = cls.getInterfaces();
|
||||
int i = 0;
|
||||
while (convertor == null && ifs != null && i < ifs.length)
|
||||
while (convertor == null && i < ifs.length)
|
||||
{
|
||||
convertor = _convertors.get(ifs[i++].getName());
|
||||
}
|
||||
|
@ -763,6 +774,39 @@ public class JSON
|
|||
_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<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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup a convertor for a named class.
|
||||
*
|
||||
|
@ -1014,7 +1058,7 @@ public class JSON
|
|||
{
|
||||
try
|
||||
{
|
||||
Class c = Loader.loadClass(classname);
|
||||
Class<?> c = Loader.loadClass(classname);
|
||||
return convertTo(c, map);
|
||||
}
|
||||
catch (ClassNotFoundException e)
|
||||
|
@ -1032,9 +1076,9 @@ public class JSON
|
|||
throw new IllegalStateException();
|
||||
|
||||
int size = 0;
|
||||
ArrayList list = null;
|
||||
List<Object> list = null;
|
||||
Object item = null;
|
||||
boolean coma = true;
|
||||
boolean comma = true;
|
||||
|
||||
while (source.hasNext())
|
||||
{
|
||||
|
@ -1046,33 +1090,38 @@ public class JSON
|
|||
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 (coma)
|
||||
if (comma)
|
||||
throw new IllegalStateException();
|
||||
coma = true;
|
||||
comma = true;
|
||||
source.next();
|
||||
break;
|
||||
|
||||
default:
|
||||
if (Character.isWhitespace(c))
|
||||
{
|
||||
source.next();
|
||||
}
|
||||
else
|
||||
{
|
||||
coma = false;
|
||||
comma = false;
|
||||
if (size++ == 0)
|
||||
{
|
||||
item = contextForArray().parse(source);
|
||||
}
|
||||
else if (list == null)
|
||||
{
|
||||
list = new ArrayList();
|
||||
list = new ArrayList<>();
|
||||
list.add(item);
|
||||
item = contextForArray().parse(source);
|
||||
list.add(item);
|
||||
|
@ -1085,6 +1134,7 @@ public class JSON
|
|||
item = null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1319,7 +1369,7 @@ public class JSON
|
|||
break doubleLoop;
|
||||
}
|
||||
}
|
||||
return new Double(buffer.toString());
|
||||
return Double.valueOf(buffer.toString());
|
||||
}
|
||||
|
||||
protected void seekTo(char seek, Source source)
|
||||
|
@ -1696,7 +1746,7 @@ public class JSON
|
|||
*/
|
||||
public static class Literal implements Generator
|
||||
{
|
||||
private String _json;
|
||||
private final String _json;
|
||||
|
||||
/**
|
||||
* Construct a literal JSON instance for use by
|
||||
|
|
|
@ -24,15 +24,19 @@ import java.util.Collections;
|
|||
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 class AsyncJSONTest
|
|||
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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue