Skip to content

Commit

Permalink
Merge pull request #22396 from michalszynkiewicz/rcr-param-converters
Browse files Browse the repository at this point in the history
REST Client Reactive - param converter support
  • Loading branch information
gsmet authored Dec 20, 2021
2 parents b81c20f + f23d1f2 commit a9edea5
Show file tree
Hide file tree
Showing 12 changed files with 502 additions and 77 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.BiFunction;

import javax.ws.rs.RuntimeType;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.ext.ParamConverterProvider;

import org.jboss.resteasy.reactive.client.impl.ClientProxies;
import org.jboss.resteasy.reactive.client.impl.ClientSerialisers;
Expand Down Expand Up @@ -37,7 +39,8 @@ public static GenericTypeMapping getGenericTypeMapping() {
return genericTypeMapping;
}

public void setupClientProxies(Map<String, RuntimeValue<Function<WebTarget, ?>>> clientImplementations,
public void setupClientProxies(
Map<String, RuntimeValue<BiFunction<WebTarget, List<ParamConverterProvider>, ?>>> clientImplementations,
Map<String, String> failures) {
clientProxies = createClientImpls(clientImplementations, failures);
}
Expand All @@ -49,10 +52,12 @@ public Serialisers createSerializers() {
return s;
}

private ClientProxies createClientImpls(Map<String, RuntimeValue<Function<WebTarget, ?>>> clientImplementations,
private ClientProxies createClientImpls(
Map<String, RuntimeValue<BiFunction<WebTarget, List<ParamConverterProvider>, ?>>> clientImplementations,
Map<String, String> failureMessages) {
Map<Class<?>, Function<WebTarget, ?>> map = new HashMap<>();
for (Map.Entry<String, RuntimeValue<Function<WebTarget, ?>>> entry : clientImplementations.entrySet()) {
Map<Class<?>, BiFunction<WebTarget, List<ParamConverterProvider>, ?>> map = new HashMap<>();
for (Map.Entry<String, RuntimeValue<BiFunction<WebTarget, List<ParamConverterProvider>, ?>>> entry : clientImplementations
.entrySet()) {
map.put(loadClass(entry.getKey()), entry.getValue().getValue());
}
Map<Class<?>, String> failures = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.quarkus.jaxrs.client.reactive.runtime;

import java.io.Closeable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;

public abstract class RestClientBase implements Closeable {
private final List<ParamConverterProvider> paramConverterProviders;
private final Map<Class<?>, ParamConverterProvider> providerForClass = new ConcurrentHashMap<>();

public RestClientBase(List<ParamConverterProvider> providers) {
this.paramConverterProviders = providers;
}

@SuppressWarnings("unused") // used by generated code
public <T> Object[] convertParamArray(T[] value, Class<T> type) {
ParamConverter<T> converter = getConverter(type, null, null);

if (converter == null) {
return value;
} else {
Object[] result = new Object[value.length];

for (int i = 0; i < value.length; i++) {
result[i] = converter.toString(value[i]);
}
return result;
}
}

@SuppressWarnings("unused") // used by generated code
public <T> Object convertParam(T value, Class<T> type) {
ParamConverter<T> converter = getConverter(type, null, null);
if (converter != null) {
return converter.toString(value);
} else {
return value;
}
}

private <T> ParamConverter<T> getConverter(Class<T> type, Type genericType, Annotation[] annotations) {
ParamConverterProvider converterProvider = providerForClass.get(type);

if (converterProvider == null) {
for (ParamConverterProvider provider : paramConverterProviders) {
ParamConverter<T> converter = provider.getConverter(type, null, null);
if (converter != null) {
providerForClass.put(type, provider);
return converter;
}
}
providerForClass.put(type, NO_PROVIDER);
} else if (converterProvider != NO_PROVIDER) {
return converterProvider.getConverter(type, null, null);
}
return null;
}

private static final ParamConverterProvider NO_PROVIDER = new ParamConverterProvider() {
@Override
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType, Annotation[] annotations) {
return null;
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
package io.quarkus.rest.client.reactive.converter;

import static org.assertj.core.api.Assertions.assertThat;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.net.URI;

import javax.ws.rs.BeanParam;
import javax.ws.rs.CookieParam;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.QueryParam;
import javax.ws.rs.ext.ParamConverterProvider;

import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
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 ParamConverterProviderTest {
@RegisterExtension
static final QuarkusUnitTest TEST = new QuarkusUnitTest();

@TestHTTPResource
URI baseUri;

@Test
void shouldConvertPathParam() {
Client client = RestClientBuilder.newBuilder().baseUri(baseUri)
.build(Client.class);
assertThat(client.get(Param.FIRST)).isEqualTo("1");
assertThat(client.sub().get(Param.SECOND)).isEqualTo("2");

Bean bean = new Bean();
bean.param = Param.FIRST;
assertThat(client.get(bean)).isEqualTo("1");
}

@Test
void shouldConvertQueryParams() {
Client client = RestClientBuilder.newBuilder().baseUri(baseUri)
.build(Client.class);
assertThat(client.getWithQuery(Param.FIRST)).isEqualTo("1");
assertThat(client.sub().getWithQuery(Param.SECOND)).isEqualTo("2");

Bean bean = new Bean();
bean.param = Param.SECOND;
bean.queryParam = Param.FIRST;
assertThat(client.getWithQuery(bean)).isEqualTo("1");
}

@Test
void shouldConvertHeaderParams() {
Client client = RestClientBuilder.newBuilder().baseUri(baseUri)
.build(Client.class);
assertThat(client.getWithHeader(Param.FIRST)).isEqualTo("1");
assertThat(client.sub().getWithHeader(Param.SECOND)).isEqualTo("2");

Bean bean = new Bean();
bean.param = Param.SECOND;
bean.queryParam = Param.SECOND;
bean.headerParam = Param.FIRST;
assertThat(client.getWithHeader(bean)).isEqualTo("1");
}

@Test
void shouldConvertCookieParams() {
Client client = RestClientBuilder.newBuilder().baseUri(baseUri)
.build(Client.class);
assertThat(client.getWithHeader(Param.FIRST)).isEqualTo("1");
assertThat(client.sub().getWithCookie(Param.SECOND)).isEqualTo("2");

Bean bean = new Bean();
bean.param = Param.SECOND;
bean.queryParam = Param.SECOND;
bean.headerParam = Param.SECOND;
bean.cookieParam = Param.FIRST;
assertThat(client.getWithCookie(bean)).isEqualTo("1");
}

@Path("/echo")
@RegisterProvider(ParamConverter.class)
interface Client {
@Path("/sub")
SubClient sub();

@GET
@Path("/param/{param}")
String get(@PathParam("param") Param param);

@GET
@Path("/param/{param}")
String get(@BeanParam Bean beanParam);

@GET
@Path("/query")
String getWithQuery(@QueryParam("param") Param param);

@GET
@Path("/query")
String getWithQuery(@BeanParam Bean beanParam);

@GET
@Path("/header")
String getWithHeader(@HeaderParam("param") Param param);

@GET
@Path("/header")
String getWithHeader(@BeanParam Bean beanParam);

@GET
@Path("/cookie")
String getWithCookie(@HeaderParam("cookie-param") Param param);

@GET
@Path("/cookie")
String getWithCookie(@BeanParam Bean beanParam);
}

interface SubClient {
@GET
@Path("/param/{param}")
String get(@PathParam("param") Param param);

@GET
@Path("/query")
String getWithQuery(@QueryParam("param") Param param);

@GET
@Path("/header")
String getWithHeader(@HeaderParam("param") Param param);

@GET
@Path("cookie")
String getWithCookie(@CookieParam("cookie-param") Param param);
}

public static class Bean {
@PathParam("param")
public Param param;
@QueryParam("param")
public Param queryParam;
@HeaderParam("param")
public Param headerParam;
@CookieParam("cookie-param")
public Param cookieParam;
}

enum Param {
FIRST,
SECOND
}

public static class ParamConverter implements ParamConverterProvider {
@SuppressWarnings("unchecked")
@Override
public <T> javax.ws.rs.ext.ParamConverter<T> getConverter(Class<T> rawType, Type genericType,
Annotation[] annotations) {
if (rawType == Param.class) {
return (javax.ws.rs.ext.ParamConverter<T>) new javax.ws.rs.ext.ParamConverter<Param>() {
@Override
public Param fromString(String value) {
return null;
}

@Override
public String toString(Param value) {
if (value == null) {
return null;
}
switch (value) {
case FIRST:
return "1";
case SECOND:
return "2";
default:
return "unexpected";
}
}
};
}
return null;
}
}

@Path("/echo")
public static class EchoEndpoint {
@Path("/param/{param}")
@GET
public String echoPath(@PathParam("param") String param) {
return param;
}

@Path("/sub/param/{param}")
@GET
public String echoSubPath(@PathParam("param") String param) {
return param;
}

@GET
@Path("/query")
public String get(@QueryParam("param") String param) {
return param;
}

@Path("/sub/query")
@GET
public String getSub(@QueryParam("param") String param) {
return param;
}

@GET
@Path("/header")
public String getHeader(@HeaderParam("param") String param) {
return param;
}

@Path("/sub/header")
@GET
public String getSubHeader(@HeaderParam("param") String param) {
return param;
}

@GET
@Path("/cookie")
public String getCookie(@CookieParam("cookie-param") String param) {
return param;
}

@Path("/sub/cookie")
@GET
public String getSubCookie(@CookieParam("cookie-param") String param) {
return param;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import javax.net.ssl.SSLContext;
import javax.ws.rs.RuntimeType;
import javax.ws.rs.core.Configuration;
import javax.ws.rs.ext.ParamConverterProvider;

import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.rest.client.RestClientBuilder;
Expand Down Expand Up @@ -46,6 +47,7 @@ public class RestClientBuilderImpl implements RestClientBuilder {
private final ClientBuilderImpl clientBuilder = (ClientBuilderImpl) new ClientBuilderImpl()
.withConfig(new ConfigurationImpl(RuntimeType.CLIENT));
private final List<ResponseExceptionMapper<?>> exceptionMappers = new ArrayList<>();
private final List<ParamConverterProvider> paramConverterProviders = new ArrayList<>();

private URI uri;
private boolean followRedirects;
Expand Down Expand Up @@ -221,7 +223,8 @@ public RestClientBuilder baseUri(URI uri) {
}

private void registerMpSpecificProvider(Class<?> componentClass) {
if (ResponseExceptionMapper.class.isAssignableFrom(componentClass)) {
if (ResponseExceptionMapper.class.isAssignableFrom(componentClass)
|| ParamConverterProvider.class.isAssignableFrom(componentClass)) {
try {
registerMpSpecificProvider(componentClass.getDeclaredConstructor().newInstance());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
Expand All @@ -235,6 +238,9 @@ private void registerMpSpecificProvider(Object component) {
if (component instanceof ResponseExceptionMapper) {
exceptionMappers.add((ResponseExceptionMapper<?>) component);
}
if (component instanceof ParamConverterProvider) {
paramConverterProviders.add((ParamConverterProvider) component);
}
}

@Override
Expand Down Expand Up @@ -287,6 +293,7 @@ public <T> T build(Class<T> aClass) throws IllegalStateException, RestClientDefi

ClientImpl client = clientBuilder.build();
WebTargetImpl target = (WebTargetImpl) client.target(uri);
target.setParamConverterProviders(paramConverterProviders);
try {
return target.proxy(aClass);
} catch (InvalidRestClientDefinitionException e) {
Expand Down
Loading

0 comments on commit a9edea5

Please sign in to comment.