Created
July 21, 2020 20:56
-
-
Save TaylanUB/c58447c87f63c8923cce747160a0cdf6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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