/* * Hibernate Search, full-text search for your domain model * * License: GNU Lesser General Public License (LGPL), version 2.1 or later * See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. */ package org.hibernate.search.elasticsearch.gson.impl; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.hibernate.search.elasticsearch.gson.impl.AbstractExtraPropertiesJsonAdapter.ExtraPropertyAdapter; import org.hibernate.search.elasticsearch.gson.impl.AbstractExtraPropertiesJsonAdapter.FieldAdapter; import org.hibernate.search.exception.AssertionFailure; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapterFactory; import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; /** * @author Yoann Rodiere */ public abstract class AbstractConfiguredExtraPropertiesJsonAdapterFactory implements TypeAdapterFactory { @Override public final <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) { Builder<T> builder = new Builder<>( gson, type ); addFields( builder ); return builder.build(); } protected abstract <T> void addFields(Builder<T> builder); protected static final class Builder<T> { private final Gson gson; private final TypeToken<T> type; private final Map<String, FieldAdapter<? super T>> fieldAdapters = new LinkedHashMap<>(); private Builder(Gson gson, TypeToken<T> type) { super(); this.gson = gson; this.type = type; } public <F> Builder<T> add(String fieldName, Class<F> fieldType) { return add( fieldName, TypeToken.get( fieldType ) ); } public <F> Builder<T> add(String fieldName, TypeToken<F> fieldType) { Field field = getField( type, fieldName ); TypeAdapter<F> adapter = gson.getAdapter( fieldType ); boolean first = true; for ( String name : getFieldNames( field ) ) { boolean serialized = first; fieldAdapters.put( name, new ReflectiveFieldAdapter<T, F>( field, adapter, serialized ) ); first = false; } return this; } private TypeAdapter<T> build() { return new Adapter<>( fieldAdapters, getExtraPropertyAdapter( gson, type ), getConstructor( type ) ); } } private static Field getField(TypeToken<?> type, String fieldName) { Class<?> rawType = type.getRawType(); while ( rawType != null ) { try { Field field = rawType.getDeclaredField( fieldName ); field.setAccessible( true ); return field; } catch (NoSuchFieldException ignored) { } rawType = rawType.getSuperclass(); } throw new AssertionFailure( "Missing or inaccessible field " + fieldName + " on type " + type ); } private static <T> ExtraPropertyAdapter<T> getExtraPropertyAdapter(Gson gson, TypeToken<T> type) { Class<?> rawType = type.getRawType(); while ( rawType != null ) { for ( Field field : rawType.getDeclaredFields() ) { SerializeExtraProperties annotation = field.getAnnotation( SerializeExtraProperties.class ); if ( annotation != null ) { field.setAccessible( true ); return new ReflectiveExtraPropertyAdapter<>( field, gson ); } } rawType = rawType.getSuperclass(); } throw new AssertionFailure( "Missing or inaccessible field annotated with " + SerializeExtraProperties.class + " on type " + type ); } @SuppressWarnings("unchecked") private static <T> Constructor<T> getConstructor(TypeToken<T> type) { try { return (Constructor<T>) type.getRawType().getConstructor(); } catch (NoSuchMethodException | SecurityException e) { throw new AssertionFailure( "Missing or inaccessible no-arg constructor on type " + type ); } } private static List<String> getFieldNames(Field f) { SerializedName serializedName = f.getAnnotation( SerializedName.class ); List<String> fieldNames = new LinkedList<String>(); if ( serializedName == null ) { fieldNames.add( f.getName() ); } else { fieldNames.add( serializedName.value() ); for ( String alternate : serializedName.alternate() ) { fieldNames.add( alternate ); } } return fieldNames; } private static class ReflectiveExtraPropertyAdapter<T, F> implements ExtraPropertyAdapter<T> { private final Field extraPropertiesField; private final TypeAdapter<JsonElement> propertyValueAdapter; public ReflectiveExtraPropertyAdapter(Field extraPropertiesField, Gson gson) { super(); this.extraPropertiesField = extraPropertiesField; this.propertyValueAdapter = gson.getAdapter( JsonElement.class ); } @Override public void readOne(JsonReader in, String name, T instance) throws IOException { JsonElement propertyValue = propertyValueAdapter.read( in ); Map<String, JsonElement> extraProperties = getExtraProperties( instance ); if ( extraProperties == null ) { extraProperties = new LinkedHashMap<>(); setExtraProperties( instance, extraProperties ); } extraProperties.put( name, propertyValue ); } @Override public void writeAll(JsonWriter out, T instance) throws IOException { Map<String, JsonElement> extraProperties = getExtraProperties( instance ); if ( extraProperties == null ) { return; } for ( Map.Entry<String, JsonElement> entry : extraProperties.entrySet() ) { out.name( entry.getKey() ); propertyValueAdapter.write( out, entry.getValue() ); } } private void setExtraProperties(T instance, Map<String, JsonElement> extraProperties) { try { extraPropertiesField.set( instance, extraProperties ); } catch (IllegalArgumentException e) { throw new AssertionFailure( "Field " + extraPropertiesField + " annotated with " + SerializeExtraProperties.class + " has the wrong type on " + instance.getClass() ); } catch (IllegalAccessException e) { throw new AssertionFailure( "Field " + extraPropertiesField + " annotated with " + SerializeExtraProperties.class + " is inaccessible on " + instance.getClass() ); } } @SuppressWarnings("unchecked") private Map<String, JsonElement> getExtraProperties(T instance) { try { return (Map<String, JsonElement>) extraPropertiesField.get( instance ); } catch (IllegalAccessException e) { throw new AssertionFailure( "Field " + extraPropertiesField + " annotated with " + SerializeExtraProperties.class + " is inaccessible on " + instance.getClass() ); } } } private static class ReflectiveFieldAdapter<T, F> implements FieldAdapter<T> { private final Field field; private final TypeAdapter<F> typeAdapter; private final boolean serialized; public ReflectiveFieldAdapter(Field field, TypeAdapter<F> typeAdapter, boolean serialized) { super(); this.field = field; this.typeAdapter = typeAdapter; this.serialized = serialized; } @Override public void read(JsonReader in, T instance) throws IOException { try { field.set( instance, typeAdapter.read( in ) ); } catch (IllegalAccessException e) { throw new AssertionFailure( "Field " + field + " is not accessible.", e ); } } @Override public boolean serialized() { return serialized; } @Override @SuppressWarnings("unchecked") public void write(JsonWriter out, T instance) throws IOException { if ( !serialized ) { throw new AssertionFailure( "The property with this name should not be serialized" ); } try { typeAdapter.write( out, (F) field.get( instance ) ); } catch (IllegalAccessException e) { throw new AssertionFailure( "Field " + field + " is not accessible.", e ); } } } private static class Adapter<T> extends AbstractExtraPropertiesJsonAdapter<T> { private final Constructor<T> constructor; public Adapter(Map<String, ? extends FieldAdapter<? super T>> fieldAdapters, ExtraPropertyAdapter<? super T> extraPropertyAdapter, Constructor<T> constructor) { super( fieldAdapters, extraPropertyAdapter ); this.constructor = constructor; } @Override protected T createInstance() { try { return constructor.newInstance(); } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { throw new AssertionFailure( "Constructor " + constructor + " is not accessible.", e ); } } } }