Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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;
Expand Down Expand Up @@ -62,6 +64,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 @@ -75,8 +79,31 @@ public static class Factory
{
private Index.Mutable<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 @@ -870,9 +897,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 @@ -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<String, Object> object)
{
Object result = convertObject("x-class", object);
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -81,6 +84,7 @@ public class JSON

private final Map<String, Convertor> _convertors = new ConcurrentHashMap<>();
private int _stringBufferSize = 1024;
private Function<List<?>, Object> _arrayConverter = List::toArray;

/**
* @return the initial stringBuffer size to use when creating JSON strings
Expand Down Expand Up @@ -461,7 +465,9 @@ protected Map<String, Object> 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];
Expand Down Expand Up @@ -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<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>Although the JSON specification does not allow comments (of any kind)
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -970,6 +1000,7 @@ else if (list == null)
item = null;
}
}
break;
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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<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));
}
}