From 6ad2287f40888fc81feca1322bbf941e076f4c19 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Thu, 24 Aug 2023 01:07:25 +0200 Subject: feat: implemented processor --- build.gradle | 2 +- .../java/ftbsc/geb/processor/GEBProcessor.java | 191 +++++++++++++++------ 2 files changed, 136 insertions(+), 57 deletions(-) diff --git a/build.gradle b/build.gradle index 4fbb165..f7ca645 100644 --- a/build.gradle +++ b/build.gradle @@ -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 { diff --git a/src/main/java/ftbsc/geb/processor/GEBProcessor.java b/src/main/java/ftbsc/geb/processor/GEBProcessor.java index c72bf8c..d464549 100644 --- a/src/main/java/ftbsc/geb/processor/GEBProcessor.java +++ b/src/main/java/ftbsc/geb/processor/GEBProcessor.java @@ -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> listenerMap = new HashMap<>(); + + private final Set generatedClasses = new HashSet<>(); + @Override public boolean process(Set set, RoundEnvironment env) { - boolean claimed = false; - for(TypeElement ann : set) { - BiConsumer 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; + for(Element e : env.getElementsAnnotatedWith(ann)) + this.processListener(e); + if(!this.listenerMap.isEmpty()) { + this.generateClasses(); + this.generateServiceProvider(); + return true; + } else return false; + } - claimed = true; + private final TypeMirror listenerInterface = this.processingEnv.getElementUtils() + .getTypeElement("ftbsc.geb.api.IListener").asType(); - for(Element e : env.getElementsAnnotatedWith(ann)) - processMethod.accept(this, e); - } - return claimed; - } + private final TypeMirror eventInterface = this.processingEnv.getElementUtils() + .getTypeElement("ftbsc.geb.api.IEvent").asType(); - private final Map> listeners = new HashMap<>(); + private final TypeMirror dispatcherInterface = this.processingEnv.getElementUtils() + .getTypeElement("ftbsc.geb.api.IEventDispatcher").asType(); - private static List getMembersAnnotatedWith(TypeElement typeElement, Class ann) { - return typeElement.getEnclosedElements() - .stream() - .filter(elem -> elem.getAnnotation(ann) != null) - .collect(Collectors.toList()); + private void processListener(Element target) { + ExecutableElement listener = (ExecutableElement) target; //this cast will never fail + + //ensure the parent is instance of IListener + TypeMirror parentType = listener.getEnclosingElement().asType(); + if(!this.processingEnv.getTypeUtils().isAssignable(parentType, this.listenerInterface)) + return; //TODO throw error, parent doesn't implement the interface + + //ensure the listener method has only a single IEvent parameter + List 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)); } - private void processListener(Element target) { - ExecutableElement listener = (ExecutableElement) target; //this will never fail - Listen listenerAnn = target.getAnnotation(Listen.class); + private void generateClasses() { + this.listenerMap.forEach((event, listeners) -> { + TypeElement eventClass = (TypeElement) this.processingEnv.getTypeUtils().asElement(event); - //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(); + //reorder the injectors to follow priority + List ordered = listeners.stream().sorted(Comparator.comparingInt( + container -> container.annotation.priority() + )).collect(Collectors.toList()); - //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; - - 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 - } - - List instanceSources = getMembersAnnotatedWith(parent, ListenerInstance.class); - - if(instanceSources.size() != 1) - return; //TODO throw error, there should always be only one per class - - Element instanceSource = instanceSources.get(0); - List 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); + } + + 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 void processEvent(Element target) { - //TODO + 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); + } } } -- cgit v1.2.3-56-ga3b1