Skip to content

[Resteasy] rely also on AnnotationStore when processing BeanParam #42226

Open

Description

Description

I would like to generate dynamically my endpoints from CommandHandler. One CommandHandler need to produce an Endpoint.

Here is a sample:

public interface AdminCommandHandler<C extends Command> {
    void execute(C command) throws ExecutionException;
}
public interface Command {
}

And an implementation likes this one

@ApplicationScoped
public class ClearCacheAdminCommandHandlerWithCommandToEnhance implements AdminCommandHandler<ClearCacheCommandToEnhance> {
    private final ClearCacheExecutor clearCacheExecutor;

    public ClearCacheAdminCommandHandlerWithCommandToEnhance(final ClearCacheExecutor clearCacheExecutor) {
        this.clearCacheExecutor = Objects.requireNonNull(clearCacheExecutor);
    }

    @Override
    public void execute(final ClearCacheCommandToEnhance command) throws ExecutionException {
        clearCacheExecutor.clear(command.cacheName);
    }
}
public class ClearCacheCommandToEnhance implements Command {
    @FormParam("cacheName")
    public String cacheName;
}
@Path("/admin")
public class ClearCacheAdminCommandHandlerEndpointToGenerate {
    private final ClearCacheAdminCommandHandlerWithCommandToEnhance adminCommandHandler;

    public ClearCacheAdminCommandHandlerEndpointToGenerate(
            final ClearCacheAdminCommandHandlerWithCommandToEnhance clearCacheAdminCommandHandlerWithCommandToEnhance) {
        this.adminCommandHandler = clearCacheAdminCommandHandlerWithCommandToEnhance;
    }

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Path("/clearCacheAdminCommandHandler")
    public void execute(@BeanParam final ClearCacheCommandToEnhance command) {
        adminCommandHandler.execute(command);
    }
}

The generated part must be done following a convention. Each endpoint should be generated following each CommandHandler implementation and the Command must be enhanced by adding a @FormParam on each fields.

So everything should be generated from theses classes:

@ApplicationScoped
public class ClearCacheAdminCommandHandler implements AdminCommandHandler<ClearCacheCommand> {
    private final ClearCacheExecutor clearCacheExecutor;

    public ClearCacheAdminCommandHandlerWithCommand(final ClearCacheExecutor clearCacheExecutor) {
        this.clearCacheExecutor = Objects.requireNonNull(clearCacheExecutor);
    }

    @Override
    public void execute(final ClearCacheCommand command) throws ExecutionException {
        clearCacheExecutor.clear(command.cacheName);
    }
}
public class ClearCacheCommandToEnhance implements Command {
    public String cacheName;
}

To do it I've done it this way:

@BuildStep
    List<CommandHandlerDiscoveredBuildItem> discoverCommandHandlers(final ApplicationIndexBuildItem applicationIndexBuildItem) {
        final Class<?> adminCommandHandlerClazz = AdminCommandHandler.class;
        final Index index = applicationIndexBuildItem.getIndex();
        return index
                .getAllKnownImplementors(adminCommandHandlerClazz)
                .stream()
                .map(implementor -> {
                    final List<DotName> interfaceDotNames = implementor.interfaceNames();
                    final int position = IntStream.range(0, interfaceDotNames.size())
                            .filter(idx -> adminCommandHandlerClazz.getName().equals(interfaceDotNames.get(idx).toString()))
                            .findFirst()
                            .orElseThrow(() -> new IllegalStateException("Should not be here"));
                    final ParameterizedType adminCommandHandlerType = implementor.interfaceTypes().get(position)
                            .asParameterizedType();
                    final Type first = adminCommandHandlerType.arguments().getFirst();
                    final ClassInfo command = index.getClassByName(first.name());
                    return new CommandHandlerDiscoveredBuildItem(implementor, command);
                })
                .toList();
    }

    @BuildStep
    void generateAdminEndpoints(final List<CommandHandlerDiscoveredBuildItem> commandHandlerDiscoveredBuildItems,
                                final BuildProducer<GeneratedJaxRsResourceBuildItem> generatedJaxRsResourceBuildItemsProducer) {
        commandHandlerDiscoveredBuildItems.forEach(commandHandlerDiscoveredBuildItem -> {
            final String className = commandHandlerDiscoveredBuildItem.commandHandler().simpleName() + "EndpointGenerated";
            try (final ClassCreator beanClassCreator = ClassCreator.builder()
                    .classOutput(new GeneratedJaxRsResourceGizmoAdaptor(generatedJaxRsResourceBuildItemsProducer))
                    .className(className)
                    .setFinal(false)
                    .build()) {
                beanClassCreator.addAnnotation(Path.class).add("value", "/admin");

                final FieldCreator adminCommandHandlerField = beanClassCreator
                        .getFieldCreator("adminCommandHandler", AdminCommandHandler.class)
                        .setModifiers(ACC_PRIVATE | ACC_FINAL);

                final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                final Class<?> commandHandlerClazz = contextClassLoader
                        .loadClass(commandHandlerDiscoveredBuildItem.commandHandler().name().toString());
                final Class<?> commandClazz = contextClassLoader
                        .loadClass(commandHandlerDiscoveredBuildItem.command().name().toString());
                // constructor
                try (final MethodCreator constructorMethod = beanClassCreator.getMethodCreator(MethodDescriptor.INIT,
                        void.class, commandHandlerClazz)) {
                    constructorMethod.setModifiers(ACC_PUBLIC);
                    constructorMethod.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class),
                            constructorMethod.getThis());
                    constructorMethod.writeInstanceField(adminCommandHandlerField.getFieldDescriptor(),
                            constructorMethod.getThis(), constructorMethod.getMethodParam(0));
                    constructorMethod.returnVoid();
                }

                // execute method
                try (final MethodCreator executeMethod = beanClassCreator.getMethodCreator("execute", void.class,
                        commandClazz)) {
                    executeMethod.setModifiers(ACC_PUBLIC);

                    executeMethod.addAnnotation(POST.class);
                    executeMethod.addAnnotation(Consumes.class).add("value",
                            new String[]{MediaType.APPLICATION_FORM_URLENCODED});
                    executeMethod.addAnnotation(Path.class).add("value",
                            endpointNaming(commandHandlerDiscoveredBuildItem.commandHandler().simpleName()));

                    executeMethod.getParameterAnnotations(0).addAnnotation(BeanParam.class);

                    final ResultHandle commandHandlerResult = executeMethod
                            .readInstanceField(adminCommandHandlerField.getFieldDescriptor(), executeMethod.getThis());
                    executeMethod
                            .invokeVirtualMethod(
                                    MethodDescriptor.ofMethod(commandHandlerClazz, "execute",
                                            void.class, commandHandlerDiscoveredBuildItem.command().name().toString()),
                                    commandHandlerResult, executeMethod.getMethodParam(0));

                    executeMethod.returnVoid();
                }
                beanClassCreator.writeTo((name, bytes) -> {
                    // for debugging purpose:  open the file with IntelliJ to see the class generated
                    final File file = new File(String.format("target/%s.class", name));
                    try (FileOutputStream outputStream = new FileOutputStream(file)) {
                        outputStream.write(bytes);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                });
            } catch (ClassNotFoundException e) {
                throw new RuntimeException(e);
            }
        });
    }

    @BuildStep
    AnnotationsTransformerBuildItem bindCommandFieldsWithJaxRSFormParam(final ApplicationIndexBuildItem applicationIndexBuildItem) {
        final List<ClassInfo> commandImplementors = applicationIndexBuildItem.getIndex()
                .getAllKnownImplementors(Command.class)
                .stream().toList();
        return new AnnotationsTransformerBuildItem(AnnotationTransformation.forFields()
                .whenField(fieldInfo -> commandImplementors.contains(fieldInfo.declaringClass()))
                .transform(transformationContext -> {
                    transformationContext.add(AnnotationInstance.builder(FormParam.class)
                            .add("value", transformationContext.declaration().asField().name())
                            .build());
                })
        );
    }
}
public final class CommandHandlerDiscoveredBuildItem extends MultiBuildItem {
    private final ClassInfo commandHandler;
    private final ClassInfo command;

    public CommandHandlerDiscoveredBuildItem(final ClassInfo commandHandler, ClassInfo command) {
        this.commandHandler = Objects.requireNonNull(commandHandler);
        this.command = Objects.requireNonNull(command);
    }

    public ClassInfo commandHandler() {
        return commandHandler;
    }

    public ClassInfo command() {
        return command;
    }
}

@BeanParam with @FormParam is an easy way to do the binding.

However it is not working because the AnnotationsTransformerBuildItem will not change the index used by the ServerEndpointIndexer so the @FormParam is not present and it fails this way

java.lang.RuntimeException: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
	[error]: Build step io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor#setupEndpoints threw an exception: java.lang.RuntimeException: java.lang.RuntimeException: Failed to process method 'ClearCacheAdminCommandHandlerEndpointGenerated#execute'
	at org.jboss.resteasy.reactive.common.processor.EndpointIndexer.createEndpoints(EndpointIndexer.java:333)
	at io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveProcessor.setupEndpoints(ResteasyReactiveProcessor.java:665)
	at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
	at java.base/java.lang.reflect.Method.invoke(Method.java:580)
	at io.quarkus.deployment.ExtensionLoader$3.execute(ExtensionLoader.java:849)
	at io.quarkus.builder.BuildContext.run(BuildContext.java:256)
	at org.jboss.threads.ContextHandler$1.runWith(ContextHandler.java:18)
	at org.jboss.threads.EnhancedQueueExecutor$Task.doRunWith(EnhancedQueueExecutor.java:2516)
	at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2495)
	at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1521)
	at java.base/java.lang.Thread.run(Thread.java:1583)
	at org.jboss.threads.JBossThread.run(JBossThread.java:483)
Caused by: java.lang.RuntimeException: Failed to process method 'ClearCacheAdminCommandHandlerEndpointGenerated#execute'
	at org.jboss.resteasy.reactive.common.processor.EndpointIndexer.createResourceMethod(EndpointIndexer.java:781)
	at org.jboss.resteasy.reactive.common.processor.EndpointIndexer.createEndpoints(EndpointIndexer.java:422)
	at org.jboss.resteasy.reactive.common.processor.EndpointIndexer.createEndpoints(EndpointIndexer.java:300)
	... 11 more
Caused by: jakarta.enterprise.inject.spi.DeploymentException: No annotations found on fields at 'com.lodh.arte.quarkus.loadmin.deployment.bean.ClearCacheCommand'. Annotations like `@QueryParam` should be used in fields, not in methods.
	at org.jboss.resteasy.reactive.server.processor.ServerEndpointIndexer.handleBeanParam(ServerEndpointIndexer.java:198)
	at org.jboss.resteasy.reactive.common.processor.EndpointIndexer.createResourceMethod(EndpointIndexer.java:634)
	... 13 more

Implementation ideas

When using AnnotationsTransformerBuildItem we should rely on AnnotationStore to get changes.

ServerEndpointIndexer is already relying on it.

Is it possible to apply changes on this method

@Override
    protected boolean handleBeanParam(ClassInfo actualEndpointInfo, Type paramType, MethodParameter[] methodParameters, int i,
            Set<String> fileFormNames) {
        ClassInfo beanParamClassInfo = index.getClassByName(paramType.name());
        InjectableBean injectableBean = scanInjectableBean(beanParamClassInfo,
                actualEndpointInfo,
                existingConverters, additionalReaders, injectableBeans, hasRuntimeConverters);
        if ((injectableBean.getFieldExtractorsCount() == 0) && !injectableBean.isInjectionRequired()) {
            throw new DeploymentException(String.format("No annotations found on fields at '%s'. "
                    + "Annotations like `@QueryParam` should be used in fields, not in methods.",
                    beanParamClassInfo.name()));
        }
        fileFormNames.addAll(injectableBean.getFileFormNames());
        return injectableBean.isFormParamRequired();
    }

to use the AnnotationStore here instead of the immutable index.

ClassInfo beanParamClassInfo = index.getClassByName(paramType.name());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions