-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Update MP REST Client to 4.0 #43959
base: main
Are you sure you want to change the base?
Update MP REST Client to 4.0 #43959
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,7 @@ | |
import static org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder.PROPERTY_PROXY_PORT; | ||
import static org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder.PROPERTY_PROXY_SCHEME; | ||
|
||
import java.io.Closeable; | ||
import java.lang.annotation.Annotation; | ||
import java.lang.reflect.AnnotatedElement; | ||
import java.lang.reflect.Field; | ||
|
@@ -22,14 +23,17 @@ | |
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.TimeUnit; | ||
import java.util.stream.Stream; | ||
|
||
import javax.net.ssl.HostnameVerifier; | ||
import javax.net.ssl.SSLContext; | ||
|
@@ -44,6 +48,7 @@ | |
import jakarta.ws.rs.Priorities; | ||
import jakarta.ws.rs.core.Configuration; | ||
import jakarta.ws.rs.core.MediaType; | ||
import jakarta.ws.rs.core.MultivaluedMap; | ||
import jakarta.ws.rs.ext.ParamConverterProvider; | ||
|
||
import org.eclipse.microprofile.config.Config; | ||
|
@@ -64,6 +69,7 @@ | |
import org.jboss.resteasy.client.jaxrs.internal.LocalResteasyProviderFactory; | ||
import org.jboss.resteasy.concurrent.ContextualExecutorService; | ||
import org.jboss.resteasy.concurrent.ContextualExecutors; | ||
import org.jboss.resteasy.core.Headers; | ||
import org.jboss.resteasy.microprofile.client.ConfigurationWrapper; | ||
import org.jboss.resteasy.microprofile.client.DefaultMediaTypeFilter; | ||
import org.jboss.resteasy.microprofile.client.DefaultResponseExceptionMapper; | ||
|
@@ -86,6 +92,10 @@ | |
import io.quarkus.runtime.graal.DisabledSSLContext; | ||
import io.quarkus.runtime.ssl.SslContextConfiguration; | ||
|
||
/** | ||
* This is mostly a copy from {@link org.jboss.resteasy.microprofile.client.RestClientBuilderImpl}. It is required to | ||
* remove the reference to org.jboss.resteasy.cdi.CdiInjectorFactory so we don't require the RESTEasy CDI dependency. | ||
*/ | ||
public class QuarkusRestClientBuilder implements RestClientBuilder { | ||
|
||
private static final String RESTEASY_PROPERTY_PREFIX = "resteasy."; | ||
|
@@ -94,16 +104,22 @@ public class QuarkusRestClientBuilder implements RestClientBuilder { | |
private static final Logger LOGGER = Logger.getLogger(QuarkusRestClientBuilder.class); | ||
private static final DefaultMediaTypeFilter DEFAULT_MEDIA_TYPE_FILTER = new DefaultMediaTypeFilter(); | ||
private static final String TLS_TRUST_ALL = "quarkus.tls.trust-all"; | ||
|
||
private static final Collection<Method> IGNORED_METHODS = new ArrayList<>(); | ||
public static final MethodInjectionFilter METHOD_INJECTION_FILTER = new MethodInjectionFilter(); | ||
public static final ClientHeadersRequestFilter HEADERS_REQUEST_FILTER = new ClientHeadersRequestFilter(); | ||
|
||
static ResteasyProviderFactory PROVIDER_FACTORY; | ||
|
||
static { | ||
Collections.addAll(IGNORED_METHODS, Closeable.class.getMethods()); | ||
Collections.addAll(IGNORED_METHODS, AutoCloseable.class.getMethods()); | ||
} | ||
|
||
public static void setProviderFactory(ResteasyProviderFactory providerFactory) { | ||
PROVIDER_FACTORY = providerFactory; | ||
} | ||
|
||
private final MultivaluedMap<String, Object> headers; | ||
|
||
public QuarkusRestClientBuilder() { | ||
builderDelegate = new MpClientBuilderImpl(); | ||
|
||
|
@@ -126,6 +142,7 @@ public QuarkusRestClientBuilder() { | |
} catch (Throwable e) { | ||
|
||
} | ||
headers = new Headers<>(); | ||
} | ||
|
||
public Configuration getConfigurationWrapper() { | ||
|
@@ -148,6 +165,13 @@ public RestClientBuilder queryParamStyle(QueryParamStyle queryParamStyle) { | |
return this; | ||
} | ||
|
||
@Override | ||
public RestClientBuilder header(final String name, final Object value) { | ||
geoand marked this conversation as resolved.
Show resolved
Hide resolved
|
||
headers.add(Objects.requireNonNull(name, "A header name is required."), | ||
Objects.requireNonNull(value, "Value for header is required.")); | ||
return this; | ||
} | ||
|
||
@Override | ||
public RestClientBuilder proxyAddress(String host, int port) { | ||
if (host == null) { | ||
|
@@ -288,7 +312,7 @@ public <T> T build(Class<T> aClass, ClientHttpEngine httpEngine) | |
} | ||
resteasyClientBuilder.register(DEFAULT_MEDIA_TYPE_FILTER); | ||
resteasyClientBuilder.register(METHOD_INJECTION_FILTER); | ||
resteasyClientBuilder.register(HEADERS_REQUEST_FILTER); | ||
resteasyClientBuilder.register(new ClientHeadersRequestFilter(headers)); | ||
register(new MpPublisherMessageBodyReader(executorService)); | ||
resteasyClientBuilder.sslContext(sslContext); | ||
resteasyClientBuilder.trustStore(trustStore); | ||
|
@@ -551,7 +575,7 @@ private void verifyBeanPathParam(Class<?> beanType, Map<String, Object> paramMap | |
|
||
private <T> void verifyInterface(Class<T> typeDef) { | ||
|
||
Method[] methods = typeDef.getMethods(); | ||
Method[] methods = resolveMethods(typeDef); | ||
|
||
// multiple verbs | ||
for (Method method : methods) { | ||
|
@@ -806,6 +830,16 @@ private static BeanManager getBeanManager() { | |
} | ||
} | ||
|
||
private static Method[] resolveMethods(final Class<?> type) { | ||
// If the type extends Closeable or AutoCloseable, we need to filter out their methods | ||
if (AutoCloseable.class.isAssignableFrom(type)) { | ||
return Stream.of(type.getMethods()) | ||
.filter(method -> !IGNORED_METHODS.contains(method)) | ||
.toArray(Method[]::new); | ||
Comment on lines
+836
to
+838
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm definitely no fan of this, but we can let it go in :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is a copy from the original RESTEasy Builder code: Initially, we used the one provided by RESTEasy, but we made a copy to remove references to their CDI integration and also to remove the CDI dependency. They diverged a bit, so I've tried to reconcile them again for consistency. |
||
} | ||
return type.getMethods(); | ||
} | ||
|
||
private final MpClientBuilderImpl builderDelegate; | ||
|
||
private final ConfigurationWrapper configurationWrapper; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
package io.quarkus.rest.client.reactive.headers; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
import java.io.IOException; | ||
import java.net.URI; | ||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
import jakarta.json.Json; | ||
import jakarta.json.JsonArray; | ||
import jakarta.json.JsonArrayBuilder; | ||
import jakarta.json.JsonObject; | ||
import jakarta.json.JsonObjectBuilder; | ||
import jakarta.json.JsonString; | ||
import jakarta.json.JsonValue; | ||
import jakarta.ws.rs.GET; | ||
import jakarta.ws.rs.HeaderParam; | ||
import jakarta.ws.rs.Path; | ||
import jakarta.ws.rs.client.ClientRequestContext; | ||
import jakarta.ws.rs.client.ClientRequestFilter; | ||
import jakarta.ws.rs.core.MultivaluedMap; | ||
import jakarta.ws.rs.core.Response; | ||
|
||
import org.eclipse.microprofile.rest.client.RestClientBuilder; | ||
import org.eclipse.microprofile.rest.client.annotation.ClientHeaderParam; | ||
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 DefaultBuilderHeadersTest { | ||
@RegisterExtension | ||
static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot(jar -> { | ||
}); | ||
|
||
@TestHTTPResource | ||
URI baseUri; | ||
|
||
@Test | ||
void headers() { | ||
RestClientBuilder builder = RestClientBuilder.newBuilder().baseUri("http://localhost:8080/"); | ||
builder.register(ReturnWithAllDuplicateClientHeadersFilter.class); | ||
builder.header("InterfaceAndBuilderHeader", "builder"); | ||
ClientBuilderHeaderMethodClient client = builder.build(ClientBuilderHeaderMethodClient.class); | ||
|
||
checkHeaders(client.getAllHeaders("headerparam"), "method"); | ||
} | ||
|
||
@Path("/") | ||
public interface ClientBuilderHeaderMethodClient { | ||
@GET | ||
@ClientHeaderParam(name = "InterfaceAndBuilderHeader", value = "method") | ||
JsonObject getAllHeaders(@HeaderParam("HeaderParam") String param); | ||
} | ||
|
||
public static class ReturnWithAllDuplicateClientHeadersFilter implements ClientRequestFilter { | ||
|
||
@Override | ||
public void filter(ClientRequestContext clientRequestContext) throws IOException { | ||
JsonObjectBuilder allClientHeaders = Json.createObjectBuilder(); | ||
MultivaluedMap<String, Object> clientHeaders = clientRequestContext.getHeaders(); | ||
for (String headerName : clientHeaders.keySet()) { | ||
List<Object> header = clientHeaders.get(headerName); | ||
final JsonArrayBuilder headerValues = Json.createArrayBuilder(); | ||
header.forEach(h -> headerValues.add(h.toString())); | ||
allClientHeaders.add(headerName, headerValues); | ||
} | ||
clientRequestContext.abortWith(Response.ok(allClientHeaders.build()).build()); | ||
} | ||
} | ||
|
||
private static void checkHeaders(final JsonObject headers, final String clientHeaderParamName) { | ||
final List<String> clientRequestHeaders = headerValues(headers, "InterfaceAndBuilderHeader"); | ||
|
||
assertTrue(clientRequestHeaders.contains("builder"), | ||
"Header InterfaceAndBuilderHeader did not container \"builder\": " + clientRequestHeaders); | ||
assertTrue(clientRequestHeaders.contains(clientHeaderParamName), | ||
"Header InterfaceAndBuilderHeader did not container \"" + clientHeaderParamName + "\": " | ||
+ clientRequestHeaders); | ||
|
||
final List<String> headerParamHeaders = headerValues(headers, "HeaderParam"); | ||
assertTrue(headerParamHeaders.contains("headerparam"), | ||
"Header HeaderParam did not container \"headerparam\": " + headerParamHeaders); | ||
} | ||
|
||
private static List<String> headerValues(final JsonObject headers, final String headerName) { | ||
final JsonArray headerValues = headers.getJsonArray(headerName); | ||
assertNotNull(headerValues, | ||
String.format("Expected header '%s' to be present in %s", headerName, headers)); | ||
return headerValues.stream().map( | ||
v -> (v.getValueType() == JsonValue.ValueType.STRING ? ((JsonString) v).getString() : v.toString())) | ||
.collect(Collectors.toList()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package io.quarkus.rest.client.reactive.runtime; | ||
|
||
import java.util.List; | ||
import java.util.function.BiConsumer; | ||
|
||
import jakarta.annotation.Priority; | ||
import jakarta.ws.rs.client.ClientRequestContext; | ||
import jakarta.ws.rs.client.ClientRequestFilter; | ||
import jakarta.ws.rs.core.MultivaluedMap; | ||
|
||
import org.jboss.resteasy.reactive.common.util.CaseInsensitiveMap; | ||
|
||
@Priority(Integer.MIN_VALUE + 10) | ||
public class DefaultClientHeadersRequestFilter implements ClientRequestFilter { | ||
private final MultivaluedMap<String, Object> defaultHeaders; | ||
|
||
public DefaultClientHeadersRequestFilter(final MultivaluedMap<String, Object> defaultHeaders) { | ||
this.defaultHeaders = new CaseInsensitiveMap<>(); | ||
this.defaultHeaders.putAll(defaultHeaders); | ||
} | ||
|
||
@Override | ||
public void filter(ClientRequestContext requestContext) { | ||
this.defaultHeaders.forEach(new BiConsumer<String, List<Object>>() { | ||
@Override | ||
public void accept(final String name, final List<Object> values) { | ||
requestContext.getHeaders().addAll(name, values); | ||
} | ||
}); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that I mind, but I would like to know why this is now necessary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also if it's necessary, we might have to depend on the
quarkus-resteasy-multipart
extension instead.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the dependency from RESTEasy that provides the
EntityPart.Builder
implementation. Note that for the REST Client Classic, I've tried to keep it as the original implementation: resteasy/resteasy-microprofile@bb8b433