From bb8091d169f9ff909243361ddfd3577b9cb15d89 Mon Sep 17 00:00:00 2001 From: "Wippermueller, Frank" Date: Fri, 21 Jan 2022 15:32:06 +0100 Subject: [PATCH] Rest-Client-Reactive: Allow FormParams to be used in BeanParams --- .../JaxrsClientReactiveProcessor.java | 113 ++++++--- .../reactive/beanparam/BeanFormParamTest.java | 121 ++++++++++ .../client/processor/pom.xml | 13 +- .../processor/beanparam/BeanParamParser.java | 219 ++++++++++-------- .../processor/beanparam/FormParamItem.java | 21 ++ .../client/processor/beanparam/ItemType.java | 1 + .../beanparam/BeanParamParserTest.java | 106 +++++++++ 7 files changed, 467 insertions(+), 127 deletions(-) create mode 100644 extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/beanparam/BeanFormParamTest.java create mode 100644 independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java create mode 100644 independent-projects/resteasy-reactive/client/processor/src/test/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParserTest.java diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 372f2f24ced1c..b5a374ab54b10 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -80,6 +80,7 @@ import org.jboss.resteasy.reactive.client.processor.beanparam.BeanParamItem; import org.jboss.resteasy.reactive.client.processor.beanparam.ClientBeanParamInfo; import org.jboss.resteasy.reactive.client.processor.beanparam.CookieParamItem; +import org.jboss.resteasy.reactive.client.processor.beanparam.FormParamItem; import org.jboss.resteasy.reactive.client.processor.beanparam.HeaderParamItem; import org.jboss.resteasy.reactive.client.processor.beanparam.Item; import org.jboss.resteasy.reactive.client.processor.beanparam.PathParamItem; @@ -851,11 +852,13 @@ A more full example of generated client (with sub-resource) can is at the bottom AssignableResultHandle invocationBuilderRef = handleBeanParamMethod .createVariable(Invocation.Builder.class); handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - addBeanParamData(methodCreator, handleBeanParamMethod, + formParams = addBeanParamData(methodCreator, handleBeanParamMethod, invocationBuilderRef, beanParam.getItems(), methodCreator.getMethodParam(paramIdx), methodTarget, index, + restClientInterface.getClassName(), methodCreator.getThis(), - handleBeanParamMethod.getThis()); + handleBeanParamMethod.getThis(), + formParams); handleBeanParamMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleBeanParamDescriptor, methodCreator.getMethodParam(paramIdx)); @@ -900,22 +903,8 @@ A more full example of generated client (with sub-resource) can is at the bottom invocationBuilderEnrichers.put(handleHeaderDescriptor, methodCreator.getMethodParam(paramIdx)); } else if (param.parameterType == ParameterType.FORM) { formParams = createIfAbsent(methodCreator, formParams); - ResultHandle convertedFormParam = methodCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(RestClientBase.class, "convertParam", Object.class, Object.class, - Class.class), - methodCreator.getThis(), methodCreator.getMethodParam(paramIdx), - methodCreator.loadClass(param.type)); - ResultHandle isString = methodCreator.instanceOf(convertedFormParam, String.class); - BranchResult isStringBranch = methodCreator.ifTrue(isString); - isStringBranch.falseBranch().throwException(IllegalStateException.class, - "Form parameter '" + param.name - + "' could not be converted to 'String' for REST Client interface '" - + restClientInterface.getClassName() + "'. A proper implementation of '" - + ParamConverter.class.getName() + "' needs to be returned by a '" - + ParamConverterProvider.class.getName() - + "' that is registered with the client via the @RegisterProvider annotation on the REST Client interface."); - isStringBranch.trueBranch().invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, - methodCreator.load(param.name), convertedFormParam); + addFormParam(methodCreator, param.name, methodCreator.getMethodParam(paramIdx), param.type, + restClientInterface.getClassName(), methodCreator.getThis(), formParams); } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { if (multipartForm != null) { throw new IllegalArgumentException("MultipartForm data set twice for method " @@ -1142,12 +1131,14 @@ private void handleSubResourceMethod(List AssignableResultHandle invocationBuilderRef = handleBeanParamMethod .createVariable(Invocation.Builder.class); handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - addBeanParamData(subMethodCreator, handleBeanParamMethod, + formParams = addBeanParamData(subMethodCreator, handleBeanParamMethod, invocationBuilderRef, beanParam.getItems(), paramValue, methodTarget, index, + interfaceClass.name().toString(), subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), handleBeanParamMethod.readInstanceField(clientField, - handleBeanParamMethod.getThis())); + handleBeanParamMethod.getThis()), + formParams); handleBeanParamMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleBeanParamDescriptor, paramValue); @@ -1235,12 +1226,14 @@ private void handleSubResourceMethod(List AssignableResultHandle invocationBuilderRef = handleBeanParamMethod .createVariable(Invocation.Builder.class); handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - addBeanParamData(subMethodCreator, handleBeanParamMethod, + formParams = addBeanParamData(subMethodCreator, handleBeanParamMethod, invocationBuilderRef, beanParam.getItems(), subMethodCreator.getMethodParam(paramIdx), methodTarget, index, + interfaceClass.name().toString(), subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), handleBeanParamMethod.readInstanceField(clientField, - handleBeanParamMethod.getThis())); + handleBeanParamMethod.getThis()), + formParams); handleBeanParamMethod.returnValue(invocationBuilderRef); invocationBuilderEnrichers.put(handleBeanParamDescriptor, @@ -2114,15 +2107,39 @@ private Optional getJavaMethod(ClassInfo interfaceClass, ResourceMet return maybeMethod; } - private void addBeanParamData(BytecodeCreator methodCreator, + private AssignableResultHandle addBeanParamData(MethodCreator methodCreator, BytecodeCreator invocationBuilderEnricher, // Invocation.Builder executePut$$enrichInvocationBuilder${noOfBeanParam}(Invocation.Builder) AssignableResultHandle invocationBuilder, List beanParamItems, ResultHandle param, AssignableResultHandle target, // can only be used in the current method, not in `invocationBuilderEnricher` IndexView index, + String restClientInterfaceClassName, ResultHandle client, - ResultHandle invocationEnricherClient) { // this client or containing client if this is a sub-client + ResultHandle invocationEnricherClient, // this client or containing client if this is a sub-client + AssignableResultHandle formParams) { + // Form params collector must be initialized at method root level before any inner blocks that may use it + if (areFormParamsDefinedIn(beanParamItems)) { + formParams = createIfAbsent(methodCreator, formParams); + } + + addSubBeanParamData(methodCreator, invocationBuilderEnricher, invocationBuilder, beanParamItems, param, target, + index, restClientInterfaceClassName, client, invocationEnricherClient, formParams); + + return formParams; + } + + private void addSubBeanParamData(BytecodeCreator methodCreator, + BytecodeCreator invocationBuilderEnricher, // Invocation.Builder executePut$$enrichInvocationBuilder${noOfBeanParam}(Invocation.Builder) + AssignableResultHandle invocationBuilder, + List beanParamItems, + ResultHandle param, + AssignableResultHandle target, // can only be used in the current method, not in `invocationBuilderEnricher` + IndexView index, + String restClientInterfaceClassName, + ResultHandle client, + ResultHandle invocationEnricherClient, // this client or containing client if this is a sub-client + AssignableResultHandle formParams) { BytecodeCreator creator = methodCreator.ifNotNull(param).trueBranch(); BytecodeCreator invoEnricher = invocationBuilderEnricher.ifNotNull(invocationBuilderEnricher.getMethodParam(1)) .trueBranch(); @@ -2131,8 +2148,9 @@ private void addBeanParamData(BytecodeCreator methodCreator, case BEAN_PARAM: BeanParamItem beanParamItem = (BeanParamItem) item; ResultHandle beanParamElementHandle = beanParamItem.extract(creator, param); - addBeanParamData(creator, invoEnricher, invocationBuilder, beanParamItem.items(), - beanParamElementHandle, target, index, client, invocationEnricherClient); + addSubBeanParamData(creator, invoEnricher, invocationBuilder, beanParamItem.items(), + beanParamElementHandle, target, index, restClientInterfaceClassName, client, + invocationEnricherClient, formParams); break; case QUERY_PARAM: QueryParamItem queryParam = (QueryParamItem) item; @@ -2162,12 +2180,33 @@ private void addBeanParamData(BytecodeCreator methodCreator, pathParam.getPathParamName(), pathParam.extract(creator, param), pathParam.getParamType(), client); break; + case FORM_PARAM: + FormParamItem formParam = (FormParamItem) item; + addFormParam(creator, formParam.getFormParamName(), formParam.extract(creator, param), + formParam.getParamType(), restClientInterfaceClassName, client, formParams); + break; default: - throw new IllegalStateException("Unimplemented"); // TODO form params, etc + throw new IllegalStateException("Unimplemented"); } } } + private boolean areFormParamsDefinedIn(List beanParamItems) { + for (Item item : beanParamItems) { + switch (item.type()) { + case FORM_PARAM: + return true; + case BEAN_PARAM: + if (areFormParamsDefinedIn(((BeanParamItem) item).items())) { + return true; + } + break; + } + } + + return false; + } + // takes a result handle to target as one of the parameters, returns a result handle to a modified target private ResultHandle addQueryParam(BytecodeCreator methodCreator, ResultHandle target, @@ -2250,6 +2289,26 @@ private void addPathParam(BytecodeCreator methodCreator, AssignableResultHandle methodCreator.load(paramName), handle)); } + private void addFormParam(BytecodeCreator methodCreator, String paramName, ResultHandle formParamHandle, + String parameterType, String restClientInterfaceClassName, + ResultHandle client, AssignableResultHandle formParams) { + ResultHandle convertedFormParam = methodCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(RestClientBase.class, "convertParam", Object.class, Object.class, Class.class), + client, formParamHandle, + methodCreator.loadClass(parameterType)); + ResultHandle isString = methodCreator.instanceOf(convertedFormParam, String.class); + BranchResult isStringBranch = methodCreator.ifTrue(isString); + isStringBranch.falseBranch().throwException(IllegalStateException.class, + "Form parameter '" + paramName + + "' could not be converted to 'String' for REST Client interface '" + + restClientInterfaceClassName + "'. A proper implementation of '" + + ParamConverter.class.getName() + "' needs to be returned by a '" + + ParamConverterProvider.class.getName() + + "' that is registered with the client via the @RegisterProvider annotation on the REST Client interface."); + isStringBranch.trueBranch().invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, + methodCreator.load(paramName), convertedFormParam); + } + private void addCookieParam(BytecodeCreator invoBuilderEnricher, AssignableResultHandle invocationBuilder, String paramName, ResultHandle cookieParamHandle, String paramType, ResultHandle client) { cookieParamHandle = invoBuilderEnricher.invokeVirtualMethod( diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/beanparam/BeanFormParamTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/beanparam/BeanFormParamTest.java new file mode 100644 index 0000000000000..b3e339d497d5e --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/beanparam/BeanFormParamTest.java @@ -0,0 +1,121 @@ +package io.quarkus.rest.client.reactive.beanparam; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URI; + +import javax.ws.rs.BeanParam; +import javax.ws.rs.Consumes; +import javax.ws.rs.FormParam; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.ParamConverterProvider; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class BeanFormParamTest { + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest(); + + @TestHTTPResource + URI baseUri; + + @Test + void shouldPassFormParamsFromBeanParam() { + assertThat(formTestClient().postFormParams(new BeanWithFormParams("value1", "value2", Param.SECOND))) + .isEqualTo( + "received value1-value2-2"); + } + + private FormTestClient formTestClient() { + return RestClientBuilder.newBuilder().baseUri(baseUri).register(ParamConverter.class).build(FormTestClient.class); + } + + @Path("/form") + public interface FormTestClient { + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + String postFormParams(@BeanParam BeanWithFormParams beanParam); + } + + public static class BeanWithFormParams { + private final String param1; + private final String param2; + private final Param param3; + + public BeanWithFormParams(String param1, String param2, Param param3) { + this.param1 = param1; + this.param2 = param2; + this.param3 = param3; + } + + @FormParam("param1") + public String getParam1() { + return param1; + } + + @FormParam("param2") + public String getParam2() { + return param2; + } + + @FormParam("param3") + public Param getParam3() { + return param3; + } + } + + @Path("/form") + public static class FormTestResource { + @POST + public String post(@FormParam("param1") String param1, @FormParam("param2") String param2, + @FormParam("param3") String param3) { + return String.format("received %s-%s-%s", param1, param2, param3); + } + } + + enum Param { + FIRST, + SECOND + } + + public static class ParamConverter implements ParamConverterProvider { + @SuppressWarnings("unchecked") + @Override + public javax.ws.rs.ext.ParamConverter getConverter(Class rawType, Type genericType, + Annotation[] annotations) { + if (rawType == BeanFormParamTest.Param.class) { + return (javax.ws.rs.ext.ParamConverter) new javax.ws.rs.ext.ParamConverter() { + @Override + public BeanFormParamTest.Param fromString(String value) { + return null; + } + + @Override + public String toString(BeanFormParamTest.Param value) { + if (value == null) { + return null; + } + switch (value) { + case FIRST: + return "1"; + case SECOND: + return "2"; + default: + return "unexpected"; + } + } + }; + } + return null; + } + } +} diff --git a/independent-projects/resteasy-reactive/client/processor/pom.xml b/independent-projects/resteasy-reactive/client/processor/pom.xml index 9b0ee6c18fc2f..e33ad4140828b 100644 --- a/independent-projects/resteasy-reactive/client/processor/pom.xml +++ b/independent-projects/resteasy-reactive/client/processor/pom.xml @@ -38,23 +38,26 @@ jakarta.enterprise jakarta.enterprise.cdi-api - jakarta.annotation jakarta.annotation-api + + org.jboss.logging + jboss-logging + + org.junit.jupiter junit-jupiter test - - org.jboss.logging - jboss-logging + org.assertj + assertj-core + test - diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java index 4bc0d551afdcf..bc1185c94d38d 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParser.java @@ -2,13 +2,18 @@ import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.BEAN_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.COOKIE_PARAM; +import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.FORM_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.HEADER_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.PATH_PARAM; import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.QUERY_PARAM; import java.util.ArrayList; +import java.util.Collections; +import java.util.IdentityHashMap; import java.util.List; -import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.stream.Collectors; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; @@ -22,111 +27,89 @@ public class BeanParamParser { public static List parse(ClassInfo beanParamClass, IndexView index) { - List resultList = new ArrayList<>(); + Set processedBeanParamClasses = Collections.newSetFromMap(new IdentityHashMap<>()); + return parseInternal(beanParamClass, index, processedBeanParamClasses); + } - // Parse class tree recursively - if (!JandexUtil.DOTNAME_OBJECT.equals(beanParamClass.superName())) { - resultList.addAll(parse(index.getClassByName(beanParamClass.superName()), index)); + private static List parseInternal(ClassInfo beanParamClass, IndexView index, + Set processedBeanParamClasses) { + if (!processedBeanParamClasses.add(beanParamClass)) { + throw new IllegalArgumentException("Cycle detected in BeanParam annotations; already processed class " + + beanParamClass.name()); } - Map> annotations = beanParamClass.annotations(); - List queryParams = annotations.get(QUERY_PARAM); - if (queryParams != null) { - for (AnnotationInstance annotation : queryParams) { - AnnotationTarget target = annotation.target(); - if (target.kind() == AnnotationTarget.Kind.FIELD) { - FieldInfo fieldInfo = target.asField(); - resultList.add(new QueryParamItem(annotation.value().asString(), - new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()), - fieldInfo.type())); - } else if (target.kind() == AnnotationTarget.Kind.METHOD) { - MethodInfo getterMethod = getGetterMethod(beanParamClass, target.asMethod()); - resultList.add(new QueryParamItem(annotation.value().asString(), - new GetterExtractor(getterMethod), getterMethod.returnType())); - } - } - } - List beanParams = annotations.get(BEAN_PARAM); - if (beanParams != null) { - for (AnnotationInstance annotation : beanParams) { - AnnotationTarget target = annotation.target(); - if (target.kind() == AnnotationTarget.Kind.FIELD) { - FieldInfo fieldInfo = target.asField(); - Type type = fieldInfo.type(); - if (type.kind() == Type.Kind.CLASS) { - List subBeanParamItems = parse(index.getClassByName(type.asClassType().name()), index); - resultList.add(new BeanParamItem(subBeanParamItems, - new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()))); - } else { - throw new IllegalArgumentException("BeanParam annotation used on a field that is not an object: " - + beanParamClass.name() + "." + fieldInfo.name()); - } - } else if (target.kind() == AnnotationTarget.Kind.METHOD) { - // this should be getter or setter - MethodInfo methodInfo = target.asMethod(); - MethodInfo getter = getGetterMethod(beanParamClass, methodInfo); - Type returnType = getter.returnType(); - List items = parse(index.getClassByName(returnType.name()), index); - resultList.add(new BeanParamItem(items, new GetterExtractor(getter))); - } + try { + List resultList = new ArrayList<>(); + + // Parse class tree recursively + if (!JandexUtil.DOTNAME_OBJECT.equals(beanParamClass.superName())) { + resultList + .addAll(parseInternal(index.getClassByName(beanParamClass.superName()), index, + processedBeanParamClasses)); } - } - List cookieParams = annotations.get(COOKIE_PARAM); - if (cookieParams != null) { - for (AnnotationInstance annotation : cookieParams) { - AnnotationTarget target = annotation.target(); - if (target.kind() == AnnotationTarget.Kind.FIELD) { - FieldInfo fieldInfo = target.asField(); - resultList.add(new CookieParamItem(annotation.value().asString(), + resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, QUERY_PARAM, + (annotationValue, fieldInfo) -> new QueryParamItem(annotationValue, + new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()), + fieldInfo.type()), + (annotationValue, getterMethod) -> new QueryParamItem(annotationValue, new GetterExtractor(getterMethod), + getterMethod.returnType()))); + + resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, BEAN_PARAM, + (annotationValue, fieldInfo) -> { + Type type = fieldInfo.type(); + if (type.kind() == Type.Kind.CLASS) { + List subBeanParamItems = parseInternal(index.getClassByName(type.asClassType().name()), index, + processedBeanParamClasses); + return new BeanParamItem(subBeanParamItems, + new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())); + } else { + throw new IllegalArgumentException("BeanParam annotation used on a field that is not an object: " + + beanParamClass.name() + "." + fieldInfo.name()); + } + }, + (annotationValue, getterMethod) -> { + Type returnType = getterMethod.returnType(); + List items = parseInternal(index.getClassByName(returnType.name()), index, + processedBeanParamClasses); + return new BeanParamItem(items, new GetterExtractor(getterMethod)); + })); + + resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, COOKIE_PARAM, + (annotationValue, fieldInfo) -> new CookieParamItem(annotationValue, new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()), - fieldInfo.type().name().toString())); - } else if (target.kind() == AnnotationTarget.Kind.METHOD) { - MethodInfo getterMethod = getGetterMethod(beanParamClass, target.asMethod()); - resultList.add(new CookieParamItem(annotation.value().asString(), - new GetterExtractor(getterMethod), getterMethod.returnType().name().toString())); - } - } - } + fieldInfo.type().name().toString()), + (annotationValue, getterMethod) -> new CookieParamItem(annotationValue, + new GetterExtractor(getterMethod), getterMethod.returnType().name().toString()))); - List headerParams = annotations.get(HEADER_PARAM); - if (headerParams != null) { - for (AnnotationInstance headerParamAnnotation : headerParams) { - AnnotationTarget target = headerParamAnnotation.target(); - if (target.kind() == AnnotationTarget.Kind.FIELD) { - FieldInfo fieldInfo = target.asField(); - String paramType = fieldInfo.type().name().toString(); - resultList.add(new HeaderParamItem(headerParamAnnotation.value().asString(), + resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, HEADER_PARAM, + (annotationValue, fieldInfo) -> new HeaderParamItem(annotationValue, new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()), - paramType)); - } else if (target.kind() == AnnotationTarget.Kind.METHOD) { - MethodInfo getterMethod = getGetterMethod(beanParamClass, target.asMethod()); - resultList.add(new HeaderParamItem(headerParamAnnotation.value().asString(), - new GetterExtractor(getterMethod), getterMethod.returnType().name().toString())); - } - } - } + fieldInfo.type().name().toString()), + (annotationValue, getterMethod) -> new HeaderParamItem(annotationValue, + new GetterExtractor(getterMethod), getterMethod.returnType().name().toString()))); - List pathParams = annotations.get(PATH_PARAM); - if (pathParams != null) { - for (AnnotationInstance pathParamAnnotation : pathParams) { - AnnotationTarget target = pathParamAnnotation.target(); - if (target.kind() == AnnotationTarget.Kind.FIELD) { - FieldInfo fieldInfo = target.asField(); - String fieldType = fieldInfo.type().name().toString(); - resultList.add(new PathParamItem(pathParamAnnotation.value().asString(), fieldType, - new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString()))); - } else if (target.kind() == AnnotationTarget.Kind.METHOD) { - MethodInfo getterMethod = getGetterMethod(beanParamClass, target.asMethod()); - String paramType = getterMethod.returnType().name().toString(); - resultList.add(new PathParamItem(pathParamAnnotation.value().asString(), paramType, - new GetterExtractor(getterMethod))); - } - } - } + resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, PATH_PARAM, + (annotationValue, fieldInfo) -> new PathParamItem(annotationValue, fieldInfo.type().name().toString(), + new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())), + (annotationValue, getterMethod) -> new PathParamItem(annotationValue, + getterMethod.returnType().name().toString(), + new GetterExtractor(getterMethod)))); + + resultList.addAll(paramItemsForFieldsAndMethods(beanParamClass, FORM_PARAM, + (annotationValue, fieldInfo) -> new FormParamItem(annotationValue, + fieldInfo.type().name().toString(), + new FieldExtractor(null, fieldInfo.name(), fieldInfo.declaringClass().name().toString())), + (annotationValue, getterMethod) -> new FormParamItem(annotationValue, + getterMethod.returnType().name().toString(), + new GetterExtractor(getterMethod)))); + + return resultList; - return resultList; + } finally { + processedBeanParamClasses.remove(beanParamClass); + } } private static MethodInfo getGetterMethod(ClassInfo beanParamClass, MethodInfo methodInfo) { @@ -148,6 +131,52 @@ private static MethodInfo getGetterMethod(ClassInfo beanParamClass, MethodInfo m return getter; } + private static List paramItemsForFieldsAndMethods(ClassInfo beanParamClass, DotName parameterType, + BiFunction fieldExtractor, BiFunction methodExtractor) { + return ParamTypeAnnotations.of(beanParamClass, parameterType).itemsForFieldsAndMethods(fieldExtractor, methodExtractor); + } + private BeanParamParser() { } + + private static class ParamTypeAnnotations { + private final ClassInfo beanParamClass; + private final List annotations; + + private ParamTypeAnnotations(ClassInfo beanParamClass, DotName parameterType) { + this.beanParamClass = beanParamClass; + + List relevantAnnotations = beanParamClass.annotations().get(parameterType); + this.annotations = relevantAnnotations == null + ? Collections.emptyList() + : relevantAnnotations.stream().filter(this::isFieldOrMethodAnnotation).collect(Collectors.toList()); + } + + private static ParamTypeAnnotations of(ClassInfo beanParamClass, DotName parameterType) { + return new ParamTypeAnnotations(beanParamClass, parameterType); + } + + private List itemsForFieldsAndMethods(BiFunction itemFromFieldExtractor, + BiFunction itemFromMethodExtractor) { + return annotations.stream() + .map(annotation -> toItem(annotation, itemFromFieldExtractor, itemFromMethodExtractor)) + .collect(Collectors.toList()); + } + + private T toItem(AnnotationInstance annotation, + BiFunction itemFromFieldExtractor, + BiFunction itemFromMethodExtractor) { + String annotationValue = annotation.value() == null ? null : annotation.value().asString(); + + return annotation.target().kind() == AnnotationTarget.Kind.FIELD + ? itemFromFieldExtractor.apply(annotationValue, annotation.target().asField()) + : itemFromMethodExtractor.apply(annotationValue, + getGetterMethod(beanParamClass, annotation.target().asMethod())); + } + + private boolean isFieldOrMethodAnnotation(AnnotationInstance annotation) { + return annotation.target().kind() == AnnotationTarget.Kind.FIELD + || annotation.target().kind() == AnnotationTarget.Kind.METHOD; + } + } } diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java new file mode 100644 index 0000000000000..4a138bf61ff74 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/FormParamItem.java @@ -0,0 +1,21 @@ +package org.jboss.resteasy.reactive.client.processor.beanparam; + +public class FormParamItem extends Item { + + private final String formParamName; + private final String paramType; + + public FormParamItem(String formParamName, String paramType, ValueExtractor valueExtractor) { + super(ItemType.FORM_PARAM, valueExtractor); + this.formParamName = formParamName; + this.paramType = paramType; + } + + public String getFormParamName() { + return formParamName; + } + + public String getParamType() { + return paramType; + } +} diff --git a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/ItemType.java b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/ItemType.java index c7c5f1d8891f3..7d0b9f8a97ec5 100644 --- a/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/ItemType.java +++ b/independent-projects/resteasy-reactive/client/processor/src/main/java/org/jboss/resteasy/reactive/client/processor/beanparam/ItemType.java @@ -6,5 +6,6 @@ public enum ItemType { COOKIE, HEADER_PARAM, PATH_PARAM, + FORM_PARAM, // TODO: more } diff --git a/independent-projects/resteasy-reactive/client/processor/src/test/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParserTest.java b/independent-projects/resteasy-reactive/client/processor/src/test/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParserTest.java new file mode 100644 index 0000000000000..298fb78d8e4f4 --- /dev/null +++ b/independent-projects/resteasy-reactive/client/processor/src/test/java/org/jboss/resteasy/reactive/client/processor/beanparam/BeanParamParserTest.java @@ -0,0 +1,106 @@ +package org.jboss.resteasy.reactive.client.processor.beanparam; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; +import javax.ws.rs.BeanParam; +import javax.ws.rs.CookieParam; +import javax.ws.rs.FormParam; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.PathParam; +import javax.ws.rs.QueryParam; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.junit.jupiter.api.Test; + +/** + * Test for {@link BeanParamParser}. + */ +public class BeanParamParserTest { + + @Test + public void mustRecursivelyParseAllParamTypes() throws IOException { + Index index = Index.of(BeanExample.class, BeanExample.InnerBean.class); + List parseResult = BeanParamParser.parse(index.getClassByName(DotName.createSimple(BeanExample.class.getName())), + index); + assertNotNull(parseResult); + parseResult.sort(Comparator.comparing(Item::type)); + + assertThat(parseResult).hasSize(4); + Iterator itemIterator = parseResult.iterator(); + + assertThatNextItemSatisfies(itemIterator, BeanParamItem.class, item -> { + List beanParamItems = item.items(); + beanParamItems.sort(Comparator.comparing(Item::type)); + assertThat(beanParamItems).hasSize(2); + Iterator subItemIterator = beanParamItems.iterator(); + assertThatNextItemSatisfies(subItemIterator, QueryParamItem.class, + subItem -> assertThat(subItem.name()).isEqualTo("queryParam")); + assertThatNextItemSatisfies(subItemIterator, HeaderParamItem.class, + subItem -> assertThat(subItem.getHeaderName()).isEqualTo("headerParam")); + }); + + assertThatNextItemSatisfies(itemIterator, CookieParamItem.class, + subItem -> assertThat(subItem.getCookieName()).isEqualTo("cookieParam")); + assertThatNextItemSatisfies(itemIterator, PathParamItem.class, + subItem -> assertThat(subItem.getPathParamName()).isEqualTo("pathParam")); + assertThatNextItemSatisfies(itemIterator, FormParamItem.class, + subItem -> assertThat(subItem.getFormParamName()).isEqualTo("formParam")); + } + + @Test + public void mustDetectCycleInBeanParamsChain() throws IOException { + Index index = Index.of(CyclingBeanParamsExample.class, CyclingBeanParamsExample.InnerBean.class); + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> BeanParamParser.parse( + index.getClassByName(DotName.createSimple(CyclingBeanParamsExample.class.getName())), + index)); + assertThat(thrown.getMessage()).isEqualTo( + "Cycle detected in BeanParam annotations; already processed class " + CyclingBeanParamsExample.class.getName()); + } + + @SuppressWarnings("unchecked") + private void assertThatNextItemSatisfies(Iterator itemIterator, Class clazz, + Consumer condition) { + Item nextItem = itemIterator.next(); + assertThat(nextItem).isInstanceOf(clazz); + condition.accept((T) nextItem); + } + + private static class BeanExample { + @FormParam("formParam") + String formParam; + + @CookieParam("cookieParam") + String cookieParam; + + @PathParam("pathParam") + String pathParam; + + @BeanParam + InnerBean innerBean; + + private static class InnerBean { + @QueryParam("queryParam") + String queryParam; + + @HeaderParam("headerParam") + String headerParam; + } + } + + private static class CyclingBeanParamsExample { + @BeanParam + InnerBean inner; + + private static class InnerBean { + @BeanParam + CyclingBeanParamsExample outer; + } + } +}