com.mx.path.api.GatewayClassElement Maven / Gradle / Ivy
The newest version!
package com.mx.path.api;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.MirroredTypeException;
import com.mx.path.api.reporting.ClassGenerationException;
import com.mx.path.core.common.gateway.GatewayAPI;
import com.mx.path.core.common.gateway.GatewayBaseClass;
import com.mx.path.core.common.lang.Strings;
import com.mx.path.gateway.accessor.Accessor;
public class GatewayClassElement {
private final List methods = new ArrayList<>();
private final List fields = new ArrayList<>();
private final String simpleName;
private final String basePackage;
private final Class> target;
private final TypeElement baseClass;
private final GatewayBaseClass annotation;
private final String targetBasePackage;
private final String accessorFollow;
private boolean rootGateway = false;
public GatewayClassElement(GatewayClassElement parent, Class> target, String accessorFollow) {
this.target = target;
this.basePackage = parent.basePackage;
this.baseClass = parent.baseClass;
this.simpleName = target.getSimpleName().replace("BaseAccessor", "Gateway");
this.annotation = parent.annotation;
this.targetBasePackage = parent.targetBasePackage;
if (Strings.isNotBlank(accessorFollow)) {
this.accessorFollow = parent.accessorFollow + "." + accessorFollow;
} else {
this.accessorFollow = parent.accessorFollow;
}
setFields(target);
setMethods(target);
validateClassStructure();
}
public GatewayClassElement(TypeElement baseClassElement) throws IllegalArgumentException {
this.baseClass = baseClassElement;
this.annotation = baseClassElement.getAnnotation(GatewayBaseClass.class);
this.target = calculateTargetClass(annotation);
this.targetBasePackage = target.getPackage().getName();
this.accessorFollow = "";
validateAnnotation(annotation);
setFields(target);
setMethods(target);
this.simpleName = annotation.className();
this.basePackage = calculatePackageName(annotation.namespace() + "." + annotation.className(), simpleName);
validateClassStructure();
}
public final GatewayBaseClass getAnnotation() {
return annotation;
}
public final List getMethods() {
return methods;
}
public final TypeElement getBaseClass() {
return baseClass;
}
public final Class> getTarget() {
return target;
}
public final String getBasePackage() {
return basePackage;
}
public final String getAccessorFollow() {
return accessorFollow;
}
public final String getPackage() {
return chomp(this.basePackage + target.getPackage().getName().replace(targetBasePackage, ""), '.');
}
public final String getQualifiedName() {
return getPackage() + "." + simpleName;
}
public final String getSimpleName() {
return simpleName;
}
public final List getFields() {
return fields;
}
public final boolean isRootGateway() {
return this.rootGateway;
}
public final void setRootGateway(boolean rootGateway) {
this.rootGateway = rootGateway;
}
private String calculatePackageName(String qualifiedClassName, String simpleClassName) {
String pakage = qualifiedClassName.replace(simpleClassName, "");
pakage = chomp(pakage, '.');
return pakage;
}
private String chomp(String pakage, char toChomp) {
while (pakage.endsWith(String.valueOf(toChomp))) {
pakage = pakage.substring(0, pakage.length() - 1);
}
return pakage;
}
private void setFields(Class> targetClass) {
Field[] declaredFields = targetClass.getDeclaredFields();
List annotatedFields = Arrays.stream(declaredFields).filter(f -> {
Annotation[] annotations = f.getAnnotationsByType(GatewayAPI.class);
return annotations.length > 0;
}).collect(Collectors.toList());
fields.addAll(annotatedFields);
}
private void validateAnnotation(GatewayBaseClass gatewayBaseClassAnnotation) {
// This is a nice-to-have validation on the GatewayBaseClass annotation.
// todo: Figure out how to check this. Getting a TypeMirror error. Has something to do with rearranging models.
// if (!Accessor.class.isAssignableFrom(gatewayBaseClassAnnotation.target())) {
// throw new IllegalArgumentException("GatewayBaseClass accessorClass must extend " + Accessor.class.getCanonicalName());
// }
if (Strings.isBlank(gatewayBaseClassAnnotation.namespace()) || Strings.isBlank(gatewayBaseClassAnnotation.className())) {
throw new IllegalArgumentException("GatewayBaseClass namespace and className are required");
}
}
private void setMethods(Class> targetClass) {
Method[] declaredMethods = targetClass.getDeclaredMethods();
List filteredMethods = Arrays.stream(declaredMethods)
.filter(method -> {
GatewayAPI gatewayAPIAnnotation = method.getDeclaredAnnotation(GatewayAPI.class);
return gatewayAPIAnnotation != null;
})
.map(ApiMethod::new)
.collect(Collectors.toList());
methods.addAll(filteredMethods);
}
private Class> calculateTargetClass(GatewayBaseClass gatewayBaseClassAnnotation) {
Class> targetClass;
try {
targetClass = gatewayBaseClassAnnotation.target();
} catch (MirroredTypeException mte) {
DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
try {
targetClass = Class.forName(classTypeElement.getQualifiedName().toString());
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("target class must be compiled. Move it to a compiled dependency and include in this project. (" + classTypeElement.getQualifiedName().toString() + ")", e);
}
}
return targetClass;
}
/**
* Scans the fields and methods of the class to ensure the Gateway generator will be able to successful generate
* a Gateway from this class. If an error is found an actionable exception is thrown.
*/
private void validateClassStructure() {
AnsiWrapper ansi = new AnsiWrapper();
// Verify that all sub-accessor fields have a @GatewayAPI annotation.
List accessorProviderMethods = Arrays.stream(target.getDeclaredMethods())
.filter(method -> !method.isAnnotationPresent(GatewayAPI.class) && !method.getName().startsWith("get"))
.filter(method -> Accessor.class.isAssignableFrom(method.getReturnType()))
.collect(Collectors.toList());
for (Method method : accessorProviderMethods) {
if (fields.stream().noneMatch(field -> method.getReturnType().equals(field.getType()))) {
throw new ClassGenerationException()
.withClassName(target.getName())
.withHumanReadableError("No " + ansi.yellow("@GatewayAPI") + " annotation was found for field " + ansi.yellow(method.getName()) + ", but a getter method (" + ansi.yellow(method.getName()) + "()) was found.")
.withFixInstructions("Annotate the field " + ansi.yellow(method.getName()) + " with " + ansi.yellow("@GatewayAPI") + ".");
}
}
// Verify that all sub-accessor fields have setters.
List accessorSetterMethods = Arrays.stream(target.getDeclaredMethods())
.filter(method -> !method.isAnnotationPresent(GatewayAPI.class))
.filter(method -> method.getParameters().length == 1 && Accessor.class.isAssignableFrom(method.getParameters()[0].getType()))
.collect(Collectors.toList());
for (Field field : fields) {
if (accessorSetterMethods.stream().noneMatch(method -> method.getParameters()[0].getType().equals(field.getType()))) {
throw new ClassGenerationException()
.withClassName(target.getName())
.withHumanReadableError("No setter method was found for field " + ansi.yellow(field.getName()) + " of type " + ansi.yellow(field.getType().getName()) + ".")
.withFixInstructions("Add a setter method (annotated with the " + ansi.yellow("@API") + " annotation) named " + ansi.yellow("set") + ansi.yellow(field.getName().substring(0, 1).toUpperCase(Locale.ROOT)) + ansi.yellow(field.getName().substring(1)) + "(...) that sets the " + ansi.yellow(field.getName()) + " field.");
}
}
}
}