package com.breeze.jpa;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import javax.persistence.PersistenceUnitUtil;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
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;
/**
* TypeAdapter class for serializing objects that are under JPA control.
* Uses the PersistenceUnitUtil to avoid lazy-loading properties.
* TODO use an interface and custom wrapper around PersistenceUnitUtil / HibernateUtil so this class is reusable for both
*
* @param <T> Gson creates a different instance for each type
*/
public class JPATypeAdapter<T> extends TypeAdapter<T> {
private PersistenceUnitUtil puu;
private List<BoundField> boundFields;
JPATypeAdapter(PersistenceUnitUtil puu, List<BoundField> boundFields) {
this.puu = puu;
this.boundFields = boundFields;
}
@Override
public T read(JsonReader in) throws IOException {
throw new UnsupportedOperationException("Not supported");
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Override
public void write(JsonWriter out, T value) throws IOException {
if (value == null) {
out.nullValue();
return;
}
out.beginObject();
try {
for (BoundField bf : this.boundFields) {
if (!bf.isCore && !puu.isLoaded(value, bf.fieldName)) {
out.name(bf.jsonName).nullValue();
} else {
Object fieldValue = bf.field.get(value);
if (fieldValue == null) {
out.name(bf.jsonName).nullValue();
} else if (fieldValue != value) {
out.name(bf.jsonName);
bf.typeAdapter.write(out, fieldValue);
}
}
}
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
throw new IOException(e);
}
out.endObject();
}
/**
* Creates JPATypeAdapter instance for a given type.
* Creates a list of BoundFields, using reflection, to pass to the JPATypeAdapter constructor.
* This avoids reflection at serialization time.
*/
public static class Factory implements TypeAdapterFactory {
private static int MODIFIER_EXCLUDE = Modifier.ABSTRACT | Modifier.INTERFACE | Modifier.STATIC;
private PersistenceUnitUtil puu;
public Factory(PersistenceUnitUtil puu) {
this.puu = puu;
}
public static boolean isCoreType(Class clazz, boolean langOnly) {
// eliminate enums
if (clazz.isEnum()) return true;
String typeName = clazz.getCanonicalName();
// eliminate all simple types
String prefix = langOnly ? "java.lang." : "java.";
if ((typeName.indexOf('.') == -1) || typeName.startsWith(prefix)) {
return true;
}
return false;
}
public final <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
Class rawType = type.getRawType();
if (isCoreType(rawType, false)) return null;
List<BoundField> boundFields = new ArrayList<BoundField>();
// populate list with all JSON-serializable fields on the type
Class raw = type.getRawType();
while (raw != Object.class) {
Field[] fields = raw.getDeclaredFields();
for (Field field : fields) {
int mod = field.getModifiers();
if ((mod & MODIFIER_EXCLUDE) != 0) continue;
field.setAccessible(true);
String fieldName = field.getName();
SerializedName serializedName = field.getAnnotation(SerializedName.class);
String jsonName = serializedName != null ? serializedName.value() : FieldNamingPolicy.IDENTITY.translateName(field);
Class fieldType = field.getType();
boundFields.add(new BoundField(field, fieldName, jsonName, gson.getAdapter(fieldType), isCoreType(fieldType, true)));
}
raw = (Class) raw.getGenericSuperclass(); // go up the hierarchy
}
return new JPATypeAdapter<T>(puu, boundFields);
}
}
/**
* Represents a field on a serializable object
*/
public static class BoundField
{
public BoundField(Field field, String fieldName, String jsonName, TypeAdapter typeAdapter, boolean isCore) {
super();
this.field = field;
this.fieldName = fieldName;
this.jsonName = jsonName;
this.typeAdapter = typeAdapter;
this.isCore = isCore;
}
/** Reflection field */
Field field;
/** Name of the field on the object */
String fieldName;
/** Name of the field in JSON */
String jsonName;
/** TypeAdapter for serializing the field */
TypeAdapter typeAdapter;
/** Field is primitive or java.lang.* */
boolean isCore;
}
}