feat: implemented processor

This commit is contained in:
zaaarf 2023-08-24 01:07:25 +02:00
parent 2f798cb8a7
commit 6ad2287f40
No known key found for this signature in database
GPG key ID: 6445A5CD15E5B40C
2 changed files with 135 additions and 56 deletions

View file

@ -18,8 +18,8 @@ repositories {
}
dependencies {
implementation 'ftbsc:geb:0.1.4'
implementation 'com.squareup:javapoet:1.13.0'
implementation 'ftbsc:geb:0.1.0'
}
jar {

View file

@ -1,84 +1,163 @@
package ftbsc.geb.processor;
import ftbsc.geb.api.annotations.Event;
import com.squareup.javapoet.*;
import ftbsc.geb.api.annotations.Listen;
import ftbsc.geb.api.annotations.ListenerInstance;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;
import java.lang.annotation.Annotation;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
/**
*
*/
@SupportedAnnotationTypes({"ftbsc.geb.api.annotations.*"})
public class GEBProcessor extends AbstractProcessor {
private final Map<TypeMirror, Set<ListenerContainer>> listenerMap = new HashMap<>();
private final Set<String> generatedClasses = new HashSet<>();
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment env) {
boolean claimed = false;
for(TypeElement ann : set) {
BiConsumer<GEBProcessor, Element> processMethod;
for(TypeElement ann : set)
if(ann.getQualifiedName().contentEquals(Listen.class.getName()))
processMethod = GEBProcessor::processListener;
else if(ann.getQualifiedName().contentEquals(Event.class.getName()))
processMethod = GEBProcessor::processEvent;
else continue;
claimed = true;
for(Element e : env.getElementsAnnotatedWith(ann))
processMethod.accept(this, e);
}
return claimed;
this.processListener(e);
if(!this.listenerMap.isEmpty()) {
this.generateClasses();
this.generateServiceProvider();
return true;
} else return false;
}
private final Map<Element, List<ExecutableElement>> listeners = new HashMap<>();
private final TypeMirror listenerInterface = this.processingEnv.getElementUtils()
.getTypeElement("ftbsc.geb.api.IListener").asType();
private static List<Element> getMembersAnnotatedWith(TypeElement typeElement, Class<? extends Annotation> ann) {
return typeElement.getEnclosedElements()
.stream()
.filter(elem -> elem.getAnnotation(ann) != null)
.collect(Collectors.toList());
}
private final TypeMirror eventInterface = this.processingEnv.getElementUtils()
.getTypeElement("ftbsc.geb.api.IEvent").asType();
private final TypeMirror dispatcherInterface = this.processingEnv.getElementUtils()
.getTypeElement("ftbsc.geb.api.IEventDispatcher").asType();
private void processListener(Element target) {
ExecutableElement listener = (ExecutableElement) target; //this will never fail
Listen listenerAnn = target.getAnnotation(Listen.class);
//ensure the parent is a class
if(!(target.getEnclosingElement() instanceof TypeElement))
return; //TODO throw error, means the annotated field was in a method
TypeElement parent = (TypeElement) target.getEnclosingElement();
ExecutableElement listener = (ExecutableElement) target; //this cast will never fail
//ensure the parent is instance of IListener
TypeElement cursor = parent;
TypeMirror listenerInterface = this.processingEnv.getElementUtils().getTypeElement("ftbsc.geb.api.IListener").asType()
; while(cursor != null) {
if(cursor.getInterfaces().contains(listenerInterface))
break;
TypeMirror parentType = listener.getEnclosingElement().asType();
if(!this.processingEnv.getTypeUtils().isAssignable(parentType, this.listenerInterface))
return; //TODO throw error, parent doesn't implement the interface
Element superclass = this.processingEnv.getTypeUtils().asElement(cursor.getSuperclass());
if(superclass instanceof TypeElement)
cursor = (TypeElement) superclass;
else return; //TODO throw error, parent doesnt implement the interface
//ensure the listener method has only a single IEvent parameter
List<? extends VariableElement> params = listener.getParameters();
if(listener.getParameters().size() != 1)
return; //TODO throw error, bad parameter amount
TypeMirror event = params.get(0).asType();
if(!this.processingEnv.getTypeUtils().isAssignable(event, this.eventInterface))
return; //TODO throw error, bad parameter type
if(!this.listenerMap.containsKey(event))
this.listenerMap.put(event, new HashSet<>());
this.listenerMap.get(event).add(new ListenerContainer(listener));
}
List<Element> instanceSources = getMembersAnnotatedWith(parent, ListenerInstance.class);
private void generateClasses() {
this.listenerMap.forEach((event, listeners) -> {
TypeElement eventClass = (TypeElement) this.processingEnv.getTypeUtils().asElement(event);
if(instanceSources.size() != 1)
return; //TODO throw error, there should always be only one per class
//reorder the injectors to follow priority
List<ListenerContainer> ordered = listeners.stream().sorted(Comparator.comparingInt(
container -> container.annotation.priority()
)).collect(Collectors.toList());
Element instanceSource = instanceSources.get(0);
List<ExecutableElement> listenerList = listeners.computeIfAbsent(instanceSource, k -> new ArrayList<>());
listenerList.add(listener);
ParameterSpec eventParam = ParameterSpec.builder(TypeName.get(this.eventInterface), "event").build();
ParameterSpec listenersParam = ParameterSpec.builder(ParameterizedTypeName.get(
ClassName.get("java.util", "Map"), ParameterizedTypeName.get(
ClassName.get("java.lang", "Class"),
WildcardTypeName.subtypeOf(TypeName.get(this.dispatcherInterface))),
ClassName.get(this.listenerInterface)), "listeners")
.build();
MethodSpec.Builder callListenersBuilder = MethodSpec.methodBuilder("callListeners")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class) //because why not
.addMember("value" , "{$S}", "unchecked").build())
.addParameter(eventParam)
.addParameter(listenersParam)
.returns(boolean.class);
int counter = 0;
for(ListenerContainer listener : ordered) {
String varName = String.format("listener%d", counter);
callListenersBuilder
.addStatement("$T $L = $N.get($T.class)", varName, this.listenerInterface, listenersParam, listener.parent)
.addStatement("if($L.isActive()) (($T) $L).$L($N)", varName, listener.parent,
listener.method.getSimpleName().toString(), eventParam);
}
private void processEvent(Element target) {
//TODO
MethodSpec eventType = MethodSpec.methodBuilder("eventType")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(Class.class)
.addStatement("return $T.class", event)
.build();
String clazzName = String.format("%sDispatcher", eventClass.getSimpleName());
TypeSpec clazz = TypeSpec.classBuilder(clazzName)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(this.dispatcherInterface)
.addMethod(callListenersBuilder.build())
.addMethod(eventType)
.build();
String packageName = "ftbsc.geb.generated";
JavaFile javaFile = JavaFile.builder(packageName, clazz).build();
String resultingClassName = String.format("%s.%s", packageName, clazzName);
try {
JavaFileObject injectorFile = processingEnv.getFiler().createSourceFile(resultingClassName);
PrintWriter out = new PrintWriter(injectorFile.openWriter());
javaFile.writeTo(out);
out.close();
} catch(IOException e) {
throw new RuntimeException(e);
}
});
}
/**
* Generates the Service Provider file for the dispatchers.
*/
private void generateServiceProvider() {
try {
FileObject serviceProvider = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "",
"META-INF/services/ftbsc.geb.api.IEventDispatcher");
PrintWriter out = new PrintWriter(serviceProvider.openWriter());
this.generatedClasses.forEach(out::println);
out.close();
} catch(IOException e) {
throw new RuntimeException(e);
}
}
private static class ListenerContainer {
public final ExecutableElement method;
public final TypeMirror parent;
public final Listen annotation;
public ListenerContainer(ExecutableElement method) {
this.method = method;
this.parent = method.getEnclosingElement().asType();
this.annotation = method.getAnnotation(Listen.class);
}
}
}