/* * 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; } }