Browse Source
* Add support for JSON-P API (JSR 374) * Add support for JSON-B API (JSR 367) * Fix Jakarta EE 9 breaking unit tests * Remove Import-Package instr for JSON-P/JSON-B implementations * Fix whitespace in unit test classes * Proxy JSON-P objects and arrays to add mutability * Update project README for Jakarta JSON providers Co-authored-by: Leonid Malikov <leonid@percival.co.uk>pull/469/head
Leonid
3 years ago
committed by
GitHub
10 changed files with 2135 additions and 10 deletions
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,636 @@
|
||||
/* |
||||
* Copyright 2011 the original author or authors. |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
package com.jayway.jsonpath.spi.mapper; |
||||
|
||||
import java.lang.reflect.Constructor; |
||||
import java.lang.reflect.GenericArrayType; |
||||
import java.lang.reflect.Method; |
||||
import java.lang.reflect.Modifier; |
||||
import java.lang.reflect.ParameterizedType; |
||||
import java.lang.reflect.Type; |
||||
import java.math.BigDecimal; |
||||
import java.math.BigInteger; |
||||
import java.util.ArrayDeque; |
||||
import java.util.Arrays; |
||||
import java.util.Collection; |
||||
import java.util.Deque; |
||||
import java.util.Iterator; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.NoSuchElementException; |
||||
import java.util.Queue; |
||||
import java.util.Set; |
||||
|
||||
import com.jayway.jsonpath.Configuration; |
||||
import com.jayway.jsonpath.TypeRef; |
||||
|
||||
import jakarta.json.JsonArray; |
||||
import jakarta.json.JsonArrayBuilder; |
||||
import jakarta.json.JsonException; |
||||
import jakarta.json.JsonNumber; |
||||
import jakarta.json.JsonObject; |
||||
import jakarta.json.JsonObjectBuilder; |
||||
import jakarta.json.JsonString; |
||||
import jakarta.json.JsonStructure; |
||||
import jakarta.json.JsonValue; |
||||
import jakarta.json.bind.Jsonb; |
||||
import jakarta.json.bind.JsonbBuilder; |
||||
import jakarta.json.bind.JsonbConfig; |
||||
import jakarta.json.bind.JsonbException; |
||||
import jakarta.json.stream.JsonLocation; |
||||
import jakarta.json.stream.JsonParser; |
||||
|
||||
public class JakartaMappingProvider implements MappingProvider { |
||||
|
||||
private final Jsonb jsonb; |
||||
private Method jsonToClassMethod, jsonToTypeMethod; |
||||
|
||||
public JakartaMappingProvider() { |
||||
this.jsonb = JsonbBuilder.create(); |
||||
this.jsonToClassMethod = findMethod(jsonb.getClass(), "fromJson", JsonParser.class, Class.class); |
||||
this.jsonToTypeMethod = findMethod(jsonb.getClass(), "fromJson", JsonParser.class, Type.class); |
||||
} |
||||
|
||||
public JakartaMappingProvider(JsonbConfig jsonbConfiguration) { |
||||
this.jsonb = JsonbBuilder.create(jsonbConfiguration); |
||||
this.jsonToClassMethod = findMethod(jsonb.getClass(), "fromJson", JsonParser.class, Class.class); |
||||
this.jsonToTypeMethod = findMethod(jsonb.getClass(), "fromJson", JsonParser.class, Type.class); |
||||
} |
||||
|
||||
/** |
||||
* Maps supplied JSON source {@code Object} to a given target class or collection. |
||||
* This implementation ignores the JsonPath's {@link Configuration} argument. |
||||
*/ |
||||
@Override |
||||
public <T> T map(Object source, Class<T> targetType, Configuration configuration) { |
||||
@SuppressWarnings("unchecked") |
||||
T result = (T) mapImpl(source, targetType); |
||||
return result; |
||||
} |
||||
|
||||
/** |
||||
* Maps supplied JSON source {@code Object} to a given target type or collection. |
||||
* This implementation ignores the JsonPath's {@link Configuration} argument. |
||||
* <p> |
||||
* Method <em>may</em> produce a {@code ClassCastException} on an attempt to cast |
||||
* the result of JSON mapping operation to a requested target type, especially if |
||||
* a parameterized generic type is used. |
||||
*/ |
||||
@Override |
||||
public <T> T map(Object source, final TypeRef<T> targetType, Configuration configuration) { |
||||
@SuppressWarnings("unchecked") |
||||
T result = (T) mapImpl(source, targetType.getType()); |
||||
return result; |
||||
} |
||||
|
||||
private Object mapImpl(Object source, final Type targetType) { |
||||
if (source == null || source == JsonValue.NULL) { |
||||
return null; |
||||
} |
||||
if (source == JsonValue.TRUE) { |
||||
if (Boolean.class.equals(targetType)) { |
||||
return Boolean.TRUE; |
||||
} else { |
||||
String className = targetType.toString(); |
||||
throw new MappingException("JSON boolean (true) cannot be mapped to " + className); |
||||
} |
||||
} |
||||
if (source == JsonValue.FALSE) { |
||||
if (Boolean.class.equals(targetType)) { |
||||
return Boolean.FALSE; |
||||
} else { |
||||
String className = targetType.toString(); |
||||
throw new MappingException("JSON boolean (false) cannot be mapped to " + className); |
||||
} |
||||
} else if (source instanceof JsonString) { |
||||
if (String.class.equals(targetType)) { |
||||
return ((JsonString) source).getChars(); |
||||
} else { |
||||
String className = targetType.toString(); |
||||
throw new MappingException("JSON string cannot be mapped to " + className); |
||||
} |
||||
} else if (source instanceof JsonNumber) { |
||||
JsonNumber jsonNumber = (JsonNumber) source; |
||||
if (jsonNumber.isIntegral()) { |
||||
return mapIntegralJsonNumber(jsonNumber, getRawClass(targetType)); |
||||
} else { |
||||
return mapDecimalJsonNumber(jsonNumber, getRawClass(targetType)); |
||||
} |
||||
} |
||||
if (source instanceof JsonArrayBuilder) { |
||||
source = ((JsonArrayBuilder) source).build(); |
||||
} else if (source instanceof JsonObjectBuilder) { |
||||
source = ((JsonObjectBuilder) source).build(); |
||||
} |
||||
if (source instanceof Collection) { |
||||
// this covers both List<JsonValue> and JsonArray from JSON-P spec
|
||||
Class<?> rawTargetType = getRawClass(targetType); |
||||
Type targetTypeArg = getFirstTypeArgument(targetType); |
||||
Collection<Object> result = newCollectionOfType(rawTargetType); |
||||
for (Object srcValue : (Collection<?>) source) { |
||||
if (srcValue instanceof JsonObject) { |
||||
if (targetTypeArg != null) { |
||||
result.add(mapImpl(srcValue, targetTypeArg)); |
||||
} else { |
||||
result.add(srcValue); |
||||
} |
||||
} else { |
||||
result.add(unwrapJsonValue(srcValue)); |
||||
} |
||||
} |
||||
return result; |
||||
} else if (source instanceof JsonObject) { |
||||
if (targetType instanceof Class) { |
||||
if (jsonToClassMethod != null) { |
||||
try { |
||||
JsonParser jsonParser = new JsonStructureToParserAdapter((JsonStructure) source); |
||||
return jsonToClassMethod.invoke(jsonb, jsonParser, (Class<?>) targetType); |
||||
} catch (Exception e){ |
||||
throw new MappingException(e); |
||||
} |
||||
} else { |
||||
try { |
||||
// Fallback databinding approach for JSON-B API implementations without
|
||||
// explicit support for use of JsonParser in their public API. The approach
|
||||
// is essentially first to serialize given value into JSON, and then bind
|
||||
// it to data object of given class.
|
||||
String json = source.toString(); |
||||
return jsonb.fromJson(json, (Class<?>) targetType); |
||||
} catch (JsonbException e){ |
||||
throw new MappingException(e); |
||||
} |
||||
} |
||||
} else if (targetType instanceof ParameterizedType) { |
||||
if (jsonToTypeMethod != null) { |
||||
try { |
||||
JsonParser jsonParser = new JsonStructureToParserAdapter((JsonStructure) source); |
||||
return jsonToTypeMethod.invoke(jsonb, jsonParser, (Type) targetType); |
||||
} catch (Exception e){ |
||||
throw new MappingException(e); |
||||
} |
||||
} else { |
||||
try { |
||||
// Fallback databinding approach for JSON-B API implementations without
|
||||
// explicit support for use of JsonParser in their public API. The approach
|
||||
// is essentially first to serialize given value into JSON, and then bind
|
||||
// the JSON string to data object of given type.
|
||||
String json = source.toString(); |
||||
return jsonb.fromJson(json, (Type) targetType); |
||||
} catch (JsonbException e){ |
||||
throw new MappingException(e); |
||||
} |
||||
} |
||||
} else { |
||||
throw new MappingException("JSON object cannot be databind to " + targetType); |
||||
} |
||||
} else { |
||||
return source; |
||||
} |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private <T> T mapIntegralJsonNumber(JsonNumber jsonNumber, Class<?> targetType) { |
||||
if (targetType.isPrimitive()) { |
||||
if (int.class.equals(targetType)) { |
||||
return (T) Integer.valueOf(jsonNumber.intValueExact()); |
||||
} else if (long.class.equals(targetType)) { |
||||
return (T) Long.valueOf(jsonNumber.longValueExact()); |
||||
} |
||||
} else if (Integer.class.equals(targetType)) { |
||||
return (T) Integer.valueOf(jsonNumber.intValueExact()); |
||||
} else if (Long.class.equals(targetType)) { |
||||
return (T) Long.valueOf(jsonNumber.longValueExact()); |
||||
} else if (BigInteger.class.equals(targetType)) { |
||||
return (T) jsonNumber.bigIntegerValueExact(); |
||||
} else if (BigDecimal.class.equals(targetType)) { |
||||
return (T) jsonNumber.bigDecimalValue(); |
||||
} |
||||
|
||||
String className = targetType.getSimpleName(); |
||||
throw new MappingException("JSON integral number cannot be mapped to " + className); |
||||
} |
||||
|
||||
@SuppressWarnings("unchecked") |
||||
private <T> T mapDecimalJsonNumber(JsonNumber jsonNumber, Class<?> targetType) { |
||||
if (targetType.isPrimitive()) { |
||||
if (float.class.equals(targetType)) { |
||||
return (T) new Float(jsonNumber.doubleValue()); |
||||
} else if (double.class.equals(targetType)) { |
||||
return (T) Double.valueOf(jsonNumber.doubleValue()); |
||||
} |
||||
} else if (Float.class.equals(targetType)) { |
||||
return (T) new Float(jsonNumber.doubleValue()); |
||||
} else if (Double.class.equals(targetType)) { |
||||
return (T) Double.valueOf(jsonNumber.doubleValue()); |
||||
} else if (BigDecimal.class.equals(targetType)) { |
||||
return (T) jsonNumber.bigDecimalValue(); |
||||
} |
||||
|
||||
String className = targetType.getSimpleName(); |
||||
throw new MappingException("JSON decimal number cannot be mapped to " + className); |
||||
} |
||||
|
||||
private Object unwrapJsonValue(Object jsonValue) { |
||||
if (jsonValue == null) { |
||||
return null; |
||||
} |
||||
if (!(jsonValue instanceof JsonValue)) { |
||||
return jsonValue; |
||||
} |
||||
switch (((JsonValue) jsonValue).getValueType()) { |
||||
case ARRAY: |
||||
// TODO do we unwrap JsonObjectArray proxies?
|
||||
//return ((JsonArray) jsonValue).getValuesAs(JsonValue.class);
|
||||
return ((JsonArray) jsonValue).getValuesAs((JsonValue v) -> unwrapJsonValue(v)); |
||||
case OBJECT: |
||||
throw new IllegalArgumentException("Use map() method to databind a JsonObject"); |
||||
case STRING: |
||||
return ((JsonString) jsonValue).getString(); |
||||
case NUMBER: |
||||
if (((JsonNumber) jsonValue).isIntegral()) { |
||||
//return ((JsonNumber) jsonValue).bigIntegerValueExact();
|
||||
try { |
||||
return ((JsonNumber) jsonValue).intValueExact(); |
||||
} catch (ArithmeticException e) { |
||||
return ((JsonNumber) jsonValue).longValueExact(); |
||||
} |
||||
} else { |
||||
//return ((JsonNumber) jsonValue).bigDecimalValue();
|
||||
return ((JsonNumber) jsonValue).doubleValue(); |
||||
} |
||||
case TRUE: |
||||
return Boolean.TRUE; |
||||
case FALSE: |
||||
return Boolean.FALSE; |
||||
case NULL: |
||||
return null; |
||||
default: |
||||
return jsonValue; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Creates new instance of {@code Collection} type specified by the |
||||
* argument. If the argument refers to an interface, then a matching |
||||
* Java standard implementation is returned; if it is a concrete class, |
||||
* then method attempts to instantiate an object of that class given |
||||
* there is a public no-arg constructor available. |
||||
* |
||||
* @param collectionType collection type; may be an interface or a class
|
||||
* @return instance of collection type identified by the argument |
||||
* @throws MappingException on a type that cannot be safely instantiated |
||||
*/ |
||||
private Collection<Object> newCollectionOfType(Class<?> collectionType) throws MappingException { |
||||
if (Collection.class.isAssignableFrom(collectionType)) { |
||||
if (!collectionType.isInterface()) { |
||||
@SuppressWarnings("unchecked") |
||||
Collection<Object> coll = (Collection<Object>) newNoArgInstance(collectionType); |
||||
return coll; |
||||
} else if (List.class.isAssignableFrom(collectionType)) { |
||||
return new java.util.LinkedList<Object>(); |
||||
} else if (Set.class.isAssignableFrom(collectionType)) { |
||||
return new java.util.LinkedHashSet<Object>(); |
||||
} else if (Queue.class.isAssignableFrom(collectionType)) { |
||||
return new java.util.LinkedList<Object>(); |
||||
} |
||||
} |
||||
String className = collectionType.getSimpleName(); |
||||
throw new MappingException("JSON array cannot be mapped to " + className); |
||||
} |
||||
|
||||
/** |
||||
* Lists all publicly accessible constructors for the {@code Class} |
||||
* identified by the argument, including any constructors inherited |
||||
* from superclasses, and uses a no-args constructor, if available, |
||||
* to create a new instance of the class. If argument is interface, |
||||
* this method returns {@code null}. |
||||
* |
||||
* @param targetType class type to create instance of |
||||
* @return an instance of the class represented by the argument |
||||
* @throws MappingException if no-arg public constructor is not there |
||||
*/ |
||||
private Object newNoArgInstance(Class<?> targetType) throws MappingException { |
||||
if (targetType.isInterface()) { |
||||
return null; |
||||
} else { |
||||
for (Constructor<?> ctr : targetType.getConstructors()) { |
||||
if (ctr.getParameterCount() == 0) { |
||||
try { |
||||
return ctr.newInstance(); |
||||
} catch (ReflectiveOperationException e) { |
||||
throw new MappingException(e); |
||||
} catch (IllegalArgumentException e) { |
||||
// never happens
|
||||
} |
||||
} |
||||
} |
||||
String className = targetType.getSimpleName(); |
||||
throw new MappingException("Unable to find no-arg ctr for " + className); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Locates optional API method on the supplied JSON-B API implementation class with |
||||
* the supplied name and parameter types. Searches the superclasses up to |
||||
* {@code Object}, but ignores interfaces and default interface methods. Returns |
||||
* {@code null} if no {@code Method} can be found. |
||||
* |
||||
* @param clazz the implementation class to reflect upon |
||||
* @param name the name of the method |
||||
* @param paramTypes the parameter types of the method |
||||
* @return the {@code Method} reference, or {@code null} if none found |
||||
*/ |
||||
private Method findMethod(Class<?> clazz, String name, Class<?>... paramTypes) { |
||||
while (clazz != null && !clazz.isInterface()) { |
||||
for (Method method : clazz.getDeclaredMethods()) { |
||||
final int mods = method.getModifiers(); |
||||
if (Modifier.isPublic(mods) && !Modifier.isAbstract(mods) && |
||||
name.equals(method.getName()) && |
||||
Arrays.equals(paramTypes, method.getParameterTypes())) { |
||||
return method; |
||||
} |
||||
} |
||||
clazz = clazz.getSuperclass(); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
private Class<?> getRawClass(Type targetType) { |
||||
if (targetType instanceof Class) { |
||||
return (Class<?>) targetType; |
||||
} else if (targetType instanceof ParameterizedType) { |
||||
return (Class<?>) ((ParameterizedType) targetType).getRawType(); |
||||
} else if (targetType instanceof GenericArrayType) { |
||||
String typeName = targetType.getTypeName(); |
||||
throw new MappingException("Cannot map JSON element to " + typeName); |
||||
} else { |
||||
String typeName = targetType.getTypeName(); |
||||
throw new IllegalArgumentException("TypeRef not supported: " + typeName); |
||||
} |
||||
} |
||||
|
||||
private Type getFirstTypeArgument(Type targetType) { |
||||
if (targetType instanceof ParameterizedType) { |
||||
Type[] args = ((ParameterizedType) targetType).getActualTypeArguments(); |
||||
if (args != null && args.length > 0) { |
||||
if (args[0] instanceof Class) { |
||||
return (Class<?>) args[0]; |
||||
} else if (args[0] instanceof ParameterizedType) { |
||||
return (ParameterizedType) args[0]; |
||||
} |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
/** |
||||
* Runtime adapter for {@link JsonParser} to pull the JSON objects and values from |
||||
* a {@link JsonStructure} content tree instead of plain JSON string. |
||||
* <p> |
||||
* JSON-B API 1.0 final specification does not include any public methods to read JSON |
||||
* content from pre-parsed {@link JsonStructure} tree, so this parser is used by the |
||||
* Jakarta EE mapping provider above to feed in JSON content to JSON-B implementation. |
||||
*/ |
||||
private static class JsonStructureToParserAdapter implements JsonParser { |
||||
|
||||
private JsonStructureScope scope; |
||||
private Event state; |
||||
private final Deque<JsonStructureScope> ancestry = new ArrayDeque<>(); |
||||
|
||||
JsonStructureToParserAdapter(JsonStructure jsonStruct) { |
||||
scope = createScope(jsonStruct); |
||||
} |
||||
|
||||
@Override |
||||
public boolean hasNext() { |
||||
return !((state == Event.END_ARRAY || state == Event.END_OBJECT) && ancestry.isEmpty()); |
||||
} |
||||
|
||||
@Override |
||||
public Event next() { |
||||
if (!hasNext()) { |
||||
throw new NoSuchElementException(); |
||||
} |
||||
if (state == null) { |
||||
state = scope instanceof JsonArrayScope ? Event.START_ARRAY : Event.START_OBJECT; |
||||
} else { |
||||
if (state == Event.END_ARRAY || state == Event.END_OBJECT) { |
||||
scope = ancestry.pop(); |
||||
} |
||||
if (scope instanceof JsonArrayScope) { // array scope
|
||||
if (scope.hasNext()) { |
||||
scope.next(); |
||||
state = getState(scope.getValue()); |
||||
if (state == Event.START_ARRAY || state == Event.START_OBJECT) { |
||||
ancestry.push(scope); |
||||
scope = createScope(scope.getValue()); |
||||
} |
||||
} else { |
||||
state = Event.END_ARRAY; |
||||
} |
||||
} else { // object scope
|
||||
if (state == Event.KEY_NAME) { |
||||
state = getState(scope.getValue()); |
||||
if (state == Event.START_ARRAY || state == Event.START_OBJECT) { |
||||
ancestry.push(scope); |
||||
scope = createScope(scope.getValue()); |
||||
} |
||||
} else { |
||||
if (scope.hasNext()) { |
||||
scope.next(); |
||||
state = Event.KEY_NAME; |
||||
} else { |
||||
state = Event.END_OBJECT; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
return state; |
||||
} |
||||
|
||||
@Override |
||||
public String getString() { |
||||
switch (state) { |
||||
case KEY_NAME: |
||||
return ((JsonObjectScope) scope).getKey(); |
||||
case VALUE_STRING: |
||||
return ((JsonString) scope.getValue()).getString(); |
||||
case VALUE_NUMBER: |
||||
return ((JsonNumber) scope.getValue()).toString(); |
||||
default: |
||||
throw new IllegalStateException("Parser is not in KEY_NAME, VALUE_STRING, or VALUE_NUMBER state"); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public boolean isIntegralNumber() { |
||||
if (state == Event.VALUE_NUMBER) { |
||||
return ((JsonNumber) scope.getValue()).isIntegral(); |
||||
} |
||||
throw new IllegalStateException("Target json value must a number, not " + state); |
||||
} |
||||
|
||||
@Override |
||||
public int getInt() { |
||||
if (state == Event.VALUE_NUMBER) { |
||||
return ((JsonNumber) scope.getValue()).intValue(); |
||||
} |
||||
throw new IllegalStateException("Target json value must a number, not " + state); |
||||
} |
||||
|
||||
@Override |
||||
public long getLong() { |
||||
if (state == Event.VALUE_NUMBER) { |
||||
return ((JsonNumber) scope.getValue()).longValue(); |
||||
} |
||||
throw new IllegalStateException("Target json value must a number, not " + state); |
||||
} |
||||
|
||||
@Override |
||||
public BigDecimal getBigDecimal() { |
||||
if (state == Event.VALUE_NUMBER) { |
||||
return ((JsonNumber) scope.getValue()).bigDecimalValue(); |
||||
} |
||||
throw new IllegalStateException("Target json value must a number, not " + state); |
||||
} |
||||
|
||||
@Override |
||||
public JsonLocation getLocation() { |
||||
throw new UnsupportedOperationException("JSON-P adapter does not support getLocation()"); |
||||
} |
||||
|
||||
@Override |
||||
public void skipArray() { |
||||
if (scope instanceof JsonArrayScope) { |
||||
while (scope.hasNext()) { |
||||
scope.next(); |
||||
} |
||||
state = Event.END_ARRAY; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void skipObject() { |
||||
if (scope instanceof JsonObjectScope) { |
||||
while (scope.hasNext()) { |
||||
scope.next(); |
||||
} |
||||
state = Event.END_OBJECT; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void close() { |
||||
// JSON objects are read-only
|
||||
} |
||||
|
||||
private JsonStructureScope createScope(JsonValue value) { |
||||
if (value instanceof JsonArray) { |
||||
return new JsonArrayScope((JsonArray) value); |
||||
} else if (value instanceof JsonObject) { |
||||
return new JsonObjectScope((JsonObject) value); |
||||
} |
||||
throw new JsonException("Cannot create JSON iterator for " + value); |
||||
} |
||||
|
||||
private Event getState(JsonValue value) { |
||||
switch (value.getValueType()) { |
||||
case ARRAY: |
||||
return Event.START_ARRAY; |
||||
case OBJECT: |
||||
return Event.START_OBJECT; |
||||
case STRING: |
||||
return Event.VALUE_STRING; |
||||
case NUMBER: |
||||
return Event.VALUE_NUMBER; |
||||
case TRUE: |
||||
return Event.VALUE_TRUE; |
||||
case FALSE: |
||||
return Event.VALUE_FALSE; |
||||
case NULL: |
||||
return Event.VALUE_NULL; |
||||
default: |
||||
throw new JsonException("Unknown value type " + value.getValueType()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private static abstract class JsonStructureScope implements Iterator<JsonValue> { |
||||
/** |
||||
* Returns current {@link JsonValue}, that the parser is pointing on. Before |
||||
* the {@link #next()} method has been called, this returns {@code null}. |
||||
* |
||||
* @return JsonValue value object. |
||||
*/ |
||||
abstract JsonValue getValue(); |
||||
} |
||||
|
||||
private static class JsonArrayScope extends JsonStructureScope { |
||||
private final Iterator<JsonValue> it; |
||||
private JsonValue value; |
||||
|
||||
JsonArrayScope(JsonArray array) { |
||||
this.it = array.iterator(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean hasNext() { |
||||
return it.hasNext(); |
||||
} |
||||
|
||||
@Override |
||||
public JsonValue next() { |
||||
value = it.next(); |
||||
return value; |
||||
} |
||||
|
||||
@Override |
||||
JsonValue getValue() { |
||||
return value; |
||||
} |
||||
} |
||||
|
||||
private static class JsonObjectScope extends JsonStructureScope { |
||||
private final Iterator<Map.Entry<String, JsonValue>> it; |
||||
private JsonValue value; |
||||
private String key; |
||||
|
||||
JsonObjectScope(JsonObject object) { |
||||
this.it = object.entrySet().iterator(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean hasNext() { |
||||
return it.hasNext(); |
||||
} |
||||
|
||||
@Override |
||||
public JsonValue next() { |
||||
Map.Entry<String, JsonValue> next = it.next(); |
||||
this.key = next.getKey(); |
||||
this.value = next.getValue(); |
||||
return value; |
||||
} |
||||
|
||||
@Override |
||||
JsonValue getValue() { |
||||
return value; |
||||
} |
||||
|
||||
String getKey() { |
||||
return key; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,381 @@
|
||||
package com.jayway.jsonpath; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import jakarta.json.JsonObject; |
||||
import jakarta.json.JsonString; |
||||
|
||||
import java.io.InputStream; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
import static com.jayway.jsonpath.JsonPath.parse; |
||||
import static com.jayway.jsonpath.JsonPath.using; |
||||
import static java.util.Collections.emptyMap; |
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
public class JakartaJsonProviderTest extends BaseTest { |
||||
|
||||
private static final Map<String, Object> EMPTY_MAP = emptyMap(); |
||||
|
||||
@Test |
||||
public void an_object_can_be_read() { |
||||
JsonObject book = using(JAKARTA_JSON_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.read("$.store.book[0]"); |
||||
|
||||
assertThat(((JsonString) book.get("author")).getChars()).isEqualTo("Nigel Rees"); |
||||
} |
||||
|
||||
@Test |
||||
public void a_property_can_be_read() { |
||||
JsonString category = using(JAKARTA_JSON_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.read("$.store.book[0].category"); |
||||
|
||||
assertThat(category.getString()).isEqualTo("reference"); |
||||
} |
||||
|
||||
@Test |
||||
public void a_filter_can_be_applied() { |
||||
List<Object> fictionBooks = using(JAKARTA_JSON_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.read("$.store.book[?(@.category == 'fiction')]"); |
||||
|
||||
assertThat(fictionBooks.size()).isEqualTo(3); |
||||
} |
||||
|
||||
@Test |
||||
public void result_can_be_mapped_to_object() { |
||||
@SuppressWarnings("unchecked") |
||||
List<Map<String, Object>> books = using(JAKARTA_JSON_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.read("$.store.book", List.class); |
||||
|
||||
assertThat(books.size()).isEqualTo(4); |
||||
} |
||||
|
||||
@Test |
||||
public void read_books_with_isbn() { |
||||
List<Object> books = using(JAKARTA_JSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$..book[?(@.isbn)]"); |
||||
|
||||
assertThat(books.size()).isEqualTo(2); |
||||
} |
||||
|
||||
/** |
||||
* Functions take parameters, the length parameter for example takes an entire document which we anticipate |
||||
* will compute to a document that is an array of elements which can determine its length. |
||||
* |
||||
* Since we translate this query from $..books.length() to length($..books) verify that this particular translation |
||||
* works as anticipated. |
||||
*/ |
||||
@Test |
||||
public void read_book_length_using_translated_query() { |
||||
Integer result = using(JAKARTA_JSON_CONFIGURATION) |
||||
.parse(JSON_BOOK_STORE_DOCUMENT) |
||||
.read("$..book.length()"); |
||||
assertThat(result).isEqualTo(4); |
||||
} |
||||
|
||||
@Test |
||||
public void read_book_length() { |
||||
Object result = using(JAKARTA_JSON_CONFIGURATION) |
||||
.parse(JSON_BOOK_STORE_DOCUMENT) |
||||
.read("$.length($..book)"); |
||||
assertThat(result).isEqualTo(4); |
||||
} |
||||
|
||||
@Test |
||||
public void issue_97() { |
||||
String json = "{ \"books\": [ " + |
||||
"{ \"category\": \"fiction\" }, " + |
||||
"{ \"category\": \"reference\" }, " + |
||||
"{ \"category\": \"fiction\" }, " + |
||||
"{ \"category\": \"fiction\" }, " + |
||||
"{ \"category\": \"reference\" }, " + |
||||
"{ \"category\": \"fiction\" }, " + |
||||
"{ \"category\": \"reference\" }, " + |
||||
"{ \"category\": \"reference\" }, " + |
||||
"{ \"category\": \"reference\" }, " + |
||||
"{ \"category\": \"reference\" }, " + |
||||
"{ \"category\": \"reference\" } ] }"; |
||||
|
||||
DocumentContext dc = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(json) |
||||
.delete("$.books[?(@.category == 'reference')]"); |
||||
//System.out.println((Object) dc.read("$"));
|
||||
@SuppressWarnings("unchecked") |
||||
List<String> categories = dc.read("$..category", List.class); |
||||
|
||||
assertThat(categories).containsOnly("fiction"); |
||||
} |
||||
|
||||
@Test |
||||
public void test_delete_2() { |
||||
DocumentContext dc = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse("[" + |
||||
"{\"top\": {\"middle\": null}}," + |
||||
"{\"top\": {\"middle\": {} }}," + |
||||
"{\"top\": {\"middle\": {\"bottom\": 2} }}" + |
||||
"]") |
||||
.delete(JsonPath.compile("$[*].top.middle.bottom")); |
||||
Object ans = dc.read("$"); |
||||
//System.out.println(ans);
|
||||
assert(ans.toString().equals("[{\"top\":{\"middle\":null}},{\"top\":{\"middle\":{}}},{\"top\":{\"middle\":{}}}]")); |
||||
} |
||||
|
||||
@Test |
||||
public void an_root_property_can_be_updated() { |
||||
Object o = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.set("$.int-max-property", 1) |
||||
.json(); |
||||
|
||||
Integer result = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o) |
||||
.read("$.int-max-property", Integer.class); |
||||
|
||||
assertThat(result).isEqualTo(1); |
||||
} |
||||
|
||||
@Test |
||||
public void an_deep_scan_can_update() { |
||||
Object o = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.set("$..display-price", 1) |
||||
.json(); |
||||
|
||||
List<Integer> result = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o) |
||||
.read("$..display-price", new TypeRef<List<Integer>>() {}); |
||||
|
||||
assertThat(result).containsExactly(1, 1, 1, 1, 1); |
||||
} |
||||
|
||||
@Test |
||||
public void an_filter_can_update() { |
||||
final String updatePathFunction = "$.store.book[?(@.display-price)].display-price"; |
||||
Object o = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.set(updatePathFunction, 1) |
||||
.json(); |
||||
|
||||
List<Integer> result = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o) |
||||
.read(updatePathFunction, new TypeRef<List<Integer>>() {}); |
||||
|
||||
assertThat(result).containsExactly(1, 1, 1, 1); |
||||
} |
||||
|
||||
@Test |
||||
public void a_path_can_be_deleted() { |
||||
final String deletePath = "$.store.book[*].display-price"; |
||||
Object o = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.delete(deletePath) |
||||
.json(); |
||||
|
||||
List<Integer> result = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o) |
||||
.read(deletePath, new TypeRef<List<Integer>>() {}); |
||||
|
||||
assertThat(result).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
public void operations_can_chained() { |
||||
Object o = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.delete("$.store.book[*].display-price") |
||||
.set("$.store.book[*].category", "A") |
||||
.json(); |
||||
|
||||
List<Integer> prices = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o) |
||||
.read("$.store.book[*].display-price", new TypeRef<List<Integer>>() {}); |
||||
List<String> categories = using(JAKARTA_JSON_RW_CONFIGURATION).parse(o) |
||||
.read("$.store.book[*].category", new TypeRef<List<String>>() {}); |
||||
|
||||
assertThat(prices).isEmpty(); |
||||
assertThat(categories).containsExactly("A", "A", "A", "A"); |
||||
} |
||||
|
||||
@Test |
||||
public void an_array_index_can_be_updated() { |
||||
String res = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.set("$.store.book[0]", "a") |
||||
.read("$.store.book[0]", String.class); |
||||
|
||||
assertThat(res).isEqualTo("a"); |
||||
} |
||||
|
||||
@Test |
||||
public void an_array_slice_can_be_updated() { |
||||
List<String> res = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.set("$.store.book[0:2]", "a") |
||||
.read("$.store.book[0:2]", new TypeRef<List<String>>() {}); |
||||
|
||||
assertThat(res).containsExactly("a", "a"); |
||||
} |
||||
|
||||
@Test |
||||
public void an_array_criteria_can_be_updated() { |
||||
List<String> res = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.set("$.store.book[?(@.category == 'fiction')]", "a") |
||||
.read("$.store.book[?(@ == 'a')]", new TypeRef<List<String>>() {}); |
||||
|
||||
assertThat(res).containsExactly("a", "a", "a"); |
||||
} |
||||
|
||||
@Test |
||||
public void an_array_criteria_can_be_deleted() { |
||||
List<String> res = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.delete("$.store.book[?(@.category == 'fiction')]") |
||||
.read("$.store.book[*].category", new TypeRef<List<String>>() {}); |
||||
|
||||
assertThat(res).containsExactly("reference"); |
||||
} |
||||
|
||||
@Test |
||||
public void an_array_criteria_with_multiple_results_can_be_deleted(){ |
||||
InputStream stream = this.getClass().getResourceAsStream("/json_array_multiple_delete.json"); |
||||
String deletePath = "$._embedded.mandates[?(@.count=~/0/)]"; |
||||
DocumentContext dc = using(JAKARTA_JSON_RW_CONFIGURATION).parse(stream).delete(deletePath); |
||||
List<Object> result = dc.read(deletePath); |
||||
assertThat(result.size()).isEqualTo(0); |
||||
} |
||||
|
||||
@Test |
||||
public void multi_prop_delete() { |
||||
List<Map<String, Object>> res = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.delete("$.store.book[*]['author', 'category']") |
||||
.read("$.store.book[*]['author', 'category']", new TypeRef<List<Map<String, Object>>>() {}); |
||||
|
||||
assertThat(res).containsExactly(EMPTY_MAP, EMPTY_MAP, EMPTY_MAP, EMPTY_MAP); |
||||
} |
||||
|
||||
@Test |
||||
public void multi_prop_update() { |
||||
@SuppressWarnings("serial") |
||||
Map<String, Object> expected = new HashMap<String, Object>(){{ |
||||
put("author", "a"); |
||||
put("category", "a"); |
||||
}}; |
||||
List<Map<String, Object>> res = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.set("$.store.book[*]['author', 'category']", "a") |
||||
.read("$.store.book[*]['author', 'category']", new TypeRef<List<Map<String, Object>>>() {}); |
||||
assertThat(res).containsExactly(expected, expected, expected, expected); |
||||
} |
||||
|
||||
@Test |
||||
public void add_to_array() { |
||||
Object res = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.add("$.store.book", 1) |
||||
.read("$.store.book[4]"); |
||||
res = JAKARTA_JSON_RW_CONFIGURATION.jsonProvider().unwrap(res); |
||||
assertThat(res).isEqualTo(1); |
||||
} |
||||
|
||||
@Test |
||||
public void add_to_object() { |
||||
Object res = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.put("$.store.book[0]", "new-key", "new-value") |
||||
.read("$.store.book[0].new-key"); |
||||
res = JAKARTA_JSON_RW_CONFIGURATION.jsonProvider().unwrap(res); |
||||
assertThat(res).isEqualTo("new-value"); |
||||
} |
||||
|
||||
@Test(expected = InvalidModificationException.class) |
||||
public void add_to_object_on_array() { |
||||
using(JAKARTA_JSON_RW_CONFIGURATION).parse(JSON_DOCUMENT).put("$.store.book", "new-key", "new-value"); |
||||
} |
||||
|
||||
@Test(expected = InvalidModificationException.class) |
||||
public void add_to_array_on_object() { |
||||
using(JAKARTA_JSON_RW_CONFIGURATION).parse(JSON_DOCUMENT).add("$.store.book[0]", "new-value"); |
||||
} |
||||
|
||||
@Test |
||||
public void a_path_can_be_renamed(){ |
||||
Object o = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.renameKey("$.store", "book", "updated-book") |
||||
.json(); |
||||
List<Object> result = parse(o).read("$.store.updated-book"); |
||||
|
||||
assertThat(result).isNotEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
public void map_array_items_can_be_renamed(){ |
||||
Object o = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.renameKey("$.store.book[*]", "category", "renamed-category") |
||||
.json(); |
||||
List<Object> result = parse(o).read("$.store.book[*].renamed-category"); |
||||
assertThat(result).isNotEmpty(); |
||||
} |
||||
|
||||
@Test(expected = PathNotFoundException.class) |
||||
public void non_existent_key_rename_not_allowed() { |
||||
using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.renameKey("$", "fake", "new-fake") |
||||
.json(); |
||||
} |
||||
|
||||
@Test |
||||
public void single_match_value_can_be_mapped() { |
||||
MapFunction mapFunction = new ToStringMapFunction(); |
||||
String stringResult = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.map("$.string-property", mapFunction) |
||||
.read("$.string-property", String.class); |
||||
assertThat(stringResult.endsWith("converted")).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void object_can_be_mapped() { |
||||
TypeRef<List<String>> typeRef = new TypeRef<List<String>>() {}; |
||||
MapFunction mapFunction = new ToStringMapFunction(); |
||||
DocumentContext dc = using(JAKARTA_JSON_RW_CONFIGURATION).parse(JSON_DOCUMENT); |
||||
Object list = dc.read("$..book"); |
||||
assertThat(list).isInstanceOf(List.class); |
||||
Object res = dc.map("$..book", mapFunction).read("$..book", typeRef).get(0); |
||||
assertThat(res).isInstanceOf(String.class); |
||||
assertThat((String) res).endsWith("converted"); |
||||
} |
||||
|
||||
@Test |
||||
public void multi_match_path_can_be_mapped() { |
||||
MapFunction mapFunction = new ToStringMapFunction(); |
||||
List<Double> doubleResult = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.read("$..display-price", new TypeRef<List<Double>>() {}); |
||||
for (Double dRes : doubleResult){ |
||||
assertThat(dRes).isInstanceOf(Double.class); |
||||
} |
||||
List<String> stringResult = using(JAKARTA_JSON_RW_CONFIGURATION) |
||||
.parse(JSON_DOCUMENT) |
||||
.map("$..display-price", mapFunction) |
||||
.read("$..display-price", new TypeRef<List<String>>() {}); |
||||
for (String sRes : stringResult){ |
||||
assertThat(sRes).isInstanceOf(String.class); |
||||
assertThat(sRes.endsWith("converted")).isTrue(); |
||||
} |
||||
} |
||||
|
||||
// Helper converter implementation for test cases.
|
||||
private class ToStringMapFunction implements MapFunction { |
||||
|
||||
@Override |
||||
public Object map(Object currentValue, Configuration configuration) { |
||||
return currentValue.toString()+"converted"; |
||||
} |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue