package nl.elastique.poetry.reflection;
import java.lang.reflect.Field;
import java.util.HashMap;
import nl.elastique.poetry.annotations.Nullable;
import nl.elastique.poetry.json.annotations.MapFrom;
/**
* FieldRetriever caches {@link Field} objects to improve performance.
*
* In a test with hundreds of objects, OrmliteReflection.findField() took over 50% CPU time.
* When looking at the Android source code it shows that this is a fairly heavy method.
* Considering that Poetry uses only a certain amount of model classes and fields, it
* makes sense to cache this in memory.
*
* Reference: http://grepcode.com/file/repo1.maven.org/maven2/org.robolectric/android-all/4.4_r1-robolectric-1/libcore/reflect/AnnotationAccess.java#AnnotationAccess.getDeclaredAnnotation%28java.lang.reflect.AnnotatedElement%2Cjava.lang.Class%29
*/
public class FieldRetriever
{
/**
* Maps: model class -> json type -> Field instance
*/
private final HashMap<Class<?>, HashMap<String, Field>> mFieldJsonCache = new HashMap<>();
/**
* Maps: model class -> field type -> Field instance
*/
private final HashMap<Class<?>, HashMap<Class<?>, Field>> mFieldTypeCache = new HashMap<>();
/**
* Retrieve a {@link Field} for a model.
*
* @param modelClass the class to search for the Field
* @param jsonKey the json key that is mapped to the field
* @return the found Field or null
*/
public @Nullable Field getField(Class<?> modelClass, String jsonKey)
{
// Try to retrieve it from cache
Field field = getCachedField(modelClass, jsonKey);
// If not cached, try reflection
if (field == null)
{
field = findField(modelClass, jsonKey);
// Null values are also cached because it will make the next failure quicker
setCachedField(modelClass, jsonKey, field);
}
return field;
}
private @Nullable Field getCachedField(Class<?> classObject, String fieldName)
{
HashMap<String, Field> field_map = mFieldJsonCache.get(classObject);
return (field_map != null) ? field_map.get(fieldName) : null;
}
private void setCachedField(Class<?> classObject, String fieldName, Field field)
{
HashMap<String, Field> field_map = mFieldJsonCache.get(classObject);
if (field_map == null)
{
field_map = new HashMap<>();
mFieldJsonCache.put(classObject, field_map);
}
field_map.put(fieldName, field);
}
/**
* Find a field in a model, providing its JSON attribute name
*
* @param modelClass the model class
* @param name the name of the JSON field
* @return the Field that is found or null
*/
private @Nullable Field findField(Class<?> modelClass, String name)
{
// Check all the fields in the model
for (Field field : modelClass.getDeclaredFields())
{
// Direct match?
if (field.getName().equals(name))
{
return field;
}
// MapFrom-annotated match?
MapFrom map_from = field.getAnnotation(MapFrom.class);
if (map_from != null && name.equals(map_from.value()))
{
return field;
}
}
if (modelClass.getSuperclass() != null)
{
// Recursively check superclass
return findField(modelClass.getSuperclass(), name);
}
else
{
return null;
}
}
/**
* Retrieve a {@link Field} for a model.
*
* @param parentClass the class to search for the Field
* @param fieldClass the Field class to search for
* @return the found Field or null
*/
public @Nullable Field getFirstFieldOfType(Class<?> parentClass, Class<?> fieldClass)
{
// Try to retrieve it from cache
Field field = getCachedField(parentClass, fieldClass);
// If not cached, try reflection
if (field == null)
{
field = findFirstFieldOfType(parentClass, fieldClass);
// Null values are also cached because it will make the next failure quicker
setCachedField(parentClass, fieldClass, field);
}
return field;
}
private @Nullable Field getCachedField(Class<?> parentClass, Class<?> fieldClass)
{
HashMap<Class<?>, Field> field_map = mFieldTypeCache.get(parentClass);
return (field_map != null) ? field_map.get(fieldClass) : null;
}
private void setCachedField(Class<?> parentClass, Class<?> fieldClass, Field field)
{
HashMap<Class<?>, Field> field_map = mFieldTypeCache.get(parentClass);
if (field_map == null)
{
field_map = new HashMap<>();
mFieldTypeCache.put(parentClass, field_map);
}
field_map.put(fieldClass, field);
}
/**
* Finds a field of a certain type in a given parent type
*
* @param parentClass the parent class that holds the field
* @param fieldClass the field class to search for
* @return the found first Field of the specified type or null
*/
public static @Nullable Field findFirstFieldOfType(Class<?> parentClass, Class<?> fieldClass)
{
for (Field field : parentClass.getDeclaredFields())
{
if (field.getType().equals(fieldClass))
{
return field;
}
}
if (parentClass.getSuperclass() != null)
{
// Recursively check superclass
return findFirstFieldOfType(parentClass.getSuperclass(), fieldClass);
}
else
{
return null;
}
}
}