Skip to content

Instantly share code, notes, and snippets.

@TaylanUB
Created July 21, 2020 20:56
Show Gist options
  • Save TaylanUB/c58447c87f63c8923cce747160a0cdf6 to your computer and use it in GitHub Desktop.
Save TaylanUB/c58447c87f63c8923cce747160a0cdf6 to your computer and use it in GitHub Desktop.
package de.cobi.wms.json;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.$Gson$Types;
import com.google.gson.internal.ConstructorConstructor;
import com.google.gson.internal.Excluder;
import com.google.gson.internal.ObjectConstructor;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import de.cobi.wms.BuildConfig;
import de.cobi.wms.util.Model;
import de.cobi.wms.util.Util;
/**
* Offers Gson instances which support dynamic type information and object identity (deduplication
* and cyclic references) for <tt>Model</tt> classes.
* <br/><br/>
* When a <tt>Model</tt> object is first written, the JSON Object it's serialized to starts with a
* special <tt>@id</tt> key whose value is a linearly growing long integer. This is followed with
* a <tt>@class</tt> key which records the actual class of the object at run-time, as it could be a
* subclass of the statically known type. Whenever the same object is written again, it's
* serialized to a JSON Object that contains only the special <tt>@ref</tt> key, whose value is the
* previously generated long integer.
* <br/><br/>
* During deserialization, the <tt>@id</tt> and <tt>@ref</tt> entries of the JSON Objects are used
* to make sure that identical objects aren't duplicated, and the <tt>@class</tt> entry makes sure
* that created objects are of the same class as those that were serialized.
* <br/><br/>
* The recorded object identities are tied to the Gson instance returned by this class via the
* <tt>create</tt> method. I.e. when you create a new Gson instance by calling that method, it
* won't know about any objects serialized or deserialized by another instance.
*/
class ImprovedGson {
private static class FieldCache {
private static final Map<TypeToken<?>, Map<String, TypedField>> cache = new HashMap<>();
void put(TypeToken<?> key, Map<String, TypedField> value) {
synchronized (cache) {
cache.put(key, value);
}
}
Map<String, TypedField> get(TypeToken<?> key) {
synchronized (cache) {
return cache.get(key);
}
}
}
private static class ObjectCache {
final Map<Long, Object> keyToObject = new HashMap<>();
final Map<Object, Long> objectToKey = new IdentityHashMap<>();
long counter;
}
private static final FieldCache fieldCache = new FieldCache();
static Gson create() {
ObjectCache objectCache = new ObjectCache();
return new GsonBuilder()
.enableComplexMapKeySerialization()
.setDateFormat("yyyy-MM-dd_HH-mm")
.registerTypeAdapterFactory(
new ModelAdapterFactory(objectCache, fieldCache)
)
.setPrettyPrinting()
.create();
}
private static class ModelAdapterFactory implements TypeAdapterFactory {
private final ObjectCache objectCache;
private final FieldCache fieldCache;
ModelAdapterFactory(ObjectCache objectCache, FieldCache fieldCache) {
this.objectCache = objectCache;
this.fieldCache = fieldCache;
}
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class<?> cls = type.getRawType();
if (Model.class.isAssignableFrom(cls)) {
return new ModelAdapter<>(type, gson, objectCache, fieldCache);
} else if (Enum.class.isAssignableFrom(cls)) {
return null;
} else if (cls.getName().startsWith("de.cobi.wms") && BuildConfig.DEBUG) {
// All of our own model classes should have been caught by the
// Model.class.isAssignableFrom check above, so this is an error.
throw new IllegalArgumentException("Not a model class: " + cls.getName());
} else {
return null;
}
}
}
private static class ModelAdapter<T> extends TypeAdapter<T> {
private static final String ID = "@id";
private static final String CLASS = "@class";
private static final String REF = "@ref";
private static final ConstructorConstructor constructorConstructor =
new ConstructorConstructor(Collections.emptyMap());
private final ObjectConstructor<T> fallbackConstructor;
private final Map<String, TypedField> fallbackDeserializeFields;
private final Gson gson;
private final ObjectCache objectCache;
private final FieldCache fieldCache;
ModelAdapter(TypeToken<T> type, Gson gson, ObjectCache objectCache, FieldCache fieldCache) {
fallbackConstructor = constructorConstructor.get(type);
fallbackDeserializeFields = getFields(fieldCache, type, false);
this.gson = gson;
this.objectCache = objectCache;
this.fieldCache = fieldCache;
}
@Override
public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
out.beginObject();
Long id = objectCache.objectToKey.get(value);
if (id != null) {
out.name(REF);
out.value(id);
} else {
// noinspection unchecked
Class<T> cls = (Class<T>) value.getClass();
long newId = objectCache.counter++;
objectCache.objectToKey.put(value, newId);
out.name(ID);
out.value(newId);
out.name(CLASS);
out.value(cls.getName());
Map<String, TypedField> fields = getFields(fieldCache, cls, true);
for (TypedField field : fields.values()) {
field.write(gson, out, value);
}
}
out.endObject();
}
@Override
public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
T value;
Map<String, TypedField> fields;
// This is for a hack (see below) because we can't "un-consume" values from the reader.
String firstField;
// For backwards compatibility with the old AbstractClassTypeAdapter, which used to
// output an array where the first element was the class name and the second element
// the actual object.
boolean isArray;
if (in.peek() == JsonToken.BEGIN_ARRAY) {
isArray = true;
firstField = null;
in.beginArray();
Class<T> cls = classWithName(in.nextString());
value = construct(cls);
fields = getFields(fieldCache, cls, false);
in.beginObject();
} else {
isArray = false;
in.beginObject();
String field = in.nextName();
if (field.equals(REF)) {
long id = in.nextLong();
in.endObject();
// noinspection unchecked
return (T) objectCache.keyToObject.get(id);
}
long id = -1;
Class<T> cls = null;
if (field.equals(ID)) {
firstField = null;
id = in.nextLong();
if (!in.nextName().equals(CLASS)) {
throw new IllegalStateException(
"When @id is present, second key must be @class."
);
}
cls = classWithName(in.nextString());
// Sanity and security check.
if (cls != null && !Model.class.isAssignableFrom(cls)) {
cls = null;
}
} else {
// The name we consumed wasn't "@id", which means we consumed the name of a
// normal field. We can't un-consume it, so save it for below.
firstField = field;
}
if (cls != null) {
value = construct(cls);
objectCache.keyToObject.put(id, value);
fields = getFields(fieldCache, cls, false);
} else {
value = fallbackConstructor.construct();
fields = fallbackDeserializeFields;
}
}
if (firstField != null) {
TypedField field = fields.get(firstField);
if (field != null) {
field.read(gson, in, value);
} else {
in.skipValue();
}
}
while (in.hasNext()) {
TypedField field = fields.get(in.nextName());
if (field != null) {
field.read(gson, in, value);
} else {
in.skipValue();
}
}
in.endObject();
if (isArray) {
in.endArray();
}
return value;
}
private T construct(Class<T> cls) {
TypeToken<T> typeToken = TypeToken.get(cls);
ObjectConstructor<T> constructor = constructorConstructor.get(typeToken);
return constructor.construct();
}
private static Map<String, TypedField> getFields(
FieldCache cache, Class<?> cls, boolean serialize
) {
return getFields(cache, TypeToken.get(cls), serialize);
}
private static Map<String, TypedField> getFields(
FieldCache cache, TypeToken<?> typeToken, boolean serialize
) {
Map<String, TypedField> cachedFields = cache.get(typeToken);
if (cachedFields != null) {
return cachedFields;
}
Map<String, TypedField> fields = new HashMap<>();
Type type = typeToken.getType();
Class<?> cls = typeToken.getRawType();
while (cls != Object.class && cls != null) {
for (Field f : cls.getDeclaredFields()) {
if (Excluder.DEFAULT.excludeField(f, serialize)) {
continue;
}
f.setAccessible(true);
fields.put(f.getName(), new TypedField(type, cls, f));
}
Type superType = $Gson$Types.resolve(type, cls, cls.getGenericSuperclass());
if (superType == null) {
break;
}
typeToken = TypeToken.get(superType);
type = typeToken.getType();
cls = typeToken.getRawType();
}
cache.put(typeToken, fields);
return fields;
}
private static <T> Class<T> classWithName(String name) {
try {
// noinspection unchecked
return (Class<T>) Class.forName(name);
} catch (ClassNotFoundException exception) {
return null;
}
}
}
private static class TypedField {
private final Field field;
private final Type type;
private TypedField(Type staticContext, Class<?> actualClass, Field field) {
this.field = field;
Type type = $Gson$Types.resolve(staticContext, actualClass, field.getGenericType());
// As of Gson version 2.8.6, there is absolutely no way of making it use something
// other than ObjectTypeAdapter for a Type that is a TypeVariable. For that reason
// we change the type here to the upper bound of the type variable.
if (type instanceof TypeVariable) {
this.type = Util.ifNull(getUpperBound(type), Object.class);
} else {
this.type = type;
}
}
private void write(Gson gson, JsonWriter out, Object value) throws IOException {
try {
Object fieldValue = field.get(value);
if (fieldValue != null) {
out.name(field.getName());
gson.toJson(fieldValue, type, out);
}
} catch (IllegalAccessException exception) {
throw new RuntimeException(exception);
}
}
private void read(Gson gson, JsonReader in, Object value) {
try {
Object fieldValue = gson.fromJson(in, type);
field.set(value, fieldValue);
} catch (IllegalAccessException exception) {
throw new RuntimeException(exception);
}
}
private static Type getUpperBound(Type type) {
if (isNormalClass(type)) {
return type;
} else if (type instanceof ParameterizedType) {
return ((ParameterizedType) type).getRawType();
} else if (type instanceof TypeVariable) {
for (Type bound : ((TypeVariable<?>) type).getBounds()) {
Type upperBound = getUpperBound(bound);
if (upperBound != null) {
return upperBound;
}
}
} else if (type instanceof WildcardType) {
for (Type bound : ((WildcardType) type).getUpperBounds()) {
Type upperBound = getUpperBound(bound);
if (upperBound != null) {
return upperBound;
}
}
}
return null;
}
private static boolean isNormalClass(Type type) {
return type instanceof Class && ((Class<?>) type).isAssignableFrom(Object.class);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment