JakartaMappingProvider.java
/*
* 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;
}
}
}