From a535cc0df19d52d77eaf28e03c14897d1de7744c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Szynkiewicz?= Date: Thu, 21 Apr 2022 13:07:12 +0200 Subject: [PATCH] Fix REST Client Reactive path handling for sub-resources fixes #25028 --- .../JaxrsClientReactiveProcessor.java | 673 ++++++++++-------- .../client/reactive/subresource/Resource.java | 26 +- .../reactive/subresource/SubResourceTest.java | 10 +- 3 files changed, 382 insertions(+), 327 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 d8b2ecc6c4b9d..d198c210afc5c 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 @@ -744,6 +744,10 @@ A more full example of generated client (with sub-resource) can is at the bottom MethodDescriptor.ofMethod(WebTarget.class, "path", WebTarget.class, String.class), classContext.constructor.getMethodParam(0), classContext.constructor.load(restClientInterface.getPath()))); + FieldDescriptor baseTargetField = classContext.classCreator + .getFieldCreator("baseTarget", WebTargetImpl.class.getName()) + .getFieldDescriptor(); + classContext.constructor.writeInstanceField(baseTargetField, classContext.constructor.getThis(), baseTarget); for (JaxrsClientReactiveEnricherBuildItem enricher : enrichers) { enricher.getEnricher().forClass(classContext.constructor, baseTarget, interfaceClass, index); @@ -753,7 +757,6 @@ A more full example of generated client (with sub-resource) can is at the bottom // go through all the methods of the jaxrs interface. Create specific WebTargets (in the constructor) and methods // int methodIndex = 0; - List webTargets = new ArrayList<>(); for (ResourceMethod method : restClientInterface.getMethods()) { methodIndex++; @@ -776,10 +779,8 @@ A more full example of generated client (with sub-resource) can is at the bottom if (method.getHttpMethod() == null) { handleSubResourceMethod(enrichers, generatedClasses, interfaceClass, index, defaultMediaType, - httpAnnotationToMethod, - name, classContext, classContext.constructor, - baseTarget, methodIndex, webTargets, method, javaMethodParameters, jandexMethod, - multipartResponseTypes); + httpAnnotationToMethod, name, classContext, baseTarget, methodIndex, method, + javaMethodParameters, jandexMethod, multipartResponseTypes, Collections.emptyList()); } else { FieldDescriptor methodField = classContext.createJavaMethodField(interfaceClass, jandexMethod, methodIndex); @@ -794,7 +795,6 @@ A more full example of generated client (with sub-resource) can is at the bottom // constructor: initializing the immutable part of the method-specific web target FieldDescriptor webTargetForMethod = FieldDescriptor.of(name, "target" + methodIndex, WebTargetImpl.class); classContext.classCreator.getFieldCreator(webTargetForMethod).setModifiers(Modifier.FINAL); - webTargets.add(webTargetForMethod); AssignableResultHandle constructorTarget = createWebTargetForMethod(classContext.constructor, baseTarget, method); @@ -971,15 +971,14 @@ A more full example of generated client (with sub-resource) can is at the bottom classContext.clinit.returnValue(null); // create `void close()` method: + // we only close the RestClient of the base target - all targets share the same one MethodCreator closeCreator = classContext.classCreator .getMethodCreator(MethodDescriptor.ofMethod(Closeable.class, "close", void.class)); - for (FieldDescriptor target : webTargets) { - ResultHandle webTarget = closeCreator.readInstanceField(target, closeCreator.getThis()); - ResultHandle webTargetImpl = closeCreator.checkCast(webTarget, WebTargetImpl.class); - ResultHandle restClient = closeCreator.invokeVirtualMethod( - MethodDescriptor.ofMethod(WebTargetImpl.class, "getRestClient", ClientImpl.class), webTargetImpl); - closeCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(ClientImpl.class, "close", void.class), restClient); - } + ResultHandle webTarget = closeCreator.readInstanceField(baseTargetField, closeCreator.getThis()); + ResultHandle webTargetImpl = closeCreator.checkCast(webTarget, WebTargetImpl.class); + ResultHandle restClient = closeCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(WebTargetImpl.class, "getRestClient", ClientImpl.class), webTargetImpl); + closeCreator.invokeVirtualMethod(MethodDescriptor.ofMethod(ClientImpl.class, "close", void.class), restClient); closeCreator.returnValue(null); } @@ -1032,17 +1031,16 @@ private ClassInfo returnTypeAsClass(MethodInfo jandexMethod, IndexView index) { private void handleSubResourceMethod(List enrichers, BuildProducer generatedClasses, ClassInfo interfaceClass, IndexView index, String defaultMediaType, Map httpAnnotationToMethod, String name, - ClassRestClientContext classContext, MethodCreator constructor, AssignableResultHandle baseTarget, int methodIndex, - List webTargets, + ClassRestClientContext ownerContext, ResultHandle ownerTarget, int methodIndex, ResourceMethod method, String[] javaMethodParameters, MethodInfo jandexMethod, - Set multipartResponseTypes) { + Set multipartResponseTypes, List ownerSubResourceParameters) { Type returnType = jandexMethod.returnType(); if (returnType.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())) { + ClassInfo subInterface = index.getClassByName(returnType.name()); + if (!Modifier.isInterface(subInterface.flags())) { throw new IllegalArgumentException( "Client interface method: " + jandexMethod.declaringClass().name() + "#" + jandexMethod + " has no HTTP method annotation (@GET, @POST, etc) and it's return type: " @@ -1051,321 +1049,333 @@ private void handleSubResourceMethod(List + "If it's not, it has to have one of the HTTP method annotations."); } - classContext.createJavaMethodField(interfaceClass, jandexMethod, methodIndex); - Supplier methodParamAnnotationsField = classContext.getLazyJavaMethodParamAnnotationsField( - methodIndex); - Supplier methodGenericParametersField = classContext.getLazyJavaMethodGenericParametersField( - methodIndex); + ownerContext.createJavaMethodField(interfaceClass, jandexMethod, methodIndex); - // generate implementation for a method from the jaxrs interface: - MethodCreator methodCreator = classContext.classCreator.getMethodCreator(method.getName(), method.getSimpleReturnType(), + // generate implementation for a method that returns the sub-client: + MethodCreator ownerMethod = ownerContext.classCreator.getMethodCreator(method.getName(), method.getSimpleReturnType(), javaMethodParameters); - String subName = subResourceInterface.name().toString() + HashUtil.sha1(name) + methodIndex; - try (ClassRestClientContext subClassContext = new ClassRestClientContext(subName, generatedClasses, - subResourceInterface.name().toString())) { + String subName = subInterface.name().toString() + HashUtil.sha1(name) + methodIndex; + MethodDescriptor subConstructorDescriptor = MethodDescriptor.ofConstructor(subName, WebTargetImpl.class.getName()); + try (ClassRestClientContext subContext = new ClassRestClientContext(subName, subConstructorDescriptor, + generatedClasses, Object.class, subInterface.name().toString())) { + + subContext.constructor.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), + subContext.constructor.getThis()); + + AssignableResultHandle constructorTarget = createWebTargetForMethod(ownerContext.constructor, ownerTarget, + method); - subClassContext.constructor.invokeSpecialMethod(MethodDescriptor.ofConstructor(Object.class), - subClassContext.constructor.getThis()); + FieldDescriptor forMethodTargetDesc = ownerContext.classCreator + .getFieldCreator("targetInOwner" + methodIndex, WebTargetImpl.class).getFieldDescriptor(); + ownerContext.constructor.writeInstanceField(forMethodTargetDesc, ownerContext.constructor.getThis(), + constructorTarget); - ResultHandle subInstance = methodCreator.newInstance(MethodDescriptor.ofConstructor(subName)); + ResultHandle subInstance = ownerMethod.newInstance(subConstructorDescriptor, + ownerMethod.readInstanceField(forMethodTargetDesc, ownerMethod.getThis())); - Map paramFields = new HashMap<>(); + List subParamFields = new ArrayList<>(); + + for (SubResourceParameter ownerParameter : ownerSubResourceParameters) { + FieldDescriptor paramField = subContext.classCreator.getFieldCreator(ownerParameter.field.getName() + "$_", + ownerParameter.typeName) + .setModifiers(Modifier.PUBLIC) + .getFieldDescriptor(); + ownerMethod.writeInstanceField(paramField, subInstance, + ownerMethod.readInstanceField(ownerParameter.field, ownerMethod.getThis())); + subParamFields.add(new SubResourceParameter(ownerParameter.methodParameter, ownerParameter.typeName, + ownerParameter.type, paramField, ownerParameter.paramAnnotationsField, + ownerParameter.genericsParametersField, + ownerParameter.paramIndex)); + } - FieldDescriptor clientField = createRestClientField(name, classContext.classCreator, methodCreator, - subClassContext.classCreator, subInstance); + FieldDescriptor clientField = createRestClientField(name, ownerContext.classCreator, ownerMethod, + subContext.classCreator, subInstance); + Supplier methodParamAnnotationsField = ownerContext.getLazyJavaMethodParamAnnotationsField( + methodIndex); + Supplier methodGenericParametersField = ownerContext.getLazyJavaMethodGenericParametersField( + methodIndex); + // method parameters are rewritten to sub client fields (directly, public fields): for (int i = 0; i < method.getParameters().length; i++) { - FieldDescriptor paramField = subClassContext.classCreator.getFieldCreator("param" + i, + FieldDescriptor paramField = subContext.classCreator.getFieldCreator("param" + i, method.getParameters()[i].type) .setModifiers(Modifier.PUBLIC) .getFieldDescriptor(); - methodCreator.writeInstanceField(paramField, subInstance, methodCreator.getMethodParam(i)); - paramFields.put(i, paramField); + ownerMethod.writeInstanceField(paramField, subInstance, ownerMethod.getMethodParam(i)); + subParamFields.add(new SubResourceParameter(method.getParameters()[i], method.getParameters()[i].type, + jandexMethod.parameters().get(i), paramField, methodParamAnnotationsField, methodGenericParametersField, + i)); } ResultHandle multipartForm = null; int subMethodIndex = 0; for (ResourceMethod subMethod : method.getSubResourceMethods()) { - MethodInfo jandexSubMethod = getJavaMethod(subResourceInterface, subMethod, + MethodInfo jandexSubMethod = getJavaMethod(subInterface, subMethod, subMethod.getParameters(), index) .orElseThrow(() -> new RuntimeException( "Failed to find matching java method for " + subMethod + " on " - + subResourceInterface + + subInterface + ". It may have unresolved parameter types (generics)")); subMethodIndex++; - // WebTarget field in the root stub implementation (not to recreate it on each call): - FieldDescriptor subMethodField = subClassContext.createJavaMethodField(subResourceInterface, jandexSubMethod, - subMethodIndex); - Supplier subMethodParamAnnotationsField = subClassContext - .getLazyJavaMethodParamAnnotationsField(subMethodIndex); - Supplier subMethodGenericParametersField = subClassContext - .getLazyJavaMethodGenericParametersField(subMethodIndex); - - // initializing the web target in the root stub constructor: - FieldDescriptor webTargetForSubMethod = FieldDescriptor.of(name, "target" + methodIndex + "_" + subMethodIndex, - WebTarget.class); - classContext.classCreator.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 = subClassContext.classCreator.getFieldCreator("target" + subMethodIndex, - WebTarget.class) - .setModifiers(Modifier.PUBLIC) - .getFieldDescriptor(); - methodCreator.writeInstanceField(subWebTarget, subInstance, - methodCreator.readInstanceField(webTargetForSubMethod, methodCreator.getThis())); - - MethodCreator subMethodCreator = subClassContext.classCreator.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(jandexMethod, subMethodCreator, methodTarget, param.name, - paramValue, jandexMethod.parameters().get(paramIdx), index, - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(methodGenericParametersField.get()), - subMethodCreator.readStaticField(methodParamAnnotationsField.get()), - paramIdx)); - } 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 = subClassContext.classCreator.getMethodCreator( - handleBeanParamDescriptor); - - AssignableResultHandle invocationBuilderRef = handleBeanParamMethod - .createVariable(Invocation.Builder.class); - handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, - invocationBuilderRef, beanParam.getItems(), - paramValue, methodTarget, index, - interfaceClass.name().toString(), - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), - formParams, - methodGenericParametersField, methodParamAnnotationsField, paramIdx); - - handleBeanParamMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleBeanParamDescriptor, paramValue); - } else if (param.parameterType == ParameterType.PATH) { - // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); - addPathParam(subMethodCreator, methodTarget, param.name, paramValue, - param.type, - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(methodGenericParametersField.get()), - subMethodCreator.readStaticField(methodParamAnnotationsField.get()), - paramIdx); - } 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 = subClassContext.classCreator.getMethodCreator( - handleHeaderDescriptor); - - AssignableResultHandle invocationBuilderRef = handleHeaderMethod - .createVariable(Invocation.Builder.class); - handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); - addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, - handleHeaderMethod.getMethodParam(1), - param.type, - handleHeaderMethod.readInstanceField(clientField, handleHeaderMethod.getThis()), - methodGenericParametersField.get(), methodParamAnnotationsField.get(), paramIdx); - 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 = subClassContext.classCreator.getMethodCreator( - handleCookieDescriptor); - - AssignableResultHandle invocationBuilderRef = handleCookieMethod - .createVariable(Invocation.Builder.class); - handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); - addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, - handleCookieMethod.getMethodParam(1), - param.type, - handleCookieMethod.readInstanceField(clientField, handleCookieMethod.getThis()), - methodGenericParametersField.get(), methodParamAnnotationsField.get(), paramIdx); - 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); + boolean isSubResourceMethod = subMethod.getHttpMethod() == null; + if (!isSubResourceMethod) { + // java method data: + FieldDescriptor subMethodField = subContext.createJavaMethodField(subInterface, jandexSubMethod, + subMethodIndex); + Supplier subMethodParamAnnotationsField = subContext + .getLazyJavaMethodParamAnnotationsField(subMethodIndex); + Supplier subMethodGenericParametersField = subContext + .getLazyJavaMethodGenericParametersField(subMethodIndex); + + MethodCreator subMethodCreator = subContext.classCreator.getMethodCreator(subMethod.getName(), + jandexSubMethod.returnType().name().toString(), + parametersAsStringArray(jandexSubMethod)); + // initializing the web target in the sub constructor: + FieldDescriptor subMethodTarget = FieldDescriptor.of(subName, "target" + subMethodIndex, + WebTarget.class); + subContext.classCreator.getFieldCreator(subMethodTarget).setModifiers(Modifier.FINAL); + + AssignableResultHandle subMethodTargetV = subContext.constructor.createVariable(WebTargetImpl.class); + subContext.constructor.assign(subMethodTargetV, subContext.constructor.getMethodParam(0)); + if (subMethod.getPath() != null) { + appendPath(subContext.constructor, subMethod.getPath(), subMethodTargetV); } - } - // 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(jandexMethod, subMethodCreator, methodTarget, param.name, - subMethodCreator.getMethodParam(paramIdx), - jandexSubMethod.parameters().get(paramIdx), index, - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(subMethodGenericParametersField.get()), - subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), - paramIdx)); - } 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 = classContext.classCreator.getMethodCreator( - handleBeanParamDescriptor); - - AssignableResultHandle invocationBuilderRef = handleBeanParamMethod - .createVariable(Invocation.Builder.class); - handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); - formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, - invocationBuilderRef, beanParam.getItems(), - subMethodCreator.getMethodParam(paramIdx), methodTarget, index, - interfaceClass.name().toString(), - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), - formParams, - subMethodGenericParametersField, subMethodParamAnnotationsField, paramIdx); - - handleBeanParamMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleBeanParamDescriptor, - subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.PATH) { - addPathParam(subMethodCreator, methodTarget, param.name, - subMethodCreator.getMethodParam(paramIdx), param.type, - subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), - subMethodCreator.readStaticField(subMethodGenericParametersField.get()), - subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), - 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 = subClassContext.classCreator.getMethodCreator( - handleHeaderDescriptor); - - AssignableResultHandle invocationBuilderRef = handleHeaderMethod - .createVariable(Invocation.Builder.class); - handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); - addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, - handleHeaderMethod.getMethodParam(1), param.type, - handleHeaderMethod.readInstanceField(clientField, handleHeaderMethod.getThis()), - subMethodGenericParametersField.get(), subMethodParamAnnotationsField.get(), paramIdx); - handleHeaderMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleHeaderDescriptor, subMethodCreator.getMethodParam(paramIdx)); - } else if (param.parameterType == ParameterType.COOKIE) { - // cookies are added at the invocation builder level - MethodDescriptor handleCookieDescriptor = MethodDescriptor.ofMethod(subName, - subMethod.getName() + "$$" + subMethodIndex + "$$handleCookie$$" + paramIdx, - Invocation.Builder.class, - Invocation.Builder.class, param.type); - MethodCreator handleCookieMethod = subClassContext.classCreator.getMethodCreator( - handleCookieDescriptor); - - AssignableResultHandle invocationBuilderRef = handleCookieMethod - .createVariable(Invocation.Builder.class); - handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); - addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, - handleCookieMethod.getMethodParam(1), - param.type, - handleCookieMethod.readInstanceField(clientField, handleCookieMethod.getThis()), - subMethodGenericParametersField.get(), subMethodParamAnnotationsField.get(), paramIdx); - handleCookieMethod.returnValue(invocationBuilderRef); - invocationBuilderEnrichers.put(handleCookieDescriptor, 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()); + + subContext.constructor.writeInstanceField(subMethodTarget, subContext.constructor.getThis(), + subMethodTargetV); + + AssignableResultHandle methodTarget = subMethodCreator.createVariable(WebTarget.class); + subMethodCreator.assign(methodTarget, + subMethodCreator.readInstanceField(subMethodTarget, subMethodCreator.getThis())); + + ResultHandle bodyParameterValue = null; + AssignableResultHandle formParams = null; + Map invocationBuilderEnrichers = new HashMap<>(); + + int inheritedParamIndex = 0; + for (SubResourceParameter subParamField : subParamFields) { + inheritedParamIndex++; + MethodParameter param = subParamField.methodParameter; + ResultHandle paramValue = subMethodCreator.readInstanceField(subParamField.field, + 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(jandexMethod, subMethodCreator, methodTarget, param.name, + paramValue, subParamField.type, index, + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + subMethodCreator.readStaticField(subParamField.genericsParametersField.get()), + subMethodCreator.readStaticField(subParamField.paramAnnotationsField.get()), + subParamField.paramIndex)); + } 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$$" + inheritedParamIndex + + "$" + subParamField.paramIndex, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleBeanParamMethod = subContext.classCreator.getMethodCreator( + handleBeanParamDescriptor); + + AssignableResultHandle invocationBuilderRef = handleBeanParamMethod + .createVariable(Invocation.Builder.class); + handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); + formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, + invocationBuilderRef, beanParam.getItems(), + paramValue, methodTarget, index, + interfaceClass.name().toString(), + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), + formParams, + methodGenericParametersField, methodParamAnnotationsField, subParamField.paramIndex); + + handleBeanParamMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleBeanParamDescriptor, paramValue); + } else if (param.parameterType == ParameterType.PATH) { + // methodTarget = methodTarget.resolveTemplate(paramname, paramvalue); + addPathParam(subMethodCreator, methodTarget, param.name, paramValue, + param.type, + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + subMethodCreator.readStaticField(subParamField.genericsParametersField.get()), + subMethodCreator.readStaticField(subParamField.paramAnnotationsField.get()), + subParamField.paramIndex); + } 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" + + inheritedParamIndex + "$" + subParamField.paramIndex, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleHeaderMethod = subContext.classCreator.getMethodCreator( + handleHeaderDescriptor); + + AssignableResultHandle invocationBuilderRef = handleHeaderMethod + .createVariable(Invocation.Builder.class); + handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); + addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, + handleHeaderMethod.getMethodParam(1), + param.type, + handleHeaderMethod.readInstanceField(clientField, handleHeaderMethod.getThis()), + subParamField.genericsParametersField.get(), + subParamField.paramAnnotationsField.get(), + subParamField.paramIndex); + 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" + + inheritedParamIndex + "$" + subParamField.paramIndex, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleCookieMethod = subContext.classCreator.getMethodCreator( + handleCookieDescriptor); + + AssignableResultHandle invocationBuilderRef = handleCookieMethod + .createVariable(Invocation.Builder.class); + handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); + addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, + handleCookieMethod.getMethodParam(1), + param.type, + handleCookieMethod.readInstanceField(clientField, handleCookieMethod.getThis()), + subParamField.genericsParametersField.get(), + subParamField.paramAnnotationsField.get(), + subParamField.paramIndex); + 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, subParamField.type, index); } - multipartForm = createMultipartForm(subMethodCreator, - subMethodCreator.getMethodParam(paramIdx), - jandexSubMethod.parameters().get(paramIdx), 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(jandexMethod, subMethodCreator, methodTarget, param.name, + subMethodCreator.getMethodParam(paramIdx), + jandexSubMethod.parameters().get(paramIdx), index, + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + subMethodCreator.readStaticField(subMethodGenericParametersField.get()), + subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), + paramIdx)); + } 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 = ownerContext.classCreator.getMethodCreator( + handleBeanParamDescriptor); + + AssignableResultHandle invocationBuilderRef = handleBeanParamMethod + .createVariable(Invocation.Builder.class); + handleBeanParamMethod.assign(invocationBuilderRef, handleBeanParamMethod.getMethodParam(0)); + formParams = addBeanParamData(jandexMethod, subMethodCreator, handleBeanParamMethod, + invocationBuilderRef, beanParam.getItems(), + subMethodCreator.getMethodParam(paramIdx), methodTarget, index, + interfaceClass.name().toString(), + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + handleBeanParamMethod.readInstanceField(clientField, handleBeanParamMethod.getThis()), + formParams, + subMethodGenericParametersField, subMethodParamAnnotationsField, paramIdx); + + handleBeanParamMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleBeanParamDescriptor, + subMethodCreator.getMethodParam(paramIdx)); + } else if (param.parameterType == ParameterType.PATH) { + addPathParam(subMethodCreator, methodTarget, param.name, + subMethodCreator.getMethodParam(paramIdx), param.type, + subMethodCreator.readInstanceField(clientField, subMethodCreator.getThis()), + subMethodCreator.readStaticField(subMethodGenericParametersField.get()), + subMethodCreator.readStaticField(subMethodParamAnnotationsField.get()), + 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 = subContext.classCreator.getMethodCreator( + handleHeaderDescriptor); + + AssignableResultHandle invocationBuilderRef = handleHeaderMethod + .createVariable(Invocation.Builder.class); + handleHeaderMethod.assign(invocationBuilderRef, handleHeaderMethod.getMethodParam(0)); + addHeaderParam(handleHeaderMethod, invocationBuilderRef, param.name, + handleHeaderMethod.getMethodParam(1), param.type, + handleHeaderMethod.readInstanceField(clientField, handleHeaderMethod.getThis()), + subMethodGenericParametersField.get(), subMethodParamAnnotationsField.get(), paramIdx); + handleHeaderMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleHeaderDescriptor, subMethodCreator.getMethodParam(paramIdx)); + } else if (param.parameterType == ParameterType.COOKIE) { + // cookies are added at the invocation builder level + MethodDescriptor handleCookieDescriptor = MethodDescriptor.ofMethod(subName, + subMethod.getName() + "$$" + subMethodIndex + "$$handleCookie$$" + paramIdx, + Invocation.Builder.class, + Invocation.Builder.class, param.type); + MethodCreator handleCookieMethod = subContext.classCreator.getMethodCreator( + handleCookieDescriptor); + + AssignableResultHandle invocationBuilderRef = handleCookieMethod + .createVariable(Invocation.Builder.class); + handleCookieMethod.assign(invocationBuilderRef, handleCookieMethod.getMethodParam(0)); + addCookieParam(handleCookieMethod, invocationBuilderRef, param.name, + handleCookieMethod.getMethodParam(1), + param.type, + handleCookieMethod.readInstanceField(clientField, handleCookieMethod.getThis()), + subMethodGenericParametersField.get(), subMethodParamAnnotationsField.get(), paramIdx); + handleCookieMethod.returnValue(invocationBuilderRef); + invocationBuilderEnrichers.put(handleCookieDescriptor, 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, subClassContext, subMethodCreator, - methodTarget, subMethodIndex, webTargets, subMethod, subJavaMethodParameters, jandexSubMethod, - multipartResponseTypes); - } else { // if the response is multipart, let's add it's class to the appropriate collection: addResponseTypeIfMultipart(multipartResponseTypes, jandexSubMethod, index); @@ -1397,30 +1407,44 @@ private void handleSubResourceMethod(List for (JaxrsClientReactiveEnricherBuildItem enricher : enrichers) { enricher.getEnricher() - .forSubResourceMethod(subClassContext.classCreator, subClassContext.constructor, - subClassContext.clinit, subMethodCreator, interfaceClass, - subResourceInterface, jandexSubMethod, jandexMethod, builder, index, + .forSubResourceMethod(subContext.classCreator, subContext.constructor, + subContext.clinit, subMethodCreator, interfaceClass, + subInterface, jandexSubMethod, jandexMethod, builder, index, generatedClasses, methodIndex, subMethodIndex, subMethodField); } String[] consumes = extractProducesConsumesValues( jandexSubMethod.declaringClass().classAnnotation(CONSUMES), method.getConsumes()); consumes = extractProducesConsumesValues(jandexSubMethod.annotation(CONSUMES), consumes); - handleReturn(subResourceInterface, defaultMediaType, + handleReturn(subInterface, defaultMediaType, getHttpMethod(jandexSubMethod, subMethod.getHttpMethod(), httpAnnotationToMethod), consumes, jandexSubMethod, subMethodCreator, formParams, multipartForm, bodyParameterValue, builder); + } else { + // 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; + } + ResultHandle subMethodTarget = subContext.constructor.getMethodParam(0); + handleSubResourceMethod(enrichers, generatedClasses, subInterface, index, + defaultMediaType, httpAnnotationToMethod, subName, subContext, subMethodTarget, + subMethodIndex, subMethod, subJavaMethodParameters, jandexSubMethod, + multipartResponseTypes, subParamFields); } + } - subClassContext.constructor.returnValue(null); - subClassContext.clinit.returnValue(null); + subContext.constructor.returnValue(null); + subContext.clinit.returnValue(null); - methodCreator.returnValue(subInstance); + ownerMethod.returnValue(subInstance); } } - private AssignableResultHandle createWebTargetForMethod(MethodCreator constructor, AssignableResultHandle baseTarget, + private AssignableResultHandle createWebTargetForMethod(MethodCreator constructor, ResultHandle baseTarget, ResourceMethod method) { AssignableResultHandle target = constructor.createVariable(WebTarget.class); constructor.assign(target, baseTarget); @@ -2343,8 +2367,7 @@ private ResultHandle addQueryParam(MethodInfo jandexMethod, BytecodeCreator meth } // get the new WebTarget addQueryParamToWebTarget(loopCreator, key, result, client, genericType, paramAnnotations, - paramIndex, - paramArray, componentType, result); + paramIndex, paramArray, componentType, result); } else { ResultHandle paramArray; String componentType = null; @@ -2545,4 +2568,30 @@ private enum ReturnCategory { COROUTINE } + private static class SubResourceParameter { + final MethodParameter methodParameter; + final String typeName; + + final Type type; + final FieldDescriptor field; + + final Supplier paramAnnotationsField; + + final Supplier genericsParametersField; + + final int paramIndex; + + private SubResourceParameter(MethodParameter methodParameter, String typeName, Type type, FieldDescriptor field, + Supplier paramAnnotationsField, Supplier genericsParametersField, + int paramIndex) { + this.methodParameter = methodParameter; + this.typeName = typeName; + this.type = type; + this.field = field; + this.paramAnnotationsField = paramAnnotationsField; + this.genericsParametersField = genericsParametersField; + this.paramIndex = paramIndex; + } + } + } 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 541a1ee0507eb..653850c982c85 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 @@ -16,6 +16,18 @@ @Path("/path") public class Resource { + @GET + @Path("{part1}/{part2}/{part3}") + public String getUriParts(@RestPath String part1, @RestPath String part2, @RestPath String part3) { + return String.format("%s/%s/%s", part1, part2, part3); + } + + @GET + @Path("{part1}/{part2}/{part3}/{part4}") + public String getUriParts(@RestPath String part1, @RestPath String part2, @RestPath String part3, @RestPath String part4) { + return String.format("%s/%s/%s/%s", part1, part2, part3, part4); + } + @GET @Path("{part1}/{part2}/{part3}/{part4}/{part5}") public String getUriParts(@RestPath String part1, @RestPath String part2, @RestPath String part3, @RestPath String part4, @@ -23,12 +35,6 @@ public String getUriParts(@RestPath String part1, @RestPath String part2, @RestP 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) { - return String.format("%s/%s/%s", part1, part2, part3); - } - @POST @Path("{part1}/{part2}") public Response getUriEntityAndQueryParam(@RestPath String part1, @RestPath String part2, @RestQuery String queryParam, @@ -46,11 +52,11 @@ public Response getUriEntityAndQueryParam(@RestPath String part1, @RestPath Stri } @POST - @Path("{part1}/{part2}/{part3}/{part4}") + @Path("{part1}/{part2}/{part3}") 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)); + @RestPath String part3, @RestQuery String queryParam, String entity, @Context HttpHeaders headers) { + Response.ResponseBuilder responseBuilder = Response + .ok(String.format("%s/%s/%s:%s:%s", part1, part2, part3, entity, queryParam)); for (Map.Entry> headerEntry : headers.getRequestHeaders().entrySet()) { String headerName = headerEntry.getKey(); 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 5cb7c853d44bc..fad7d095aea4a 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 @@ -54,10 +54,10 @@ void shouldPassParamsToSubResource() { @Test void shouldPassParamsToSubSubResource() { - // should result in sending GET /path/rt/mthd/sub/sub/simple + // should result in sending GET /path/rt/mthd/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"); + assertThat(result).isEqualTo("rt/mthd/sub/subSimple"); } @Test @@ -84,14 +84,14 @@ void shouldDoMultiplePostsInSubSubResource() { SubSubClient sub = rootClient.sub("rt", "mthd").sub(); Response result = sub.postWithQueryParam("prm", "ent1t1"); - assertThat(result.readEntity(String.class)).isEqualTo("rt/mthd:ent1t1:prm"); + assertThat(result.readEntity(String.class)).isEqualTo("rt/mthd/sub: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"); + assertThat(result.readEntity(String.class)).isEqualTo("rt/mthd/sub:ent1t1:prm"); } @Path("/path/{rootParam}") @@ -136,7 +136,7 @@ default String fillingMethod() { @Produces("text/plain") interface SubSubClient { @GET - @Path("/simple") + @Path("/subSimple") String simpleSub(); @POST