blob: 8640863b6d04363619a1184c550ccaa89575d8e2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.johnzon.mapper;
import org.apache.johnzon.core.JsonLongImpl;
import org.apache.johnzon.core.JsonReaderImpl;
import org.apache.johnzon.mapper.access.AccessMode;
import org.apache.johnzon.mapper.converter.EnumConverter;
import org.apache.johnzon.mapper.internal.AdapterKey;
import org.apache.johnzon.mapper.internal.ConverterAdapter;
import org.apache.johnzon.mapper.reflection.JohnzonParameterizedType;
import javax.json.JsonArray;
import javax.json.JsonNumber;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonStructure;
import javax.json.JsonValue;
import javax.xml.bind.DatatypeConverter;
import java.lang.reflect.Array;
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.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static java.util.Arrays.asList;
/**
* This class is not concurrently usable as it contains state.
*/
public class MappingParserImpl implements MappingParser {
private static final Adapter<Object, String> FALLBACK_CONVERTER = new ConverterAdapter<Object>(new FallbackConverter());
private static final JohnzonParameterizedType ANY_LIST = new JohnzonParameterizedType(List.class, Object.class);
protected final ConcurrentMap<Adapter<?, ?>, AdapterKey> reverseAdaptersRegistry;
private final MapperConfig config;
private final Mappings mappings;
private final JsonReader jsonReader;
public MappingParserImpl(MapperConfig config, Mappings mappings, JsonReader jsonReader) {
this.config = config;
this.mappings = mappings;
this.jsonReader = jsonReader;
reverseAdaptersRegistry = new ConcurrentHashMap<Adapter<?, ?>, AdapterKey>(config.getAdapters().size());
}
@Override
public <T> T readObject(Type targetType) {
try {
if (jsonReader instanceof JsonReaderImpl) {
// later in JSON-P 1.1 we can remove this hack again
return readObject(((JsonReaderImpl) jsonReader).readValue(), targetType);
}
return readObject(jsonReader.read(), targetType);
} finally {
if (config.isClose()) {
jsonReader.close();
}
}
}
@Override
public <T> T readObject(JsonValue jsonValue, Type targetType) {
return readObject(jsonValue, targetType, targetType instanceof Class || targetType instanceof ParameterizedType);
}
private <T> T readObject(JsonValue jsonValue, Type targetType, boolean applyObjectConverter) {
if (JsonStructure.class == targetType || JsonObject.class == targetType || JsonValue.class == targetType) {
return (T) jsonValue;
}
if (JsonObject.class.isInstance(jsonValue)) {
return (T) buildObject(targetType, JsonObject.class.cast(jsonValue), applyObjectConverter);
}
if (JsonString.class.isInstance(jsonValue) && (targetType == String.class || targetType == Object.class)) {
return (T) JsonString.class.cast(jsonValue).getString();
}
if (JsonNumber.class.isInstance(jsonValue)) {
final JsonNumber number = JsonNumber.class.cast(jsonValue);
if (targetType == int.class || targetType == Integer.class) {
return (T) Integer.valueOf(number.intValue());
}
if (targetType == long.class || targetType == Long.class) {
return (T) Long.valueOf(number.longValue());
}
if (targetType == double.class || targetType == Double.class || targetType == Object.class) {
return (T) Double.valueOf(number.doubleValue());
}
if (targetType == BigDecimal.class) {
return (T) number.bigDecimalValue();
}
if (targetType == BigInteger.class) {
return (T) number.bigIntegerValue();
}
}
if (JsonArray.class.isInstance(jsonValue)) {
JsonArray jsonArray = (JsonArray) jsonValue;
if (Class.class.isInstance(targetType) && ((Class) targetType).isArray()) {
return (T) buildArrayWithComponentType(jsonArray, ((Class) targetType).getComponentType(), null);
}
if (ParameterizedType.class.isInstance(targetType)) {
final Mappings.CollectionMapping mapping = mappings.findCollectionMapping((ParameterizedType) targetType);
if (mapping == null) {
throw new UnsupportedOperationException("type " + targetType + " not supported");
}
return (T) mapCollection(mapping, jsonArray, null);
}
if (Object.class == targetType) {
return (T) new ArrayList(asList(Object[].class.cast(buildArrayWithComponentType(jsonArray, Object.class, null))));
}
}
if (JsonValue.NULL == jsonValue) {
return null;
}
if (JsonValue.TRUE == jsonValue && (Boolean.class == targetType || boolean.class == targetType || Object.class == targetType)) {
return (T) Boolean.TRUE;
}
if (JsonValue.FALSE == jsonValue && (Boolean.class == targetType || boolean.class == targetType || Object.class == targetType)) {
return (T) Boolean.FALSE;
}
throw new IllegalArgumentException("Unsupported " + jsonValue + " for type " + targetType);
}
private Object buildObject(final Type inType, final JsonObject object, final boolean applyObjectConverter) {
Type type = inType;
if (inType == Object.class) {
type = new JohnzonParameterizedType(Map.class, String.class, Object.class);
}
if (applyObjectConverter && !(type instanceof JohnzonParameterizedType)) {
if (!(type instanceof Class)) {
throw new MapperException("ObjectConverters are only supported for Classes not Types");
}
ObjectConverter.Reader objectConverter = config.findObjectConverterReader((Class) type);
if (objectConverter != null) {
return objectConverter.fromJson(object, type, new SuppressConversionMappingParser(this, object));
}
}
final Mappings.ClassMapping classMapping = mappings.findOrCreateClassMapping(type);
if (classMapping == null) {
if (ParameterizedType.class.isInstance(type)) {
final ParameterizedType aType = ParameterizedType.class.cast(type);
final Type[] fieldArgTypes = aType.getActualTypeArguments();
if (fieldArgTypes.length >= 2) {
final Class<?> raw = Class.class.cast(aType.getRawType());
final Map map;
if (LinkedHashMap.class == raw) {
map = new LinkedHashMap();
} else if (SortedMap.class.isAssignableFrom(raw) || NavigableMap.class == raw || TreeMap.class == raw) {
map = new TreeMap();
} else if (ConcurrentMap.class.isAssignableFrom(raw)) {
map = new ConcurrentHashMap(object.size());
} else if (EnumMap.class.isAssignableFrom(raw)) {
map = new EnumMap(Class.class.cast(fieldArgTypes[0]));
} else if (Map.class.isAssignableFrom(raw)) {
map = new HashMap(object.size());
} else {
map = null;
}
if (map != null) {
Type keyType;
if (ParameterizedType.class.isInstance(fieldArgTypes[0])) {
keyType = fieldArgTypes[0];
} else {
keyType = fieldArgTypes[0];
}
final boolean any = fieldArgTypes.length < 2 || fieldArgTypes[1] == Object.class;
for (final Map.Entry<String, JsonValue> value : object.entrySet()) {
final JsonValue jsonValue = value.getValue();
if (JsonNumber.class.isInstance(jsonValue) && any) {
final JsonNumber number = JsonNumber.class.cast(jsonValue);
if (JsonLongImpl.class.isInstance(number)) {
final int integer = number.intValue();
final long asLong = number.longValue();
if (integer == asLong) {
map.put(value.getKey(), integer);
} else {
map.put(value.getKey(), asLong);
}
} else {
map.put(value.getKey(), !number.isIntegral() ? number.bigDecimalValue() : number.intValue());
}
} else if (JsonString.class.isInstance(jsonValue) && any) {
map.put(value.getKey(), JsonString.class.cast(jsonValue).getString());
} else {
map.put(convertTo(keyType, value.getKey()), toObject(null, jsonValue, fieldArgTypes[1], null));
}
}
return map;
}
}
} else if (Map.class == type || HashMap.class == type || LinkedHashMap.class == type) {
final LinkedHashMap<String, Object> map = new LinkedHashMap<String, Object>();
for (final Map.Entry<String, JsonValue> value : object.entrySet()) {
map.put(value.getKey(), toObject(null, value.getValue(), Object.class, null));
}
return map;
}
}
if (classMapping == null) {
throw new MapperException("Can't map " + type);
}
if (classMapping.factory == null) {
throw new MapperException(classMapping.clazz + " not instantiable");
}
final Object t = classMapping.factory.getParameterTypes().length == 0 ?
classMapping.factory.create(null) : classMapping.factory.create(createParameters(classMapping, object));
for (final Map.Entry<String, Mappings.Setter> setter : classMapping.setters.entrySet()) {
final JsonValue jsonValue = object.get(setter.getKey());
final Mappings.Setter value = setter.getValue();
if (JsonValue.class == value.paramType) {
setter.getValue().writer.write(t, jsonValue);
continue;
}
if (jsonValue == null) {
continue;
}
final AccessMode.Writer setterMethod = value.writer;
if (jsonValue == JsonValue.NULL) { // forced
setterMethod.write(t, null);
} else {
Object existingInstance = null;
if (config.isReadAttributeBeforeWrite()) {
final Mappings.Getter getter = classMapping.getters.get(setter.getKey());
if (getter != null) {
try {
existingInstance = getter.reader.read(t);
} catch (final RuntimeException re) {
// backward compatibility
}
}
}
final Object convertedValue = toValue(existingInstance, jsonValue, value.converter, value.itemConverter, value.paramType, value.objectConverter);
if (convertedValue != null) {
setterMethod.write(t, convertedValue);
}
}
}
return t;
}
private Object convertTo(final Adapter converter, final JsonValue jsonValue) {
if (jsonValue.getValueType() == JsonValue.ValueType.OBJECT) {
//X TODO maybe we can put this into MapperConfig?
//X config.getAdapter(AdapterKey)
//X config.getAdapterKey(Adapter)
AdapterKey adapterKey = reverseAdaptersRegistry.get(converter);
if (adapterKey == null) {
for (final Map.Entry<AdapterKey, Adapter<?, ?>> entry : config.getAdapters().entrySet()) {
if (entry.getValue() == converter) {
adapterKey = entry.getKey();
reverseAdaptersRegistry.put(converter, adapterKey);
break;
}
}
}
final Object param;
try {
Type to = adapterKey.getTo();
param = buildObject(to, JsonObject.class.cast(jsonValue), to instanceof Class);
} catch (final Exception e) {
throw new MapperException(e);
}
return converter.to(param);
}
return converter.to(jsonValue.toString());
}
private Object toObject(final Object baseInstance, final JsonValue jsonValue,
final Type type, final Adapter itemConverter) {
if (jsonValue == null || JsonValue.NULL == jsonValue) {
return null;
}
if (type == Boolean.class || type == boolean.class) {
if (jsonValue == JsonValue.TRUE) {
return true;
}
if (jsonValue == JsonValue.FALSE) {
return false;
}
throw new MapperException("Unable to parse " + jsonValue + " to boolean");
}
if(config.isTreatByteArrayAsBase64() && jsonValue.getValueType() == JsonValue.ValueType.STRING && (type == byte[].class /*|| type == Byte[].class*/)) {
return DatatypeConverter.parseBase64Binary(((JsonString)jsonValue).getString());
}
if (Object.class == type) { // handling specific types here to keep exception in standard handling
if (jsonValue == JsonValue.TRUE) {
return true;
}
if (jsonValue == JsonValue.FALSE) {
return false;
}
if (JsonNumber.class.isInstance(jsonValue)) {
final JsonNumber jsonNumber = JsonNumber.class.cast(jsonValue);
if(jsonNumber.isIntegral()) {
return jsonNumber.intValue();
}
return jsonNumber.doubleValue();
}
}
if (type == Character.class || type == char.class) {
return convertTo(Class.class.cast(type), (JsonString.class.cast(jsonValue).getString()));
}
if (JsonObject.class.isInstance(jsonValue)) {
if (JsonObject.class == type || JsonStructure.class == type) {
return jsonValue;
}
final boolean typedAdapter = TypeAwareAdapter.class.isInstance(itemConverter);
final Object object = buildObject(
baseInstance != null ? baseInstance.getClass() : (
typedAdapter ? TypeAwareAdapter.class.cast(itemConverter).getTo() : type),
JsonObject.class.cast(jsonValue), type instanceof Class);
return typedAdapter ? itemConverter.to(object) : object;
} else if (JsonArray.class.isInstance(jsonValue)) {
if (JsonArray.class == type || JsonStructure.class == type) {
return jsonValue;
}
return buildArray(type, JsonArray.class.cast(jsonValue), itemConverter);
} else if (JsonNumber.class.isInstance(jsonValue)) {
if (JsonNumber.class == type) {
return jsonValue;
}
final JsonNumber number = JsonNumber.class.cast(jsonValue);
if (type == Integer.class || type == int.class) {
return number.intValue();
}
if (type == Long.class || type == long.class) {
return number.longValue();
}
if (type == Short.class || type == short.class) {
return (short) number.intValue();
}
if (type == Byte.class || type == byte.class) {
return (byte) number.intValue();
}
if (type == Float.class || type == float.class) {
return (float) number.doubleValue();
}
if (type == Double.class || type == double.class) {
return number.doubleValue();
}
if (type == BigInteger.class) {
return number.bigIntegerValue();
}
if (type == BigDecimal.class) {
return number.bigDecimalValue();
}
} else if (JsonString.class.isInstance(jsonValue)) {
if (JsonString.class == type) {
return jsonValue;
}
final String string = JsonString.class.cast(jsonValue).getString();
if (itemConverter == null) {
return convertTo(Class.class.cast(type), string);
} else {
return itemConverter.to(string);
}
}
throw new MapperException("Unable to parse " + jsonValue + " to " + type);
}
private Object buildArray(final Type type, final JsonArray jsonArray, final Adapter itemConverter) {
if (Class.class.isInstance(type)) {
final Class clazz = Class.class.cast(type);
if (clazz.isArray()) {
final Class<?> componentType = clazz.getComponentType();
return buildArrayWithComponentType(jsonArray, componentType, itemConverter);
}
}
if (ParameterizedType.class.isInstance(type)) {
final Mappings.CollectionMapping mapping = mappings.findCollectionMapping(ParameterizedType.class.cast(type));
if (mapping != null) {
return mapCollection(mapping, jsonArray, itemConverter);
}
}
if (Object.class == type) {
return buildArray(ANY_LIST, jsonArray, null);
}
throw new UnsupportedOperationException("type " + type + " not supported");
}
private Object buildArrayWithComponentType(final JsonArray jsonArray, final Class<?> componentType, final Adapter itemConverter) {
final Object array = Array.newInstance(componentType, jsonArray.size());
int i = 0;
for (final JsonValue value : jsonArray) {
Array.set(array, i++, toObject(null, value, componentType, itemConverter));
}
return array;
}
private <T> Collection<T> mapCollection(final Mappings.CollectionMapping mapping, final JsonArray jsonArray,
final Adapter itemConverter) {
final Collection collection;
if (SortedSet.class == mapping.raw || NavigableSet.class == mapping.raw || TreeSet.class == mapping.raw) {
collection = new TreeSet<T>();
} else if (Set.class == mapping.raw || HashSet.class == mapping.raw) {
collection = new HashSet<T>(jsonArray.size());
} else if (Queue.class == mapping.raw || ArrayBlockingQueue.class == mapping.raw) {
collection = new ArrayBlockingQueue<T>(jsonArray.size());
} else if (List.class == mapping.raw || Collection.class == mapping.raw || ArrayList.class == mapping.raw || EnumSet.class == mapping.raw) {
collection = new ArrayList<T>(jsonArray.size());
} else if (LinkedHashSet.class == mapping.raw) {
collection = new LinkedHashSet<T>(jsonArray.size());
} else if (Deque.class == mapping.raw || ArrayDeque.class == mapping.raw) {
collection = new ArrayDeque(jsonArray.size());
} else if (Queue.class == mapping.raw || PriorityQueue.class == mapping.raw) {
collection = new PriorityQueue(jsonArray.size());
} else {
throw new IllegalStateException("not supported collection type: " + mapping.raw.getName());
}
for (final JsonValue value : jsonArray) {
collection.add(value == JsonValue.NULL ? null : toObject(null, value, mapping.arg, itemConverter));
}
if (EnumSet.class == mapping.raw) {
if (collection.isEmpty()) {
return EnumSet.noneOf(Class.class.cast(mapping.arg));
} else if (collection.size() == 1) {
return Collection.class.cast(EnumSet.of(Enum.class.cast(collection.iterator().next())));
} else {
final List<Enum<?>> list = List.class.cast(collection);
return Collection.class.cast(EnumSet.of(list.get(0), list.subList(1, list.size()).toArray(new Enum[list.size() - 1])));
}
}
return collection;
}
private Object[] createParameters(final Mappings.ClassMapping mapping, final JsonObject object) {
final int length = mapping.factory.getParameterTypes().length;
final Object[] objects = new Object[length];
for (int i = 0; i < length; i++) {
objects[i] = toValue(null,
object.get(mapping.factory.getParameterNames()[i]),
mapping.factory.getParameterConverter()[i],
mapping.factory.getParameterItemConverter()[i],
mapping.factory.getParameterTypes()[i],
null); //X TODO ObjectConverter in @JOhnzonConverter with Constructors!
}
return objects;
}
private Object toValue(final Object baseInstance, final JsonValue jsonValue, final Adapter converter,
final Adapter itemConverter, final Type type, final ObjectConverter.Reader objectConverter) {
if (objectConverter != null) {
if (jsonValue instanceof JsonObject) {
return objectConverter.fromJson((JsonObject) jsonValue, type, this);
} else {
throw new UnsupportedOperationException("Array handling with ObjectConverter currently not implemented");
}
}
return converter == null ? toObject(baseInstance, jsonValue, type, itemConverter)
: jsonValue.getValueType() == JsonValue.ValueType.STRING ? converter.to(JsonString.class.cast(jsonValue).getString())
: convertTo(converter, jsonValue);
}
/**
* @deprecated see MapperConfig
*/
private Object convertTo(final Type aClass, final String text) {
if (Object.class == aClass || String.class == aClass) {
return text;
}
final Adapter converter = findAdapter(aClass);
if (converter == null) {
config.getAdapters().putIfAbsent(new AdapterKey(String.class, aClass), FALLBACK_CONVERTER);
return FALLBACK_CONVERTER.to(text);
}
return converter.to(text);
}
/**
* @deprecated see MapperConfig
*/
private Adapter findAdapter(final Type aClass) {
final Adapter<?, ?> converter = config.getAdapters().get(new AdapterKey(aClass, String.class));
if (converter != null) {
return converter;
}
if (Class.class.isInstance(aClass)) {
final Class<?> clazz = Class.class.cast(aClass);
if (clazz.isEnum()) {
final Adapter<?, ?> enumConverter = new ConverterAdapter(new EnumConverter(clazz));
config.getAdapters().putIfAbsent(new AdapterKey(String.class, aClass), enumConverter);
return enumConverter;
}
}
return null;
}
private static class FallbackConverter implements Converter<Object> {
@Override
public String toString(final Object instance) {
return instance.toString();
}
@Override
public Object fromString(final String text) {
throw new MapperException("Using fallback converter, " +
"this only works in write mode but not in read. Please register a custom converter to do so.");
}
}
/**
* Internal class to suppress {@link ObjectConverter} lookup if and only if
* the {@link JsonValue} is the same refernece than the lookup was done before.
*/
private static class SuppressConversionMappingParser implements MappingParser {
private final MappingParserImpl delegate;
private final JsonObject suppressConversionFor;
public SuppressConversionMappingParser(MappingParserImpl delegate, JsonObject suppressConversionFor) {
this.delegate = delegate;
this.suppressConversionFor = suppressConversionFor;
}
@Override
public <T> T readObject(Type targetType) {
return delegate.readObject(targetType);
}
@Override
public <T> T readObject(JsonValue jsonValue, Type targetType) {
if (suppressConversionFor == jsonValue) {
return delegate.readObject(jsonValue, targetType, false);
}
return delegate.readObject(jsonValue, targetType);
}
}
}