Skip to content

Commit

Permalink
Issue #6558 - improved json array converter (#6571)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
sbordet committed Aug 3, 2021
1 parent 51e6335 commit 342396c
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 23 deletions.
Expand Up @@ -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;
Expand Down Expand Up @@ -68,6 +70,8 @@
* </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
{
Expand All @@ -81,8 +85,31 @@ public static class Factory
{
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
*/
Expand Down Expand Up @@ -873,9 +900,10 @@ private boolean parseArray(ByteBuffer buffer)
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 ',':
Expand Down Expand Up @@ -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<String, Object> object)
{
Object result = convertObject("x-class", object);
Expand Down
92 changes: 71 additions & 21 deletions jetty-util-ajax/src/main/java/org/eclipse/jetty/util/ajax/JSON.java
Expand Up @@ -25,10 +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;
Expand Down Expand Up @@ -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()
{
Expand Down Expand Up @@ -307,13 +312,13 @@ else if (c < 0x20 || c == 0x7F) // all control characters
* 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
{
Expand Down Expand Up @@ -665,9 +670,15 @@ protected String toString(char[] buffer, int offset, int length)

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];
Expand Down Expand Up @@ -739,7 +750,7 @@ protected Convertor getConvertor(Class forClass)
{
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());
}
Expand All @@ -763,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<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.
*
Expand Down Expand Up @@ -1014,7 +1058,7 @@ protected Object parseObject(Source source)
{
try
{
Class c = Loader.loadClass(classname);
Class<?> c = Loader.loadClass(classname);
return convertTo(c, map);
}
catch (ClassNotFoundException e)
Expand All @@ -1032,9 +1076,9 @@ protected Object parseArray(Source source)
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())
{
Expand All @@ -1046,33 +1090,38 @@ 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 (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);
Expand All @@ -1085,6 +1134,7 @@ else if (list == null)
item = null;
}
}
break;
}
}

Expand Down Expand Up @@ -1319,7 +1369,7 @@ public Number parseNumber(Source source)
break doubleLoop;
}
}
return new Double(buffer.toString());
return Double.valueOf(buffer.toString());
}

protected void seekTo(char seek, Source source)
Expand Down Expand Up @@ -1696,7 +1746,7 @@ public interface Generator
*/
public static class Literal implements Generator
{
private String _json;
private final String _json;

/**
* Construct a literal JSON instance for use by
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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));
}
}

0 comments on commit 342396c

Please sign in to comment.