diff --git a/spring-web/src/main/java/org/springframework/http/converter/DefaultHttpMessageConverters.java b/spring-web/src/main/java/org/springframework/http/converter/DefaultHttpMessageConverters.java new file mode 100644 index 0000000000..bbf4d331d0 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/DefaultHttpMessageConverters.java @@ -0,0 +1,604 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import org.jspecify.annotations.Nullable; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter; +import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter; +import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; +import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; +import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; +import org.springframework.http.converter.json.GsonHttpMessageConverter; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; +import org.springframework.http.converter.json.JsonbHttpMessageConverter; +import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter; +import org.springframework.http.converter.smile.JacksonSmileHttpMessageConverter; +import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; +import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; +import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.http.converter.yaml.JacksonYamlHttpMessageConverter; +import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Default implementation for {@link HttpMessageConverters}. + * + * @author Brian Clozel + */ +@SuppressWarnings("removal") +class DefaultHttpMessageConverters implements HttpMessageConverters { + + private final List> clientMessageConverters; + + private final List> serverMessageConverters; + + DefaultHttpMessageConverters(List> clientMessageConverters, List> serverMessageConverters) { + this.clientMessageConverters = clientMessageConverters; + this.serverMessageConverters = serverMessageConverters; + } + + @Override + public Iterable> forClient() { + return this.clientMessageConverters; + } + + @Override + public Iterable> forServer() { + return this.serverMessageConverters; + } + + static class DefaultBuilder implements HttpMessageConverters.Builder { + + private final DefaultMessageConverterConfigurer commonMessageConverters; + + private final DefaultClientMessageConverterConfigurer clientMessageConverterConfigurer; + + private final DefaultServerMessageConverterConfigurer serverMessageConverterConfigurer; + + + DefaultBuilder(boolean registerDefaults) { + this(registerDefaults, DefaultHttpMessageConverters.class.getClassLoader()); + } + + DefaultBuilder(boolean registerDefaults, ClassLoader classLoader) { + this.commonMessageConverters = new DefaultMessageConverterConfigurer(); + this.clientMessageConverterConfigurer = new DefaultClientMessageConverterConfigurer(this.commonMessageConverters); + this.serverMessageConverterConfigurer = new DefaultServerMessageConverterConfigurer(this.commonMessageConverters); + if (registerDefaults) { + this.commonMessageConverters.registerDefaults(classLoader); + this.clientMessageConverterConfigurer.registerDefaults(classLoader); + this.serverMessageConverterConfigurer.registerDefaults(classLoader); + } + } + + @Override + public Builder configureClient(Consumer consumer) { + consumer.accept(this.clientMessageConverterConfigurer); + return this; + } + + @Override + public Builder configureServer(Consumer consumer) { + consumer.accept(this.serverMessageConverterConfigurer); + return this; + } + + @Override + public Builder stringMessageConverter(HttpMessageConverter stringMessageConverter) { + this.commonMessageConverters.setStringMessageConverter(stringMessageConverter); + return this; + } + + @Override + public DefaultBuilder jsonMessageConverter(HttpMessageConverter jsonMessageConverter) { + this.commonMessageConverters.setJsonMessageConverter(jsonMessageConverter); + return this; + } + + @Override + public DefaultBuilder xmlMessageConverter(HttpMessageConverter xmlMessageConverter) { + this.commonMessageConverters.setXmlMessageConverter(xmlMessageConverter); + return this; + } + + @Override + public DefaultBuilder smileMessageConverter(HttpMessageConverter smileMessageConverter) { + this.commonMessageConverters.setSmileMessageConverter(smileMessageConverter); + return this; + } + + @Override + public Builder cborMessageConverter(HttpMessageConverter cborMessageConverter) { + this.commonMessageConverters.setCborMessageConverter(cborMessageConverter); + return this; + } + + @Override + public Builder yamlMessageConverter(HttpMessageConverter yamlMessageConverter) { + this.commonMessageConverters.setYamlMessageConverter(yamlMessageConverter); + return this; + } + + @Override + public DefaultBuilder additionalMessageConverter(HttpMessageConverter customConverter) { + Assert.notNull(customConverter, "'customConverter' must not be null"); + this.commonMessageConverters.additionalMessageConverters.add(customConverter); + return this; + } + + @Override + public DefaultHttpMessageConverters build() { + return new DefaultHttpMessageConverters(this.clientMessageConverterConfigurer.getMessageConverters(), + this.serverMessageConverterConfigurer.getMessageConverters()); + } + + } + + static class DefaultMessageConverterConfigurer { + + private final @Nullable DefaultMessageConverterConfigurer inheritedMessageConverters; + + private @Nullable ByteArrayHttpMessageConverter byteArrayMessageConverter; + + private @Nullable HttpMessageConverter stringMessageConverter; + + List> resourceMessageConverters = Collections.emptyList(); + + private @Nullable HttpMessageConverter jsonMessageConverter; + + private @Nullable HttpMessageConverter xmlMessageConverter; + + private @Nullable HttpMessageConverter smileMessageConverter; + + private @Nullable HttpMessageConverter cborMessageConverter; + + private @Nullable HttpMessageConverter yamlMessageConverter; + + private final List> additionalMessageConverters = new ArrayList<>(); + + DefaultMessageConverterConfigurer() { + this(null); + } + + DefaultMessageConverterConfigurer(@Nullable DefaultMessageConverterConfigurer inheritedMessageConverters) { + this.inheritedMessageConverters = inheritedMessageConverters; + } + + void setStringMessageConverter(HttpMessageConverter stringMessageConverter) { + Assert.isTrue(stringMessageConverter.getSupportedMediaTypes().contains(MediaType.TEXT_PLAIN), + "stringMessageConverter should support 'text/plain'"); + this.stringMessageConverter = stringMessageConverter; + } + + void setJsonMessageConverter(HttpMessageConverter jsonMessageConverter) { + Assert.isTrue(jsonMessageConverter.getSupportedMediaTypes().contains(MediaType.APPLICATION_JSON), + "jsonMessageConverter should support 'application/json'"); + this.jsonMessageConverter = jsonMessageConverter; + } + + void setXmlMessageConverter(HttpMessageConverter xmlMessageConverter) { + Assert.isTrue(xmlMessageConverter.getSupportedMediaTypes().contains(MediaType.TEXT_XML), + "xmlMessageConverter should support 'text/xml'"); + this.xmlMessageConverter = xmlMessageConverter; + } + + void setSmileMessageConverter(HttpMessageConverter smileMessageConverter) { + Assert.isTrue(smileMessageConverter.getSupportedMediaTypes().contains(new MediaType("application", "x-jackson-smile")), + "smileMessageConverter should support 'application/x-jackson-smile'"); + this.smileMessageConverter = smileMessageConverter; + } + + void setCborMessageConverter(HttpMessageConverter cborMessageConverter) { + Assert.isTrue(cborMessageConverter.getSupportedMediaTypes().contains(MediaType.APPLICATION_CBOR), + "cborMessageConverter should support 'application/cbor'"); + this.cborMessageConverter = cborMessageConverter; + } + + void setYamlMessageConverter(HttpMessageConverter yamlMessageConverter) { + Assert.isTrue(yamlMessageConverter.getSupportedMediaTypes().contains(MediaType.APPLICATION_YAML), + "yamlMessageConverter should support 'application/yaml'"); + this.yamlMessageConverter = yamlMessageConverter; + } + + List> getBaseConverters() { + List> converters = new ArrayList<>(); + if (this.byteArrayMessageConverter != null) { + converters.add(this.byteArrayMessageConverter); + } + else if (this.inheritedMessageConverters != null && + this.inheritedMessageConverters.byteArrayMessageConverter != null) { + converters.add(this.inheritedMessageConverters.byteArrayMessageConverter); + } + if (this.stringMessageConverter != null) { + converters.add(this.stringMessageConverter); + } + else if (this.inheritedMessageConverters != null && + this.inheritedMessageConverters.stringMessageConverter != null) { + converters.add(this.inheritedMessageConverters.stringMessageConverter); + } + return converters; + } + + List> getCoreConverters() { + List> converters = new ArrayList<>(); + if (this.jsonMessageConverter != null) { + converters.add(this.jsonMessageConverter); + } + else if (this.inheritedMessageConverters != null && + this.inheritedMessageConverters.jsonMessageConverter != null) { + converters.add(this.inheritedMessageConverters.jsonMessageConverter); + } + if (this.smileMessageConverter != null) { + converters.add(this.smileMessageConverter); + } + else if (this.inheritedMessageConverters != null && + this.inheritedMessageConverters.smileMessageConverter != null) { + converters.add(this.inheritedMessageConverters.smileMessageConverter); + } + if (this.cborMessageConverter!= null) { + converters.add(this.cborMessageConverter); + } + else if (this.inheritedMessageConverters != null && + this.inheritedMessageConverters.cborMessageConverter != null) { + converters.add(this.inheritedMessageConverters.cborMessageConverter); + } + if (this.yamlMessageConverter!= null) { + converters.add(this.yamlMessageConverter); + } + else if (this.inheritedMessageConverters != null && + this.inheritedMessageConverters.yamlMessageConverter != null) { + converters.add(this.inheritedMessageConverters.yamlMessageConverter); + } + if (this.xmlMessageConverter!= null) { + converters.add(this.xmlMessageConverter); + } + else if (this.inheritedMessageConverters != null && + this.inheritedMessageConverters.xmlMessageConverter != null) { + converters.add(this.inheritedMessageConverters.xmlMessageConverter); + } + return converters; + } + + List> getCustomConverters() { + List> result = new ArrayList<>(this.additionalMessageConverters); + if (this.inheritedMessageConverters != null) { + result.addAll(this.inheritedMessageConverters.additionalMessageConverters); + } + return result; + } + + void registerDefaults(ClassLoader classLoader) { + this.byteArrayMessageConverter = new ByteArrayHttpMessageConverter(); + this.stringMessageConverter = new StringHttpMessageConverter(); + + if (isJacksonPresent(classLoader)) { + this.jsonMessageConverter = new JacksonJsonHttpMessageConverter(); + } + else if (isJackson2Present(classLoader)) { + this.jsonMessageConverter = new MappingJackson2HttpMessageConverter(); + } + else if (isGsonPresent(classLoader)) { + this.jsonMessageConverter = new GsonHttpMessageConverter(); + } + else if (isJsonbPresent(classLoader)) { + this.jsonMessageConverter = new JsonbHttpMessageConverter(); + } + else if (isKotlinSerializationJsonPresent(classLoader)) { + this.jsonMessageConverter = new KotlinSerializationJsonHttpMessageConverter(); + } + + if (isJacksonXmlPresent(classLoader)) { + this.xmlMessageConverter = new JacksonXmlHttpMessageConverter(); + } + else if (isJackson2XmlPresent(classLoader)) { + this.xmlMessageConverter = new MappingJackson2XmlHttpMessageConverter(); + } + else if (isJaxb2Present(classLoader)) { + this.xmlMessageConverter = new Jaxb2RootElementHttpMessageConverter(); + } + + if (isJacksonSmilePresent(classLoader)) { + this.smileMessageConverter = new JacksonSmileHttpMessageConverter(); + } + else if (isJackson2SmilePresent(classLoader)) { + this.smileMessageConverter = new MappingJackson2SmileHttpMessageConverter(); + } + if (isJacksonCborPresent(classLoader)) { + this.cborMessageConverter = new JacksonCborHttpMessageConverter(); + } + else if (isJackson2CborPresent(classLoader)) { + this.cborMessageConverter = new MappingJackson2CborHttpMessageConverter(); + } + else if (isKotlinSerializationCborPresent(classLoader)) { + this.cborMessageConverter = new KotlinSerializationCborHttpMessageConverter(); + } + + if (isJacksonYamlPresent(classLoader)) { + this.yamlMessageConverter = new JacksonYamlHttpMessageConverter(); + } + else if (isJackson2YamlPresent(classLoader)) { + this.yamlMessageConverter = new MappingJackson2YamlHttpMessageConverter(); + } + + if (isKotlinSerializationProtobufPresent(classLoader)) { + this.additionalMessageConverters.add(new KotlinSerializationProtobufHttpMessageConverter()); + } + + if (isRomePresent(classLoader)) { + this.additionalMessageConverters.add(new AtomFeedHttpMessageConverter()); + this.additionalMessageConverters.add(new RssChannelHttpMessageConverter()); + } + } + + private static boolean isRomePresent(ClassLoader classLoader) { + return ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader); + } + + private static boolean isJacksonPresent(ClassLoader classLoader) { + return ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", classLoader); + } + + private static boolean isJackson2Present(ClassLoader classLoader) { + return ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && + ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader); + } + + private static boolean isGsonPresent(ClassLoader classLoader) { + return ClassUtils.isPresent("com.google.gson.Gson", classLoader); + } + + private static boolean isJsonbPresent(ClassLoader classLoader) { + return ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); + } + + private static boolean isKotlinSerializationJsonPresent(ClassLoader classLoader) { + return ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); + } + + private static boolean isJacksonSmilePresent(ClassLoader classLoader) { + return isJacksonPresent(classLoader) && ClassUtils.isPresent("tools.jackson.dataformat.smile.SmileMapper", classLoader); + } + + private static boolean isJackson2SmilePresent(ClassLoader classLoader) { + return ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); + } + + private static boolean isJaxb2Present(ClassLoader classLoader) { + return ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader); + } + + private static boolean isJacksonXmlPresent(ClassLoader classLoader) { + return isJacksonPresent(classLoader) && ClassUtils.isPresent("tools.jackson.dataformat.xml.XmlMapper", classLoader); + } + + private static boolean isJackson2XmlPresent(ClassLoader classLoader) { + return ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); + } + + private static boolean isJacksonCborPresent(ClassLoader classLoader) { + return isJacksonPresent(classLoader) && ClassUtils.isPresent("tools.jackson.dataformat.cbor.CBORMapper", classLoader); + } + + private static boolean isJackson2CborPresent(ClassLoader classLoader) { + return ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader); + } + + private static boolean isJacksonYamlPresent(ClassLoader classLoader) { + return isJacksonPresent(classLoader) && ClassUtils.isPresent("tools.jackson.dataformat.yaml.YAMLMapper", classLoader); + } + + private static boolean isJackson2YamlPresent(ClassLoader classLoader) { + return ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader); + } + + private static boolean isKotlinSerializationCborPresent(ClassLoader classLoader) { + return ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); + } + + private static boolean isKotlinSerializationProtobufPresent(ClassLoader classLoader) { + return ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader); + } + + } + + static class DefaultClientMessageConverterConfigurer extends DefaultMessageConverterConfigurer implements ClientMessageConverterConfigurer { + + private @Nullable Consumer> configurer; + + private final DefaultMessageConverterConfigurer clientMessageConverters; + + + public DefaultClientMessageConverterConfigurer(DefaultMessageConverterConfigurer parentMessageConverters) { + this.clientMessageConverters = new DefaultMessageConverterConfigurer(parentMessageConverters); + } + + @Override + public ClientMessageConverterConfigurer stringMessageConverter(HttpMessageConverter stringMessageConverter) { + this.clientMessageConverters.setStringMessageConverter(stringMessageConverter); + return this; + } + + @Override + public ClientMessageConverterConfigurer jsonMessageConverter(HttpMessageConverter jsonMessageConverter) { + this.clientMessageConverters.setJsonMessageConverter(jsonMessageConverter); + return this; + } + + @Override + public ClientMessageConverterConfigurer xmlMessageConverter(HttpMessageConverter xmlMessageConverter) { + this.clientMessageConverters.setXmlMessageConverter(xmlMessageConverter); + return this; + } + + @Override + public ClientMessageConverterConfigurer smileMessageConverter(HttpMessageConverter smileMessageConverter) { + this.clientMessageConverters.setSmileMessageConverter(smileMessageConverter); + return this; + } + + @Override + public ClientMessageConverterConfigurer cborMessageConverter(HttpMessageConverter cborMessageConverter) { + this.clientMessageConverters.setCborMessageConverter(cborMessageConverter); + return this; + } + + @Override + public ClientMessageConverterConfigurer yamlMessageConverter(HttpMessageConverter yamlMessageConverter) { + this.clientMessageConverters.setYamlMessageConverter(yamlMessageConverter); + return this; + } + + @Override + public ClientMessageConverterConfigurer additionalMessageConverter(HttpMessageConverter customConverter) { + Assert.notNull(customConverter, "'customConverter' must not be null"); + this.clientMessageConverters.additionalMessageConverters.add(customConverter); + return this; + } + + @Override + public ClientMessageConverterConfigurer configureClientMessageConverters(Consumer> configurer) { + this.configurer = (this.configurer != null) ? configurer.andThen(this.configurer) : configurer; + return this; + } + + @Override + void registerDefaults(ClassLoader classLoader) { + this.resourceMessageConverters = Collections.singletonList(new ResourceHttpMessageConverter(false)); + } + + List> getMessageConverters() { + List> allConverters = new ArrayList<>(); + List> partConverters = new ArrayList<>(); + + partConverters.addAll(this.clientMessageConverters.getCoreConverters()); + partConverters.addAll(this.clientMessageConverters.getCustomConverters()); + + allConverters.addAll(this.clientMessageConverters.getBaseConverters()); + allConverters.addAll(this.resourceMessageConverters); + if (!partConverters.isEmpty()) { + allConverters.add(new AllEncompassingFormHttpMessageConverter(partConverters)); + } + allConverters.addAll(this.clientMessageConverters.getCoreConverters()); + allConverters.addAll(this.clientMessageConverters.getCustomConverters()); + + if (this.configurer != null) { + allConverters.forEach(this.configurer); + } + return allConverters; + } + } + + static class DefaultServerMessageConverterConfigurer extends DefaultMessageConverterConfigurer implements ServerMessageConverterConfigurer { + + private @Nullable Consumer> configurer; + + private final DefaultMessageConverterConfigurer serverMessageConverters; + + + DefaultServerMessageConverterConfigurer(DefaultMessageConverterConfigurer commonMessageConverters) { + this.serverMessageConverters = new DefaultMessageConverterConfigurer(commonMessageConverters); + } + + @Override + public ServerMessageConverterConfigurer stringMessageConverter(HttpMessageConverter stringMessageConverter) { + this.serverMessageConverters.setStringMessageConverter(stringMessageConverter); + return this; + } + + @Override + public ServerMessageConverterConfigurer jsonMessageConverter(HttpMessageConverter jsonMessageConverter) { + this.serverMessageConverters.setJsonMessageConverter(jsonMessageConverter); + return this; + } + + @Override + public ServerMessageConverterConfigurer xmlMessageConverter(HttpMessageConverter xmlMessageConverter) { + this.serverMessageConverters.setXmlMessageConverter(xmlMessageConverter); + return this; + } + + @Override + public ServerMessageConverterConfigurer smileMessageConverter(HttpMessageConverter smileMessageConverter) { + this.serverMessageConverters.setSmileMessageConverter(smileMessageConverter); + return this; + } + + @Override + public ServerMessageConverterConfigurer cborMessageConverter(HttpMessageConverter cborMessageConverter) { + this.serverMessageConverters.setCborMessageConverter(cborMessageConverter); + return this; + } + + @Override + public ServerMessageConverterConfigurer yamlMessageConverter(HttpMessageConverter yamlMessageConverter) { + this.serverMessageConverters.setYamlMessageConverter(yamlMessageConverter); + return this; + } + + @Override + public ServerMessageConverterConfigurer additionalMessageConverter(HttpMessageConverter customConverter) { + Assert.notNull(customConverter, "'customConverter' must not be null"); + this.serverMessageConverters.additionalMessageConverters.add(customConverter); + return this; + } + + @Override + public ServerMessageConverterConfigurer configureServerMessageConverters(Consumer> configurer) { + this.configurer = (this.configurer != null) ? configurer.andThen(this.configurer) : configurer; + return this; + } + + @Override + void registerDefaults(ClassLoader classLoader) { + this.resourceMessageConverters = Arrays.asList(new ResourceHttpMessageConverter(), new ResourceRegionHttpMessageConverter()); + } + + List> getMessageConverters() { + List> allConverters = new ArrayList<>(); + List> partConverters = new ArrayList<>(); + + partConverters.addAll(this.serverMessageConverters.getCoreConverters()); + partConverters.addAll(this.serverMessageConverters.getCustomConverters()); + + allConverters.addAll(this.serverMessageConverters.getBaseConverters()); + allConverters.addAll(this.resourceMessageConverters); + allConverters.addAll(this.serverMessageConverters.getCoreConverters()); + allConverters.addAll(this.serverMessageConverters.getCustomConverters()); + if (!partConverters.isEmpty()) { + allConverters.add(new AllEncompassingFormHttpMessageConverter(partConverters)); + } + if (this.configurer != null) { + allConverters.forEach(this.configurer); + } + return allConverters; + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverters.java b/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverters.java new file mode 100644 index 0000000000..9d86fee1b5 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/HttpMessageConverters.java @@ -0,0 +1,199 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter; + +import java.util.function.Consumer; + +/** + * Utility for building and configuring an immutable collection of {@link HttpMessageConverter} + * instances for client and server usage. You can {@link #create() create} + * a new empty instance or ask to {@link #withDefaults() register default converters}, + * if available in your classpath. + * + *

This class offers a flexible arrangement for {@link HttpMessageConverters.Builder configuring message converters shared between} + * client and server, or {@link HttpMessageConverters.Builder#configureClient(Consumer) configuring client-specific} + * and {@link HttpMessageConverters.Builder#configureServer(Consumer) server-specific} converters. + * + *

The following HTTP message converters will be detected and registered if available, in order. + * For {@link #forClient() client side converters}: + *

    + *
  1. {@link ByteArrayHttpMessageConverter} + *
  2. {@link StringHttpMessageConverter} with the {@link java.nio.charset.StandardCharsets#ISO_8859_1} charset + *
  3. {@link ResourceHttpMessageConverter}, with resource streaming support disabled + *
  4. a Multipart converter, using all detected and custom converters for part conversion + *
  5. A JSON converter + *
  6. A Smile converter + *
  7. A CBOR converter + *
  8. A YAML converter + *
  9. An XML converter + *
  10. An ProtoBuf converter + *
  11. ATOM and RSS converters + *
  12. All custom message converters configured with the builder + *
+ * + * For {@link #forClient() client side converters}: + *
    + *
  1. {@link ByteArrayHttpMessageConverter} + *
  2. {@link StringHttpMessageConverter} with the {@link java.nio.charset.StandardCharsets#ISO_8859_1} charset + *
  3. {@link ResourceHttpMessageConverter} + *
  4. {@link ResourceRegionHttpMessageConverter} + *
  5. A JSON converter + *
  6. A Smile converter + *
  7. A CBOR converter + *
  8. A YAML converter + *
  9. An XML converter + *
  10. An ProtoBuf converter + *
  11. ATOM and RSS converters + *
  12. All custom message converters configured with the builder + *
  13. a Multipart converter, using all detected and custom converters for part conversion + *
+ * + * @author Brian Clozel + * @since 7.0 + */ +public interface HttpMessageConverters { + + /** + * Return the list of configured message converters, tailored for HTTP client usage. + */ + Iterable> forClient(); + + /** + * Return the list of configured message converters, tailored for HTTP server usage. + */ + Iterable> forServer(); + + /** + * Create a builder instance, without any message converter pre-configured. + */ + static Builder create() { + return new DefaultHttpMessageConverters.DefaultBuilder(false); + } + + /** + * Create a builder instance with default message converters pre-configured. + */ + static Builder withDefaults() { + return new DefaultHttpMessageConverters.DefaultBuilder(true); + } + + + interface MessageConverterConfigurer> { + + /** + * Override the default String {@code HttpMessageConverter} + * with any converter supporting String conversion. + * @param stringMessageConverter the converter instance to use + * @see StringHttpMessageConverter + */ + T stringMessageConverter(HttpMessageConverter stringMessageConverter); + + /** + * Override the default Jackson 3.x JSON {@code HttpMessageConverter} + * with any converter supporting the JSON format. + * @param jsonMessageConverter the converter instance to use + * @see org.springframework.http.converter.json.JacksonJsonHttpMessageConverter + */ + T jsonMessageConverter(HttpMessageConverter jsonMessageConverter); + + /** + * Override the default Jackson 3.x XML {@code HttpMessageConverter} + * with any converter supporting the XML format. + * @param xmlMessageConverter the converter instance to use + * @see org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter + */ + T xmlMessageConverter(HttpMessageConverter xmlMessageConverter); + + /** + * Override the default Jackson 3.x Smile {@code HttpMessageConverter} + * with any converter supporting the Smile format. + * @param smileMessageConverter the converter instance to use + * @see org.springframework.http.converter.smile.JacksonSmileHttpMessageConverter + */ + T smileMessageConverter(HttpMessageConverter smileMessageConverter); + + /** + * Override the default Jackson 3.x CBOR {@code HttpMessageConverter} + * with any converter supporting the CBOR format. + * @param cborMessageConverter the converter instance to use + * @see org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter + */ + T cborMessageConverter(HttpMessageConverter cborMessageConverter); + + /** + * Override the default Jackson 3.x Yaml {@code HttpMessageConverter} + * with any converter supporting the Yaml format. + * @param yamlMessageConverter the converter instance to use + * @see org.springframework.http.converter.yaml.JacksonYamlHttpMessageConverter + */ + T yamlMessageConverter(HttpMessageConverter yamlMessageConverter); + + /** + * Add a custom {@code HttpMessageConverter} to the list of converters. + * @param customConverter the converter instance to add + */ + T additionalMessageConverter(HttpMessageConverter customConverter); + + } + + /** + * Builder for an {@link HttpMessageConverters}. + * This builder manages the configuration of common and client/server-specific message converters. + */ + interface Builder extends MessageConverterConfigurer { + + /** + * Configure client-specific message converters. + * If no opinion is provided here, message converters defined in this builder will be used. + */ + Builder configureClient(Consumer consumer); + + /** + * Configure server-specific message converters. + * If no opinion is provided here, message converters defined in this builder will be used. + */ + Builder configureServer(Consumer consumer); + + /** + * Build and return the {@link HttpMessageConverters} instance configured by this builder. + */ + HttpMessageConverters build(); + } + + interface ClientMessageConverterConfigurer extends MessageConverterConfigurer { + + /** + * Register a consumer to apply to configured converter instances. + * This can be used to configure rather than replace one or more specific converters. + * @param configurer the consumer to apply + */ + ClientMessageConverterConfigurer configureClientMessageConverters(Consumer> configurer); + + } + + interface ServerMessageConverterConfigurer extends MessageConverterConfigurer { + + /** + * Register a consumer to apply to configured converter instances. + * This can be used to configure rather than replace one or more specific converters. + * @param configurer the consumer to apply + */ + ServerMessageConverterConfigurer configureServerMessageConverters(Consumer> configurer); + + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java b/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java new file mode 100644 index 0000000000..8d526bf1cd --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/DefaultHttpMessageConvertersTests.java @@ -0,0 +1,434 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.converter; + + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import com.google.gson.Gson; +import com.rometools.rome.feed.WireFeed; +import jakarta.json.bind.Jsonb; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.dataformat.cbor.CBORMapper; +import tools.jackson.dataformat.smile.SmileMapper; +import tools.jackson.dataformat.xml.XmlMapper; +import tools.jackson.dataformat.yaml.YAMLMapper; + +import org.springframework.core.SmartClassLoader; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.converter.cbor.JacksonCborHttpMessageConverter; +import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter; +import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; +import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; +import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; +import org.springframework.http.converter.json.GsonHttpMessageConverter; +import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; +import org.springframework.http.converter.json.JsonbHttpMessageConverter; +import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter; +import org.springframework.http.converter.smile.JacksonSmileHttpMessageConverter; +import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; +import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; +import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.http.converter.yaml.JacksonYamlHttpMessageConverter; +import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link DefaultHttpMessageConverters}. + */ +@SuppressWarnings("removal") +class DefaultHttpMessageConvertersTests { + + + @ParameterizedTest + @MethodSource("emptyMessageConverters") + void emptyConverters(Iterable> converters) { + assertThat(converters).isEmpty(); + } + + static Stream emptyMessageConverters() { + return Stream.of( + Arguments.of(HttpMessageConverters.create().build().forClient()), + Arguments.of(HttpMessageConverters.create().build().forServer()) + ); + } + + @Test + void clientAndServerConvertersAreShared() { + var converters = HttpMessageConverters.withDefaults().build(); + Set> allConverters = new HashSet<>(); + converters.forClient().forEach(allConverters::add); + converters.forServer().forEach(allConverters::add); + assertThat(allConverters).hasSize(15); + } + + @Test + void failsWhenStringConverterDoesNotSupportMediaType() { + assertThatIllegalArgumentException().isThrownBy(() -> + HttpMessageConverters.create().stringMessageConverter(new CustomHttpMessageConverter()).build()) + .withMessage("stringMessageConverter should support 'text/plain'"); + } + + @Test + void failsWhenJsonConverterDoesNotSupportMediaType() { + assertThatIllegalArgumentException().isThrownBy(() -> + HttpMessageConverters.create().jsonMessageConverter(new CustomHttpMessageConverter()).build()) + .withMessage("jsonMessageConverter should support 'application/json'"); + } + + @Test + void failsWhenXmlConverterDoesNotSupportMediaType() { + assertThatIllegalArgumentException().isThrownBy(() -> + HttpMessageConverters.create().xmlMessageConverter(new CustomHttpMessageConverter()).build()) + .withMessage("xmlMessageConverter should support 'text/xml'"); + } + + @Test + void failsWhenSmileConverterDoesNotSupportMediaType() { + assertThatIllegalArgumentException().isThrownBy(() -> + HttpMessageConverters.create().smileMessageConverter(new CustomHttpMessageConverter()).build()) + .withMessage("smileMessageConverter should support 'application/x-jackson-smile'"); + } + + @Test + void failsWhenCborConverterDoesNotSupportMediaType() { + assertThatIllegalArgumentException().isThrownBy(() -> + HttpMessageConverters.create().cborMessageConverter(new CustomHttpMessageConverter()).build()) + .withMessage("cborMessageConverter should support 'application/cbor'"); + } + + @Test + void failsWhenYamlConverterDoesNotSupportMediaType() { + assertThatIllegalArgumentException().isThrownBy(() -> + HttpMessageConverters.create().yamlMessageConverter(new CustomHttpMessageConverter()).build()) + .withMessage("yamlMessageConverter should support 'application/yaml'"); + } + + @Nested + class ClientConvertersTests { + + @Test + void defaultConverters() { + var converters = HttpMessageConverters.withDefaults().build(); + assertThat(converters.forClient()).hasExactlyElementsOfTypes(ByteArrayHttpMessageConverter.class, + StringHttpMessageConverter.class, ResourceHttpMessageConverter.class, + AllEncompassingFormHttpMessageConverter.class, + JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class, + JacksonCborHttpMessageConverter.class, JacksonYamlHttpMessageConverter.class, + JacksonXmlHttpMessageConverter.class, KotlinSerializationProtobufHttpMessageConverter.class, + AtomFeedHttpMessageConverter.class, RssChannelHttpMessageConverter.class); + } + + + @Test + void multipartConverterContainsOtherConverters() { + var converters = HttpMessageConverters.withDefaults().build(); + AllEncompassingFormHttpMessageConverter multipartConverter = findMessageConverter(AllEncompassingFormHttpMessageConverter.class, converters.forClient()); + + assertThat(multipartConverter.getPartConverters()).hasExactlyElementsOfTypes( + ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class, + ResourceHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class, + JacksonSmileHttpMessageConverter.class, JacksonCborHttpMessageConverter.class, + JacksonYamlHttpMessageConverter.class, JacksonXmlHttpMessageConverter.class, + KotlinSerializationProtobufHttpMessageConverter.class, AtomFeedHttpMessageConverter.class, + RssChannelHttpMessageConverter.class); + } + + @Test + void registerCustomMessageConverter() { + var converters = HttpMessageConverters.create() + .additionalMessageConverter(new CustomHttpMessageConverter()).build(); + assertThat(converters.forClient()).hasExactlyElementsOfTypes(AllEncompassingFormHttpMessageConverter.class, CustomHttpMessageConverter.class); + } + + @Test + void registerCustomConverterInMultipartConverter() { + var converters = HttpMessageConverters.withDefaults() + .additionalMessageConverter(new CustomHttpMessageConverter()).build(); + AllEncompassingFormHttpMessageConverter multipartConverter = findMessageConverter(AllEncompassingFormHttpMessageConverter.class, converters.forClient()); + assertThat(multipartConverter.getPartConverters()).hasAtLeastOneElementOfType(CustomHttpMessageConverter.class); + } + + @Test + void shouldUseServerSpecificConverter() { + JacksonJsonHttpMessageConverter jacksonConverter = new JacksonJsonHttpMessageConverter(); + var converters = HttpMessageConverters.withDefaults() + .configureClient(configurer -> configurer.jsonMessageConverter(jacksonConverter)).build(); + + JacksonJsonHttpMessageConverter customConverter = findMessageConverter(JacksonJsonHttpMessageConverter.class, converters.forClient()); + assertThat(customConverter).isEqualTo(jacksonConverter); + } + + @Test + void shouldConfigureConverter() { + CustomHttpMessageConverter customConverter = new CustomHttpMessageConverter(); + var converters = HttpMessageConverters.withDefaults() + .additionalMessageConverter(customConverter) + .configureClient(configurer -> { + configurer.configureClientMessageConverters(converter -> { + if (converter instanceof CustomHttpMessageConverter custom) { + custom.processed = true; + } + }); + }).build(); + + assertThat(customConverter.processed).isTrue(); + } + + } + + @Nested + class ServerConvertersTests { + + @Test + void defaultConverters() { + var converters = HttpMessageConverters.withDefaults().build(); + assertThat(converters.forServer()).hasExactlyElementsOfTypes( + ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class, + ResourceHttpMessageConverter.class, ResourceRegionHttpMessageConverter.class, + JacksonJsonHttpMessageConverter.class, JacksonSmileHttpMessageConverter.class, + JacksonCborHttpMessageConverter.class, JacksonYamlHttpMessageConverter.class, + JacksonXmlHttpMessageConverter.class, KotlinSerializationProtobufHttpMessageConverter.class, + AtomFeedHttpMessageConverter.class, RssChannelHttpMessageConverter.class, + AllEncompassingFormHttpMessageConverter.class); + } + + @Test + void multipartConverterContainsOtherConverters() { + var converters = HttpMessageConverters.withDefaults().build(); + AllEncompassingFormHttpMessageConverter multipartConverter = findMessageConverter(AllEncompassingFormHttpMessageConverter.class, converters.forServer()); + + assertThat(multipartConverter.getPartConverters()).hasExactlyElementsOfTypes( + ByteArrayHttpMessageConverter.class, StringHttpMessageConverter.class, + ResourceHttpMessageConverter.class, JacksonJsonHttpMessageConverter.class, + JacksonSmileHttpMessageConverter.class, JacksonCborHttpMessageConverter.class, + JacksonYamlHttpMessageConverter.class, JacksonXmlHttpMessageConverter.class, + KotlinSerializationProtobufHttpMessageConverter.class, AtomFeedHttpMessageConverter.class, + RssChannelHttpMessageConverter.class); + } + + @Test + void registerCustomMessageConverter() { + var converters = HttpMessageConverters.create() + .additionalMessageConverter(new CustomHttpMessageConverter()).build(); + assertThat(converters.forServer()).hasExactlyElementsOfTypes(CustomHttpMessageConverter.class, AllEncompassingFormHttpMessageConverter.class); + } + + @Test + void registerCustomConverterInMultipartConverter() { + var converters = HttpMessageConverters.withDefaults() + .additionalMessageConverter(new CustomHttpMessageConverter()).build(); + AllEncompassingFormHttpMessageConverter multipartConverter = findMessageConverter(AllEncompassingFormHttpMessageConverter.class, converters.forServer()); + assertThat(multipartConverter.getPartConverters()).hasAtLeastOneElementOfType(CustomHttpMessageConverter.class); + } + + @Test + void shouldUseServerSpecificConverter() { + JacksonJsonHttpMessageConverter jacksonConverter = new JacksonJsonHttpMessageConverter(); + var converters = HttpMessageConverters.withDefaults() + .configureServer(configurer -> configurer.jsonMessageConverter(jacksonConverter)).build(); + + JacksonJsonHttpMessageConverter customConverter = findMessageConverter(JacksonJsonHttpMessageConverter.class, converters.forServer()); + assertThat(customConverter).isEqualTo(jacksonConverter); + } + + @Test + void shouldConfigureConverter() { + CustomHttpMessageConverter customConverter = new CustomHttpMessageConverter(); + var converters = HttpMessageConverters.withDefaults() + .additionalMessageConverter(customConverter) + .configureServer(configurer -> { + configurer.configureServerMessageConverters(converter -> { + if (converter instanceof CustomHttpMessageConverter custom) { + custom.processed = true; + } + }); + }).build(); + + assertThat(customConverter.processed).isTrue(); + } + } + + @Nested + class ClasspathDetectionTests { + + @Test + void jsonUsesJackson2WhenJacksonNotPresent() { + var classLoader = new FilteredClassLoader(ObjectMapper.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).hasAtLeastOneElementOfType(MappingJackson2HttpMessageConverter.class) + .doesNotHaveAnyElementsOfTypes(JacksonJsonHttpMessageConverter.class); + } + + @Test + void jsonUsesGsonWhenJacksonNotPresent() { + var classLoader = new FilteredClassLoader(ObjectMapper.class, com.fasterxml.jackson.databind.ObjectMapper.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).hasAtLeastOneElementOfType(GsonHttpMessageConverter.class) + .doesNotHaveAnyElementsOfTypes(JacksonJsonHttpMessageConverter.class, MappingJackson2HttpMessageConverter.class); + } + + @Test + void jsonUsesJsonbWhenJacksonAndGsonNotPresent() { + var classLoader = new FilteredClassLoader(ObjectMapper.class, com.fasterxml.jackson.databind.ObjectMapper.class, Gson.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).hasAtLeastOneElementOfType(JsonbHttpMessageConverter.class) + .doesNotHaveAnyElementsOfTypes(JacksonJsonHttpMessageConverter.class, MappingJackson2HttpMessageConverter.class, + GsonHttpMessageConverter.class); + } + + @Test + void jsonUsesKotlinWhenOthersNotPresent() { + var classLoader = new FilteredClassLoader(ObjectMapper.class, com.fasterxml.jackson.databind.ObjectMapper.class, Gson.class, Jsonb.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).hasAtLeastOneElementOfType(KotlinSerializationJsonHttpMessageConverter.class) + .doesNotHaveAnyElementsOfTypes(JacksonJsonHttpMessageConverter.class, MappingJackson2HttpMessageConverter.class, + GsonHttpMessageConverter.class, JsonbHttpMessageConverter.class); + } + + @Test + void xmlUsesJackson2WhenJacksonNotPresent() { + var classLoader = new FilteredClassLoader(XmlMapper.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).hasAtLeastOneElementOfType(MappingJackson2XmlHttpMessageConverter.class) + .doesNotHaveAnyElementsOfTypes(JacksonXmlHttpMessageConverter.class); + } + + @Test + void xmlUsesJaxbWhenJacksonNotPresent() { + var classLoader = new FilteredClassLoader(XmlMapper.class, com.fasterxml.jackson.dataformat.xml.XmlMapper.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).hasAtLeastOneElementOfType(Jaxb2RootElementHttpMessageConverter.class) + .doesNotHaveAnyElementsOfTypes(JacksonXmlHttpMessageConverter.class, MappingJackson2XmlHttpMessageConverter.class); + } + + @Test + void smileUsesJackson2WhenJacksonNotPresent() { + var classLoader = new FilteredClassLoader(SmileMapper.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).hasAtLeastOneElementOfType(MappingJackson2SmileHttpMessageConverter.class) + .doesNotHaveAnyElementsOfTypes(JacksonSmileHttpMessageConverter.class); + } + + @Test + void cborUsesJackson2WhenJacksonNotPresent() { + var classLoader = new FilteredClassLoader(CBORMapper.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).hasAtLeastOneElementOfType(MappingJackson2CborHttpMessageConverter.class) + .doesNotHaveAnyElementsOfTypes(JacksonCborHttpMessageConverter.class); + } + + @Test + void cborUsesKotlinWhenJacksonNotPresent() { + var classLoader = new FilteredClassLoader(CBORMapper.class, com.fasterxml.jackson.dataformat.cbor.CBORFactory.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).hasAtLeastOneElementOfType(KotlinSerializationCborHttpMessageConverter.class) + .doesNotHaveAnyElementsOfTypes(JacksonCborHttpMessageConverter.class, MappingJackson2CborHttpMessageConverter.class); + } + + @Test + void yamlUsesJackson2WhenJacksonNotPresent() { + var classLoader = new FilteredClassLoader(YAMLMapper.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).hasAtLeastOneElementOfType(MappingJackson2YamlHttpMessageConverter.class) + .doesNotHaveAnyElementsOfTypes(JacksonYamlHttpMessageConverter.class); + } + + @Test + void atomAndRssNotConfiguredWhenRomeNotPresent() { + var classLoader = new FilteredClassLoader(WireFeed.class); + var converters = new DefaultHttpMessageConverters.DefaultBuilder(true, classLoader).build(); + assertThat(converters.forServer()).doesNotHaveAnyElementsOfTypes(AtomFeedHttpMessageConverter.class, RssChannelHttpMessageConverter.class); + } + + } + + + @SuppressWarnings("unchecked") + private T findMessageConverter(Class converterType, Iterable> converters) { + return (T) StreamSupport + .stream(converters.spliterator(), false) + .filter(converter -> converter.getClass().equals(converterType)) + .findFirst().orElseThrow(); + } + + + static class FilteredClassLoader extends URLClassLoader implements SmartClassLoader { + + private final Collection> hiddenClasses; + + public FilteredClassLoader(Class... hiddenClasses) { + this(java.util.Arrays.asList(hiddenClasses)); + } + + FilteredClassLoader(Collection> hiddenClasses) { + super(new URL[0], FilteredClassLoader.class.getClassLoader()); + this.hiddenClasses = hiddenClasses; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + for (Class hiddenClass : this.hiddenClasses) { + if (hiddenClass.getName().equals(name)) { + throw new ClassNotFoundException(); + } + } + return super.loadClass(name, resolve); + } + + } + + static class CustomHttpMessageConverter extends AbstractHttpMessageConverter { + + boolean processed = false; + + @Override + protected boolean supports(Class clazz) { + return false; + } + + @Override + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + return null; + } + + @Override + protected void writeInternal(Object o, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + + } + } + +}