diff --git a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index 8bdb9b9878a33..d8b3eb056ffe1 100644 --- a/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-jaxrs/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -29,8 +29,6 @@ import java.io.InputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; -import java.net.URI; -import java.net.URL; import java.nio.file.Path; import java.util.AbstractMap; import java.util.ArrayList; @@ -1152,7 +1150,7 @@ A more full example of generated client (with sub-resource) can is at the bottom formParams = createFormDataIfAbsent(methodCreator, formParams, multipart); // NOTE: don't use type here, because we're not going through the collection converters and stuff Type parameterType = jandexMethod.parameterType(paramIdx); - addFormParam(methodCreator, param.name, methodCreator.getMethodParam(paramIdx), + addFormParam(jandexMethod, methodCreator, param.name, methodCreator.getMethodParam(paramIdx), parameterType, param.signature, index, restClientInterface.getClassName(), methodCreator.getThis(), formParams, getGenericTypeFromArray(methodCreator, methodGenericParametersField, paramIdx), @@ -2660,7 +2658,7 @@ private void addSubBeanParamData(MethodInfo jandexMethod, int paramIndex, Byteco break; case FORM_PARAM: FormParamItem formParam = (FormParamItem) item; - addFormParam(creator, formParam.getFormParamName(), formParam.extract(creator, param), + addFormParam(jandexMethod, creator, formParam.getFormParamName(), formParam.extract(creator, param), formParam.getParamType(), formParam.getParamSignature(), index, restClientInterfaceClassName, client, @@ -2929,7 +2927,7 @@ private void addPathParam(BytecodeCreator methodCreator, AssignableResultHandle methodCreator.load(paramName), handle)); } - private void addFormParam(BytecodeCreator methodCreator, + private void addFormParam(MethodInfo jandexMethod, BytecodeCreator methodCreator, String paramName, ResultHandle formParamHandle, Type parameterType, @@ -2964,10 +2962,56 @@ private void addFormParam(BytecodeCreator methodCreator, ResultHandle convertedParamArray = creator.invokeVirtualMethod( MethodDescriptor.ofMethod(RestClientBase.class, "convertParamArray", Object[].class, Object[].class, Class.class, java.lang.reflect.Type.class, Annotation[].class), - client, paramArray, creator.loadClassFromTCCL(componentType), genericType, creator.newArray( - Annotation.class, 0)); + client, paramArray, creator.loadClassFromTCCL(componentType), genericType, + creator.newArray(Annotation.class, 0)); creator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD_ALL, formParams, creator.load(paramName), convertedParamArray); + } else if (isMap(parameterType, index)) { + var resolvesTypes = resolveMapTypes(parameterType, index, jandexMethod); + var keyType = resolvesTypes.getKey(); + if (!ResteasyReactiveDotNames.STRING.equals(keyType.name())) { + throw new IllegalArgumentException( + "Map parameter types must have String keys. Offending method is: " + jandexMethod); + } + // Loop through the keys + ResultHandle keySet = creator.invokeInterfaceMethod(ofMethod(Map.class, "keySet", Set.class), + formParamHandle); + ResultHandle keysIterator = creator.invokeInterfaceMethod( + ofMethod(Set.class, "iterator", Iterator.class), keySet); + BytecodeCreator loopCreator = creator.whileLoop(c -> iteratorHasNext(c, keysIterator)).block(); + ResultHandle key = loopCreator.invokeInterfaceMethod( + ofMethod(Iterator.class, "next", Object.class), keysIterator); + // get the value and convert + ResultHandle value = loopCreator.invokeInterfaceMethod(ofMethod(Map.class, "get", Object.class, Object.class), + formParamHandle, key); + var valueType = resolvesTypes.getValue(); + String componentType = valueType.name().toString(); + ResultHandle paramArray; + if (isCollection(valueType, index)) { + if (valueType.kind() == PARAMETERIZED_TYPE) { + Type paramType = valueType.asParameterizedType().arguments().get(0); + if ((paramType.kind() == CLASS) || (paramType.kind() == PARAMETERIZED_TYPE)) { + componentType = paramType.name().toString(); + } + } + if (componentType == null) { + componentType = DotNames.OBJECT.toString(); + } + paramArray = loopCreator.invokeStaticMethod( + MethodDescriptor.ofMethod(ToObjectArray.class, "collection", Object[].class, Collection.class), + value); + } else { + paramArray = loopCreator + .invokeStaticMethod(ofMethod(ToObjectArray.class, "value", Object[].class, Object.class), + value); + } + ResultHandle convertedParamArray = loopCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(RestClientBase.class, "convertParamArray", Object[].class, Object[].class, + Class.class, java.lang.reflect.Type.class, Annotation[].class), + client, paramArray, loopCreator.loadClassFromTCCL(componentType), genericType, + loopCreator.newArray(Annotation.class, 0)); + loopCreator.invokeInterfaceMethod(MULTIVALUED_MAP_ADD_ALL, formParams, + key, convertedParamArray); } else { ResultHandle convertedFormParam = convertParamToString(creator, client, formParamHandle, parameterType.name().toString(), diff --git a/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormMapTest.java b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormMapTest.java new file mode 100644 index 0000000000000..96ef295b74973 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client/deployment/src/test/java/io/quarkus/rest/client/reactive/FormMapTest.java @@ -0,0 +1,83 @@ +package io.quarkus.rest.client.reactive; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import jakarta.ws.rs.BeanParam; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MultivaluedMap; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.jboss.resteasy.reactive.RestForm; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class FormMapTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar.addClasses(Resource.class, Client.class)); + + @TestHTTPResource + URI baseUri; + + @Test + void test() { + Client client = RestClientBuilder.newBuilder().baseUri(baseUri).build(Client.class); + String response = client.call(Map.of("foo", "bar", "k1", "v1", "k2", "v2"), new Holder(Collections.emptyMap())); + assertThat(response).isEqualTo("foo=bar-k1=v1-k2=v2"); + + String response2 = client.call(Collections.emptyMap(), new Holder(Map.of("foo", List.of("bar"), "k1", List.of("v1")))); + assertThat(response2).isEqualTo("foo=bar-k1=v1"); + + String response3 = client.call(Map.of("foo", "bar"), new Holder(Map.of("k", List.of("v1", "v2")))); + assertThat(response3).isEqualTo("foo=bar-k=v1,v2"); + } + + @Path("/test") + public static class Resource { + + @POST + @Consumes("application/x-www-form-urlencoded") + public String response(MultivaluedMap all) { + StringBuilder sb = new StringBuilder(); + boolean isFirst = true; + List keys = new ArrayList<>(all.keySet()); + Collections.sort(keys); + for (var key : keys) { + if (!isFirst) { + sb.append("-"); + } + isFirst = false; + sb.append(key).append("=").append(String.join(",", all.get(key))); + } + return sb.toString(); + } + } + + @Path("/test") + public interface Client { + + @POST + String call(@RestForm Map formParams, @BeanParam Holder holder); + } + + public static class Holder { + @RestForm + public Map> more; + + public Holder(Map> more) { + this.more = more; + } + } +}