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