/*
* 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 gobblin.util.io;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import java.io.IOException;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.util.Collection;
import java.util.Map;
import org.apache.commons.lang3.ClassUtils;
import com.google.common.base.Optional;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.Streams;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* A {@link Gson} interface adapter that makes it possible to serialize and deserialize polymorphic objects.
*
* <p>
* This adapter will capture all instances of {@link #baseClass} and write them as
* {"object-type":"class.name", "object-data":"data"}, allowing for correct serialization and deserialization of
* polymorphic objects. The following types will not be captured by the adapter (i.e. they will be written by the
* default GSON writer):
* - Primitives and boxed primitives
* - Arrays
* - Collections
* - Maps
* Additionally, generic classes (e.g. class MyClass<T>) cannot be correctly decoded.
* </p>
*
* <p>
* To use:
* <pre>
* {@code
* MyClass object = new MyClass();
* Gson gson = GsonInterfaceAdapter.getGson(MyBaseClass.class);
* String json = gson.toJson(object);
* Myclass object2 = gson.fromJson(json, MyClass.class);
* }
* </pre>
* </p>
*
* <p>
* Note: a useful case is GsonInterfaceAdapter.getGson(Object.class), which will correctly serialize / deserialize
* all types except for java generics.
* </p>
*
* @param <T> The interface or abstract type to be serialized and deserialized with {@link Gson}.
*/
@RequiredArgsConstructor
public class GsonInterfaceAdapter implements TypeAdapterFactory {
protected static final String OBJECT_TYPE = "object-type";
protected static final String OBJECT_DATA = "object-data";
private final Class<?> baseClass;
@Override
public <R> TypeAdapter<R> create(Gson gson, TypeToken<R> type) {
if (ClassUtils.isPrimitiveOrWrapper(type.getRawType()) || type.getType() instanceof GenericArrayType
|| CharSequence.class.isAssignableFrom(type.getRawType())
|| (type.getType() instanceof ParameterizedType && (Collection.class.isAssignableFrom(type.getRawType())
|| Map.class.isAssignableFrom(type.getRawType())))) {
// delegate primitives, arrays, collections, and maps
return null;
}
if (!this.baseClass.isAssignableFrom(type.getRawType())) {
// delegate anything not assignable from base class
return null;
}
TypeAdapter<R> adapter = new InterfaceAdapter<>(gson, this, type);
return adapter;
}
@AllArgsConstructor
private static class InterfaceAdapter<R> extends TypeAdapter<R> {
private final Gson gson;
private final TypeAdapterFactory factory;
private final TypeToken<R> typeToken;
@Override
public void write(JsonWriter out, R value) throws IOException {
if (Optional.class.isAssignableFrom(this.typeToken.getRawType())) {
Optional opt = (Optional) value;
if (opt != null && opt.isPresent()) {
Object actualValue = opt.get();
writeObject(actualValue, out);
} else {
out.beginObject();
out.endObject();
}
} else {
writeObject(value, out);
}
}
@Override
public R read(JsonReader in) throws IOException {
JsonElement element = Streams.parse(in);
if (element.isJsonNull()) {
return readNull();
}
JsonObject jsonObject = element.getAsJsonObject();
if (this.typeToken.getRawType() == Optional.class) {
if (jsonObject.has(OBJECT_TYPE)) {
return (R) Optional.of(readValue(jsonObject, null));
} else if (jsonObject.entrySet().isEmpty()) {
return (R) Optional.absent();
} else {
throw new IOException("No class found for Optional value.");
}
}
return this.readValue(jsonObject, this.typeToken);
}
private <S> S readNull() {
if (this.typeToken.getRawType() == Optional.class) {
return (S) Optional.absent();
}
return null;
}
private <S> void writeObject(S value, JsonWriter out) throws IOException {
if (value != null) {
JsonObject jsonObject = new JsonObject();
jsonObject.add(OBJECT_TYPE, new JsonPrimitive(value.getClass().getName()));
TypeAdapter<S> delegate =
(TypeAdapter<S>) this.gson.getDelegateAdapter(this.factory, TypeToken.get(value.getClass()));
jsonObject.add(OBJECT_DATA, delegate.toJsonTree(value));
Streams.write(jsonObject, out);
} else {
out.nullValue();
}
}
private <S> S readValue(JsonObject jsonObject, TypeToken<S> defaultTypeToken) throws IOException {
try {
TypeToken<S> actualTypeToken;
if (jsonObject.isJsonNull()) {
return null;
} else if (jsonObject.has(OBJECT_TYPE)) {
String className = jsonObject.get(OBJECT_TYPE).getAsString();
Class<S> klazz = (Class<S>) Class.forName(className);
actualTypeToken = TypeToken.get(klazz);
} else if (defaultTypeToken != null) {
actualTypeToken = defaultTypeToken;
} else {
throw new IOException("Could not determine TypeToken.");
}
TypeAdapter<S> delegate = this.gson.getDelegateAdapter(this.factory, actualTypeToken);
S value = delegate.fromJsonTree(jsonObject.get(OBJECT_DATA));
return value;
} catch (ClassNotFoundException cnfe) {
throw new IOException(cnfe);
}
}
}
public static <T> Gson getGson(Class<T> clazz) {
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new GsonInterfaceAdapter(clazz)).create();
return gson;
}
}