All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.antoniomacri.reactivegwt.processor.ReactiveGwtProcessor Maven / Gradle / Ivy

The newest version!
package com.github.antoniomacri.reactivegwt.processor;

import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeVariableName;
import com.squareup.javapoet.WildcardTypeName;
import jakarta.annotation.Generated;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeVariable;
import javax.tools.Diagnostic.Kind;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import static java.util.Collections.singleton;
import static javax.lang.model.element.Modifier.ABSTRACT;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;

public class ReactiveGwtProcessor extends AbstractProcessor {

    private static final String ASYNC_FIELD = "async$";
    private static final String GWT_RPC = "com.google.gwt.user.client.rpc";
    private static final Set SUPPORTED_ANNOTATIONS = Set.of(GWT_RPC + ".RemoteServiceRelativePath");

    private static final ClassName AsyncCallback = ClassName.get(GWT_RPC, "AsyncCallback");
    private static final ClassName Request = ClassName.get("com.google.gwt.http.client", "Request");
    private static final ClassName Uni = ClassName.get("io.smallrye.mutiny", "Uni");
    private static final ClassName UniEmitter = ClassName.get("io.smallrye.mutiny.subscription", "UniEmitter");


    @Override
    public Set getSupportedOptions() {
        return singleton("debug");
    }

    @Override
    public Set getSupportedAnnotationTypes() {
        return SUPPORTED_ANNOTATIONS;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        if (roundEnv.processingOver()) {
            return false;
        }
        annotations.stream()
                .flatMap(a -> roundEnv.getElementsAnnotatedWith(a).stream())
                .filter(e -> e.getKind().isInterface() && e instanceof TypeElement)
                .map(e -> (TypeElement) e)
                .forEach(rpcService -> {
                    try {
                        process(rpcService);
                    } catch (Exception e) {
                        if (e instanceof RuntimeException) {
                            throw (RuntimeException) e;
                        } else {
                            throw new RuntimeException("Error processing RPC service " + rpcService, e);
                        }
                    }
                });
        return false;
    }

    private void process(TypeElement rpcService) throws Exception {
        ClassName rpcName = ClassName.get(rpcService);
        log("RPC service interface: " + rpcName);

        List methods = rpcService.getEnclosedElements().stream()
                .filter(e -> e.getKind() == ElementKind.METHOD && e instanceof ExecutableElement)
                .map(e -> (ExecutableElement) e)
                .filter(method -> !(method.getModifiers().contains(STATIC) || method.isDefault()))
                .toList();

        AnnotationSpec generated = AnnotationSpec.builder(Generated.class)
                .addMember("value", "$S", getClass().getName()).build();

        Filer filer = processingEnv.getFiler();

        ClassName asyncName = ClassName.get(rpcName.packageName(), rpcName.simpleName() + "Async");
        JavaFile asyncInterface = createAsyncInterface(rpcService, rpcName, methods, asyncName, generated);
        asyncInterface.writeTo(filer);

        JavaFile reactiveAdapter = createReactiveAdapter(rpcService, rpcName, methods, asyncName, generated);
        reactiveAdapter.writeTo(filer);
    }

    private JavaFile createAsyncInterface(TypeElement rpcService, ClassName rpcName, List methods, ClassName asyncName, AnnotationSpec generated) {
        TypeSpec.Builder asyncTypeBuilder = TypeSpec.interfaceBuilder(asyncName.simpleName())
                .addOriginatingElement(rpcService)
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(generated);

        for (ExecutableElement method : methods) {
            String methodName = method.getSimpleName().toString();

            MethodSpec.Builder asyncMethod = MethodSpec.methodBuilder(methodName)
                    .addModifiers(PUBLIC, ABSTRACT)
                    .returns(Request);
            getDoc(method).ifPresent(asyncMethod::addJavadoc);

            for (TypeParameterElement typeParameterElement : method.getTypeParameters()) {
                asyncMethod.addTypeVariable(TypeVariableName.get((TypeVariable) typeParameterElement.asType()));
            }

            for (VariableElement parameter : method.getParameters()) {
                TypeName type = TypeName.get(parameter.asType());
                String name = parameter.getSimpleName().toString();
                asyncMethod.addParameter(ParameterSpec.builder(type, name).build());
            }

            TypeName returnTypeName = TypeName.get(method.getReturnType());
            final TypeName returnType = returnTypeName.box();
            asyncMethod.addParameter(ParameterSpec
                    .builder(ParameterizedTypeName.get(AsyncCallback, returnType), "callback")
                    .build());
            asyncTypeBuilder.addMethod(asyncMethod.build());
        }

        return JavaFile.builder(rpcName.packageName(), asyncTypeBuilder.build()).build();
    }

    private JavaFile createReactiveAdapter(TypeElement rpcService, ClassName rpcName, List methods, ClassName asyncName, AnnotationSpec generated) {
        ClassName reactiveName = ClassName.get(rpcName.packageName(), rpcName.simpleName() + "Mutiny");

        TypeVariableName targetType = TypeVariableName.get("T");
        TypeSpec callback = TypeSpec.anonymousClassBuilder("")
                .addSuperinterface(ParameterizedTypeName.get(AsyncCallback, targetType))
                .addMethod(MethodSpec.methodBuilder("onFailure")
                        .addAnnotation(Override.class)
                        .addModifiers(PUBLIC)
                        .addParameter(Throwable.class, "e")
                        .addStatement("$N.fail($N)", "em", "e")
                        .build())
                .addMethod(MethodSpec.methodBuilder("onSuccess")
                        .addAnnotation(Override.class)
                        .addModifiers(PUBLIC)
                        .addParameter(targetType, "result")
                        .addStatement("$N.complete($N)", "em", "result")
                        .build())
                .build();

        TypeSpec.Builder reactiveTypeBuilder = TypeSpec.classBuilder(reactiveName.simpleName())
                .addOriginatingElement(rpcService)
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(generated)
                .addField(asyncName, ASYNC_FIELD, PRIVATE, FINAL)
                .addMethod(MethodSpec.constructorBuilder()
                        .addModifiers(PUBLIC)
                        .addParameter(asyncName, "async")
                        .addStatement("this.$L = async", ASYNC_FIELD).build());

        reactiveTypeBuilder.addMethod(MethodSpec.methodBuilder("createCallback")
                .addTypeVariable(targetType)
                .addModifiers(PRIVATE)
                .addParameter(ParameterizedTypeName.get(UniEmitter, WildcardTypeName.supertypeOf(targetType)), "em")
                .addStatement("return $L", callback)
                .returns(ParameterizedTypeName.get(AsyncCallback, targetType)).build());

        for (ExecutableElement method : methods) {
            String methodName = method.getSimpleName().toString();

            MethodSpec.Builder reactiveMethod = MethodSpec.methodBuilder(methodName)
                    .addModifiers(PUBLIC);
            getDoc(method).ifPresent(reactiveMethod::addJavadoc);

            for (TypeParameterElement typeParameterElement : method.getTypeParameters()) {
                reactiveMethod.addTypeVariable(TypeVariableName.get((TypeVariable) typeParameterElement.asType()));
            }

            StringBuilder params = new StringBuilder(); // accumulate params name to use in the async call
            for (VariableElement parameter : method.getParameters()) {
                TypeName type = TypeName.get(parameter.asType());
                String name = parameter.getSimpleName().toString();
                params.append(name).append(", ");
                reactiveMethod.addParameter(ParameterSpec.builder(type, name).addModifiers(FINAL).build());
            }

            TypeName returnTypeName = TypeName.get(method.getReturnType());
            final TypeName returnType = returnTypeName.box();
            reactiveMethod.addStatement("return $T.createFrom().emitter(em -> $L.$L($LcreateCallback(em)))",
                    Uni, ASYNC_FIELD, methodName, params.toString());
            reactiveMethod.returns(ParameterizedTypeName.get(Uni, returnType));
            reactiveTypeBuilder.addMethod(reactiveMethod.build());
        }

        return JavaFile.builder(rpcName.packageName(), reactiveTypeBuilder.build()).build();
    }

    private Optional getDoc(ExecutableElement method) {
        String docComment = processingEnv.getElementUtils().getDocComment(method);
        if (docComment == null || docComment.trim().isEmpty()) {
            return Optional.empty();
        }
        if (!docComment.endsWith("\n")) {
            docComment += "\n";
        }
        return Optional.of(docComment);
    }

    private void log(String msg) {
        if (processingEnv.getOptions().containsKey("debug")) {
            processingEnv.getMessager().printMessage(Kind.NOTE, msg);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy