From 62706f7015d6bc388f448679d97034fe53401a65 Mon Sep 17 00:00:00 2001 From: Jose Date: Mon, 20 Dec 2021 19:07:19 +0100 Subject: [PATCH] RestClient Reactive: Support inheritance of sub resources Allow to use sub resources in client resources: Usage for first level: ```java @Path("/path") @RegisterRestClient(baseUri = "http://localhost:8081") @Consumes("text/plain") @Produces("text/plain") public interface RootResource { @Path("/sub") SubClient sub(); } ``` Second level: ``` @Consumes("text/plain") @Produces("text/plain") interface SubClient { @Path("/sub") SubSubClient sub(); } ``` Third and N levels (this was unsupported and now it's supported): ``` @Consumes("text/plain") @Produces("text/plain") interface SubSubClient { @GET @Path("/simple") String simpleGet(); } ``` Fix https://github.com/quarkusio/quarkus/issues/22055 --- .../JaxrsClientReactiveProcessor.java | 668 +++++++++--------- .../client/reactive/subresource/Resource.java | 25 + .../reactive/subresource/SubResourceTest.java | 78 +- 3 files changed, 448 insertions(+), 323 deletions(-) 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 67dacc9029692..c94739bec2282 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 @@ -528,328 +528,10 @@ A more full example of generated client (with sub-resource) can is at the bottom } if (method.getHttpMethod() == null) { - Type returnType = jandexMethod.returnType(); - if (returnType.kind() != Type.Kind.CLASS) { - // sort of sub-resource method that returns a thing that isn't a class - throw new IllegalArgumentException("Sub resource type is not a class: " + returnType.name().toString()); - } - ClassInfo subResourceInterface = index.getClassByName(returnType.name()); - if (!Modifier.isInterface(subResourceInterface.flags())) { - throw new IllegalArgumentException( - "Client interface method: " + jandexMethod.declaringClass().name() + "#" + jandexMethod - + " has no HTTP method annotation (@GET, @POST, etc) and it's return type: " - + returnType.name().toString() + " is not an interface. " - + "If it's a sub resource method, it has to return an interface. " - + "If it's not, it has to have one of the HTTP method annotations."); - } - // generate implementation for a method from the jaxrs interface: - MethodCreator methodCreator = c.getMethodCreator(method.getName(), method.getSimpleReturnType(), - javaMethodParameters); - - String subName = subResourceInterface.name().toString() + HashUtil.sha1(name) + methodIndex; - try (ClassCreator sub = new ClassCreator( - new GeneratedClassGizmoAdaptor(generatedClasses, true), - subName, null, Object.class.getName(), subResourceInterface.name().toString())) { - - ResultHandle subInstance = methodCreator.newInstance(MethodDescriptor.ofConstructor(subName)); - - MethodCreator subConstructor = null; - MethodCreator subClinit = null; - if (!enrichers.isEmpty()) { - subConstructor = sub.getMethodCreator(MethodDescriptor.ofConstructor(subName)); - subConstructor.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), - subConstructor.getThis()); - subClinit = sub.getMethodCreator(MethodDescriptor.ofMethod(subName, "", void.class)); - subClinit.setModifiers(Opcodes.ACC_STATIC); - } - - Map paramFields = new HashMap<>(); - for (int i = 0; i < method.getParameters().length; i++) { - FieldDescriptor paramField = sub.getFieldCreator("param" + i, method.getParameters()[i].type) - .setModifiers(Modifier.PUBLIC) - .getFieldDescriptor(); - methodCreator.writeInstanceField(paramField, subInstance, methodCreator.getMethodParam(i)); - paramFields.put(i, paramField); - } - - ResultHandle multipartForm = null; - - int subMethodIndex = 0; - for (ResourceMethod subMethod : method.getSubResourceMethods()) { - if (subMethod.getHttpMethod() == null) { - continue; - } - MethodInfo jandexSubMethod = getJavaMethod(subResourceInterface, subMethod, - subMethod.getParameters(), index) - .orElseThrow(() -> new RuntimeException( - "Failed to find matching java method for " + method + " on " - + interfaceClass - + ". It may have unresolved parameter types (generics)")); - subMethodIndex++; - // WebTarget field in the root stub implementation (not to recreate it on each call): - - // initializing the web target in the root stub constructor: - FieldDescriptor webTargetForSubMethod = FieldDescriptor.of(name, - "target" + methodIndex + "_" + subMethodIndex, - WebTarget.class); - c.getFieldCreator(webTargetForSubMethod).setModifiers(Modifier.FINAL); - webTargets.add(webTargetForSubMethod); - - AssignableResultHandle constructorTarget = createWebTargetForMethod(constructor, baseTarget, - method); - if (subMethod.getPath() != null) { - appendPath(constructor, subMethod.getPath(), constructorTarget); - } - - constructor.writeInstanceField(webTargetForSubMethod, constructor.getThis(), constructorTarget); - - // set the sub stub target field value to the target created above: - FieldDescriptor subWebTarget = sub.getFieldCreator("target" + subMethodIndex, WebTarget.class) - .setModifiers(Modifier.PUBLIC) - .getFieldDescriptor(); - methodCreator.writeInstanceField(subWebTarget, subInstance, - methodCreator.readInstanceField(webTargetForSubMethod, methodCreator.getThis())); - - MethodCreator subMethodCreator = sub.getMethodCreator(subMethod.getName(), - jandexSubMethod.returnType().name().toString(), - parametersAsStringArray(jandexSubMethod)); - - AssignableResultHandle methodTarget = subMethodCreator.createVariable(WebTarget.class); - subMethodCreator.assign(methodTarget, - subMethodCreator.readInstanceField(subWebTarget, subMethodCreator.getThis())); - - ResultHandle bodyParameterValue = null; - AssignableResultHandle formParams = null; - Map invocationBuilderEnrichers = new HashMap<>(); - - // first go through parameters of the root stub method, we have them copied to fields in the sub stub - for (int paramIdx = 0; paramIdx < method.getParameters().length; ++paramIdx) { - MethodParameter param = method.getParameters()[paramIdx]; - ResultHandle paramValue = subMethodCreator.readInstanceField(paramFields.get(paramIdx), - subMethodCreator.getThis()); - if (param.parameterType == ParameterType.QUERY) { - //TODO: converters - - // query params have to be set on a method-level web target (they vary between invocations) - subMethodCreator.assign(methodTarget, - addQueryParam(subMethodCreator, methodTarget, param.name, - paramValue, jandexMethod.parameters().get(paramIdx), index)); - } else if (param.parameterType == ParameterType.BEAN) { - // bean params require both, web-target and Invocation.Builder, modifications - // The web target changes have to be done on the method level. - // Invocation.Builder changes are offloaded to a separate method - // so that we can generate bytecode for both, web target and invocation builder modifications - // at once - ClientBeanParamInfo beanParam = (ClientBeanParamInfo) param; - MethodDescriptor handleBeanParamDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + methodIndex + "$$handleBeanParam$$" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleBeanParamMethod = sub.getMethodCreator(handleBeanParamDescriptor); - - AssignableResultHandle invocationBuilderRef = handleBeanParamMethod - .createVariable(Invocation.Builder.class); - handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - addBeanParamData(subMethodCreator, handleBeanParamMethod, - invocationBuilderRef, beanParam.getItems(), - paramValue, methodTarget, index); - - handleBeanParamMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleBeanParamDescriptor, paramValue); - } else if (param.parameterType == ParameterType.PATH) { - // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); - subMethodCreator.assign(methodTarget, - subMethodCreator.invokeInterfaceMethod(WEB_TARGET_RESOLVE_TEMPLATE_METHOD, - methodTarget, - subMethodCreator.load(param.name), paramValue)); - } else if (param.parameterType == ParameterType.BODY) { - // just store the index of parameter used to create the body, we'll use it later - bodyParameterValue = paramValue; - } else if (param.parameterType == ParameterType.HEADER) { - // headers are added at the invocation builder level - MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleHeader$$param" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleHeaderMethod = sub.getMethodCreator(handleHeaderDescriptor); - - AssignableResultHandle invocationBuilderRef = handleHeaderMethod - .createVariable(Invocation.Builder.class); - handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); - addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, - handleHeaderMethod.getMethodParam(1)); - handleHeaderMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleHeaderDescriptor, paramValue); - } else if (param.parameterType == ParameterType.COOKIE) { - // cookies are added at the invocation builder level - MethodDescriptor handleCookieDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleCookie$$param" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleCookieMethod = sub.getMethodCreator(handleCookieDescriptor); - - AssignableResultHandle invocationBuilderRef = handleCookieMethod - .createVariable(Invocation.Builder.class); - handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); - addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, - handleCookieMethod.getMethodParam(1)); - handleCookieMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleCookieDescriptor, paramValue); - } else if (param.parameterType == ParameterType.FORM) { - formParams = createIfAbsent(subMethodCreator, formParams); - subMethodCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, - subMethodCreator.load(param.name), paramValue); - } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { - if (multipartForm != null) { - throw new IllegalArgumentException("MultipartForm data set twice for method " - + jandexSubMethod.declaringClass().name() + "#" + jandexSubMethod.name()); - } - multipartForm = createMultipartForm(subMethodCreator, paramValue, - jandexMethod.parameters().get(paramIdx).asClassType(), index); - } - } - // handle sub-method parameters: - for (int paramIdx = 0; paramIdx < subMethod.getParameters().length; ++paramIdx) { - MethodParameter param = subMethod.getParameters()[paramIdx]; - if (param.parameterType == ParameterType.QUERY) { - //TODO: converters - - // query params have to be set on a method-level web target (they vary between invocations) - subMethodCreator.assign(methodTarget, - addQueryParam(subMethodCreator, methodTarget, param.name, - subMethodCreator.getMethodParam(paramIdx), - jandexSubMethod.parameters().get(paramIdx), index)); - } else if (param.parameterType == ParameterType.BEAN) { - // bean params require both, web-target and Invocation.Builder, modifications - // The web target changes have to be done on the method level. - // Invocation.Builder changes are offloaded to a separate method - // so that we can generate bytecode for both, web target and invocation builder modifications - // at once - ClientBeanParamInfo beanParam = (ClientBeanParamInfo) param; - MethodDescriptor handleBeanParamDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleBeanParam$$" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleBeanParamMethod = c.getMethodCreator(handleBeanParamDescriptor); - - AssignableResultHandle invocationBuilderRef = handleBeanParamMethod - .createVariable(Invocation.Builder.class); - handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - addBeanParamData(subMethodCreator, handleBeanParamMethod, - invocationBuilderRef, beanParam.getItems(), - subMethodCreator.getMethodParam(paramIdx), methodTarget, index); - - handleBeanParamMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleBeanParamDescriptor, - subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.PATH) { - // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); - subMethodCreator.assign(methodTarget, - subMethodCreator.invokeInterfaceMethod(WEB_TARGET_RESOLVE_TEMPLATE_METHOD, - methodTarget, - subMethodCreator.load(param.name), - subMethodCreator.getMethodParam(paramIdx))); - } else if (param.parameterType == ParameterType.BODY) { - // just store the index of parameter used to create the body, we'll use it later - bodyParameterValue = subMethodCreator.getMethodParam(paramIdx); - } else if (param.parameterType == ParameterType.HEADER) { - // headers are added at the invocation builder level - MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleHeader$$" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleHeaderMethod = sub.getMethodCreator(handleHeaderDescriptor); - - AssignableResultHandle invocationBuilderRef = handleHeaderMethod - .createVariable(Invocation.Builder.class); - handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); - addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, - handleHeaderMethod.getMethodParam(1)); - handleHeaderMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleHeaderDescriptor, - subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.COOKIE) { - // cookies are added at the invocation builder level - MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleCookie$$" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleCookieMethod = sub.getMethodCreator(handleHeaderDescriptor); - - AssignableResultHandle invocationBuilderRef = handleCookieMethod - .createVariable(Invocation.Builder.class); - handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); - addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, - handleCookieMethod.getMethodParam(1)); - handleCookieMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleHeaderDescriptor, - subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.FORM) { - formParams = createIfAbsent(subMethodCreator, formParams); - subMethodCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, - subMethodCreator.load(param.name), - subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { - if (multipartForm != null) { - throw new IllegalArgumentException("MultipartForm data set twice for method " - + jandexSubMethod.declaringClass().name() + "#" + jandexSubMethod.name()); - } - multipartForm = createMultipartForm(subMethodCreator, - subMethodCreator.getMethodParam(paramIdx), - jandexSubMethod.parameters().get(paramIdx), index); - } - - } - - AssignableResultHandle builder = subMethodCreator.createVariable(Invocation.Builder.class); - if (method.getProduces() == null || method.getProduces().length == 0) { // this should never happen! - subMethodCreator.assign(builder, subMethodCreator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(WebTarget.class, "request", Invocation.Builder.class), - methodTarget)); - } else { - - ResultHandle array = subMethodCreator.newArray(String.class, subMethod.getProduces().length); - for (int i = 0; i < subMethod.getProduces().length; ++i) { - subMethodCreator.writeArrayValue(array, i, - subMethodCreator.load(subMethod.getProduces()[i])); - } - subMethodCreator.assign(builder, subMethodCreator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(WebTarget.class, "request", Invocation.Builder.class, - String[].class), - methodTarget, array)); - } - - for (Map.Entry invocationBuilderEnricher : invocationBuilderEnrichers - .entrySet()) { - subMethodCreator.assign(builder, - subMethodCreator.invokeVirtualMethod(invocationBuilderEnricher.getKey(), - subMethodCreator.getThis(), - builder, invocationBuilderEnricher.getValue())); - } - - for (JaxrsClientReactiveEnricherBuildItem enricher : enrichers) { - enricher.getEnricher() - .forSubResourceMethod(sub, subConstructor, subClinit, subMethodCreator, interfaceClass, - subResourceInterface, jandexSubMethod, jandexMethod, builder, index, - generatedClasses, methodIndex, subMethodIndex); - } - - String[] consumes = extractProducesConsumesValues( - jandexSubMethod.declaringClass().classAnnotation(CONSUMES), method.getConsumes()); - consumes = extractProducesConsumesValues(jandexSubMethod.annotation(CONSUMES), consumes); - handleReturn(subResourceInterface, defaultMediaType, - getHttpMethod(jandexSubMethod, subMethod.getHttpMethod(), httpAnnotationToMethod), - consumes, jandexSubMethod, subMethodCreator, formParams, multipartForm, bodyParameterValue, - builder); - } - - if (subConstructor != null) { - subConstructor.returnValue(null); - subClinit.returnValue(null); - } - - methodCreator.returnValue(subInstance); - } + handleSubResourceMethod(enrichers, generatedClasses, interfaceClass, index, defaultMediaType, + httpAnnotationToMethod, + name, c, constructor, + baseTarget, methodIndex, webTargets, method, javaMethodParameters, jandexMethod); } else { // constructor: initializing the immutable part of the method-specific web target @@ -1028,6 +710,348 @@ A more full example of generated client (with sub-resource) can is at the bottom } + private void handleSubResourceMethod(List enrichers, + BuildProducer generatedClasses, ClassInfo interfaceClass, IndexView index, + String defaultMediaType, Map httpAnnotationToMethod, String name, ClassCreator c, + MethodCreator constructor, AssignableResultHandle baseTarget, int methodIndex, List webTargets, + ResourceMethod method, String[] javaMethodParameters, MethodInfo jandexMethod) { + Type returnType = jandexMethod.returnType(); + if (returnType.kind() != Type.Kind.CLASS) { + // sort of sub-resource method that returns a thing that isn't a class + throw new IllegalArgumentException("Sub resource type is not a class: " + returnType.name().toString()); + } + ClassInfo subResourceInterface = index.getClassByName(returnType.name()); + if (!Modifier.isInterface(subResourceInterface.flags())) { + throw new IllegalArgumentException( + "Client interface method: " + jandexMethod.declaringClass().name() + "#" + jandexMethod + + " has no HTTP method annotation (@GET, @POST, etc) and it's return type: " + + returnType.name().toString() + " is not an interface. " + + "If it's a sub resource method, it has to return an interface. " + + "If it's not, it has to have one of the HTTP method annotations."); + } + // generate implementation for a method from the jaxrs interface: + MethodCreator methodCreator = c.getMethodCreator(method.getName(), method.getSimpleReturnType(), + javaMethodParameters); + + String subName = subResourceInterface.name().toString() + HashUtil.sha1(name) + methodIndex; + try (ClassCreator sub = new ClassCreator( + new GeneratedClassGizmoAdaptor(generatedClasses, true), + subName, null, Object.class.getName(), subResourceInterface.name().toString())) { + + ResultHandle subInstance = methodCreator.newInstance(MethodDescriptor.ofConstructor(subName)); + + MethodCreator subConstructor = null; + MethodCreator subClinit = null; + if (!enrichers.isEmpty()) { + subConstructor = sub.getMethodCreator(MethodDescriptor.ofConstructor(subName)); + subConstructor.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), + subConstructor.getThis()); + subClinit = sub.getMethodCreator(MethodDescriptor.ofMethod(subName, "", void.class)); + subClinit.setModifiers(Opcodes.ACC_STATIC); + } + + Map paramFields = new HashMap<>(); + for (int i = 0; i < method.getParameters().length; i++) { + FieldDescriptor paramField = sub.getFieldCreator("param" + i, method.getParameters()[i].type) + .setModifiers(Modifier.PUBLIC) + .getFieldDescriptor(); + methodCreator.writeInstanceField(paramField, subInstance, methodCreator.getMethodParam(i)); + paramFields.put(i, paramField); + } + + ResultHandle multipartForm = null; + + int subMethodIndex = 0; + for (ResourceMethod subMethod : method.getSubResourceMethods()) { + MethodInfo jandexSubMethod = getJavaMethod(subResourceInterface, subMethod, + subMethod.getParameters(), index) + .orElseThrow(() -> new RuntimeException( + "Failed to find matching java method for " + subMethod + " on " + + subResourceInterface + + ". It may have unresolved parameter types (generics)")); + subMethodIndex++; + // WebTarget field in the root stub implementation (not to recreate it on each call): + + // initializing the web target in the root stub constructor: + FieldDescriptor webTargetForSubMethod = FieldDescriptor.of(name, + "target" + methodIndex + "_" + subMethodIndex, + WebTarget.class); + c.getFieldCreator(webTargetForSubMethod).setModifiers(Modifier.FINAL); + webTargets.add(webTargetForSubMethod); + + AssignableResultHandle constructorTarget = createWebTargetForMethod(constructor, baseTarget, + method); + if (subMethod.getPath() != null) { + appendPath(constructor, subMethod.getPath(), constructorTarget); + } + + constructor.writeInstanceField(webTargetForSubMethod, constructor.getThis(), constructorTarget); + + // set the sub stub target field value to the target created above: + FieldDescriptor subWebTarget = sub.getFieldCreator("target" + subMethodIndex, WebTarget.class) + .setModifiers(Modifier.PUBLIC) + .getFieldDescriptor(); + methodCreator.writeInstanceField(subWebTarget, subInstance, + methodCreator.readInstanceField(webTargetForSubMethod, methodCreator.getThis())); + + MethodCreator subMethodCreator = sub.getMethodCreator(subMethod.getName(), + jandexSubMethod.returnType().name().toString(), + parametersAsStringArray(jandexSubMethod)); + + AssignableResultHandle methodTarget = subMethodCreator.createVariable(WebTarget.class); + subMethodCreator.assign(methodTarget, + subMethodCreator.readInstanceField(subWebTarget, subMethodCreator.getThis())); + + ResultHandle bodyParameterValue = null; + AssignableResultHandle formParams = null; + Map invocationBuilderEnrichers = new HashMap<>(); + + // first go through parameters of the root stub method, we have them copied to fields in the sub stub + for (int paramIdx = 0; paramIdx < method.getParameters().length; ++paramIdx) { + MethodParameter param = method.getParameters()[paramIdx]; + ResultHandle paramValue = subMethodCreator.readInstanceField(paramFields.get(paramIdx), + subMethodCreator.getThis()); + if (param.parameterType == ParameterType.QUERY) { + //TODO: converters + + // query params have to be set on a method-level web target (they vary between invocations) + subMethodCreator.assign(methodTarget, + addQueryParam(subMethodCreator, methodTarget, param.name, + paramValue, jandexMethod.parameters().get(paramIdx), index)); + } else if (param.parameterType == ParameterType.BEAN) { + // bean params require both, web-target and Invocation.Builder, modifications + // The web target changes have to be done on the method level. + // Invocation.Builder changes are offloaded to a separate method + // so that we can generate bytecode for both, web target and invocation builder modifications + // at once + ClientBeanParamInfo beanParam = (ClientBeanParamInfo) param; + MethodDescriptor handleBeanParamDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + methodIndex + "$$handleBeanParam$$" + paramIdx, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleBeanParamMethod = sub.getMethodCreator(handleBeanParamDescriptor); + + AssignableResultHandle invocationBuilderRef = handleBeanParamMethod + .createVariable(Invocation.Builder.class); + handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); + addBeanParamData(subMethodCreator, handleBeanParamMethod, + invocationBuilderRef, beanParam.getItems(), + paramValue, methodTarget, index); + + handleBeanParamMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleBeanParamDescriptor, paramValue); + } else if (param.parameterType == ParameterType.PATH) { + // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); + subMethodCreator.assign(methodTarget, + subMethodCreator.invokeInterfaceMethod(WEB_TARGET_RESOLVE_TEMPLATE_METHOD, + methodTarget, + subMethodCreator.load(param.name), paramValue)); + } else if (param.parameterType == ParameterType.BODY) { + // just store the index of parameter used to create the body, we'll use it later + bodyParameterValue = paramValue; + } else if (param.parameterType == ParameterType.HEADER) { + // headers are added at the invocation builder level + MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleHeader$$param" + paramIdx, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleHeaderMethod = sub.getMethodCreator(handleHeaderDescriptor); + + AssignableResultHandle invocationBuilderRef = handleHeaderMethod + .createVariable(Invocation.Builder.class); + handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); + addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, + handleHeaderMethod.getMethodParam(1)); + handleHeaderMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleHeaderDescriptor, paramValue); + } else if (param.parameterType == ParameterType.COOKIE) { + // cookies are added at the invocation builder level + MethodDescriptor handleCookieDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleCookie$$param" + paramIdx, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleCookieMethod = sub.getMethodCreator(handleCookieDescriptor); + + AssignableResultHandle invocationBuilderRef = handleCookieMethod + .createVariable(Invocation.Builder.class); + handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); + addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, + handleCookieMethod.getMethodParam(1)); + handleCookieMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleCookieDescriptor, paramValue); + } else if (param.parameterType == ParameterType.FORM) { + formParams = createIfAbsent(subMethodCreator, formParams); + subMethodCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, + subMethodCreator.load(param.name), paramValue); + } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { + if (multipartForm != null) { + throw new IllegalArgumentException("MultipartForm data set twice for method " + + jandexSubMethod.declaringClass().name() + "#" + jandexSubMethod.name()); + } + multipartForm = createMultipartForm(subMethodCreator, paramValue, + jandexMethod.parameters().get(paramIdx).asClassType(), index); + } + } + // handle sub-method parameters: + for (int paramIdx = 0; paramIdx < subMethod.getParameters().length; ++paramIdx) { + MethodParameter param = subMethod.getParameters()[paramIdx]; + if (param.parameterType == ParameterType.QUERY) { + //TODO: converters + + // query params have to be set on a method-level web target (they vary between invocations) + subMethodCreator.assign(methodTarget, + addQueryParam(subMethodCreator, methodTarget, param.name, + subMethodCreator.getMethodParam(paramIdx), + jandexSubMethod.parameters().get(paramIdx), index)); + } else if (param.parameterType == ParameterType.BEAN) { + // bean params require both, web-target and Invocation.Builder, modifications + // The web target changes have to be done on the method level. + // Invocation.Builder changes are offloaded to a separate method + // so that we can generate bytecode for both, web target and invocation builder modifications + // at once + ClientBeanParamInfo beanParam = (ClientBeanParamInfo) param; + MethodDescriptor handleBeanParamDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleBeanParam$$" + paramIdx, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleBeanParamMethod = c.getMethodCreator(handleBeanParamDescriptor); + + AssignableResultHandle invocationBuilderRef = handleBeanParamMethod + .createVariable(Invocation.Builder.class); + handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); + addBeanParamData(subMethodCreator, handleBeanParamMethod, + invocationBuilderRef, beanParam.getItems(), + subMethodCreator.getMethodParam(paramIdx), methodTarget, index); + + handleBeanParamMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleBeanParamDescriptor, + subMethodCreator.getMethodParam(paramIdx)); + } else if (param.parameterType == ParameterType.PATH) { + // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); + subMethodCreator.assign(methodTarget, + subMethodCreator.invokeInterfaceMethod(WEB_TARGET_RESOLVE_TEMPLATE_METHOD, + methodTarget, + subMethodCreator.load(param.name), + subMethodCreator.getMethodParam(paramIdx))); + } else if (param.parameterType == ParameterType.BODY) { + // just store the index of parameter used to create the body, we'll use it later + bodyParameterValue = subMethodCreator.getMethodParam(paramIdx); + } else if (param.parameterType == ParameterType.HEADER) { + // headers are added at the invocation builder level + MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleHeader$$" + paramIdx, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleHeaderMethod = sub.getMethodCreator(handleHeaderDescriptor); + + AssignableResultHandle invocationBuilderRef = handleHeaderMethod + .createVariable(Invocation.Builder.class); + handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); + addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, + handleHeaderMethod.getMethodParam(1)); + handleHeaderMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleHeaderDescriptor, + subMethodCreator.getMethodParam(paramIdx)); + } else if (param.parameterType == ParameterType.COOKIE) { + // cookies are added at the invocation builder level + MethodDescriptor handleHeaderDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleCookie$$" + paramIdx, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleCookieMethod = sub.getMethodCreator(handleHeaderDescriptor); + + AssignableResultHandle invocationBuilderRef = handleCookieMethod + .createVariable(Invocation.Builder.class); + handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); + addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, + handleCookieMethod.getMethodParam(1)); + handleCookieMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleHeaderDescriptor, + subMethodCreator.getMethodParam(paramIdx)); + } else if (param.parameterType == ParameterType.FORM) { + formParams = createIfAbsent(subMethodCreator, formParams); + subMethodCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD, formParams, + subMethodCreator.load(param.name), + subMethodCreator.getMethodParam(paramIdx)); + } else if (param.parameterType == ParameterType.MULTI_PART_FORM) { + if (multipartForm != null) { + throw new IllegalArgumentException("MultipartForm data set twice for method " + + jandexSubMethod.declaringClass().name() + "#" + jandexSubMethod.name()); + } + multipartForm = createMultipartForm(subMethodCreator, + subMethodCreator.getMethodParam(paramIdx), + jandexSubMethod.parameters().get(paramIdx), index); + } + + } + + if (subMethod.getHttpMethod() == null) { + // finding corresponding jandex method, used by enricher (MicroProfile enricher stores it in a field + // to later fill in context with corresponding java.lang.reflect.Method) + String[] subJavaMethodParameters = new String[subMethod.getParameters().length]; + for (int i = 0; i < subMethod.getParameters().length; i++) { + MethodParameter param = subMethod.getParameters()[i]; + subJavaMethodParameters[i] = param.declaredType != null ? param.declaredType : param.type; + } + + handleSubResourceMethod(enrichers, generatedClasses, subResourceInterface, index, + defaultMediaType, httpAnnotationToMethod, + subName, sub, subMethodCreator, + methodTarget, subMethodIndex, webTargets, subMethod, subJavaMethodParameters, jandexSubMethod); + } else { + AssignableResultHandle builder = subMethodCreator.createVariable(Invocation.Builder.class); + if (method.getProduces() == null || method.getProduces().length == 0) { // this should never happen! + subMethodCreator.assign(builder, subMethodCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(WebTarget.class, "request", Invocation.Builder.class), + methodTarget)); + } else { + + ResultHandle array = subMethodCreator.newArray(String.class, subMethod.getProduces().length); + for (int i = 0; i < subMethod.getProduces().length; ++i) { + subMethodCreator.writeArrayValue(array, i, + subMethodCreator.load(subMethod.getProduces()[i])); + } + subMethodCreator.assign(builder, subMethodCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(WebTarget.class, "request", Invocation.Builder.class, + String[].class), + methodTarget, array)); + } + + for (Map.Entry invocationBuilderEnricher : invocationBuilderEnrichers + .entrySet()) { + subMethodCreator.assign(builder, + subMethodCreator.invokeVirtualMethod(invocationBuilderEnricher.getKey(), + subMethodCreator.getThis(), + builder, invocationBuilderEnricher.getValue())); + } + + for (JaxrsClientReactiveEnricherBuildItem enricher : enrichers) { + enricher.getEnricher() + .forSubResourceMethod(sub, subConstructor, subClinit, subMethodCreator, interfaceClass, + subResourceInterface, jandexSubMethod, jandexMethod, builder, index, + generatedClasses, methodIndex, subMethodIndex); + } + + String[] consumes = extractProducesConsumesValues( + jandexSubMethod.declaringClass().classAnnotation(CONSUMES), method.getConsumes()); + consumes = extractProducesConsumesValues(jandexSubMethod.annotation(CONSUMES), consumes); + handleReturn(subResourceInterface, defaultMediaType, + getHttpMethod(jandexSubMethod, subMethod.getHttpMethod(), httpAnnotationToMethod), + consumes, jandexSubMethod, subMethodCreator, formParams, multipartForm, bodyParameterValue, + builder); + } + + } + + if (subConstructor != null) { + subConstructor.returnValue(null); + subClinit.returnValue(null); + } + + methodCreator.returnValue(subInstance); + } + } + /* * Translate the class to be sent as multipart to Vertx Web MultipartForm. */ diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/Resource.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/Resource.java index ad57d5da4b58a..541a1ee0507eb 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/Resource.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/Resource.java @@ -15,6 +15,14 @@ @Path("/path") public class Resource { + + @GET + @Path("{part1}/{part2}/{part3}/{part4}/{part5}") + public String getUriParts(@RestPath String part1, @RestPath String part2, @RestPath String part3, @RestPath String part4, + @RestPath String part5) { + return String.format("%s/%s/%s/%s/%s", part1, part2, part3, part4, part5); + } + @GET @Path("{part1}/{part2}/{part3}") public String getUriParts(@RestPath String part1, @RestPath String part2, @RestPath String part3) { @@ -36,4 +44,21 @@ public Response getUriEntityAndQueryParam(@RestPath String part1, @RestPath Stri } return responseBuilder.build(); } + + @POST + @Path("{part1}/{part2}/{part3}/{part4}") + public Response getUriEntityAndQueryParamFromSubResource(@RestPath String part1, @RestPath String part2, + @RestPath String part3, @RestPath String part4, + @RestQuery String queryParam, String entity, @Context HttpHeaders headers) { + Response.ResponseBuilder responseBuilder = Response.ok(String.format("%s/%s:%s:%s", part1, part2, entity, queryParam)); + + for (Map.Entry> headerEntry : headers.getRequestHeaders().entrySet()) { + String headerName = headerEntry.getKey(); + List value = headerEntry.getValue(); + if (value.size() == 1 && !"Content-Length".equalsIgnoreCase(headerName)) { + responseBuilder.header(headerName, value.get(0)); + } + } + return responseBuilder.build(); + } } diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/SubResourceTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/SubResourceTest.java index 56d2ace39bb9a..5cb7c853d44bc 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/SubResourceTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/subresource/SubResourceTest.java @@ -29,7 +29,7 @@ public class SubResourceTest { @RegisterExtension static final QuarkusUnitTest TEST = new QuarkusUnitTest() .withApplicationRoot((jar) -> jar - .addClasses(RootClient.class, SubClient.class, Resource.class)); + .addClasses(RootClient.class, SubClient.class, SubSubClient.class, Resource.class)); @TestHTTPResource URI baseUri; @@ -52,6 +52,14 @@ void shouldPassParamsToSubResource() { assertThat(result).isEqualTo("rt/mthd/simple"); } + @Test + void shouldPassParamsToSubSubResource() { + // should result in sending GET /path/rt/mthd/sub/sub/simple + RootClient rootClient = RestClientBuilder.newBuilder().baseUri(baseUri).build(RootClient.class); + String result = rootClient.sub("rt", "mthd").sub().simpleSub(); + assertThat(result).isEqualTo("rt/mthd/sub/sub/simple"); + } + @Test void shouldDoMultiplePosts() { RootClient rootClient = RestClientBuilder.newBuilder().baseUri(baseUri).build(RootClient.class); @@ -70,6 +78,22 @@ void shouldDoMultiplePosts() { assertThat(result.readEntity(String.class)).isEqualTo("rt/mthd:ent1t1:prm"); } + @Test + void shouldDoMultiplePostsInSubSubResource() { + RootClient rootClient = RestClientBuilder.newBuilder().baseUri(baseUri).build(RootClient.class); + SubSubClient sub = rootClient.sub("rt", "mthd").sub(); + + Response result = sub.postWithQueryParam("prm", "ent1t1"); + assertThat(result.readEntity(String.class)).isEqualTo("rt/mthd:ent1t1:prm"); + MultivaluedMap headers = result.getHeaders(); + assertThat(headers.get("overridable").get(0)).isEqualTo("SubSubClient"); + assertThat(headers.get("fromSubMethod").get(0)).isEqualTo("SubSubClientComputed"); + + // check that a second usage of the sub stub works + result = sub.postWithQueryParam("prm", "ent1t1"); + assertThat(result.readEntity(String.class)).isEqualTo("rt/mthd:ent1t1:prm"); + } + @Path("/path/{rootParam}") @RegisterRestClient(baseUri = "http://localhost:8081") @Consumes("text/plain") @@ -95,6 +119,9 @@ interface SubClient { @Path("/simple") String simpleGet(); + @Path("/sub") + SubSubClient sub(); + @POST @ClientHeaderParam(name = "overridable", value = "SubClient") @ClientHeaderParam(name = "fromSubMethod", value = "{fillingMethod}") @@ -104,6 +131,23 @@ default String fillingMethod() { return "SubClientComputed"; } } + + @Consumes("text/plain") + @Produces("text/plain") + interface SubSubClient { + @GET + @Path("/simple") + String simpleSub(); + + @POST + @ClientHeaderParam(name = "overridable", value = "SubSubClient") + @ClientHeaderParam(name = "fromSubMethod", value = "{fillingMethod}") + Response postWithQueryParam(@QueryParam("queryParam") String param, String entity); + + default String fillingMethod() { + return "SubSubClientComputed"; + } + } } /* @@ -131,6 +175,7 @@ default String fillingMethod() { public class SubResourceTest$RootClient$$QuarkusRestClientInterface implements Closeable, RootClient { final WebTarget target1_1; final WebTarget target1_2; + final WebTarget target1_3; public SubResourceTest$RootClient$$QuarkusRestClientInterface(WebTarget var1) { WebTarget var3 = var1.path("/path/{rootParam}"); @@ -147,6 +192,11 @@ public class SubResourceTest$RootClient$$QuarkusRestClientInterface implements C String var10 = "/simple"; var8 = var8.path(var10); this.target1_2 = var8; + String var12 = "/{methodParam}"; + WebTarget var11 = var3.path(var12); + String var13 = "/sub"; + var11 = var11.path(var13); + this.target1_3 = var11; } public SubClient sub(String var1, String var2) { @@ -157,12 +207,16 @@ public SubClient sub(String var1, String var2) { var3.target1 = var4; WebTarget var5 = this.target1_2; var3.target2 = var5; + WebTarget var6 = this.target1_3; + var3.target3 = var6; return (SubClient)var3; } public void close() { ((WebTargetImpl)this.target1_1).getRestClient().close(); ((WebTargetImpl)this.target1_2).getRestClient().close(); + ((WebTargetImpl)this.target1_3).getRestClient().close(); + ((WebTargetImpl)((SubClientf48b9cee6dde6b96b184ff11e432714265b0c2161)this).target3_1).getRestClient().close(); } } @@ -194,6 +248,9 @@ public class SubResourceTest$SubCliented77e297b94a7e0aa21c1f7f1d8ba4fbe72d61861 public WebTarget target2; private final Method javaMethod2; private final HeaderFiller headerFiller2; + private static final Method javaMethod3; + private final HeaderFiller headerFiller3; + final WebTarget target3_1; public SubResourceTest$SubCliented77e297b94a7e0aa21c1f7f1d8ba4fbe72d61861() { Class[] var1 = new Class[]{String.class, String.class}; @@ -206,6 +263,8 @@ public class SubResourceTest$SubCliented77e297b94a7e0aa21c1f7f1d8ba4fbe72d61861 this.javaMethod2 = var5; SubResourceTest$SubClient312bda50cc002ce8e85608d3afaa6aa0963d20b3$$1$$2 var6 = new SubResourceTest$SubClient312bda50cc002ce8e85608d3afaa6aa0963d20b3$$1$$2(); this.headerFiller2 = (HeaderFiller)var6; + SubResourceTest$SubClient312bda50cc002ce8e85608d3afaa6aa0963d20b3$$1$$3 var3 = new SubResourceTest$SubClient312bda50cc002ce8e85608d3afaa6aa0963d20b3$$1$$3(); + this.headerFiller3 = (HeaderFiller)var3; } public Response postWithQueryParam(String var1, String var2) { @@ -261,6 +320,23 @@ public String simpleGet() { } } } + + public SubSubClient sub() { + WebTarget var1 = this.target3; + String var2 = this.param0; + var1 = var1.resolveTemplate("rootParam", var2); + String var3 = this.param1; + var1 = var1.resolveTemplate("methodParam", var3); + SubSubClient1c9671af03ea8b4ee28b12a00217a058a10ca4033 var7 = new SubSubClient1c9671af03ea8b4ee28b12a00217a058a10ca4033(); + String var5 = "/sub"; + WebTarget var4 = var1.path(var5); + String var6 = "/simple"; + var4 = var4.path(var6); + this.target3_1 = var4; + WebTarget var8 = this.target3_1; + var7.target1 = var8; + return (SubSubClient)var7; + } }