From 9206f882e145075429254d9e0668076cd53b1dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20=C3=89pardaud?= Date: Tue, 20 Aug 2024 11:08:19 +0200 Subject: [PATCH] Support record parameter containers Also unify the parameter container detection code paths Make sure we do not turn record parameter containers into beans For records, we store every contructor parameter in a local variable until we have enough to call the constructor via a generated static method. Fixes #19686 --- docs/src/main/asciidoc/rest.adoc | 24 ++ ...ggregatedParameterContainersBuildItem.java | 38 +++ .../CustomResourceProducersGenerator.java | 32 +- .../deployment/ResteasyReactiveProcessor.java | 151 +++++++--- .../test/beanparam/BeanParamRecordTest.java | 106 +++++++ .../server/test/beanparam/BeanParamTest.java | 3 +- .../reactive/common/processor/AsmUtil.java | 100 +++++++ .../processor/ResteasyReactiveDotNames.java | 1 + .../scanning/ResourceScanningResult.java | 9 +- ...easyReactiveParameterContainerScanner.java | 11 +- .../scanning/ResteasyReactiveScanner.java | 53 ---- .../processor/ServerEndpointIndexer.java | 15 +- .../scanning/ClassInjectorTransformer.java | 276 ++++++++++++++---- .../core/ResteasyReactiveRequestContext.java | 103 +++++++ .../parameters/ContextParamExtractor.java | 85 +----- .../parameters/RecordBeanParamExtractor.java | 34 +++ .../startup/RuntimeResourceDeployment.java | 8 +- .../ResteasyReactiveInjectionContext.java | 15 + 18 files changed, 798 insertions(+), 266 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/AggregatedParameterContainersBuildItem.java create mode 100644 extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamRecordTest.java create mode 100644 independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/RecordBeanParamExtractor.java diff --git a/docs/src/main/asciidoc/rest.adoc b/docs/src/main/asciidoc/rest.adoc index e38650b354d85..8942727975771 100644 --- a/docs/src/main/asciidoc/rest.adoc +++ b/docs/src/main/asciidoc/rest.adoc @@ -394,6 +394,30 @@ public class Endpoint { ---- <1> `BeanParam` is required to comply with the Jakarta REST specification so that libraries like OpenAPI can introspect the parameters. +Record classes are also supported, so you could rewrite the previous example as a record: + +[source,java] +---- + public record Parameters( + @RestPath + String type, + + @RestMatrix + String variant, + + @RestQuery + String age, + + @RestCookie + String level, + + @RestHeader("X-Cheese-Secret-Handshake") + String secretHandshake, + + @RestForm + String smell){} +---- + [[uri-parameters]] === [[declaring-uri-parameters]] Declaring URI parameters diff --git a/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/AggregatedParameterContainersBuildItem.java b/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/AggregatedParameterContainersBuildItem.java new file mode 100644 index 0000000000000..72f5aca84df26 --- /dev/null +++ b/extensions/resteasy-reactive/rest-common/deployment/src/main/java/io/quarkus/resteasy/reactive/common/deployment/AggregatedParameterContainersBuildItem.java @@ -0,0 +1,38 @@ +package io.quarkus.resteasy.reactive.common.deployment; + +import java.util.Set; + +import org.jboss.jandex.DotName; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class AggregatedParameterContainersBuildItem extends SimpleBuildItem { + + /** + * This contains all the parameter containers (bean param classes and records) as well as resources/endpoints + */ + private final Set classNames; + /** + * This contains all the non-record parameter containers (bean param classes only) as well as resources/endpoints + */ + private final Set nonRecordClassNames; + + public AggregatedParameterContainersBuildItem(Set classNames, Set nonRecordClassNames) { + this.classNames = classNames; + this.nonRecordClassNames = nonRecordClassNames; + } + + /** + * All class names + */ + public Set getClassNames() { + return classNames; + } + + /** + * All class names minus the records + */ + public Set getNonRecordClassNames() { + return nonRecordClassNames; + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java index b141f654df785..3cd165b51b6b7 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/CustomResourceProducersGenerator.java @@ -77,24 +77,41 @@ private CustomResourceProducersGenerator() { * @Singleton * public class ResourcesWithParamProducer { * - * @Inject - * CurrentVertxRequest currentVertxRequest; + * private String getHeaderParam(String name) { + * return (String)new HeaderParamExtractor(name, true).extractParameter(getContext()); + * } + * + * private String getQueryParam(String name) { + * return (String)new QueryParamExtractor(name, true, false, null).extractParameter(getContext()); + * } + * + * private String getPathParam(int index) { + * return (String)new PathParamExtractor(index, false, true).extractParameter(getContext()); + * } + * + * private String getMatrixParam(String name) { + * return (String)new MatrixParamExtractor(name, true, false).extractParameter(getContext()); + * } + * + * private String getCookieParam(String name) { + * return (String)new CookieParamExtractor(name, null).extractParameter(getContext()); + * } * * @Produces * @RequestScoped * public QueryParamResource producer_QueryParamResource_somehash(UriInfo uriInfo) { - * return new QueryParamResource(getContext().getContext().queryParams().get("p1"), uriInfo); + * return new QueryParamResource(getQueryParam("p1"), uriInfo); * } * - * private QuarkusRestRequestContext getContext() { - * return (QuarkusRestRequestContext) currentVertxRequest.getOtherHttpContextObject(); + * private ResteasyReactiveRequestContext getContext() { + * return CurrentRequestManager.get(); * } * } * * */ public static void generate(Map resourcesThatNeedCustomProducer, - Set beanParamsThatNeedCustomProducer, + Set parameterContainersThatNeedCustomProducer, BuildProducer generatedBeanBuildItemBuildProducer, BuildProducer additionalBeanBuildItemBuildProducer) { GeneratedBeanGizmoAdaptor classOutput = new GeneratedBeanGizmoAdaptor(generatedBeanBuildItemBuildProducer); @@ -307,7 +324,8 @@ public static void generate(Map resourcesThatNeedCustomProd } // FIXME: support constructors for bean params too additionalBeanBuildItemBuildProducer - .produce(AdditionalBeanBuildItem.builder().addBeanClasses(beanParamsThatNeedCustomProducer) + .produce(AdditionalBeanBuildItem.builder() + .addBeanClasses(parameterContainersThatNeedCustomProducer.stream().map(DotName::toString).toList()) // FIXME: we should add this, but for that we also need to make the resource class request-scoped // .setDefaultScope(DOTNAME_REQUEST_SCOPED) // .setUnremovable() diff --git a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index ba6ec3f69909e..68c8b01978fd5 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -61,6 +61,7 @@ import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; @@ -156,6 +157,7 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.netty.deployment.MinNettyAllocatorMaxOrderBuildItem; +import io.quarkus.resteasy.reactive.common.deployment.AggregatedParameterContainersBuildItem; import io.quarkus.resteasy.reactive.common.deployment.ApplicationResultBuildItem; import io.quarkus.resteasy.reactive.common.deployment.FactoryUtils; import io.quarkus.resteasy.reactive.common.deployment.ParameterContainersBuildItem; @@ -301,25 +303,65 @@ void vertxIntegration(BuildProducer writerBuildItemB Priorities.USER)); } + @BuildStep + AggregatedParameterContainersBuildItem aggregateParameterContainers( + Optional resourceScanningResultBuildItem, + List parameterContainersBuildItems) { + if (!resourceScanningResultBuildItem.isPresent()) { + return new AggregatedParameterContainersBuildItem(Set.of(), Set.of()); + } + Set scannedParameterContainers = new HashSet<>(); + + for (ParameterContainersBuildItem parameterContainersBuildItem : parameterContainersBuildItems) { + scannedParameterContainers.addAll(parameterContainersBuildItem.getClassNames()); + } + IndexView index = resourceScanningResultBuildItem.get().getResult().getIndex(); + Set nonRecordParameterContainers = new HashSet<>(); + for (DotName parameterContainer : scannedParameterContainers) { + ClassInfo parameterContainerClass = index.getClassByName(parameterContainer); + if (parameterContainerClass != null && !parameterContainerClass.isRecord()) { + nonRecordParameterContainers.add(parameterContainer); + } + } + return new AggregatedParameterContainersBuildItem(scannedParameterContainers, nonRecordParameterContainers); + } + @BuildStep void generateCustomProducer(Optional resourceScanningResultBuildItem, BuildProducer generatedBeanBuildItemBuildProducer, - BuildProducer additionalBeanBuildItemBuildProducer) { + BuildProducer additionalBeanBuildItemBuildProducer, + AggregatedParameterContainersBuildItem aggregatedParameterContainersBuildItem) { if (!resourceScanningResultBuildItem.isPresent()) { return; } Map resourcesThatNeedCustomProducer = resourceScanningResultBuildItem.get().getResult() .getResourcesThatNeedCustomProducer(); - Set beanParams = resourceScanningResultBuildItem.get().getResult() - .getBeanParams(); - if (!resourcesThatNeedCustomProducer.isEmpty() || !beanParams.isEmpty()) { - CustomResourceProducersGenerator.generate(resourcesThatNeedCustomProducer, beanParams, + Set parameterContainers = getPotentialBeans(resourceScanningResultBuildItem.get().getResult().getIndex(), + aggregatedParameterContainersBuildItem.getNonRecordClassNames()); + if (!resourcesThatNeedCustomProducer.isEmpty() + || !parameterContainers.isEmpty()) { + CustomResourceProducersGenerator.generate(resourcesThatNeedCustomProducer, + parameterContainers, generatedBeanBuildItemBuildProducer, additionalBeanBuildItemBuildProducer); } } + private Set getPotentialBeans(IndexView indexView, Set paramContainerClassNames) { + // FIXME: this filters out parameter containers with non-default constructor, which are used by REST client, + // but not supported by REST server (yet). We should produce a better error message if they are used in the + // server, but we don't have logic to detect client/server usage yet + Set ret = new HashSet<>(paramContainerClassNames.size()); + for (DotName paramContainerName : paramContainerClassNames) { + ClassInfo paramContainer = indexView.getClassByName(paramContainerName); + if (paramContainer.hasNoArgsConstructor()) { + ret.add(paramContainerName); + } + } + return ret; + } + //TODO: replace with MethodLevelExceptionMappingFeature @BuildStep void handleClassLevelExceptionMappers(Optional resourceScanningResultBuildItem, @@ -364,13 +406,15 @@ void registerCustomExceptionMappers(BuildProducer resourceScanningResultBuildItem, - BuildProducer unremovableBeans) { + BuildProducer unremovableBeans, + AggregatedParameterContainersBuildItem aggregatedParameterContainersBuildItem) { if (!resourceScanningResultBuildItem.isPresent()) { return; } - Set beanParams = resourceScanningResultBuildItem.get().getResult() - .getBeanParams(); - unremovableBeans.produce(UnremovableBeanBuildItem.beanClassNames(beanParams.toArray(EMPTY_STRING_ARRAY))); + Set parameterContainers = getPotentialBeans(resourceScanningResultBuildItem.get().getResult().getIndex(), + aggregatedParameterContainersBuildItem.getNonRecordClassNames()); + unremovableBeans.produce(UnremovableBeanBuildItem + .beanClassNames(parameterContainers.stream().map(DotName::toString).collect(Collectors.toSet()))); } @BuildStep @@ -408,7 +452,7 @@ public void setupEndpoints(ApplicationIndexBuildItem applicationIndexBuildItem, BuildProducer reflectiveHierarchy, ApplicationResultBuildItem applicationResultBuildItem, ParamConverterProvidersBuildItem paramConverterProvidersBuildItem, - List parameterContainersBuildItems, + AggregatedParameterContainersBuildItem aggregatedParameterContainersBuildItem, List applicationClassPredicateBuildItems, List methodScanners, List annotationTransformerBuildItems, @@ -440,11 +484,6 @@ public void setupEndpoints(ApplicationIndexBuildItem applicationIndexBuildItem, AdditionalWriters additionalWriters = new AdditionalWriters(); Map injectableBeans = new HashMap<>(); QuarkusServerEndpointIndexer serverEndpointIndexer; - Set scannedParameterContainers = new HashSet<>(); - - for (ParameterContainersBuildItem parameterContainersBuildItem : parameterContainersBuildItems) { - scannedParameterContainers.addAll(parameterContainersBuildItem.getClassNames()); - } ParamConverterProviders paramConverterProviders = paramConverterProvidersBuildItem.getParamConverterProviders(); Function> factoryFunction = s -> FactoryUtils.factory(s, singletonClasses, recorder, @@ -477,7 +516,7 @@ public void setupEndpoints(ApplicationIndexBuildItem applicationIndexBuildItem, methodScanners.stream().map(MethodScannerBuildItem::getMethodScanner).collect(toList())) .setIndex(index) .setApplicationIndex(applicationIndexBuildItem.getIndex()) - .addParameterContainerTypes(scannedParameterContainers) + .addParameterContainerTypes(aggregatedParameterContainersBuildItem.getClassNames()) .addContextTypes(additionalContextTypes(contextTypeBuildItems)) .setFactoryCreator(new QuarkusFactoryCreator(recorder, beanContainerBuildItem.getValue())) .setEndpointInvokerFactory( @@ -798,17 +837,14 @@ public void transformEndpoints( ResourceScanningResultBuildItem resourceScanningResultBuildItem, ResourceInterceptorsBuildItem resourceInterceptorsBuildItem, BuildProducer annotationsTransformer, - BeanArchiveIndexBuildItem beanArchiveIndexBuildItem) { + BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + AggregatedParameterContainersBuildItem aggregatedParameterContainersBuildItem) { // all found resources and sub-resources Set allResources = new HashSet<>(); allResources.addAll(resourceScanningResultBuildItem.getResult().getScannedResources().keySet()); allResources.addAll(resourceScanningResultBuildItem.getResult().getPossibleSubResources().keySet()); - // all found bean params - Set beanParams = resourceScanningResultBuildItem.getResult() - .getBeanParams(); - // discovered filters and interceptors Set filtersAndInterceptors = new HashSet<>(); InterceptorContainer readerInterceptors = resourceInterceptorsBuildItem.getResourceInterceptors() @@ -829,40 +865,67 @@ public void transformEndpoints( containerResponseFilters.getGlobalResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName())); containerResponseFilters.getNameResourceInterceptors().forEach(i -> filtersAndInterceptors.add(i.getClassName())); + // parameter containers + Set nonRecordParameterContainerClassNames = aggregatedParameterContainersBuildItem.getNonRecordClassNames(); + annotationsTransformer.produce(new io.quarkus.arc.deployment.AnnotationsTransformerBuildItem( new io.quarkus.arc.processor.AnnotationsTransformer() { @Override public boolean appliesTo(AnnotationTarget.Kind kind) { - return kind == AnnotationTarget.Kind.CLASS; + return kind == AnnotationTarget.Kind.CLASS || kind == AnnotationTarget.Kind.FIELD; } @Override public void transform(TransformationContext context) { - ClassInfo clazz = context.getTarget().asClass(); - // check if the class is one of resources/sub-resources - if (allResources.contains(clazz.name()) - && clazz.declaredAnnotation(ResteasyReactiveDotNames.TYPED) == null) { - context.transform().add(createTypedAnnotationInstance(clazz, beanArchiveIndexBuildItem)).done(); - return; - } - // check if the class is one of providers, either explicitly declaring the annotation - // or discovered as resource interceptor or filter - if ((clazz.declaredAnnotation(ResteasyReactiveDotNames.PROVIDER) != null - || filtersAndInterceptors.contains(clazz.name().toString())) - && clazz.declaredAnnotation(ResteasyReactiveDotNames.TYPED) == null) { - // Add @Typed(MyResource.class) - context.transform().add(createTypedAnnotationInstance(clazz, beanArchiveIndexBuildItem)).done(); - return; - } - // check if the class is a bean param - if (beanParams.contains(clazz.name().toString()) - && clazz.declaredAnnotation(ResteasyReactiveDotNames.TYPED) == null) { - // Add @Typed(MyBean.class) - context.transform().add(createTypedAnnotationInstance(clazz, beanArchiveIndexBuildItem)).done(); - return; + if (context.getTarget().kind() == AnnotationTarget.Kind.CLASS) { + ClassInfo clazz = context.getTarget().asClass(); + // check if the class is one of resources/sub-resources + if (allResources.contains(clazz.name()) + && clazz.declaredAnnotation(ResteasyReactiveDotNames.TYPED) == null) { + context.transform().add(createTypedAnnotationInstance(clazz, beanArchiveIndexBuildItem)).done(); + return; + } + // check if the class is one of providers, either explicitly declaring the annotation + // or discovered as resource interceptor or filter + if ((clazz.declaredAnnotation(ResteasyReactiveDotNames.PROVIDER) != null + || filtersAndInterceptors.contains(clazz.name().toString())) + && clazz.declaredAnnotation(ResteasyReactiveDotNames.TYPED) == null) { + // Add @Typed(MyResource.class) + context.transform().add(createTypedAnnotationInstance(clazz, beanArchiveIndexBuildItem)).done(); + return; + } + // check if the class is a parameter container + if (nonRecordParameterContainerClassNames.contains(clazz.name()) + && clazz.declaredAnnotation(ResteasyReactiveDotNames.TYPED) == null) { + // Add @Typed(MyBean.class) + context.transform().add(createTypedAnnotationInstance(clazz, beanArchiveIndexBuildItem)).done(); + return; + } + } else if (context.getTarget().kind() == AnnotationTarget.Kind.FIELD) { + FieldInfo field = context.getTarget().asField(); + ClassInfo declaringClass = field.declaringClass(); + // remove @BeanParam annotations from record fields + if (declaringClass.isRecord() + && field.declaredAnnotation(ResteasyReactiveDotNames.BEAN_PARAM) != null) { + context.transform().remove(a -> a.name().equals(ResteasyReactiveDotNames.BEAN_PARAM)).done(); + return; + } + // also remove @BeanParam annotations targeting records + if (field.declaredAnnotation(ResteasyReactiveDotNames.BEAN_PARAM) != null + && isRecord(resourceScanningResultBuildItem.getResult().getIndex(), + field.type().asClassType().name())) { + context.transform().remove(a -> a.name().equals(ResteasyReactiveDotNames.BEAN_PARAM)).done(); + return; + } + } } + + private boolean isRecord(IndexView index, DotName name) { + ClassInfo classInfo = index.getClassByName(name); + return classInfo.isRecord(); + } })); } diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamRecordTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamRecordTest.java new file mode 100644 index 0000000000000..6a747a5dc6692 --- /dev/null +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamRecordTest.java @@ -0,0 +1,106 @@ +package io.quarkus.resteasy.reactive.server.test.beanparam; + +import static org.hamcrest.CoreMatchers.equalTo; + +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.RestHeader; +import org.jboss.resteasy.reactive.RestQuery; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +public class BeanParamRecordTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest() + .setArchiveProducer(() -> { + return ShrinkWrap.create(JavaArchive.class) + .addClasses(BeanParamRecord.class, OtherBeanParam.class, OtherBeanParamClass.class, + OtherBeanParamRecord.class); + }); + + @Test + void shouldWork() { + RestAssured + .given() + .header("Header-Param", "got it") + .queryParam("primitiveByte", "2") + .queryParam("primitiveShort", "3") + .queryParam("primitiveInt", "4") + .queryParam("primitiveLong", "5") + .queryParam("primitiveFloat", "6") + .queryParam("primitiveDouble", "7") + .queryParam("primitiveChar", "a") + .queryParam("primitiveBoolean", "true") + .queryParam("q", "query") + .get("/record") + .then() + .statusCode(200) + .body(equalTo("got it/2/3/4/5/6.0/7.0/true/a/query/query/query/query")); + + } + + public record BeanParamRecord(@RestHeader String headerParam, + @RestQuery byte primitiveByte, + @RestQuery short primitiveShort, + @RestQuery int primitiveInt, + @RestQuery long primitiveLong, + @RestQuery float primitiveFloat, + @RestQuery double primitiveDouble, + @RestQuery boolean primitiveBoolean, + @RestQuery char primitiveChar, + UriInfo uriInfo, + // record contains bp (implicit @BeanParam) + OtherBeanParam obp, + // record contains record (implicit @BeanParam) + OtherBeanParamRecord obpr) { + } + + public static class OtherBeanParam { + @RestQuery + String q; + // bp contains record + @BeanParam // no implicit annotation on fields yet + OtherBeanParamRecord obpr; + // bp contains bp + @BeanParam // no implicit annotation on fields yet + OtherBeanParamClass obpc; + } + + public record OtherBeanParamRecord(@RestQuery String q) { + } + + public static class OtherBeanParamClass { + @RestQuery + String q; + } + + @Path("/") + public static class Resource { + + @Path("/record") + @GET + public String beanParamRecord(BeanParamRecord p, @RestHeader String headerParam) { + return p.headerParam() + "/" + + p.primitiveByte() + "/" + + p.primitiveShort() + "/" + + p.primitiveInt() + "/" + + p.primitiveLong() + "/" + + p.primitiveFloat() + "/" + + p.primitiveDouble() + "/" + + p.primitiveBoolean() + "/" + + p.primitiveChar() + "/" + + p.obp().q + "/" + + p.obp().obpr.q() + "/" + + p.obp().obpc.q + "/" + + p.obpr().q; + } + } +} diff --git a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamTest.java b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamTest.java index bbfe2701f0178..c895cc8efeb5e 100644 --- a/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamTest.java +++ b/extensions/resteasy-reactive/rest/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/beanparam/BeanParamTest.java @@ -26,7 +26,8 @@ public class BeanParamTest { @Test void shouldDeployWithoutIssues() { - // we only need to check that it deploys + // Apparently, Top and MyBeanParamWithFieldsAndProperties are only there to check that it deploys + // probably we test that they run in another test? } public static class Top { diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/AsmUtil.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/AsmUtil.java index 5cecbaff915ab..ac6dfe418ab4b 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/AsmUtil.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/AsmUtil.java @@ -105,4 +105,104 @@ private static void unbox(MethodVisitor mv, String owner, String methodName, Str mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, owner, methodName, "()" + returnTypeSignature, false); } + /** + * Returns the bytecode instruction to load the given Jandex Type. This returns the specialised + * bytecodes ILOAD, DLOAD, FLOAD and LLOAD for primitives, or ALOAD otherwise. + * + * @param jandexType The Jandex Type whose load instruction to return. + * @return The bytecode instruction to load the given Jandex Type. + */ + public static int getLoadOpcode(Type jandexType) { + if (jandexType.kind() == Kind.PRIMITIVE) { + switch (jandexType.asPrimitiveType().primitive()) { + case BOOLEAN: + case BYTE: + case SHORT: + case INT: + case CHAR: + return Opcodes.ILOAD; + case DOUBLE: + return Opcodes.DLOAD; + case FLOAT: + return Opcodes.FLOAD; + case LONG: + return Opcodes.LLOAD; + default: + throw new IllegalArgumentException("Unknown primitive type: " + jandexType); + } + } + return Opcodes.ALOAD; + } + + /** + * Returns the bytecode instruction to store the given Jandex Type. This returns the specialised + * bytecodes ISTORE, DSTORE, FSTORE and LSTORE for primitives, or ASTORE otherwise. + * + * @param jandexType The Jandex Type whose store instruction to return. + * @return The bytecode instruction to store the given Jandex Type. + */ + public static int getStoreOpcode(Type jandexType) { + if (jandexType.kind() == Kind.PRIMITIVE) { + switch (jandexType.asPrimitiveType().primitive()) { + case BOOLEAN: + case BYTE: + case SHORT: + case INT: + case CHAR: + return Opcodes.ISTORE; + case DOUBLE: + return Opcodes.DSTORE; + case FLOAT: + return Opcodes.FSTORE; + case LONG: + return Opcodes.LSTORE; + default: + throw new IllegalArgumentException("Unknown primitive type: " + jandexType); + } + } + return Opcodes.ASTORE; + } + + /** + * Returns a null value suitable for initialising variables to null or its equivalent for primitives + */ + public static int getNullValueOpcode(Type jandexType) { + if (jandexType.kind() == Kind.PRIMITIVE) { + switch (jandexType.asPrimitiveType().primitive()) { + case BOOLEAN: + case BYTE: + case SHORT: + case INT: + case CHAR: + return Opcodes.ICONST_0; + case DOUBLE: + return Opcodes.DCONST_0; + case FLOAT: + return Opcodes.FCONST_0; + case LONG: + return Opcodes.LCONST_0; + default: + throw new IllegalArgumentException("Unknown primitive type: " + jandexType); + } + } + return Opcodes.ACONST_NULL; + } + + /** + * Returns the number of underlying bytecode parameters taken by the given Jandex parameter Type. + * This will be 2 for doubles and longs, 1 otherwise. + * + * @param paramType the Jandex parameter Type + * @return the number of underlying bytecode parameters required. + */ + public static int getParameterSize(Type paramType) { + if (paramType.kind() == Kind.PRIMITIVE) { + switch (paramType.asPrimitiveType().primitive()) { + case DOUBLE: + case LONG: + return 2; + } + } + return 1; + } } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java index 69d1747782895..aa7b6b19c0fe9 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/ResteasyReactiveDotNames.java @@ -270,6 +270,7 @@ public final class ResteasyReactiveDotNames { .createSimple("org.jboss.resteasy.reactive.server.WithFormRead"); public static final DotName OBJECT = DotName.createSimple(Object.class.getName()); + public static final DotName RECORD = DotName.createSimple(Record.class.getName()); public static final DotName CONTINUATION = DotName.createSimple("kotlin.coroutines.Continuation"); public static final DotName KOTLIN_UNIT = DotName.createSimple("kotlin.Unit"); diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResourceScanningResult.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResourceScanningResult.java index 1f6537fd056ef..a4879d4988e98 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResourceScanningResult.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResourceScanningResult.java @@ -2,7 +2,6 @@ import java.util.List; import java.util.Map; -import java.util.Set; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; @@ -18,7 +17,6 @@ public final class ResourceScanningResult { final Map pathInterfaces; final Map clientInterfaces; final Map resourcesThatNeedCustomProducer; - final Set beanParams; final Map httpAnnotationToMethod; final List classLevelExceptionMappers; @@ -27,7 +25,7 @@ public ResourceScanningResult(IndexView index, Map scannedRe Map possibleSubResources, Map pathInterfaces, Map clientInterfaces, Map resourcesThatNeedCustomProducer, - Set beanParams, Map httpAnnotationToMethod, List classLevelExceptionMappers) { + Map httpAnnotationToMethod, List classLevelExceptionMappers) { this.index = index; this.scannedResources = scannedResources; this.scannedResourcePaths = scannedResourcePaths; @@ -35,7 +33,6 @@ public ResourceScanningResult(IndexView index, Map scannedRe this.pathInterfaces = pathInterfaces; this.clientInterfaces = clientInterfaces; this.resourcesThatNeedCustomProducer = resourcesThatNeedCustomProducer; - this.beanParams = beanParams; this.httpAnnotationToMethod = httpAnnotationToMethod; this.classLevelExceptionMappers = classLevelExceptionMappers; } @@ -68,10 +65,6 @@ public Map getResourcesThatNeedCustomProducer() { return resourcesThatNeedCustomProducer; } - public Set getBeanParams() { - return beanParams; - } - public Map getHttpAnnotationToMethod() { return httpAnnotationToMethod; } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveParameterContainerScanner.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveParameterContainerScanner.java index 0e4b43f451e6e..509ebaa320914 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveParameterContainerScanner.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveParameterContainerScanner.java @@ -13,19 +13,18 @@ public class ResteasyReactiveParameterContainerScanner { public static Set scanParameterContainers(IndexView index, ApplicationScanningResult result) { Set res = new HashSet(); + // FIXME: this should discover parameter-containers containing parameter-containers + // NOTE: we used to call result.keepClass but the TCK doesn't list bean parameters in their Application.getClasses + // and the docs says this applies to resource, provider or feature which I don't think apply to bean params for (DotName fieldAnnotation : ResteasyReactiveDotNames.JAX_RS_ANNOTATIONS_FOR_FIELDS) { for (AnnotationInstance annotationInstance : index.getAnnotations(fieldAnnotation)) { // these annotations can be on fields or properties if (annotationInstance.target().kind() == Kind.FIELD) { ClassInfo klass = annotationInstance.target().asField().declaringClass(); - if (result.keepClass(klass.name().toString())) { - res.add(klass.name()); - } + res.add(klass.name()); } else if (annotationInstance.target().kind() == Kind.METHOD) { ClassInfo klass = annotationInstance.target().asMethod().declaringClass(); - if (result.keepClass(klass.name().toString())) { - res.add(klass.name()); - } + res.add(klass.name()); } } } diff --git a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java index e97bacc60b9a9..f7465dcfca34f 100644 --- a/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java +++ b/independent-projects/resteasy-reactive/common/processor/src/main/java/org/jboss/resteasy/reactive/common/processor/scanning/ResteasyReactiveScanner.java @@ -18,7 +18,6 @@ import java.util.Deque; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -33,7 +32,6 @@ import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; -import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; import org.jboss.jandex.Type; @@ -377,59 +375,8 @@ public static ResourceScanningResult scanResources( toScan.addAll(index.getKnownDirectSubclasses(classInfo.name())); } - Set beanParams = new HashSet<>(); - - Set beanParamAsBeanUsers = new HashSet<>(scannedResources.values()); - beanParamAsBeanUsers.addAll(possibleSubResources.values()); - - Collection unregisteredBeanParamAnnotations = new ArrayList<>( - index.getAnnotations(ResteasyReactiveDotNames.BEAN_PARAM)); - boolean newBeanParamsRegistered; - do { - newBeanParamsRegistered = false; - for (Iterator iterator = unregisteredBeanParamAnnotations.iterator(); iterator.hasNext();) { - AnnotationInstance beanParamAnnotation = iterator.next(); - AnnotationTarget target = beanParamAnnotation.target(); - // FIXME: this isn't right wrt generics - switch (target.kind()) { - case FIELD: - FieldInfo field = target.asField(); - ClassInfo beanParamDeclaringClass = field.declaringClass(); - if (beanParamAsBeanUsers.contains(beanParamDeclaringClass) - || beanParams.contains(beanParamDeclaringClass.name().toString())) { - newBeanParamsRegistered |= beanParams.add(field.type().name().toString()); - iterator.remove(); - } - break; - case METHOD: - MethodInfo setterMethod = target.asMethod(); - if (beanParamAsBeanUsers.contains(setterMethod.declaringClass()) - || beanParams.contains(setterMethod.declaringClass().name().toString())) { - Type setterParamType = setterMethod.parameterType(0); - - newBeanParamsRegistered |= beanParams.add(setterParamType.name().toString()); - iterator.remove(); - } - break; - case METHOD_PARAMETER: - MethodInfo method = target.asMethodParameter().method(); - if (beanParamAsBeanUsers.contains(method.declaringClass()) - || beanParams.contains(method.declaringClass().name().toString())) { - int paramIndex = target.asMethodParameter().position(); - Type paramType = method.parameterType(paramIndex); - newBeanParamsRegistered |= beanParams.add(paramType.name().toString()); - iterator.remove(); - } - break; - default: - break; - } - } - } while (newBeanParamsRegistered); - return new ResourceScanningResult(index, scannedResources, scannedResourcePaths, possibleSubResources, pathInterfaces, clientInterfaces, resourcesThatNeedCustomProducer, - beanParams, httpAnnotationToMethod, methodExceptionMappers); } diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java index 07ebfc324d5d9..f15d4beac65d4 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/ServerEndpointIndexer.java @@ -368,6 +368,8 @@ protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInf // LinkedHashMap the TCK expects that fields annotated with @BeanParam are handled last Map fieldExtractors = new LinkedHashMap<>(); Map beanParamFields = new LinkedHashMap<>(); + // records do not have field injection, we use their constructor, so field rules do not apply + boolean applyFieldRules = !currentClassInfo.isRecord(); for (FieldInfo field : currentClassInfo.fields()) { Map annotations = new HashMap<>(); for (AnnotationInstance i : field.annotations()) { @@ -375,7 +377,7 @@ protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInf } ServerIndexedParameter result = extractParameterInfo(currentClassInfo, actualEndpointInfo, null, existingConverters, additionalReaders, - annotations, field.type(), field.toString(), true, hasRuntimeConverters, + annotations, field.type(), field.toString(), applyFieldRules, hasRuntimeConverters, // We don't support annotation-less path params in injectable beans: only annotations Collections.emptySet(), field.name(), EMPTY_STRING_ARRAY, new HashMap<>()); if ((result.getType() != null) && (result.getType() != ParameterType.BEAN)) { @@ -421,7 +423,9 @@ protected InjectableBean scanInjectableBean(ClassInfo currentClassInfo, ClassInf DotName superClassName = currentClassInfo.superName(); boolean superTypeIsInjectable = false; - if (superClassName != null && !superClassName.equals(ResteasyReactiveDotNames.OBJECT)) { + if (superClassName != null + && !superClassName.equals(ResteasyReactiveDotNames.OBJECT) + && !superClassName.equals(ResteasyReactiveDotNames.RECORD)) { ClassInfo superClass = index.getClassByName(superClassName); if (superClass != null) { InjectableBean superInjectableBean = scanInjectableBean(superClass, actualEndpointInfo, @@ -617,6 +621,11 @@ private ParameterConverterSupplier determineTemporalConverter(DotName paramType, } private void validateMethodsForInjectableBean(ClassInfo currentClassInfo) { + // do not check methods of records, they get the annotations from their record components, but that's automatic: + // they are actually placed on the constructor parameters and also end up on the fields and methods + if (currentClassInfo.isRecord()) { + return; + } for (MethodInfo method : currentClassInfo.methods()) { for (AnnotationInstance annotation : method.annotations()) { if (annotation.target().kind() == AnnotationTarget.Kind.METHOD) { @@ -624,7 +633,7 @@ private void validateMethodsForInjectableBean(ClassInfo currentClassInfo) { if (annotation.name().equals(annotationForField)) { throw new DeploymentException(String.format( "Method '%s' of class '%s' is annotated with @%s annotation which is prohibited. " - + "Classes uses as @BeanParam parameters must have a JAX-RS parameter annotation on " + + "Classes used as @BeanParam parameters must have a JAX-RS parameter annotation on " + "fields only.", method.name(), currentClassInfo.name().toString(), annotation.name().withoutPackagePrefix())); diff --git a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/ClassInjectorTransformer.java b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/ClassInjectorTransformer.java index 261445e972f44..cd3a97dd659e2 100644 --- a/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/ClassInjectorTransformer.java +++ b/independent-projects/resteasy-reactive/server/processor/src/main/java/org/jboss/resteasy/reactive/server/processor/scanning/ClassInjectorTransformer.java @@ -15,9 +15,11 @@ import jakarta.ws.rs.core.MediaType; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; import org.jboss.jandex.PrimitiveType.Primitive; +import org.jboss.jandex.RecordComponentInfo; import org.jboss.jandex.Type.Kind; import org.jboss.resteasy.reactive.common.model.ParameterType; import org.jboss.resteasy.reactive.common.processor.AsmUtil; @@ -162,11 +164,17 @@ static class ClassInjectorVisitor extends ClassVisitor { private final boolean requireCreateBeanParams; private IndexView indexView; private boolean seenClassInit; + private boolean isRecord; + private Map fieldInfoByName; public ClassInjectorVisitor(int api, ClassVisitor classVisitor, Map fieldExtractors, boolean superTypeIsInjectable, boolean requireCreateBeanParams, IndexView indexView) { super(api, classVisitor); this.fieldExtractors = fieldExtractors; + this.fieldInfoByName = new HashMap<>(); + for (FieldInfo fieldInfo : fieldExtractors.keySet()) { + fieldInfoByName.put(fieldInfo.name(), fieldInfo); + } this.superTypeIsInjectable = superTypeIsInjectable; this.requireCreateBeanParams = requireCreateBeanParams; this.indexView = indexView; @@ -201,6 +209,7 @@ public void visit(int version, int access, String name, String signature, String } superTypeName = superName; thisName = name; + isRecord = (access & Opcodes.ACC_RECORD) != 0; } @Override @@ -227,86 +236,140 @@ public void visitEnd() { public void visitEnd() { // FIXME: handle setters // FIXME: handle multi fields - MethodVisitor injectMethod = visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC, INJECT_METHOD_NAME, - INJECT_METHOD_DESCRIPTOR, null, + // this is static for records + MethodVisitor injectMethod = visitMethod( + Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | (isRecord ? Opcodes.ACC_STATIC : 0), INJECT_METHOD_NAME, + isRecord ? "(" + QUARKUS_REST_INJECTION_CONTEXT_DESCRIPTOR + ")L" + thisName + ";" + : INJECT_METHOD_DESCRIPTOR, + null, null); injectMethod.visitParameter("ctx", 0 /* modifiers */); injectMethod.visitCode(); + // this is always false for records who can't have a supertype if (superTypeIsInjectable) { // this - injectMethod.visitIntInsn(Opcodes.ALOAD, 0); + injectMethod.visitVarInsn(Opcodes.ALOAD, 0); // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitVarInsn(Opcodes.ALOAD, 1); // call inject on our bean param field injectMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, superTypeName, INJECT_METHOD_NAME, INJECT_METHOD_DESCRIPTOR, false); } + Label end = new Label(); + Label start = new Label(); + Map fieldVariableIndices = new HashMap<>(); + // make local variables for every record component, instead of setting fields + if (isRecord) { + // 0 is ctx param for records, since it's a static method + int fieldIndex = 1; + injectMethod.visitLabel(start); + for (Entry entry : fieldExtractors.entrySet()) { + FieldInfo fieldInfo = entry.getKey(); + // FIXME: some fields are ignored and should not have variables + injectMethod.visitInsn(AsmUtil.getNullValueOpcode(fieldInfo.type())); + injectMethod.visitVarInsn(AsmUtil.getStoreOpcode(fieldInfo.type()), fieldIndex); + fieldVariableIndices.put(fieldInfo.name(), fieldIndex); + fieldIndex += AsmUtil.getParameterSize(fieldInfo.type()); + } + } + int ctxParamIndex = isRecord ? 0 : 1; for (Entry entry : fieldExtractors.entrySet()) { FieldInfo fieldInfo = entry.getKey(); ServerIndexedParameter extractor = entry.getValue(); + int fieldIndex = isRecord ? fieldVariableIndices.get(fieldInfo.name()) : -1; switch (extractor.getType()) { case BEAN: - // this - injectMethod.visitIntInsn(Opcodes.ALOAD, 0); - String typeDescriptor = fieldInfo.type().descriptor(); - if (requireCreateBeanParams) { - String type = fieldInfo.type().name().toString().replace(".", "/"); - injectMethod.visitTypeInsn(Opcodes.NEW, type); - injectMethod.visitInsn(Opcodes.DUP); - injectMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, type, "", "()V", false); - injectMethod.visitInsn(Opcodes.DUP_X1); - injectMethod.visitFieldInsn(Opcodes.PUTFIELD, thisName, fieldInfo.name(), - typeDescriptor); - } else { - // our bean param field - injectMethod.visitFieldInsn(Opcodes.GETFIELD, thisName, fieldInfo.name(), - typeDescriptor); - } - // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); - // call inject on our bean param field - injectMethod.visitMethodInsn(Opcodes.INVOKEINTERFACE, QUARKUS_REST_INJECTION_TARGET_BINARY_NAME, - INJECT_METHOD_NAME, - INJECT_METHOD_DESCRIPTOR, true); + injectBeanParameter(injectMethod, fieldInfo, fieldIndex, ctxParamIndex); break; case ASYNC_RESPONSE: case BODY: // spec says not supported break; case CONTEXT: - // already set by CDI + // already set by CDI for non-records + if (isRecord) { + injectContextParameter(injectMethod, fieldInfo, ctxParamIndex); + } break; case FORM: injectParameterWithConverter(injectMethod, "getFormParameter", fieldInfo, extractor, true, true, - fieldInfo.hasAnnotation(ResteasyReactiveDotNames.ENCODED), false); + fieldInfo.hasAnnotation(ResteasyReactiveDotNames.ENCODED), false, fieldIndex, ctxParamIndex); break; case HEADER: injectParameterWithConverter(injectMethod, "getHeader", fieldInfo, extractor, true, false, false, - false); + false, fieldIndex, ctxParamIndex); break; case MATRIX: injectParameterWithConverter(injectMethod, "getMatrixParameter", fieldInfo, extractor, true, true, - fieldInfo.hasAnnotation(ResteasyReactiveDotNames.ENCODED), false); + fieldInfo.hasAnnotation(ResteasyReactiveDotNames.ENCODED), false, fieldIndex, ctxParamIndex); break; case COOKIE: injectParameterWithConverter(injectMethod, "getCookieParameter", fieldInfo, extractor, false, false, - false, false); + false, false, fieldIndex, ctxParamIndex); break; case PATH: injectParameterWithConverter(injectMethod, "getPathParameter", fieldInfo, extractor, false, true, - fieldInfo.hasAnnotation(ResteasyReactiveDotNames.ENCODED), false); + fieldInfo.hasAnnotation(ResteasyReactiveDotNames.ENCODED), false, fieldIndex, ctxParamIndex); break; case QUERY: injectParameterWithConverter(injectMethod, "getQueryParameter", fieldInfo, extractor, true, true, - fieldInfo.hasAnnotation(ResteasyReactiveDotNames.ENCODED), true); + fieldInfo.hasAnnotation(ResteasyReactiveDotNames.ENCODED), true, fieldIndex, ctxParamIndex); break; default: break; } + // FIXME: some fields are ignored and should not have variables + fieldIndex += AsmUtil.getParameterSize(fieldInfo.type()); + } + if (isRecord) { + injectMethod.visitTypeInsn(Opcodes.NEW, thisName); + injectMethod.visitInsn(Opcodes.DUP); + ClassInfo recordClass = indexView.getClassByName(thisName.replace('/', '.')); + // records must have exactly one constructor + String constructorSignature = recordClass.constructors().get(0).descriptor(); + for (RecordComponentInfo recordComponentInfo : recordClass.unsortedRecordComponents()) { + FieldInfo fieldInfo = fieldInfoByName.get(recordComponentInfo.name()); + if (fieldInfo == null) { + throw new RuntimeException("Record component " + recordComponentInfo.name() + + " is not a valid @BeanParam member: it must be a @*Param, @Rest* or @Context annotated member"); + } + ServerIndexedParameter extractor = fieldExtractors.get(fieldInfo); + switch (extractor.getType()) { + case ASYNC_RESPONSE: + case BODY: + // spec says not supported + break; + case CONTEXT: + case BEAN: + case FORM: + case HEADER: + case MATRIX: + case COOKIE: + case PATH: + case QUERY: + int fieldIndex = fieldVariableIndices.get(fieldInfo.name()); + injectMethod.visitVarInsn(AsmUtil.getLoadOpcode(fieldInfo.type()), fieldIndex); + break; + default: + break; + } + } + injectMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, thisName, "", constructorSignature, false); + injectMethod.visitInsn(Opcodes.ARETURN); + injectMethod.visitLabel(end); + for (Entry entry : fieldExtractors.entrySet()) { + FieldInfo fieldInfo = entry.getKey(); + ServerIndexedParameter extractor = entry.getValue(); + // FIXME: some fields are ignored and should not have variables + int fieldIndex = fieldVariableIndices.get(fieldInfo.name()); + injectMethod.visitLocalVariable(fieldInfo.name(), fieldInfo.type().descriptor(), null, start, end, + fieldIndex); + } + } else { + injectMethod.visitInsn(Opcodes.RETURN); } - injectMethod.visitInsn(Opcodes.RETURN); injectMethod.visitEnd(); injectMethod.visitMaxs(0, 0); @@ -350,6 +413,93 @@ public void visitEnd() { super.visitEnd(); } + private void injectBeanParameter(MethodVisitor injectMethod, FieldInfo fieldInfo, int fieldIndex, int ctxParamIndex) { + if (!isRecord) { + // this for the put/get field + injectMethod.visitVarInsn(Opcodes.ALOAD, 0); + } + String typeDescriptor = fieldInfo.type().descriptor(); + ClassInfo memberBeanClassInfo = indexView.getClassByName(fieldInfo.type().name()); + boolean memberBeanIsRecord = memberBeanClassInfo.isRecord(); + if (!memberBeanIsRecord) { + if (isRecord) { + // we need to obtain the non-record bean param from CDI + // stack: [] + // ctx param + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); + // stack: [ctx] + // type + injectMethod.visitLdcInsn(Type.getType(fieldInfo.type().descriptor())); + // stack: [ctx, bean-param-class] + // call getContextParameter on the ctx + injectMethod.visitMethodInsn(Opcodes.INVOKEINTERFACE, QUARKUS_REST_INJECTION_CONTEXT_BINARY_NAME, + "getBeanParameter", + "(Ljava/lang/Class;)Ljava/lang/Object;", true); + // stack: [bean-param] + injectMethod.visitTypeInsn(Opcodes.CHECKCAST, fieldInfo.type().name().toString().replace('.', '/')); + injectMethod.visitInsn(Opcodes.DUP); + // stack: [bean-param, bean-param] + injectMethod.visitVarInsn(AsmUtil.getStoreOpcode(fieldInfo.type()), fieldIndex); + // stack: [bean-param] + } else if (requireCreateBeanParams) { + String type = fieldInfo.type().name().toString().replace(".", "/"); + // stack: [this] + injectMethod.visitTypeInsn(Opcodes.NEW, type); + // stack: [this, new] + injectMethod.visitInsn(Opcodes.DUP); + // stack: [this, new, new] + injectMethod.visitMethodInsn(Opcodes.INVOKESPECIAL, type, "", "()V", false); + // stack: [this, new] + injectMethod.visitInsn(Opcodes.DUP_X1); + // stack: [new, this, new] + injectMethod.visitFieldInsn(Opcodes.PUTFIELD, thisName, fieldInfo.name(), + typeDescriptor); + // stack: [bean-param] + } else { + // our bean param field + injectMethod.visitFieldInsn(Opcodes.GETFIELD, thisName, fieldInfo.name(), + typeDescriptor); + // stack: [bean-param] + } + // ctx param + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); + // call inject on our bean param field + injectMethod.visitMethodInsn(Opcodes.INVOKEINTERFACE, QUARKUS_REST_INJECTION_TARGET_BINARY_NAME, + INJECT_METHOD_NAME, + INJECT_METHOD_DESCRIPTOR, true); + } else { + // stack non-record: [this] + // stack record: [] + // ctx param + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); + // stack non-record: [this, ctx] + // stack record: [ctx] + // call member bean record factory method + injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, fieldInfo.type().name().toString('/'), + INJECT_METHOD_NAME, + "(" + QUARKUS_REST_INJECTION_CONTEXT_DESCRIPTOR + ")" + fieldInfo.type().descriptor(), false); + if (isRecord) { + // stack record: [new] + injectMethod.visitVarInsn(AsmUtil.getStoreOpcode(fieldInfo.type()), fieldIndex); + } else { + // stack non-record: [this, new] + injectMethod.visitFieldInsn(Opcodes.PUTFIELD, thisName, fieldInfo.name(), + typeDescriptor); + } + } + } + + private void injectContextParameter(MethodVisitor injectMethod, FieldInfo fieldInfo, int ctxParamIndex) { + // ctx param + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); + // type + injectMethod.visitLdcInsn(Type.getType(fieldInfo.type().descriptor())); + // call getContextParameter on the ctx + injectMethod.visitMethodInsn(Opcodes.INVOKEINTERFACE, QUARKUS_REST_INJECTION_CONTEXT_BINARY_NAME, + "getContextParameter", + "(Ljava/lang/Class;)Ljava/lang/Object;", true); + } + private void generateMultipartFormFields(FieldInfo fieldInfo, ServerIndexedParameter extractor) { /* * private static Class map_type; @@ -439,7 +589,7 @@ private void generateConverterInitMethod(FieldInfo fieldInfo, ParameterConverter initConverterMethod.visitParameter("deployment", 0 /* modifiers */); initConverterMethod.visitCode(); // deployment param - initConverterMethod.visitIntInsn(Opcodes.ALOAD, 0); + initConverterMethod.visitVarInsn(Opcodes.ALOAD, 0); // this class initConverterMethod.visitLdcInsn(Type.getType("L" + thisName + ";")); // param name @@ -520,7 +670,7 @@ private ParameterConverterSupplier removeRuntimeResolvedConverterDelegate(Parame private void injectParameterWithConverter(MethodVisitor injectMethod, String methodName, FieldInfo fieldInfo, ServerIndexedParameter extractor, boolean extraSingleParameter, boolean extraEncodedParam, boolean encoded, - boolean extraSeparatorParam) { + boolean extraSeparatorParam, int fieldIndex, int ctxParamIndex) { // spec says: /* @@ -555,9 +705,9 @@ private void injectParameterWithConverter(MethodVisitor injectMethod, String met MultipartFormParamExtractor.Type multipartType = getMultipartFormType(extractor); if (multipartType == null) { loadParameter(injectMethod, methodName, extractor, extraSingleParameter, extraEncodedParam, encoded, - extraSeparatorParam); + extraSeparatorParam, ctxParamIndex); } else { - loadMultipartParameter(injectMethod, fieldInfo, extractor, multipartType); + loadMultipartParameter(injectMethod, fieldInfo, extractor, multipartType, ctxParamIndex); } Label valueWasNull = null; if (!extractor.isOptional()) { @@ -567,9 +717,11 @@ private void injectParameterWithConverter(MethodVisitor injectMethod, String met injectMethod.visitJumpInsn(Opcodes.IFNULL, valueWasNull); } convertParameter(injectMethod, extractor, fieldInfo); - // inject this (for the put field) before the injected value - injectMethod.visitIntInsn(Opcodes.ALOAD, 0); - injectMethod.visitInsn(Opcodes.SWAP); + if (!isRecord) { + // inject this (for the put field) before the injected value + injectMethod.visitVarInsn(Opcodes.ALOAD, 0); + injectMethod.visitInsn(Opcodes.SWAP); + } if (fieldInfo.type().kind() == Kind.PRIMITIVE) { // this already does the right checkcast AsmUtil.unboxIfRequired(injectMethod, fieldInfo.type()); @@ -579,8 +731,12 @@ private void injectParameterWithConverter(MethodVisitor injectMethod, String met injectMethod.visitTypeInsn(Opcodes.CHECKCAST, fieldInfo.type().name().toString().replace('.', '/')); } // store our param field - injectMethod.visitFieldInsn(Opcodes.PUTFIELD, thisName, fieldInfo.name(), - fieldInfo.type().descriptor()); + if (isRecord) { + injectMethod.visitVarInsn(AsmUtil.getStoreOpcode(fieldInfo.type()), fieldIndex); + } else { + injectMethod.visitFieldInsn(Opcodes.PUTFIELD, thisName, fieldInfo.name(), + fieldInfo.type().descriptor()); + } Label endLabel = new Label(); injectMethod.visitJumpInsn(Opcodes.GOTO, endLabel); @@ -635,28 +791,28 @@ private void injectParameterWithConverter(MethodVisitor injectMethod, String met } private void loadMultipartParameter(MethodVisitor injectMethod, FieldInfo fieldInfo, ServerIndexedParameter param, - MultipartFormParamExtractor.Type multipartType) { + MultipartFormParamExtractor.Type multipartType, int ctxParamIndex) { switch (multipartType) { case String: /* * return single ? MultipartSupport.getString(name, context) * : MultipartSupport.getStrings(name, context); */ - invokeMultipartSupport(param, injectMethod, "getString", STRING_DESCRIPTOR); + invokeMultipartSupport(param, injectMethod, "getString", STRING_DESCRIPTOR, ctxParamIndex); break; case ByteArray: /* * return single ? MultipartSupport.getByteArray(name, context) * : MultipartSupport.getByteArrays(name, context); */ - invokeMultipartSupport(param, injectMethod, "getByteArray", BYTE_ARRAY_DESCRIPTOR); + invokeMultipartSupport(param, injectMethod, "getByteArray", BYTE_ARRAY_DESCRIPTOR, ctxParamIndex); break; case InputStream: /* * return single ? MultipartSupport.getInputStream(name, context) * : MultipartSupport.getInputStreams(name, context); */ - invokeMultipartSupport(param, injectMethod, "getInputStream", INPUT_STREAM_DESCRIPTOR); + invokeMultipartSupport(param, injectMethod, "getInputStream", INPUT_STREAM_DESCRIPTOR, ctxParamIndex); break; case FileUpload: /* @@ -668,12 +824,13 @@ private void loadMultipartParameter(MethodVisitor injectMethod, FieldInfo fieldI */ if (param.getName().equals(FileUpload.ALL)) { // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, "getFileUploads", "(" + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" + LIST_DESCRIPTOR, false); } else { - invokeMultipartSupport(param, injectMethod, "getFileUpload", DEFAULT_FILE_UPLOAD_DESCRIPTOR); + invokeMultipartSupport(param, injectMethod, "getFileUpload", DEFAULT_FILE_UPLOAD_DESCRIPTOR, + ctxParamIndex); } break; case File: @@ -689,7 +846,7 @@ private void loadMultipartParameter(MethodVisitor injectMethod, FieldInfo fieldI // name param injectMethod.visitLdcInsn(param.getName()); // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, "getFileUpload", "(" + STRING_DESCRIPTOR + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" @@ -716,7 +873,7 @@ private void loadMultipartParameter(MethodVisitor injectMethod, FieldInfo fieldI // name param injectMethod.visitLdcInsn(param.getName()); // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, "getJavaIOFileUploads", @@ -737,7 +894,7 @@ private void loadMultipartParameter(MethodVisitor injectMethod, FieldInfo fieldI // name param injectMethod.visitLdcInsn(param.getName()); // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, "getFileUpload", "(" + STRING_DESCRIPTOR + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" @@ -761,7 +918,7 @@ private void loadMultipartParameter(MethodVisitor injectMethod, FieldInfo fieldI // name param injectMethod.visitLdcInsn(param.getName()); // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, "getJavaPathFileUploads", @@ -790,7 +947,7 @@ private void loadMultipartParameter(MethodVisitor injectMethod, FieldInfo fieldI injectMethod.visitFieldInsn(Opcodes.GETSTATIC, this.thisName, fieldInfo.name() + "_mediaType", MEDIA_TYPE_DESCRIPTOR); // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); String returnDescriptor; String methodName; @@ -812,12 +969,12 @@ private void loadMultipartParameter(MethodVisitor injectMethod, FieldInfo fieldI } private void invokeMultipartSupport(ServerIndexedParameter param, MethodVisitor injectMethod, - String singleOperationName, String singleOperationReturnDescriptor) { + String singleOperationName, String singleOperationReturnDescriptor, int ctxParamIndex) { if (param.isSingle()) { // name param injectMethod.visitLdcInsn(param.getName()); // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, singleOperationName, "(" + STRING_DESCRIPTOR + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" @@ -828,7 +985,7 @@ private void invokeMultipartSupport(ServerIndexedParameter param, MethodVisitor // name param injectMethod.visitLdcInsn(param.getName()); // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); injectMethod.visitTypeInsn(Opcodes.CHECKCAST, RESTEASY_REACTIVE_REQUEST_CONTEXT_BINARY_NAME); injectMethod.visitMethodInsn(Opcodes.INVOKESTATIC, MULTIPART_SUPPORT_BINARY_NAME, singleOperationName + "s", "(" + STRING_DESCRIPTOR + RESTEASY_REACTIVE_REQUEST_CONTEXT_DESCRIPTOR + ")" + LIST_DESCRIPTOR, @@ -889,9 +1046,10 @@ private void convertParameter(MethodVisitor injectMethod, ServerIndexedParameter } private void loadParameter(MethodVisitor injectMethod, String methodName, IndexedParameter extractor, - boolean extraSingleParameter, boolean extraEncodedParam, boolean encoded, boolean extraSeparatorParam) { + boolean extraSingleParameter, boolean extraEncodedParam, boolean encoded, boolean extraSeparatorParam, + int ctxParamIndex) { // ctx param - injectMethod.visitIntInsn(Opcodes.ALOAD, 1); + injectMethod.visitVarInsn(Opcodes.ALOAD, ctxParamIndex); // name param injectMethod.visitLdcInsn(extractor.getName()); String methodSignature; diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java index 4d288748ef627..0066c10be3bde 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java @@ -17,6 +17,14 @@ import java.util.concurrent.Executor; import java.util.regex.Matcher; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.ws.rs.container.AsyncResponse; +import jakarta.ws.rs.container.CompletionCallback; +import jakarta.ws.rs.container.ResourceContext; +import jakarta.ws.rs.container.ResourceInfo; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Configuration; import jakarta.ws.rs.core.Cookie; import jakarta.ws.rs.core.GenericEntity; import jakarta.ws.rs.core.HttpHeaders; @@ -26,13 +34,17 @@ import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.UriInfo; +import jakarta.ws.rs.ext.Providers; import jakarta.ws.rs.ext.ReaderInterceptor; import jakarta.ws.rs.ext.WriterInterceptor; +import jakarta.ws.rs.sse.Sse; +import jakarta.ws.rs.sse.SseEventSink; import org.jboss.resteasy.reactive.common.NotImplementedYet; import org.jboss.resteasy.reactive.common.core.AbstractResteasyReactiveContext; import org.jboss.resteasy.reactive.common.util.Encode; import org.jboss.resteasy.reactive.common.util.PathSegmentImpl; +import org.jboss.resteasy.reactive.server.SimpleResourceInfo; import org.jboss.resteasy.reactive.server.core.multipart.FormData; import org.jboss.resteasy.reactive.server.core.serialization.EntityWriter; import org.jboss.resteasy.reactive.server.injection.ResteasyReactiveInjectionContext; @@ -42,7 +54,9 @@ import org.jboss.resteasy.reactive.server.jaxrs.HttpHeadersImpl; import org.jboss.resteasy.reactive.server.jaxrs.ProvidersImpl; import org.jboss.resteasy.reactive.server.jaxrs.RequestImpl; +import org.jboss.resteasy.reactive.server.jaxrs.ResourceContextImpl; import org.jboss.resteasy.reactive.server.jaxrs.SseEventSinkImpl; +import org.jboss.resteasy.reactive.server.jaxrs.SseImpl; import org.jboss.resteasy.reactive.server.jaxrs.UriInfoImpl; import org.jboss.resteasy.reactive.server.mapping.RuntimeResource; import org.jboss.resteasy.reactive.server.mapping.URITemplate; @@ -940,6 +954,95 @@ public Object getFormParameter(String name, boolean single, boolean encoded) { } + @Override + public T getBeanParameter(Class type) { + // FIXME: we don't check if it's a bean parameter at all, but this is only called from ClassInjectorTransformer + Instance select = CDI.current().select(type); + if (select != null) { + T instance = select.get(); + if (instance != null) { + registerCompletionCallback(new CompletionCallback() { + @Override + public void onComplete(Throwable throwable) { + select.destroy(instance); + } + }); + return (T) instance; + } + } + throw new IllegalStateException("Unsupported bean param type: " + type); + } + + @Override + public T getContextParameter(Class type) { + // NOTE: Same list for CDI at ContextProducers and in EndpointIndexer.CONTEXT_TYPES + if (type.equals(ServerRequestContext.class)) { + return (T) this; + } + if (type.equals(HttpHeaders.class)) { + return (T) getHttpHeaders(); + } + if (type.equals(UriInfo.class)) { + return (T) getUriInfo(); + } + if (type.equals(Configuration.class)) { + return (T) getDeployment().getConfiguration(); + } + if (type.equals(AsyncResponse.class)) { + AsyncResponseImpl asyncResponse = getAsyncResponse(); + if (asyncResponse == null) { + asyncResponse = new AsyncResponseImpl(this); + setAsyncResponse(asyncResponse); + } + return (T) response; + } + if (type.equals(SseEventSink.class)) { + SseEventSinkImpl sseEventSink = getSseEventSink(); + if (sseEventSink == null) { + sseEventSink = new SseEventSinkImpl(this); + setSseEventSink(sseEventSink); + } + return (T) sseEventSink; + } + if (type.equals(Request.class)) { + return (T) getRequest(); + } + if (type.equals(Providers.class)) { + return (T) getProviders(); + } + if (type.equals(Sse.class)) { + return (T) SseImpl.INSTANCE; + } + if (type.equals(ResourceInfo.class)) { + return (T) getTarget().getLazyMethod(); + } + if (type.equals(SimpleResourceInfo.class)) { + return (T) getTarget().getSimplifiedResourceInfo(); + } + if (type.equals(Application.class)) { + return (T) CDI.current().select(Application.class).get(); + } + if (type.equals(SecurityContext.class)) { + return (T) getSecurityContext(); + } + if (type.equals(ResourceContext.class)) { + return (T) ResourceContextImpl.INSTANCE; + } + Object instance = unwrap(type); + if (instance != null) { + return (T) instance; + } + Instance select = CDI.current().select(type); + if (select != null) { + instance = select.get(); + } + if (instance != null) { + return (T) instance; + } + // FIXME: move to build time + throw new IllegalStateException("Unsupported contextual type: " + type); + } + @Override public String getPathParameter(String name, boolean encoded) { // this is a slower version than getPathParam, but we can't actually bake path indices inside diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/ContextParamExtractor.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/ContextParamExtractor.java index 515af349d12e1..f3641388a9c09 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/ContextParamExtractor.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/ContextParamExtractor.java @@ -1,32 +1,10 @@ package org.jboss.resteasy.reactive.server.core.parameters; -import jakarta.enterprise.inject.Instance; -import jakarta.enterprise.inject.spi.CDI; -import jakarta.ws.rs.container.AsyncResponse; -import jakarta.ws.rs.container.ResourceContext; -import jakarta.ws.rs.container.ResourceInfo; -import jakarta.ws.rs.core.Application; -import jakarta.ws.rs.core.Configuration; -import jakarta.ws.rs.core.HttpHeaders; -import jakarta.ws.rs.core.Request; -import jakarta.ws.rs.core.SecurityContext; -import jakarta.ws.rs.core.UriInfo; -import jakarta.ws.rs.ext.Providers; -import jakarta.ws.rs.sse.Sse; -import jakarta.ws.rs.sse.SseEventSink; - -import org.jboss.resteasy.reactive.server.SimpleResourceInfo; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; -import org.jboss.resteasy.reactive.server.jaxrs.AsyncResponseImpl; -import org.jboss.resteasy.reactive.server.jaxrs.ResourceContextImpl; -import org.jboss.resteasy.reactive.server.jaxrs.SseEventSinkImpl; -import org.jboss.resteasy.reactive.server.jaxrs.SseImpl; -import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; public class ContextParamExtractor implements ParameterExtractor { private final Class type; - private volatile Instance select; public ContextParamExtractor(String type) { try { @@ -42,68 +20,7 @@ public ContextParamExtractor(Class type) { @Override public Object extractParameter(ResteasyReactiveRequestContext context) { - // NOTE: Same list for CDI at ContextProducers and in EndpointIndexer.CONTEXT_TYPES - if (type.equals(ServerRequestContext.class)) { - return context; - } - if (type.equals(HttpHeaders.class)) { - return context.getHttpHeaders(); - } - if (type.equals(UriInfo.class)) { - return context.getUriInfo(); - } - if (type.equals(Configuration.class)) { - return context.getDeployment().getConfiguration(); - } - if (type.equals(AsyncResponse.class)) { - AsyncResponseImpl response = new AsyncResponseImpl(context); - context.setAsyncResponse(response); - return response; - } - if (type.equals(SseEventSink.class)) { - SseEventSinkImpl sink = new SseEventSinkImpl(context); - context.setSseEventSink(sink); - return sink; - } - if (type.equals(Request.class)) { - return context.getRequest(); - } - if (type.equals(Providers.class)) { - return context.getProviders(); - } - if (type.equals(Sse.class)) { - return SseImpl.INSTANCE; - } - if (type.equals(ResourceInfo.class)) { - return context.getTarget().getLazyMethod(); - } - if (type.equals(SimpleResourceInfo.class)) { - return context.getTarget().getSimplifiedResourceInfo(); - } - if (type.equals(Application.class)) { - return CDI.current().select(Application.class).get(); - } - if (type.equals(SecurityContext.class)) { - return context.getSecurityContext(); - } - if (type.equals(ResourceContext.class)) { - return ResourceContextImpl.INSTANCE; - } - Object instance = context.unwrap(type); - if (instance != null) { - return instance; - } - if (select == null) { - select = CDI.current().select(type); - } - if (select != null) { - instance = select.get(); - } - if (instance != null) { - return instance; - } - // FIXME: move to build time - throw new IllegalStateException("Unsupported contextual type: " + type); + return context.getContextParameter(type); } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/RecordBeanParamExtractor.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/RecordBeanParamExtractor.java new file mode 100644 index 0000000000000..8c2263e4e83ba --- /dev/null +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/RecordBeanParamExtractor.java @@ -0,0 +1,34 @@ +package org.jboss.resteasy.reactive.server.core.parameters; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; + +import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; +import org.jboss.resteasy.reactive.server.injection.ResteasyReactiveInjectionContext; + +public class RecordBeanParamExtractor implements ParameterExtractor { + + private MethodHandle factoryMethod; + + public RecordBeanParamExtractor(Class target) { + try { + factoryMethod = MethodHandles.lookup().findStatic(target, "__quarkus_rest_inject", + MethodType.methodType(target, ResteasyReactiveInjectionContext.class)); + } catch (NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException("Failed to find target generated factory method on record @BeanParam type", e); + } + } + + @Override + public Object extractParameter(ResteasyReactiveRequestContext context) { + try { + return factoryMethod.invoke(context); + } catch (RuntimeException e) { + throw e; + } catch (Throwable e) { + e.printStackTrace(); + throw new RuntimeException("Failed to invoke generated factory method on record @BeanParam type", e); + } + } +} diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java index abe1546f86896..2bc5999117537 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/startup/RuntimeResourceDeployment.java @@ -60,6 +60,7 @@ import org.jboss.resteasy.reactive.server.core.parameters.ParameterExtractor; import org.jboss.resteasy.reactive.server.core.parameters.PathParamExtractor; import org.jboss.resteasy.reactive.server.core.parameters.QueryParamExtractor; +import org.jboss.resteasy.reactive.server.core.parameters.RecordBeanParamExtractor; import org.jboss.resteasy.reactive.server.core.parameters.converters.ParameterConverter; import org.jboss.resteasy.reactive.server.core.parameters.converters.RuntimeResolvedConverter; import org.jboss.resteasy.reactive.server.core.serialization.DynamicEntityWriter; @@ -715,7 +716,12 @@ public ParameterExtractor parameterExtractor(Map pathParameterI return extractor; case BEAN: case MULTI_PART_FORM: - return new InjectParamExtractor((BeanFactory) info.getFactoryCreator().apply(loadClass(param.type))); + Class paramClass = loadClass(param.type); + if (paramClass.isRecord()) { + return new RecordBeanParamExtractor(paramClass); + } else { + return new InjectParamExtractor((BeanFactory) info.getFactoryCreator().apply(paramClass)); + } case MULTI_PART_DATA_INPUT: return MultipartDataInputExtractor.INSTANCE; case CUSTOM: diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/injection/ResteasyReactiveInjectionContext.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/injection/ResteasyReactiveInjectionContext.java index 777202fa17a8f..b7ca3c7ff4b3e 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/injection/ResteasyReactiveInjectionContext.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/injection/ResteasyReactiveInjectionContext.java @@ -14,4 +14,19 @@ public interface ResteasyReactiveInjectionContext { Object getFormParameter(String name, boolean single, boolean encoded); T unwrap(Class theType); + + /** + * Gets the context parameter instance by type for predefined types, also calls unwrap, and CDI. + * + * @throws IllegalStateException if there is no such context object + */ + T getContextParameter(Class type); + + /** + * Gets the bean parameter instance by type via CDI, and registers a cleanup for it. This does not call __inject + * on it, and it does not work for records (which cannot be gotten via CDI). + * + * @throws IllegalStateException if there is no such context object + */ + T getBeanParameter(Class type); }