diff --git a/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java b/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java index 0690fbcdff3d7..aa08f7edb66c5 100644 --- a/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java +++ b/extensions/keycloak-admin-rest-client/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java @@ -6,13 +6,11 @@ import javax.net.ssl.SSLContext; -import jakarta.enterprise.inject.Instance; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.WebTarget; import jakarta.ws.rs.core.MediaType; -import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.resteasy.reactive.client.TlsConfig; import org.jboss.resteasy.reactive.client.api.ClientLogger; import org.jboss.resteasy.reactive.client.impl.ClientBuilderImpl; @@ -27,7 +25,6 @@ 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; import io.quarkus.tls.TlsConfiguration; import io.vertx.core.net.KeyCertOptions; @@ -71,38 +68,13 @@ private ClientBuilderImpl registerJacksonProviders(ClientBuilderImpl clientBuild if (arcContainer == null) { throw new IllegalStateException(this.getClass().getName() + " should only be used in a Quarkus application"); } else { - InstanceHandle objectMapperInstance = arcContainer.instance(ObjectMapper.class); - boolean canReuseObjectMapper = canReuseObjectMapper(objectMapperInstance, arcContainer); - if (canReuseObjectMapper) { - - 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()); - } else { - objectMapper = getObjectMapper(objectMapper, objectMapperInstance); - clientBuilder = clientBuilder.register(new ClientJacksonMessageBodyWriter(objectMapper)); - } - } else { - ObjectMapper newObjectMapper = newKeycloakAdminClientObjectMapper(); - clientBuilder = clientBuilder - .registerMessageBodyReader(new JacksonBasicMessageBodyReader(newObjectMapper), Object.class, - HANDLED_MEDIA_TYPES, true, - READER_PROVIDER_PRIORITY) - .registerMessageBodyWriter(new ClientJacksonMessageBodyWriter(newObjectMapper), Object.class, - HANDLED_MEDIA_TYPES, true, WRITER_PROVIDER_PRIORITY); - } + ObjectMapper newObjectMapper = newKeycloakAdminClientObjectMapper(); + clientBuilder = clientBuilder + .registerMessageBodyReader(new JacksonBasicMessageBodyReader(newObjectMapper), Object.class, + HANDLED_MEDIA_TYPES, true, + READER_PROVIDER_PRIORITY) + .registerMessageBodyWriter(new ClientJacksonMessageBodyWriter(newObjectMapper), Object.class, + HANDLED_MEDIA_TYPES, true, WRITER_PROVIDER_PRIORITY); InstanceHandle clientLogger = arcContainer.instance(ClientLogger.class); if (clientLogger.isAvailable()) { clientBuilder.clientLogger(clientLogger.get()); @@ -111,38 +83,6 @@ private ClientBuilderImpl registerJacksonProviders(ClientBuilderImpl clientBuild 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("quarkus.jackson")) { - return false; - } - } - return true; - } - - // the whole idea here is to reuse the ObjectMapper instance - private ObjectMapper getObjectMapper(ObjectMapper value, - InstanceHandle objectMapperInstance) { - if (value == null) { - return objectMapperInstance.isAvailable() ? objectMapperInstance.get() : newKeycloakAdminClientObjectMapper(); - } - return value; - } - // creates new ObjectMapper compatible with Keycloak Admin Client private ObjectMapper newKeycloakAdminClientObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); diff --git a/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java b/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java index 1a5726456b56c..981e6dc89cd9a 100644 --- a/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java +++ b/extensions/keycloak-admin-resteasy-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java @@ -14,8 +14,6 @@ import io.quarkus.arc.BeanDestroyer; import io.quarkus.arc.deployment.SyntheticBeanBuildItem; -import io.quarkus.deployment.Capabilities; -import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; @@ -58,11 +56,8 @@ void avoidRuntimeInitIssueInClientBuilderWrapper(ResteasyKeycloakAdminClientReco @Record(ExecutionTime.RUNTIME_INIT) @Produce(ServiceStartBuildItem.class) @BuildStep - public void integrate(ResteasyKeycloakAdminClientRecorder recorder, Capabilities capabilities, - TlsRegistryBuildItem tlsRegistryBuildItem) { - boolean areJSONBProvidersPresent = capabilities.isPresent(Capability.RESTEASY_JSON_JSONB) - || capabilities.isPresent(Capability.RESTEASY_JSON_JSONB_CLIENT); - recorder.setClientProvider(areJSONBProvidersPresent, tlsRegistryBuildItem.registry()); + public void integrate(ResteasyKeycloakAdminClientRecorder recorder, TlsRegistryBuildItem tlsRegistryBuildItem) { + recorder.setClientProvider(tlsRegistryBuildItem.registry()); } @Record(ExecutionTime.RUNTIME_INIT) diff --git a/extensions/keycloak-admin-resteasy-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-resteasy-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java index 069f72648e1b3..6cfc88870ff58 100644 --- a/extensions/keycloak-admin-resteasy-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-resteasy-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java @@ -18,6 +18,10 @@ import org.keycloak.admin.client.KeycloakBuilder; import org.keycloak.admin.client.spi.ResteasyClientProvider; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientConfig; import io.quarkus.resteasy.common.runtime.jackson.QuarkusJacksonSerializer; import io.quarkus.runtime.RuntimeValue; @@ -66,7 +70,7 @@ public Keycloak get() { }; } - public void setClientProvider(boolean areJSONBProvidersPresent, Supplier registrySupplier) { + public void setClientProvider(Supplier registrySupplier) { var registry = registrySupplier.get(); var namedTlsConfig = TlsConfiguration.from(registry, keycloakAdminClientConfigRuntimeValue.getValue().tlsConfigurationName()).orElse(null); @@ -100,12 +104,10 @@ public Client newRestEasyClient(Object customJacksonProvider, SSLContext sslCont } } - // point here is to use default Quarkus providers rather than org.keycloak.admin.client.JacksonProvider - // as it doesn't work properly in native mode - if (areJSONBProvidersPresent) { - // when both Jackson and JSONB providers are present, we need to ensure Jackson is used - builder.register(new AppJsonQuarkusJacksonSerializer(), 100); - } + // this ensures we don't customize managed (shared) ObjectMapper available in the CDI container + // and that we use QuarkusJacksonSerializer that works in native mode + builder.register(new AppJsonQuarkusJacksonSerializer(), 100); + return builder.build(); } @@ -126,6 +128,24 @@ public void avoidRuntimeInitIssueInClientBuilderWrapper() { // makes media type more specific which ensures that it will be used first @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) - static class AppJsonQuarkusJacksonSerializer extends QuarkusJacksonSerializer { + static final class AppJsonQuarkusJacksonSerializer extends QuarkusJacksonSerializer { + + private final ObjectMapper objectMapper; + + private AppJsonQuarkusJacksonSerializer() { + this.objectMapper = new ObjectMapper(); + // Same like JSONSerialization class. Makes it possible to use admin-client against older + // versions of Keycloak server where the properties on representations might be different + this.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + // The client must work with the newer versions of Keycloak server, which might contain the JSON fields + // not yet known by the client. So unknown fields will be ignored. + this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + @Override + public ObjectMapper locateMapper(Class type, MediaType mediaType) { + return objectMapper; + } + } }