diff --git a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java index 5d44d077b7d74e..c3702ea7579faa 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java +++ b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java @@ -1,9 +1,11 @@ package io.quarkus.keycloak.admin.client.reactive.runtime; +import javax.enterprise.inject.Instance; import javax.net.ssl.SSLContext; import javax.ws.rs.client.Client; import javax.ws.rs.client.WebTarget; +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; @@ -14,6 +16,7 @@ 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 { @@ -32,29 +35,61 @@ private ClientBuilderImpl registerJacksonProviders(ClientBuilderImpl clientBuild throw new IllegalStateException(this.getClass().getName() + " should only be used in a Quarkus application"); } else { InstanceHandle objectMapperInstance = arcContainer.instance(ObjectMapper.class); - ObjectMapper objectMapper = null; + boolean canReuseObjectMapper = canReuseObjectMapper(objectMapperInstance, arcContainer); + if (canReuseObjectMapper) { - InstanceHandle 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 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 writerInstance = arcContainer - .instance(ClientJacksonMessageBodyWriter.class); - if (writerInstance.isAvailable()) { - clientBuilder = clientBuilder.register(writerInstance.get()); + InstanceHandle 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.register(new JacksonBasicMessageBodyReader(newObjectMapper)) + .register(new ClientJacksonMessageBodyWriter(newObjectMapper)); } + } 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 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 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 objectMapperInstance) { diff --git a/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/AdminClientResource.java b/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/AdminClientResource.java index 970a88280b7473..fa99da14b2a70d 100644 --- a/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/AdminClientResource.java +++ b/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/AdminClientResource.java @@ -29,22 +29,22 @@ public class AdminClientResource { Keycloak keycloak; @GET - @Produces(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) @Path("realm") - public RealmRepresentation getRealm() { - return keycloak.realm("quarkus").toRepresentation(); + public String getRealm() { + return keycloak.realm("quarkus").toRepresentation().getRealm(); } @GET - @Produces(MediaType.APPLICATION_JSON) + @Produces(MediaType.TEXT_PLAIN) @Path("newrealm") - public RealmRepresentation createRealm() { + public String createRealm() { RealmRepresentation newRealm = createRealm("quarkus2"); newRealm.getClients().add(createClient("quarkus-app2")); newRealm.getUsers().add(createUser("alice", "user")); keycloak.realms().create(newRealm); - return keycloak.realm("quarkus2").toRepresentation(); + return keycloak.realm("quarkus2").toRepresentation().getRealm(); } private static RealmRepresentation createRealm(String name) { diff --git a/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/SnakeCaseObjectMapperCustomizer.java b/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/SnakeCaseObjectMapperCustomizer.java new file mode 100644 index 00000000000000..56f297f6d39ef7 --- /dev/null +++ b/integration-tests/keycloak-authorization/src/main/java/io/quarkus/it/keycloak/SnakeCaseObjectMapperCustomizer.java @@ -0,0 +1,21 @@ +package io.quarkus.it.keycloak; + +import javax.inject.Singleton; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategies; + +import io.quarkus.jackson.ObjectMapperCustomizer; + +/** + * This class is used to alter the global ObjectMapper quarkus uses. + * We ensure that KeyCloak Admin Client continues to work despite this. + */ +@Singleton +public class SnakeCaseObjectMapperCustomizer implements ObjectMapperCustomizer { + + @Override + public void customize(ObjectMapper mapper) { + mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE); + } +} diff --git a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/AdminClientTestCase.java b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/AdminClientTestCase.java index 3762f3af8c2463..ad1b1b41cad204 100644 --- a/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/AdminClientTestCase.java +++ b/integration-tests/keycloak-authorization/src/test/java/io/quarkus/it/keycloak/AdminClientTestCase.java @@ -1,10 +1,9 @@ package io.quarkus.it.keycloak; -import static io.restassured.RestAssured.given; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static io.restassured.RestAssured.when; +import static org.hamcrest.Matchers.equalTo; import org.junit.jupiter.api.Test; -import org.keycloak.representations.idm.RealmRepresentation; import io.quarkus.test.junit.QuarkusTest; @@ -13,15 +12,17 @@ public class AdminClientTestCase { @Test public void testGetExistingRealm() { - RealmRepresentation realm = given() - .when().get("/admin-client/realm").as(RealmRepresentation.class); - assertEquals("quarkus", realm.getRealm()); + when().get("/admin-client/realm") + .then() + .statusCode(200) + .body(equalTo("quarkus")); } @Test public void testGetNewRealm() { - RealmRepresentation realm = given() - .when().get("/admin-client/newrealm").as(RealmRepresentation.class); - assertEquals("quarkus2", realm.getRealm()); + when().get("/admin-client/newrealm") + .then() + .statusCode(200) + .body(equalTo("quarkus2")); } }