/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.dto.server; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonSyntaxException; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import org.eclipse.che.commons.lang.reflect.ParameterizedTypeImpl; import org.eclipse.che.dto.shared.DTO; import org.eclipse.che.dto.shared.JsonArray; import org.eclipse.che.dto.shared.JsonStringMap; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.StringReader; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.ServiceLoader; import java.util.concurrent.ConcurrentHashMap; /** * Provides implementations of DTO interfaces. * * @author andrew00x */ public final class DtoFactory { private static final LoadingCache<Type, ParameterizedType> listTypeCache = CacheBuilder.newBuilder().concurrencyLevel(16).build( new CacheLoader<Type, ParameterizedType>() { @Override public ParameterizedType load(Type type) { return new ParameterizedTypeImpl(List.class, type); } }); private static final LoadingCache<Type, ParameterizedType> mapTypeCache = CacheBuilder.newBuilder().concurrencyLevel(16).build( new CacheLoader<Type, ParameterizedType>() { @Override public ParameterizedType load(Type type) { return new ParameterizedTypeImpl(Map.class, String.class, type); } }); private static final DtoFactory INSTANCE = new DtoFactory(); public static DtoFactory getInstance() { return INSTANCE; } /** * Ge a {@link Gson} serializer that is configured to serializes/deserializes DTOs correctly. * * @return A Gson. */ public Gson getGson() { return dtoGson; } /** * Creates new instance of class which implements specified DTO interface. * * @param dtoInterface * DTO interface * @throws IllegalArgumentException * if can't provide any implementation for specified interface */ public static <T> T newDto(Class<T> dtoInterface) { return getInstance().createDto(dtoInterface); } private final Map<Class<?>, DtoProvider<?>> dtoInterface2Providers = new ConcurrentHashMap<>(); // Additional mapping for implementation of DTO interfaces. // It helps avoid reflection when need create copy of exited DTO instance. private final Map<Class<?>, DtoProvider<?>> dtoImpl2Providers = new ConcurrentHashMap<>(); private final Gson dtoGson = new GsonBuilder() .registerTypeAdapterFactory(new NullAsEmptyTAF<>(Collection.class, Collections.emptyList())) .registerTypeAdapterFactory(new NullAsEmptyTAF<>(Map.class, Collections.emptyMap())) .registerTypeAdapterFactory(new DtoInterfaceTAF()).create(); /** * Created deep copy of DTO object. * * @param origin * origin DTO object * @return copy * @throws IllegalArgumentException * if specified object doesn't implement DTO interface annotated with {@link org.eclipse.che.dto.shared.DTO @DTO} or if * specified instance implements more than one interface annotated with {@link org.eclipse.che.dto.shared.DTO @DTO} */ @SuppressWarnings("unchecked") public <T> T clone(T origin) { final Class<?> implClass = origin.getClass(); DtoProvider provider = dtoImpl2Providers.get(implClass); if (provider == null) { Class<?> dtoInterface = null; Class<?>[] interfaces = implClass.getInterfaces(); if (interfaces.length == 0) { return null; } for (Class<?> i : interfaces) { if (i.isAnnotationPresent(DTO.class)) { if (dtoInterface != null) { throw new IllegalArgumentException("Unable determine DTO interface. Type " + implClass.getName() + " implements or extends more than one interface annotated with @DTO annotation."); } dtoInterface = i; } } if (dtoInterface != null) { provider = getDtoProvider(dtoInterface); } } if (provider == null) { throw new IllegalArgumentException("Unknown DTO type " + implClass); } return (T)provider.clone(origin); } /** * Shortcut for {@code DtoFactory.getInstance().clone(T dtoObject)} * * @see #clone(Object) */ public static <T> T cloneDto(T origin) { return getInstance().clone(origin); } public <T> String toJson(T dto) { if (dto instanceof JsonSerializable) { return ((JsonSerializable)dto).toJson(); } throw new IllegalArgumentException("JsonSerializable instance required. "); } public <T> JsonElement toJsonElement(T dto) { if (dto instanceof JsonSerializable) { return ((JsonSerializable)dto).toJsonElement(); } throw new IllegalArgumentException("JsonSerializable instance required. "); } /** * Creates new instance of class which implements specified DTO interface. * * @param dtoInterface * DTO interface * @throws IllegalArgumentException * if can't provide any implementation for specified interface */ public <T> T createDto(Class<T> dtoInterface) { return getDtoProvider(dtoInterface).newInstance(); } // /** * Creates new instance of class which implements specified DTO interface, parses specified JSON string and uses parsed data for * initializing fields of DTO object. * * @param json * JSON data * @param dtoInterface * DTO interface * @throws IllegalArgumentException * if can't provide any implementation for specified interface */ public <T> T createDtoFromJson(String json, Class<T> dtoInterface) { try { return createDtoFromJson(new StringReader(json), dtoInterface); } catch (IOException e) { throw new RuntimeException(e); // won't happen } } /** * Creates new instance of class which implements specified DTO interface, uses the specific JSON data for * initializing fields of DTO object. * * @param json * JSON data * @param dtoInterface * DTO interface * @throws IllegalArgumentException * if can't provide any implementation for specified interface */ public <T> T createDtoFromJson(JsonElement json, Class<T> dtoInterface) { return getDtoProvider(dtoInterface).fromJson(json); } /** * Creates new instance of class which implements specified DTO interface, parses specified JSON data and uses parsed data for * initializing fields of DTO object. * * @param json * JSON data * @param dtoInterface * DTO interface * @throws IllegalArgumentException * if can't provide any implementation for specified interface * @throws IOException * if an i/o error occurs */ public <T> T createDtoFromJson(Reader json, Class<T> dtoInterface) throws IOException { getDtoProvider(dtoInterface); return dtoGson.fromJson(json, dtoInterface); } /** * Creates new instance of class which implements specified DTO interface, parses specified JSON data and uses parsed data for * initializing fields of DTO object. * * @param json * JSON data * @param dtoInterface * DTO interface * @throws IllegalArgumentException * if can't provide any implementation for specified interface * @throws IOException * if an i/o error occurs */ public <T> T createDtoFromJson(InputStream json, Class<T> dtoInterface) throws IOException { return createDtoFromJson(new InputStreamReader(json), dtoInterface); } // /** * Parses the JSON data from the specified sting into list of objects of the specified type. * * @param json * JSON data * @param dtoInterface * DTO interface * @return list of DTO * @throws IllegalArgumentException * if can't provide any implementation for specified interface */ public <T> JsonArray<T> createListDtoFromJson(String json, Class<T> dtoInterface) { try { return createListDtoFromJson(new StringReader(json), dtoInterface); } catch (IOException e) { throw new RuntimeException(e); // won't happen } } /** * Parses the JSON data from the specified reader into list of objects of the specified type. * * @param json * JSON data * @param dtoInterface * DTO interface * @return list of DTO * @throws IllegalArgumentException * if can't provide any implementation for specified interface */ public <T> JsonArray<T> createListDtoFromJson(Reader json, Class<T> dtoInterface) throws IOException { getDtoProvider(dtoInterface); final List<T> list = parseDto(json, listTypeCache.getUnchecked(dtoInterface)); return new JsonArrayImpl<>(list); } /** * Parses the JSON data from the specified stream into list of objects of the specified type. * * @param json * JSON data * @param dtoInterface * DTO interface * @return list of DTO * @throws IllegalArgumentException * if can't provide any implementation for specified interface * @throws IOException * if an i/o error occurs */ public <T> JsonArray<T> createListDtoFromJson(InputStream json, Class<T> dtoInterface) throws IOException { return createListDtoFromJson(new InputStreamReader(json), dtoInterface); } // /** * Parses the JSON data from the specified sting into map of objects of the specified type. * * @param json * JSON data * @param dtoInterface * DTO interface * @return map of DTO * @throws IllegalArgumentException * if can't provide any implementation for specified interface */ public <T> JsonStringMap<T> createMapDtoFromJson(String json, Class<T> dtoInterface) { try { return createMapDtoFromJson(new StringReader(json), dtoInterface); } catch (IOException e) { throw new RuntimeException(e); // won't happen } } /** * Parses the JSON data from the specified reader into map of objects of the specified type. * * @param json * JSON data * @param dtoInterface * DTO interface * @return map of DTO * @throws IllegalArgumentException * if can't provide any implementation for specified interface * @throws IOException * if an i/o error occurs */ public <T> JsonStringMap<T> createMapDtoFromJson(Reader json, Class<T> dtoInterface) throws IOException { getDtoProvider(dtoInterface); final Map<String, T> map = parseDto(json, mapTypeCache.getUnchecked(dtoInterface)); return new JsonStringMapImpl<>(map); } /** * Parse a JSON string that contains DTOs, propagating JSON exceptions correctly if they are caused by failures in * the given Reader. Real JSON syntax exceptions are propagated as-is. */ private <T> T parseDto(Reader json, Type type) throws IOException { try { return dtoGson.fromJson(json, type); } catch (JsonSyntaxException e) { final Throwable cause = e.getCause(); if (cause instanceof IOException) { throw (IOException) cause; } throw e; } } /** * Parses the JSON data from the specified stream into map of objects of the specified type. * * @param json * JSON data * @param dtoInterface * DTO interface * @return map of DTO * @throws IllegalArgumentException * if can't provide any implementation for specified interface * @throws IOException * if an i/o error occurs */ public <T> JsonStringMap<T> createMapDtoFromJson(InputStream json, Class<T> dtoInterface) throws IOException { return createMapDtoFromJson(new InputStreamReader(json), dtoInterface); } // @SuppressWarnings("unchecked") private <T> DtoProvider<T> getDtoProvider(Class<T> dtoInterface) { DtoProvider<?> dtoProvider = dtoInterface2Providers.get(dtoInterface); if (dtoProvider == null) { throw new IllegalArgumentException("Unknown DTO type " + dtoInterface); } return (DtoProvider<T>)dtoProvider; } /** * Registers DtoProvider for DTO interface. * * @param dtoInterface * DTO interface * @param provider * provider for DTO interface * @see DtoProvider */ public void registerProvider(Class<?> dtoInterface, DtoProvider<?> provider) { dtoInterface2Providers.put(dtoInterface, provider); dtoImpl2Providers.put(provider.getImplClass(), provider); } /** * Unregisters DtoProvider. * * @see #registerProvider(Class, DtoProvider) */ public DtoProvider<?> unregisterProvider(Class<?> dtoInterface) { final DtoProvider<?> dtoProvider = dtoInterface2Providers.remove(dtoInterface); if (dtoProvider != null) { dtoImpl2Providers.remove(dtoProvider.getImplClass()); } return dtoProvider; } /** Test weather or not this DtoFactory has any DtoProvider which can provide implementation of DTO interface. */ public boolean hasProvider(Class<?> dtoInterface) { return dtoInterface2Providers.get(dtoInterface) != null; } /** * A specialization of Gson's {@link ReflectiveTypeAdapterFactory} delegates operation on DTO interfaces to the * corresponding implementation classes. The implementation classes generated correctly by the DTO Gson. * * @author tareq.sha@gmail.com */ private class DtoInterfaceTAF implements TypeAdapterFactory { @Override @SuppressWarnings("unchecked") public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { DtoProvider<?> prov = dtoInterface2Providers.get(type.getRawType()); if (prov != null) { return (TypeAdapter<T>) gson.getAdapter(prov.getImplClass()); } return null; } } /** * Wraps Gson's default List/Map adapter factories serialize null List/Map fields as empty instead. * * @author tareq.sha@gmail.com */ private static class NullAsEmptyTAF<U> implements TypeAdapterFactory { final Class<?> matchedClass; final U defaultValue; NullAsEmptyTAF(Class<U> matchedClass, U defaultValue) { this.matchedClass = matchedClass; this.defaultValue = defaultValue; } @Override @SuppressWarnings("unchecked") public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { if (!matchedClass.isAssignableFrom(type.getRawType())) { return null; } TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type); return new TypeAdapter<T>() { @Override public void write(JsonWriter out, T value) throws IOException { delegate.write(out, value != null ? value : (T) defaultValue); } @Override public T read(JsonReader in) throws IOException { return delegate.read(in); } }; } } static { for (DtoFactoryVisitor visitor : ServiceLoader.load(DtoFactoryVisitor.class)) { visitor.accept(INSTANCE); } } private DtoFactory() { } }