package com.ligati.apipixie.tools;
import com.ligati.apipixie.annotation.APICollection;
import com.ligati.apipixie.annotation.APIEntity;
import com.ligati.apipixie.annotation.APIId;
import com.ligati.apipixie.annotation.APISuperClass;
import com.ligati.apipixie.exception.APIConfigurationException;
import com.ligati.apipixie.exception.APIParsingException;
import org.apache.log4j.Logger;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
public class APIHolder<T, K> {
private static final Logger logger = Logger.getLogger(APIHolder.class);
private final Class<T> clazz;
private final Map<String, Method> setters;
private final Map<String, Method> getters;
private final Map<String, ComplexField> complexFields;
private final Set<String> propertiesNames;
private final boolean failOnUnknownProperties;
private Method idGetter;
public APIHolder(Class<T> clazz, boolean failOnUnknownProperties) {
this.clazz = clazz;
this.failOnUnknownProperties = failOnUnknownProperties;
AnnotationUtil.getEntityAnnotation(clazz);
this.setters = new HashMap<>();
this.getters = new HashMap<>();
this.complexFields = new HashMap<>();
this.propertiesNames = new HashSet<>();
this.extractMethods(clazz);
}
private void extractMethods(Class<T> clazz) {
Map<Field, Method> idCandidates = new HashMap<>();
List<Field> fields = getFields(clazz);
for (Field field : fields) {
if (!TypeUtil.isBasicType(field.getType())) {
// If it is not a basic type, there are 5 possibilities:
ComplexField complexField = null;
// 1) It's a collection (basic or APIEntity)
boolean isAPICollection = field.isAnnotationPresent(APICollection.class);
if (isAPICollection) {
ComplexField.FieldType collectionType = AnnotationUtil.isAPIEntityCollection(field) ? ComplexField.FieldType.ENTITY_COLLECTION : ComplexField.FieldType.BASIC_COLLECTION;
Class<?> mappedClass = AnnotationUtil.getCollectionAnnotation(field).mappedClass();
complexField = new ComplexField(collectionType, mappedClass, (Class<? extends Collection>) field.getType());
}
// 3) It's a date
// TODO Handle the dates (an @APITemporal annotation?)
// 4) It's an enumerate
// TODO Handle the enumerates (an @APIEnum annotation?)
// 5) It's a nested APIEntity
if (AnnotationUtil.referencesAPIEntity(field))
complexField = new ComplexField(ComplexField.FieldType.NESTED_ENTITY, field.getType());
// 6) It's an error!
if (complexField == null)
throw new APIConfigurationException("The field " + field.getName() + " is not a basic type but there is no annotation defining how it should be treated.");
this.complexFields.put(field.getName(), complexField);
}
boolean isIdCandidate = "id".equals(field.getName()) || field.isAnnotationPresent(APIId.class);
String fieldName = this.getFieldNameForMethod(field);
this.propertiesNames.add(field.getName());
// Setter
Class<?> type = field.getType();
String setterName = "set" + fieldName;
try {
Method setter = clazz.getMethod(setterName, type);
this.setters.put(field.getName(), setter);
} catch (NoSuchMethodException | SecurityException e) {
throw new APIConfigurationException("An error occurred while searching for the setter: " + setterName + "(" + type + ") in the class " + clazz, e);
}
// Getter
String getterName = "get" + fieldName;
try {
Method getter = clazz.getMethod(getterName);
this.getters.put(field.getName(), getter);
if (isIdCandidate)
idCandidates.put(field, getter);
} catch (NoSuchMethodException | SecurityException e) {
throw new APIConfigurationException(
"An error occurred while searching for the getter: " + getterName + "() in the class " + clazz, e);
}
}
this.resolveId(idCandidates);
}
private static List<Field> getFields(Class<?> clazz) {
List<Field> fields = new LinkedList<>();
Set<String> names = new HashSet<>();
Class<?> tmp = clazz;
while (tmp.isAnnotationPresent(APISuperClass.class) ||
tmp.isAnnotationPresent(APIEntity.class)) {
for (Field field : tmp.getDeclaredFields()) {
fields.add(field);
names.add(field.getName());
}
tmp = tmp.getSuperclass();
}
if (fields.size() != names.size()) {
Set<String> duplicates = getDuplicatesProperties(fields);
throw new APIConfigurationException("Duplicate propert" + (duplicates.size() > 1 ? "ies" : "y") + " in type hierarchy of class " + clazz + ": " + duplicates);
}
return fields;
}
private static Set<String> getDuplicatesProperties(List<Field> fields) {
Set<String> duplicates = new HashSet<>();
List<String> names = new LinkedList<>();
for (Field field : fields) {
String name = field.getName();
if (names.contains(name))
duplicates.add(name);
names.add(name);
}
return duplicates;
}
private String getFieldNameForMethod(Field field) {
String fieldName = field.getName();
// Capitalize the first letter
String name = fieldName.substring(0, 1).toUpperCase();
// Add the following letters
if (fieldName.length() > 1)
name += fieldName.substring(1);
return name;
}
private void resolveId(Map<Field, Method> candidates) {
if (candidates.size() == 0) {
// No candidates => error
throw new APIConfigurationException("No id property nor @APIId annotation found in the entity.");
} else if (candidates.size() == 1) {
// Only one candidate => we found it!
Iterator<Method> ite = candidates.values().iterator();
this.idGetter = ite.next();
} else if (candidates.size() == 2) {
// It may be an id and an annotation
Field firstField = null;
Boolean firstFieldHasAnnotation = null;
for (Field field : candidates.keySet()) {
if (firstField == null) {
// First element
firstField = field;
firstFieldHasAnnotation = field.isAnnotationPresent(APIId.class);
} else {
// Second element
if (firstFieldHasAnnotation && field.isAnnotationPresent(APIId.class))
throw new APIConfigurationException("Too many candidates to be an id in the entity.");
else if (firstFieldHasAnnotation)
this.idGetter = candidates.get(firstField);
else
this.idGetter = candidates.get(field);
}
}
} else {
throw new APIConfigurationException("Too many candidates to be an id in the entity.");
}
}
public T create() {
try {
return clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
if (logger.isDebugEnabled())
e.printStackTrace();
throw new APIConfigurationException(
"An error occurred while calling the default constructor of the entity "
+ clazz, e);
}
}
public T set(T entity, String name, Object value) {
if (entity == null)
throw new APIParsingException("Null entity");
if (name == null || name.isEmpty())
throw new APIParsingException("Invalid property name (null or empty)");
Method setter = this.setters.get(name);
if (setter == null && this.failOnUnknownProperties)
throw new APIParsingException("Unknown property: " + name);
else if (setter != null)
try {
Method getter = this.getters.get(name);
if (value instanceof Integer && Long.class.equals(getter.getReturnType()))
setter.invoke(entity, Long.valueOf((int) value));
else
setter.invoke(entity, value);
} catch (IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
if (logger.isDebugEnabled())
e.printStackTrace();
throw new APIParsingException("Error while setting the property: " + name, e);
}
return entity;
}
public Object get(T entity, String name) {
if (entity == null)
throw new APIParsingException("Null entity");
Method getter = this.getters.get(name);
if (getter == null)
throw new APIParsingException("Unknown property: " + name);
else
try {
return getter.invoke(entity);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
if (logger.isDebugEnabled())
e.printStackTrace();
throw new APIParsingException("Unknown property: " + name, e);
}
}
@SuppressWarnings("unchecked")
public K getId(T entity) {
try {
return (K) this.idGetter.invoke(entity);
} catch (IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
if (logger.isDebugEnabled())
e.printStackTrace();
throw new APIParsingException("Error while retrieving the entity id.", e);
}
}
public Set<String> getPropertiesNames() {
return this.propertiesNames;
}
public ComplexField getComplexField(String name) {
return this.complexFields.get(name);
}
}