Skip to content

Commit

Permalink
Merge pull request #30529 from geoand/#30516
Browse files Browse the repository at this point in the history
Use custom ObjectMapper for Keycloak admin client if necessary
  • Loading branch information
geoand authored Jan 24, 2023
2 parents d27d535 + 750ca7a commit aa152df
Show file tree
Hide file tree
Showing 26 changed files with 341 additions and 72 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package io.quarkus.keycloak.admin.client.reactive.runtime;

import java.util.List;

import javax.enterprise.inject.Instance;
import javax.net.ssl.SSLContext;
import javax.ws.rs.Priorities;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.resteasy.reactive.client.impl.ClientBuilderImpl;
import org.jboss.resteasy.reactive.client.impl.WebTargetImpl;
import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader;
Expand All @@ -14,10 +20,14 @@
import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.jackson.ObjectMapperCustomizer;
import io.quarkus.rest.client.reactive.jackson.runtime.serialisers.ClientJacksonMessageBodyWriter;

public class ResteasyReactiveClientProvider implements ResteasyClientProvider {

private static final List<String> HANDLED_MEDIA_TYPES = List.of(MediaType.APPLICATION_JSON);
private static final int PROVIDER_PRIORITY = Priorities.USER + 100; // ensures that it will be used first

@Override
public Client newRestEasyClient(Object messageHandler, SSLContext sslContext, boolean disableTrustManager) {
ClientBuilderImpl clientBuilder = new ClientBuilderImpl();
Expand All @@ -32,29 +42,65 @@ private ClientBuilderImpl registerJacksonProviders(ClientBuilderImpl clientBuild
throw new IllegalStateException(this.getClass().getName() + " should only be used in a Quarkus application");
} else {
InstanceHandle<ObjectMapper> objectMapperInstance = arcContainer.instance(ObjectMapper.class);
ObjectMapper objectMapper = null;
boolean canReuseObjectMapper = canReuseObjectMapper(objectMapperInstance, arcContainer);
if (canReuseObjectMapper) {

InstanceHandle<JacksonBasicMessageBodyReader> readerInstance = arcContainer
.instance(JacksonBasicMessageBodyReader.class);
if (readerInstance.isAvailable()) {
clientBuilder = clientBuilder.register(readerInstance.get());
} else {
objectMapper = getObjectMapper(objectMapper, objectMapperInstance);
clientBuilder = clientBuilder.register(new JacksonBasicMessageBodyReader(objectMapper));
}
ObjectMapper objectMapper = null;

InstanceHandle<ClientJacksonMessageBodyWriter> writerInstance = arcContainer
.instance(ClientJacksonMessageBodyWriter.class);
if (writerInstance.isAvailable()) {
clientBuilder = clientBuilder.register(writerInstance.get());
InstanceHandle<JacksonBasicMessageBodyReader> readerInstance = arcContainer
.instance(JacksonBasicMessageBodyReader.class);
if (readerInstance.isAvailable()) {
clientBuilder = clientBuilder.register(readerInstance.get());
} else {
objectMapper = getObjectMapper(objectMapper, objectMapperInstance);
clientBuilder = clientBuilder.register(new JacksonBasicMessageBodyReader(objectMapper));
}

InstanceHandle<ClientJacksonMessageBodyWriter> writerInstance = arcContainer
.instance(ClientJacksonMessageBodyWriter.class);
if (writerInstance.isAvailable()) {
clientBuilder = clientBuilder.register(writerInstance.get());
} else {
objectMapper = getObjectMapper(objectMapper, objectMapperInstance);
clientBuilder = clientBuilder.register(new ClientJacksonMessageBodyWriter(objectMapper));
}
} else {
objectMapper = getObjectMapper(objectMapper, objectMapperInstance);
clientBuilder = clientBuilder.register(new ClientJacksonMessageBodyWriter(objectMapper));
ObjectMapper newObjectMapper = new ObjectMapper();
clientBuilder = clientBuilder
.registerMessageBodyReader(new JacksonBasicMessageBodyReader(newObjectMapper), Object.class,
HANDLED_MEDIA_TYPES, true,
PROVIDER_PRIORITY)
.registerMessageBodyWriter(new ClientJacksonMessageBodyWriter(newObjectMapper), Object.class,
HANDLED_MEDIA_TYPES, true, PROVIDER_PRIORITY);
}

}
return clientBuilder;
}

// the idea is to only reuse the ObjectMapper if no known customizations would break Keycloak
// TODO: in the future we could also look into checking the ObjectMapper bean itself to see how it has been configured
private boolean canReuseObjectMapper(InstanceHandle<ObjectMapper> objectMapperInstance, ArcContainer arcContainer) {
if (objectMapperInstance.isAvailable() && !objectMapperInstance.getBean().isDefaultBean()) {
// in this case a user provided a completely custom ObjectMapper, so we can't use it
return false;
}

Instance<ObjectMapperCustomizer> customizers = arcContainer.beanManager().createInstance()
.select(ObjectMapperCustomizer.class);
if (!customizers.isUnsatisfied()) {
// ObjectMapperCustomizer can make arbitrary changes, so in order to be safe we won't allow reuse
return false;
}
// if any Jackson properties were configured, disallow reuse - this is done in order to provide forward compatibility with new Jackson configuration options
for (String propertyName : ConfigProvider.getConfig().getPropertyNames()) {
if (propertyName.startsWith("io.quarkus.jackson")) {
return false;
}
}
return true;
}

// the whole idea here is to reuse the ObjectMapper instance
private ObjectMapper getObjectMapper(ObjectMapper value,
InstanceHandle<ObjectMapper> objectMapperInstance) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.Set;

import javax.ws.rs.Priorities;
import javax.ws.rs.RuntimeType;
import javax.ws.rs.core.MediaType;

import org.jboss.jandex.AnnotationInstance;
Expand Down Expand Up @@ -137,20 +138,22 @@ void additionalProviders(ContextResolversBuildItem contextResolversBuildItem,
new MessageBodyReaderBuildItem.Builder(ServerJacksonMessageBodyReader.class.getName(),
Object.class.getName())
.setMediaTypeStrings(HANDLED_MEDIA_TYPES)
.setBuiltin(true).build());
.setBuiltin(true).setRuntimeType(RuntimeType.SERVER).build());
additionalReaders
.produce(
new MessageBodyReaderBuildItem.Builder(VertxJsonArrayMessageBodyReader.class.getName(),
JsonArray.class.getName())
.setMediaTypeStrings(HANDLED_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.SERVER)
.build());
additionalReaders
.produce(
new MessageBodyReaderBuildItem.Builder(VertxJsonObjectMessageBodyReader.class.getName(),
JsonObject.class.getName())
.setMediaTypeStrings(HANDLED_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.SERVER)
.build());
additionalWriters
.produce(
Expand All @@ -160,20 +163,23 @@ void additionalProviders(ContextResolversBuildItem contextResolversBuildItem,
Object.class.getName())
.setMediaTypeStrings(HANDLED_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.SERVER)
.build());
additionalWriters
.produce(
new MessageBodyWriterBuildItem.Builder(VertxJsonArrayMessageBodyWriter.class.getName(),
JsonArray.class.getName())
.setMediaTypeStrings(HANDLED_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.SERVER)
.build());
additionalWriters
.produce(
new MessageBodyWriterBuildItem.Builder(VertxJsonObjectMessageBodyWriter.class.getName(),
JsonObject.class.getName())
.setMediaTypeStrings(HANDLED_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.SERVER)
.build());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
import org.apache.http.HttpStatus;
import org.jboss.resteasy.reactive.client.impl.MultiInvoker;
import org.jboss.resteasy.reactive.common.util.RestMediaType;
import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.smallrye.mutiny.Multi;
Expand Down Expand Up @@ -137,7 +140,8 @@ public void testStreamJsonMultiFromMulti() {
}

private void testJsonMulti(String path) {
Client client = ClientBuilder.newBuilder().build();
Client client = ClientBuilder.newBuilder().register(new JacksonBasicMessageBodyReader(new ObjectMapper())).build();
;
WebTarget target = client.target(uri.toString() + path);
Multi<Message> multi = target.request().rx(MultiInvoker.class).get(Message.class);
List<Message> list = multi.collect().asList().await().atMost(Duration.ofSeconds(30));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.util.Collections;
import java.util.List;

import javax.ws.rs.RuntimeType;
import javax.ws.rs.core.MediaType;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
Expand Down Expand Up @@ -42,9 +43,6 @@ void additionalProviders(
BuildProducer<AdditionalBeanBuildItem> additionalBean,
BuildProducer<MessageBodyReaderBuildItem> additionalReaders,
BuildProducer<MessageBodyWriterBuildItem> additionalWriters) {
if (!jacksonProviderDefined.isEmpty()) {
return;
}
// make these beans to they can get instantiated with the Quarkus CDI configured Jsonb object
additionalBean.produce(AdditionalBeanBuildItem.builder()
.addBeanClass(ClientJacksonMessageBodyReader.class.getName())
Expand All @@ -57,41 +55,47 @@ void additionalProviders(
Object.class.getName())
.setMediaTypeStrings(HANDLED_READ_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.CLIENT)
.build());
additionalReaders
.produce(
new MessageBodyReaderBuildItem.Builder(VertxJsonArrayBasicMessageBodyReader.class.getName(),
JsonArray.class.getName())
.setMediaTypeStrings(HANDLED_READ_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.CLIENT)
.build());
additionalReaders
.produce(
new MessageBodyReaderBuildItem.Builder(VertxJsonObjectBasicMessageBodyReader.class.getName(),
JsonObject.class.getName())
.setMediaTypeStrings(HANDLED_READ_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.CLIENT)
.build());
additionalWriters
.produce(
new MessageBodyWriterBuildItem.Builder(ClientJacksonMessageBodyWriter.class.getName(),
Object.class.getName())
.setMediaTypeStrings(HANDLED_WRITE_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.CLIENT)
.build());
additionalWriters
.produce(
new MessageBodyWriterBuildItem.Builder(VertxJsonArrayBasicMessageBodyWriter.class.getName(),
JsonArray.class.getName())
.setMediaTypeStrings(HANDLED_WRITE_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.CLIENT)
.build());
additionalWriters
.produce(
new MessageBodyWriterBuildItem.Builder(VertxJsonObjectBasicMessageBodyWriter.class.getName(),
JsonObject.class.getName())
.setMediaTypeStrings(HANDLED_WRITE_MEDIA_TYPES)
.setBuiltin(true)
.setRuntimeType(RuntimeType.CLIENT)
.build());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.rest.client.RestClientBuilder;
import org.jboss.resteasy.reactive.server.jackson.JacksonBasicMessageBodyReader;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.fasterxml.jackson.databind.ObjectMapper;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.smallrye.mutiny.Multi;
Expand All @@ -34,7 +37,7 @@ public class MultiSseTest {
@Test
void shouldConsume() {
var resultList = new CopyOnWriteArrayList<>();
RestClientBuilder.newBuilder().baseUri(uri).build(SseClient.class)
createClient()
.get()
.subscribe().with(resultList::add);
await().atMost(5, TimeUnit.SECONDS)
Expand All @@ -45,7 +48,7 @@ void shouldConsume() {
@Test
void shouldConsumeJsonEntity() {
var resultList = new CopyOnWriteArrayList<>();
RestClientBuilder.newBuilder().baseUri(uri).build(SseClient.class)
createClient()
.getJson()
.subscribe().with(resultList::add);
await().atMost(5, TimeUnit.SECONDS)
Expand All @@ -56,7 +59,7 @@ void shouldConsumeJsonEntity() {
@Test
void shouldConsumeAsParametrizedType() {
var resultList = new CopyOnWriteArrayList<>();
RestClientBuilder.newBuilder().baseUri(uri).build(SseClient.class)
createClient()
.getJsonAsMap()
.subscribe().with(resultList::add);
await().atMost(5, TimeUnit.SECONDS)
Expand All @@ -68,7 +71,7 @@ void shouldConsumeAsParametrizedType() {
@Test
void shouldSendPayloadAndConsume() {
var resultList = new CopyOnWriteArrayList<>();
RestClientBuilder.newBuilder().baseUri(uri).build(SseClient.class)
createClient()
.post("test")
.subscribe().with(resultList::add);
await().atMost(5, TimeUnit.SECONDS)
Expand All @@ -79,7 +82,7 @@ void shouldSendPayloadAndConsume() {
@Test
void shouldSendPayloadAndConsumeAsParametrizedType() {
var resultList = new CopyOnWriteArrayList<>();
RestClientBuilder.newBuilder().baseUri(uri).build(SseClient.class)
createClient()
.postAndReadAsMap("test")
.subscribe().with(resultList::add);
await().atMost(5, TimeUnit.SECONDS)
Expand All @@ -90,6 +93,11 @@ void shouldSendPayloadAndConsumeAsParametrizedType() {
Map.of("name", "foo", "value", "test")));
}

private SseClient createClient() {
return RestClientBuilder.newBuilder().baseUri(uri).register(new JacksonBasicMessageBodyReader(new ObjectMapper()))
.build(SseClient.class);
}

@Path("/sse")
public interface SseClient {
@GET
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ public class BasicRestClientTest {
static final QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(HelloClient.class, HelloResource.class, TestBean.class, HelloClient2.class,
HelloNonSimpleClient.class))
HelloNonSimpleClient.class, TestJacksonBasicMessageBodyReader.class,
TestJacksonBasicMessageBodyWriter.class))
.withConfigurationResource("basic-test-application.properties");

@Inject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import io.smallrye.mutiny.Uni;
Expand All @@ -20,6 +21,8 @@
// https://github.com/quarkusio/quarkus/issues/21375
//
@RegisterRestClient(configKey = "hello2")
@RegisterProvider(TestJacksonBasicMessageBodyReader.class)
@RegisterProvider(TestJacksonBasicMessageBodyWriter.class)
public interface HelloNonSimpleClient {
@POST
@Consumes(MediaType.TEXT_PLAIN)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.junit.jupiter.api.Test;
Expand All @@ -23,7 +24,7 @@ public class MediaTypeSuffixTest {
@RegisterExtension
static final QuarkusUnitTest TEST = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(HelloResource.class, Client.class))
.addClasses(HelloResource.class, Client.class, TestJacksonBasicMessageBodyReader.class))
.withConfigurationResource("media-type-suffix-application.properties");

@Test
Expand All @@ -37,6 +38,7 @@ public void test() {

@RegisterRestClient(configKey = "test")
@Path("/hello")
@RegisterProvider(TestJacksonBasicMessageBodyReader.class)
public interface Client {

@GET
Expand Down
Loading

0 comments on commit aa152df

Please sign in to comment.