blob: e714437fbb631c5e7af1025ad61c1a3518632d9f [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 static java.util.Collections.emptyList;
import static java.util.stream.Collectors.toList;
import org.apache.johnzon.mapper.internal.JsonPointerTracker;
import org.apache.johnzon.mapper.util.ArrayUtil;
import javax.json.JsonValue;
import javax.json.stream.JsonGenerator;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Base64;
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.Spliterator;
import java.util.Spliterators;
import java.util.stream.BaseStream;
import java.util.stream.StreamSupport;
public class MappingGeneratorImpl implements MappingGenerator {
private final MapperConfig config;
private final JsonGenerator generator;
private final Mappings mappings;
private final Boolean isDeduplicateObjects;
private Map<Object, String> jsonPointers;
MappingGeneratorImpl(MapperConfig config, JsonGenerator jsonGenerator, final Mappings mappings, Boolean isDeduplicateObjects) {
this.config = config;
this.generator = jsonGenerator;
this.mappings = mappings;
this.isDeduplicateObjects = isDeduplicateObjects;
this.jsonPointers = isDeduplicateObjects ? new HashMap<>() : Collections.emptyMap();
}
@Override
public JsonGenerator getJsonGenerator() {
return generator;
}
@Override
public MappingGenerator writeObject(final String key, final Object object, final JsonGenerator generator) {
if (object == null) {
return this;
} else if (object instanceof JsonValue) {
generator.write(key, JsonValue.class.cast(object));
} else {
final Class<?> objectClass = object.getClass();
try {
if (Map.class.isInstance(object)) {
writeValue(Map.class, false, false, false, false, true, null, key, object,
null, emptyList(), isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, generator);
} else if(writePrimitives(key, objectClass, object, generator)) {
// no-op
} else if (Enum.class.isAssignableFrom(objectClass)) {
final Adapter adapter = config.findAdapter(objectClass);
final String adaptedValue = adapter.from(object).toString(); // we know it ends as String for enums
generator.write(key, adaptedValue);
} else if (objectClass.isArray()) {
writeValue(Map.class, false, false, true, false, false, null, key, object,
null, emptyList(), isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, generator);
} else if (Iterable.class.isInstance(object)) {
writeValue(Map.class, false, false, false, true, false, null, key, object,
null, emptyList(), isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, generator);
} else {
final ObjectConverter.Writer objectConverter = config.findObjectConverterWriter(objectClass);
if (objectConverter != null) {
final DynamicMappingGenerator dynamicMappingGenerator = new DynamicMappingGenerator(this,
generator::writeStartObject, generator::writeEnd, null);
objectConverter.writeJson(object, dynamicMappingGenerator);
dynamicMappingGenerator.flushIfNeeded();
} else {
writeValue(objectClass, false, false, false, false, false, null, key, object,
null, emptyList(), isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null, generator);
}
}
} catch (final InvocationTargetException | IllegalAccessException e) {
throw new MapperException(e);
}
}
return this;
}
@Override
public MappingGenerator writeObject(final Object object, final JsonGenerator generator) {
if (object == null) {
return this;
} else if (object instanceof JsonValue) {
generator.write((JsonValue) object);
} else {
doWriteObject(object, generator, false, null, isDeduplicateObjects ? new JsonPointerTracker(null, "/") : null);
}
return this;
}
public void doWriteObject(Object object, JsonGenerator generator, boolean writeBody, final Collection<String> ignoredProperties,
JsonPointerTracker jsonPointer) {
try {
if (object instanceof Map) {
if (writeBody) {
generator.writeStartObject();
}
writeMapBody((Map<?, ?>) object, null);
if (writeBody) {
generator.writeEnd();
}
return;
}
if (writePrimitives(object)) {
return;
}
final Class<?> objectClass = object.getClass();
if (Enum.class.isAssignableFrom(objectClass)) {
final Adapter adapter = config.findAdapter(objectClass);
final String adaptedValue = adapter.from(object).toString(); // we know it ends as String for enums
generator.write(adaptedValue);
return;
}
if (objectClass.isArray()) {
final Adapter adapter = config.findAdapter(objectClass);
writeArray(objectClass, adapter, null, object, ignoredProperties, jsonPointer);
return;
}
if (object instanceof Iterable) {
doWriteIterable((Iterable) object, ignoredProperties, jsonPointer);
return;
}
final Mappings.ClassMapping classMapping = mappings.getClassMapping(objectClass); // don't create here!
if (classMapping != null && classMapping.adapter != null) {
final Object result = classMapping.adapter.from(object);
doWriteObject(result, generator, writeBody, ignoredProperties, jsonPointer);
return;
}
ObjectConverter.Writer objectConverter = config.findObjectConverterWriter(objectClass);
if (writeBody && objectConverter != null) {
if (!writeBody) {
objectConverter.writeJson(object, this);
} else {
final DynamicMappingGenerator dynamicMappingGenerator = new DynamicMappingGenerator(this,
generator::writeStartObject, generator::writeEnd, null);
objectConverter.writeJson(object, dynamicMappingGenerator);
dynamicMappingGenerator.flushIfNeeded();
}
} else {
if (writeBody) {
generator.writeStartObject();
}
final boolean writeEnd;
if (config.getSerializationPredicate() != null && config.getSerializationPredicate().test(objectClass)) {
generator.write(config.getDiscriminator(), config.getDiscriminatorMapper().apply(objectClass));
writeEnd = doWriteObjectBody(object, ignoredProperties, jsonPointer, generator);
} else {
writeEnd = doWriteObjectBody(object, ignoredProperties, jsonPointer, generator);
}
if (writeEnd && writeBody) {
generator.writeEnd();
}
}
} catch (final InvocationTargetException | IllegalAccessException e) {
throw new MapperException(e);
}
}
private JsonGenerator writeMapBody(final Map<?, ?> object, final Adapter itemConverter) throws InvocationTargetException, IllegalAccessException {
for (final Map.Entry<?, ?> entry : ((Map<?, ?>) object).entrySet()) {
final Object value = entry.getValue();
final Object key = entry.getKey();
if (value == null) {
if (config.isSkipNull()) {
continue;
} else {
generator.writeNull(key == null ? "null" : key.toString());
continue;
}
}
final Class<?> valueClass = value.getClass();
writeValue(valueClass, true,
false, false, false, false, itemConverter,
key == null ? "null" : key.toString(), value, null, null, null,
generator);
}
return generator;
}
/**
* @return {@code true} if it was a primitive, {@code false} if the value did not get handled
*/
private boolean writePrimitives(final Object value) {
boolean handled = false;
if (value == null) {
return true; // fake a write
}
final Class<?> type = value.getClass();
if (type == String.class) {
generator.write(value.toString());
handled = true;
} else if (JsonValue.class.isAssignableFrom(type)) {
generator.write(JsonValue.class.cast(value));
handled = true;
} else if (type == long.class || type == Long.class) {
final long longValue = Long.class.cast(value).longValue();
if (isInJsRange(longValue)) {
generator.write(longValue);
} else {
generator.write(value.toString());
}
handled = true;
} else if (isInt(type)) {
generator.write(Number.class.cast(value).intValue());
handled = true;
} else if (isFloat(type)) {
if (type == Float.class || type == float.class) {
if (!Float.isNaN(Float.class.cast(value))) {
generator.write(new BigDecimal(value.toString()));
}
} else {
final double doubleValue = Number.class.cast(value).doubleValue();
if (!Double.isNaN(doubleValue)) {
generator.write(doubleValue);
}
}
handled = true;
} else if (type == boolean.class || type == Boolean.class) {
generator.write(Boolean.class.cast(value));
return true;
} else if (type == BigDecimal.class) {
generator.write(BigDecimal.class.cast(value));
handled = true;
} else if (type == BigInteger.class) {
generator.write(BigInteger.class.cast(value));
handled = true;
} else if (type == char.class || type == Character.class) {
generator.write(Character.class.cast(value).toString());
handled = true;
}
return handled;
}
private boolean writePrimitives(final String key, final Class<?> type, final Object value,
final JsonGenerator generator) {
boolean handled = false;
if (type == String.class) {
generator.write(key, value.toString());
handled = true;
} else if (JsonValue.class.isAssignableFrom(type)) {
generator.write(key, JsonValue.class.cast(value));
handled = true;
} else if (type == long.class || type == Long.class) {
final long longValue = Long.class.cast(value).longValue();
if (isInJsRange(longValue)) {
generator.write(key, longValue);
} else {
generator.write(key, value.toString());
}
handled = true;
} else if (isInt(type)) {
generator.write(key, Number.class.cast(value).intValue());
handled = true;
} else if (isFloat(type)) {
if (type == Float.class || type == float.class) {
if (!Float.isNaN(Float.class.cast(value))) {
generator.write(key, new BigDecimal(value.toString()));
}
} else {
final double doubleValue = Number.class.cast(value).doubleValue();
if (!Double.isNaN(doubleValue)) {
generator.write(key, doubleValue);
}
}
handled = true;
} else if (type == boolean.class || type == Boolean.class) {
generator.write(key, Boolean.class.cast(value));
handled = true;
} else if (type == BigDecimal.class) {
generator.write(key, BigDecimal.class.cast(value));
handled = true;
} else if (type == BigInteger.class) {
generator.write(key, BigInteger.class.cast(value));
handled = true;
} else if (type == char.class || type == Character.class) {
generator.write(key, Character.class.cast(value).toString());
handled = true;
}
return handled;
}
private static boolean isInt(final Class<?> type) {
return type == int.class || type == Integer.class
|| type == byte.class || type == Byte.class
|| type == short.class || type == Short.class;
}
private static boolean isFloat(final Class<?> type) {
return type == double.class || type == Double.class
|| type == float.class || type == Float.class;
}
private boolean doWriteObjectBody(final Object object, final Collection<String> ignored,
final JsonPointerTracker jsonPointer, final JsonGenerator generator)
throws IllegalAccessException, InvocationTargetException {
if (jsonPointer != null) {
jsonPointers.put(object, jsonPointer.toString());
}
final Class<?> objectClass = object.getClass();
final Mappings.ClassMapping classMapping = mappings.findOrCreateClassMapping(objectClass);
if (classMapping == null) {
throw new MapperException("No mapping for " + objectClass.getName());
}
if (classMapping.writer != null) {
final DynamicMappingGenerator gen = new DynamicMappingGenerator.SkipEnclosingWriteEnd(this, null, generator);
classMapping.writer.writeJson(object, gen);
gen.flushIfNeeded();
return false;
}
if (classMapping.adapter != null) {
doWriteObjectBody(classMapping.adapter.from(object), ignored, jsonPointer, generator);
return true;
}
for (final Map.Entry<String, Mappings.Getter> getterEntry : classMapping.getters.entrySet()) {
final Mappings.Getter getter = getterEntry.getValue();
if (ignored != null && ignored.contains(getterEntry.getKey())) {
continue;
}
if (getter.version >= 0 && config.getVersion() >= 0 && config.getVersion() < getter.version) {
continue;
}
final Object value = getter.reader.read(object);
if (JsonValue.class.isInstance(value)) {
generator.write(getterEntry.getKey(), JsonValue.class.cast(value));
continue;
}
if (value == null) {
if (!getter.reader.isNillable(!config.isSkipNull())) {
continue;
} else {
generator.writeNull(getterEntry.getKey());
continue;
}
}
final Object val = getter.converter == null ? value : getter.converter.from(value);
String valJsonPointer = jsonPointers.get(val);
if (valJsonPointer != null) {
// write the JsonPointer instead
generator.write(getterEntry.getKey(), valJsonPointer);
} else {
writeValue(val.getClass(),
getter.dynamic,
getter.primitive,
getter.array,
getter.collection,
getter.map,
getter.itemConverter,
getterEntry.getKey(),
val,
getter.objectConverter,
getter.ignoreNested,
isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, getterEntry.getKey()) : null,
generator);
}
}
// @JohnzonAny doesn't respect comparator since it is a map and not purely in the model we append it after and
// sorting is up to the user for this part (TreeMap if desired)
if (classMapping.anyGetter != null) {
final Map<String, Object> any = Map.class.cast(classMapping.anyGetter.reader.read(object));
if (any != null) {
writeMapBody(any, null);
}
}
return true;
}
//CHECKSTYLE:OFF
private void writeValue(final Class<?> type, final boolean dynamic,
final boolean primitive, final boolean array,
final boolean collection, final boolean map,
final Adapter itemConverter,
final String key, final Object value,
final ObjectConverter.Writer objectConverter,
final Collection<String> ignoredProperties,
final JsonPointerTracker jsonPointer,
final JsonGenerator generator)
throws InvocationTargetException, IllegalAccessException {
//CHECKSTYLE:ON
if (config.getSerializeValueFilter().shouldIgnore(key, value)) {
return;
}
if ((!dynamic && array) || (dynamic && type.isArray())) {
writeArray(type, itemConverter, key, value, ignoredProperties, jsonPointer);
} else if ((!dynamic && collection) || (dynamic && Iterable.class.isAssignableFrom(type))) {
writeIterator(itemConverter, key, objectConverter, ignoredProperties, jsonPointer, generator,
Iterable.class.cast(value).iterator(), value);
} else if ((!dynamic && map) || (dynamic && Map.class.isAssignableFrom(type))) {
generator.writeStartObject(key);
writeMapBody((Map<?, ?>) value, itemConverter);
generator.writeEnd();
} else if ((!dynamic && primitive) || (dynamic && Mappings.isPrimitive(type))) {
if (objectConverter != null) {
final DynamicMappingGenerator dynamicMappingGenerator = new DynamicMappingGenerator(this,
() -> this.generator.writeStartObject(key), this.generator::writeEnd, key);
objectConverter.writeJson(value, dynamicMappingGenerator);
dynamicMappingGenerator.flushIfNeeded();
} else {
writePrimitives(key, type, value, generator);
}
} else if (BaseStream.class.isAssignableFrom(type)) {
writeIterator(itemConverter, key, objectConverter, ignoredProperties, jsonPointer, generator,
BaseStream.class.cast(value).iterator(), value);
} else if (Iterator.class.isAssignableFrom(type)) {
writeIterator(itemConverter, key, objectConverter, ignoredProperties, jsonPointer, generator,
Iterator.class.cast(value), value);
} else {
if (objectConverter != null) {
final DynamicMappingGenerator dynamicMappingGenerator = new DynamicMappingGenerator(this,
() -> this.generator.writeStartObject(key), this.generator::writeEnd, key);
objectConverter.writeJson(value, dynamicMappingGenerator);
dynamicMappingGenerator.flushIfNeeded();
return;
}
final Adapter converter = config.findAdapter(type);
if (converter != null) {
final Object adapted = doConvertFrom(value, converter);
if (writePrimitives(key, adapted.getClass(), adapted, generator)) {
return;
}
writeValue(String.class, true, true, false, false, false, null, key, adapted, null, ignoredProperties, jsonPointer, generator);
return;
} else {
ObjectConverter.Writer objectConverterToUse = objectConverter;
if (objectConverterToUse == null) {
objectConverterToUse = config.findObjectConverterWriter(type);
}
if (objectConverterToUse != null) {
final DynamicMappingGenerator dynamicMappingGenerator = new DynamicMappingGenerator(this,
() -> this.generator.writeStartObject(key), this.generator::writeEnd, key);
objectConverterToUse.writeJson(value, dynamicMappingGenerator);
dynamicMappingGenerator.flushIfNeeded();
return;
}
}
if (writePrimitives(key, type, value, generator)) {
return;
}
generator.writeStartObject(key);
if (doWriteObjectBody(value, ignoredProperties, jsonPointer, generator)) {
generator.writeEnd();
}
}
}
private void writeIterator(final Adapter itemConverter, final String key,
final ObjectConverter.Writer objectConverter,
final Collection<String> ignoredProperties,
final JsonPointerTracker jsonPointer,
final JsonGenerator generator,
final Iterator<?> iterator,
final Object originalValue) {
if (objectConverter != null && objectConverter.isGlobal()) {
final List<Object> list = List.class.isInstance(originalValue) ?
List.class.cast(originalValue) :
StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.IMMUTABLE), false)
.collect(toList());
objectConverter.writeJson(list, new DynamicMappingGenerator(
this, generator::writeStartArray, generator::writeEnd, key));
return;
}
int i = 0;
generator.writeStartArray(key);
while (iterator.hasNext()) {
final Object o = iterator.next();
String valJsonPointer = jsonPointers.get(o);
if (valJsonPointer != null) {
// write JsonPointer instead of the original object
writePrimitives(valJsonPointer);
} else {
ObjectConverter.Writer objectConverterToUse = objectConverter;
if (o != null && objectConverterToUse == null) {
objectConverterToUse = config.findObjectConverterWriter(o.getClass());
}
if (objectConverterToUse != null) {
final DynamicMappingGenerator dynamicMappingGenerator = new DynamicMappingGenerator(this,
generator::writeStartObject, generator::writeEnd, null);
objectConverterToUse.writeJson(o, dynamicMappingGenerator);
dynamicMappingGenerator.flushIfNeeded();
} else {
writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties,
isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null);
}
}
i++;
}
generator.writeEnd();
}
/**
* Write a JSON Array with a given Array Value, like byte[], int[], Person[] etc.
* @param key either the attribute key or {@code null} if the array should be rendered without key
*/
private void writeArray(Class<?> type, Adapter itemConverter, String key, Object arrayValue, Collection<String> ignoredProperties, JsonPointerTracker jsonPointer) {
final int length = ArrayUtil.getArrayLength(arrayValue);
if (length == 0 && config.isSkipEmptyArray()) {
return;
}
if(config.isTreatByteArrayAsBase64() && (type == byte[].class /*|| type == Byte[].class*/)) {
String base64EncodedByteArray = Base64.getEncoder().encodeToString((byte[]) arrayValue);
if (key != null) {
generator.write(key, base64EncodedByteArray);
} else {
generator.write(base64EncodedByteArray);
}
return;
}
if(config.isTreatByteArrayAsBase64URL() && (type == byte[].class /*|| type == Byte[].class*/)) {
if (key != null) {
generator.write(key, Base64.getUrlEncoder().encodeToString((byte[]) arrayValue));
} else {
generator.write(Base64.getUrlEncoder().encodeToString((byte[]) arrayValue));
}
return;
}
if (key != null) {
generator.writeStartArray(key);
} else {
generator.writeStartArray();
}
// some specialised arrays to speed up conversion.
// Needed since Array.get is rather slow :(
if (type == byte[].class) {
byte[] tArrayValue = (byte[]) arrayValue;
for (int i = 0; i < length; i++) {
final byte o = tArrayValue[i];
generator.write(o);
}
} else if (type == short[].class) {
short[] tArrayValue = (short[]) arrayValue;
for (int i = 0; i < length; i++) {
final short o = tArrayValue[i];
generator.write(o);
}
} else if (type == int[].class) {
int[] tArrayValue = (int[]) arrayValue;
for (int i = 0; i < length; i++) {
final int o = tArrayValue[i];
generator.write(o);
}
} else if (type == long[].class) {
long[] tArrayValue = (long[]) arrayValue;
for (int i = 0; i < length; i++) {
final long o = tArrayValue[i];
generator.write(o);
}
} else if (type == float[].class) {
float[] tArrayValue = (float[]) arrayValue;
for (int i = 0; i < length; i++) {
final float o = tArrayValue[i];
generator.write(o);
}
} else if (type == double[].class) {
double[] tArrayValue = (double[]) arrayValue;
for (int i = 0; i < length; i++) {
final double o = tArrayValue[i];
generator.write(o);
}
} else if (type == char[].class) {
char[] tArrayValue = (char[]) arrayValue;
for (int i = 0; i < length; i++) {
final char o = tArrayValue[i];
generator.write(String.valueOf(o));
}
} else if (type == boolean[].class) {
boolean[] tArrayValue = (boolean[]) arrayValue;
for (int i = 0; i < length; i++) {
final boolean o = tArrayValue[i];
generator.write(o);
}
} else if (type == Byte[].class ||
type == Short[].class ||
type == Integer[].class ||
type == Long[].class ||
type == Float[].class ||
type == Double[].class ||
type == Character[].class ||
type == Boolean[].class) {
// Wrapper types do not not need deduplication
Object[] oArrayValue = (Object[]) arrayValue;
for (int i = 0; i < length; i++) {
final Object o = oArrayValue[i];
writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties, null);
}
} else {
// must be object arrays
for (int i = 0; i < length; i++) {
Object[] oArrayValue = (Object[]) arrayValue;
final Object o = oArrayValue[i];
String valJsonPointer = jsonPointers.get(o);
if (valJsonPointer != null) {
// write the JsonPointer as String natively
generator.write(valJsonPointer);
} else if (o instanceof JsonValue) {
generator.write((JsonValue) o);
} else {
writeItem(itemConverter != null ? itemConverter.from(o) : o, ignoredProperties, isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null);
}
}
}
generator.writeEnd();
}
private void writeItem(final Object o, final Collection<String> ignoredProperties, JsonPointerTracker jsonPointer) {
if (o == null) {
generator.writeNull();
} else if (!writePrimitives(o)) {
if (Collection.class.isInstance(o)) {
doWriteIterable(Collection.class.cast(o), ignoredProperties, jsonPointer);
} else if (o.getClass().isArray()) {
final int length = ArrayUtil.getArrayLength(o);
if (length > 0 || !config.isSkipEmptyArray()) {
writeArray(o.getClass(), null, null, o, ignoredProperties, jsonPointer);
}
} else {
String valJsonPointer = jsonPointers.get(o);
if (valJsonPointer != null) {
// write the JsonPointer instead
generator.write(valJsonPointer);
} else {
doWriteObject(o, generator, true, ignoredProperties, jsonPointer);
}
}
}
}
private <T> void doWriteIterable(final Iterable<T> object, final Collection<String> ignoredProperties, JsonPointerTracker jsonPointer) {
if (object == null) {
generator.writeStartArray().writeEnd();
} else {
generator.writeStartArray();
int i = 0;
for (final T t : object) {
if (JsonValue.class.isInstance(t)) {
generator.write(JsonValue.class.cast(t));
} else {
if (t == null) {
generator.writeNull();
} else {
writeItem(t, ignoredProperties, isDeduplicateObjects ? new JsonPointerTracker(jsonPointer, i) : null);
}
}
i++;
}
generator.writeEnd();
}
}
private <T> Object doConvertFrom(final T value, final Adapter<T, Object> converter) {
if (converter == null) {
throw new MapperException("can't convert " + value + " to String");
}
return converter.from(value);
}
private boolean isInJsRange(final Number longValue) {
return !config.isUseJsRange() ||
(longValue.longValue() <= 9007199254740991L && longValue.longValue() >= -9007199254740991L);
}
}