Skip to content
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

Use custom ObjectMapper for Keycloak admin client if necessary #30529

Merged
merged 3 commits into from
Jan 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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