package xapi.model.impl;
import xapi.annotation.model.DeleterFor;
import xapi.annotation.model.GetterFor;
import xapi.annotation.model.IsModel;
import xapi.annotation.model.SetterFor;
import xapi.annotation.reflect.Fluent;
import xapi.except.NotConfiguredCorrectly;
import xapi.model.api.Model;
import xapi.model.api.ModelManifest;
import xapi.model.api.ModelManifest.MethodData;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
public class ModelUtil {
public static ModelManifest createManifest(final Class<? extends Model> cls) {
final ModelManifest manifest = new ModelManifest(cls);
final Set<Class<?>> allTypes = new LinkedHashSet<>();
collectAllTypes(allTypes, cls);
// Collect up all the fields that are user defined...
for (final Class<?> type : allTypes) {
if (type == Model.class) {
continue;
}
final IsModel isModel = cls.getAnnotation(IsModel.class);
String idField = isModel == null ? "id" : isModel.key().value();
for (final Method method : type.getMethods()) {
if (method.getDeclaringClass() != Model.class) {
if (method.isDefault()) {
continue; // no need to override default methods!
}
if (!manifest.hasSeenMethod(method.getName())) {
final MethodData property = manifest.addProperty(method.getName(), idField, method.getAnnotation(GetterFor.class),
method.getAnnotation(SetterFor.class), method.getAnnotation(DeleterFor.class));
property.setIdField(idField);
final Class<?> dataType;
final Type genericType;
if (property.isGetter(method.getName())) {
// For a getter, we will determine the field type by the return type
dataType = method.getReturnType();
genericType = method.getGenericReturnType();
} else {
// For setters and deleters, we will determine the field type by the first parameter type.
// However, a removeAll method may have no parameters, in which case we will have to wait.
if (method.getParameterTypes().length > 0) {
dataType = method.getParameterTypes()[0];
genericType = method.getGenericParameterTypes()[0];
} else {
dataType = null;
genericType = null;
}
}
if (dataType != null) {
final Class<?> oldType = property.getType();
if (oldType != null && oldType != dataType) {
throw new NotConfiguredCorrectly("Field "+property.getName()+" for "+cls+" has data type "
+ "disagreement; already saw type "+oldType+" but now saw "+dataType+". Get/set/remove methods "
+ "must have identical type information");
}
property.setType(dataType);
Class[] erasedTypes = getErasedTypeParameters(genericType);
property.setTypeParams(erasedTypes);
}
property.addAnnotations(method.getAnnotations());
}
}
}
}
return manifest;
}
static Class[] getErasedTypeParameters(Type genericType) {
if (genericType instanceof ParameterizedType) {
final ParameterizedType typed = (ParameterizedType) genericType;
final Type[] paramTypes = typed.getActualTypeArguments();
Class[] erasedTypes = new Class[paramTypes.length];
for (int i = 0, m = paramTypes.length; i < m; i++ ) {
final Type paramType = paramTypes[i];
erasedTypes[i] = getErasedType(paramType);
}
return erasedTypes;
} else if (genericType instanceof TypeVariable) {
final Type[] bounds = ((TypeVariable) genericType).getBounds();
if (bounds.length != 1) {
// throw an exception with a good message
}
return getErasedTypeParameters(bounds[0]);
} else {
return new Class[0];
}
}
static Class getErasedType(Type paramType) {
if (paramType instanceof Class) {
return (Class) paramType;
} else if (paramType instanceof ParameterizedType) {
return (Class) // We are asking the parameterized type for it's raw type, which is always Class
((ParameterizedType) paramType).getRawType();
} else if (paramType instanceof WildcardType) {
final Type[] upper = ((WildcardType) paramType).getUpperBounds();
return getErasedType(upper[0]);
} else if (paramType instanceof java.lang.reflect.TypeVariable){
final Type[] bounds = ((TypeVariable) paramType).getBounds();
if (bounds.length > 1) {
Class[] erasedBounds = new Class[bounds.length];
Class weakest = null;
for (int i = 0; i < erasedBounds.length; i++) {
erasedBounds[i] = getErasedType(bounds[i]);
if (i == 0) {
weakest = erasedBounds[i];
} else {
weakest = getWeakest(weakest, erasedBounds[i]);
}
// If we are erased to object, stop erasing...
if (weakest == Object.class) {
break;
}
}
return weakest;
}
// System.err.println("Unsupported: "+ Arrays.asList(bounds));
return getErasedType(bounds[0]);
} else {
throw new UnsupportedOperationException();
}
}
private static Class getWeakest(Class one, Class two) {
if (one.isAssignableFrom(two)) {
return one;
} else if (two.isAssignableFrom(one)) {
return two;
}
// No luck on quick check... Look in interfaces.
Set<Class<?>> oneInterfaces = getFlattenedInterfaces(one);
Set<Class<?>> twoInterfaces = getFlattenedInterfaces(two);
// Keep only shared interfaces
oneInterfaces.retainAll(twoInterfaces);
Class strongest = Object.class;
for (Class<?> cls : oneInterfaces) {
// There is a winning type...
if (strongest.isAssignableFrom(cls)){
strongest = cls;
} else if (!cls.isAssignableFrom(strongest)){
return Object.class;
}
}
// Will be Object.class if there were no shared interfaces (or shared interfaces were not compatible).
return strongest;
}
private static Set<Class<?>> getFlattenedInterfaces(Class cls) {
final Set<Class<?>> set = new HashSet<>();
while (cls != null && cls != Object.class) {
for (Class iface : cls.getInterfaces()) {
collectInterfaces(set, iface);
}
cls = cls.getSuperclass();
}
return set;
}
private static void collectAllTypes(final Collection<Class<?>> into, final Class<?> cls) {
into.add(cls);
final Class<?> superCls = cls.getSuperclass();
if (superCls != null && superCls != Object.class) {
collectAllTypes(into, cls);
}
collectInterfaces(into, cls);
}
private static void collectInterfaces(final Collection<Class<?>> into, final Class<?> cls) {
if (into.add(cls)) {
for (final Class<?> iface : cls.getInterfaces()) {
if (into.add(iface)) {
collectInterfaces(into, iface);
}
}
}
}
/**
* @param method
* @return
*/
public static boolean isFluent(final Method method) {
final Class<?> methodType = method.getDeclaringClass();
final Class<?> returnType = method.getReturnType();
if (returnType == null || returnType == void.class) {
return false;
}
if (
areAssignable(methodType, returnType)
) {
// Returning this would be allowed.
// However, we should guard against methods that may actually want to return a field
// that is the same type as itself.
final Fluent fluent = method.getAnnotation(Fluent.class);
if (fluent != null) {
return fluent.value();
}
if (method.getParameterTypes().length > 0) {
// check if there is a single parameter type which is also compatible,
// and throw an error telling the user that they must specify @Fluent(true) or @Fluent(false)
// Because the method signature is ambiguous
if (areAssignable(methodType, method.getParameterTypes()[0])) {
throw new NotConfiguredCorrectly("Method "+method.toGenericString()+" in "+methodType
+" has ambiguous return type; cannot tell if this method is Fluent. Please annotate "
+ "this method with @Fluent(true) if the method is supposed to `return this;` or "
+ "use @Fluent(false) if this method is supposed to return the first parameter.");
}
}
return true;
}
return false;
}
/**
* @return true if either type is assignable to the other.
*/
public static boolean areAssignable(final Class<?> type1, final Class<?> type2) {
return type1.isAssignableFrom(type2)
|| type2.isAssignableFrom(type1);
}
/**
* @param cls -> The class to convert into a model name.
* @return cls.getSimpleName().replace("Model", "");
*/
public static String guessModelType(final Class<? extends Model> cls) {
final IsModel isModel = cls.getAnnotation(IsModel.class);
if (isModel == null) {
return guessModelType(cls.getSimpleName());
} else {
return isModel.modelType();
}
}
public static String guessModelType(final String simpleName) {
final String type = simpleName.replace("Model", "");
assert type.length() > 0 : "Cannot have a model class named Model!";
return Character.toLowerCase(type.charAt(0)) + type.substring(1);
}
}