diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index 4e500e367c7c..03b4a0ff19a8 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -15,6 +15,7 @@ dependencies { optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml") optional("com.fasterxml.woodstox:woodstox-core") optional("com.google.code.gson:gson") optional("com.google.protobuf:protobuf-java-util") diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 4760864b24f4..336b76e94c5d 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -45,6 +45,7 @@ * @author Sebastien Deleuze * @author Kazuki Shimizu * @author Sam Brannen + * @author Hyoungjune Kim * @since 3.0 * @see * HTTP 1.1: Semantics and Content, section 3.1.1.1 @@ -311,6 +312,16 @@ public class MediaType extends MimeType implements Serializable { */ public static final String APPLICATION_XML_VALUE = "application/xml"; + /** + * Public constant media type for {@code application/yaml}. + */ + public static final MediaType APPLICATION_YAML; + + /** + * A String equivalent of {@link MediaType#APPLICATION_YAML}. + */ + public static final String APPLICATION_YAML_VALE = "application/yaml"; + /** * Public constant media type for {@code image/gif}. */ @@ -454,6 +465,7 @@ public class MediaType extends MimeType implements Serializable { APPLICATION_STREAM_JSON = new MediaType("application", "stream+json"); APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml"); APPLICATION_XML = new MediaType("application", "xml"); + APPLICATION_YAML = new MediaType("application", "yaml"); IMAGE_GIF = new MediaType("image", "gif"); IMAGE_JPEG = new MediaType("image", "jpeg"); IMAGE_PNG = new MediaType("image", "png"); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index a9f101c915f6..641ff1d677ec 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 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. @@ -56,6 +56,7 @@ import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule; import com.fasterxml.jackson.dataformat.xml.XmlFactory; import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.springframework.beans.BeanUtils; import org.springframework.context.ApplicationContext; @@ -95,6 +96,7 @@ * @author Juergen Hoeller * @author Tadaya Tsuyukubo * @author Eddú Meléndez + * @author Hyoungjune Kim * @since 4.1.1 * @see #build() * @see #configure(ObjectMapper) @@ -936,6 +938,15 @@ public static Jackson2ObjectMapperBuilder cbor() { return new Jackson2ObjectMapperBuilder().factory(new CborFactoryInitializer().create()); } + /** + * Obtain a {@link Jackson2ObjectMapperBuilder} instance in order to + * build a Yaml data format {@link ObjectMapper} instance. + * @since 6.2 + */ + public static Jackson2ObjectMapperBuilder yaml() { + return new Jackson2ObjectMapperBuilder().factory(new YamlFactoryInitializer().create()); + } + private static class XmlObjectMapperInitializer { @@ -976,4 +987,11 @@ public JsonFactory create() { } } + private static class YamlFactoryInitializer { + + public JsonFactory create() { + return new YAMLFactory(); + } + } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java new file mode 100644 index 000000000000..38d74a9094b0 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/yaml/MappingJackson2YamlHttpMessageConverter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2024 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.yaml; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.Assert; + +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter + * HttpMessageConverter} that can read and write the YAML + * data format using + * the dedicated Jackson 2.x extension. + * + *
By default, this converter supports the {@link MediaType#APPLICATION_YAML_VALE} + * media type. This can be overridden by setting the {@link #setSupportedMediaTypes + * supportedMediaTypes} property. + * + *
The default constructor uses the default configuration provided by + * {@link Jackson2ObjectMapperBuilder}. + * + * @author Hyoungjune Kim + * @since 6.2 + */ +public class MappingJackson2YamlHttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + /** + * Construct a new {@code MappingJackson2YamlHttpMessageConverter} using the + * default configuration provided by {@code Jackson2ObjectMapperBuilder}. + */ + public MappingJackson2YamlHttpMessageConverter() { + this(Jackson2ObjectMapperBuilder.yaml().build()); + } + + /** + * Construct a new {@code MappingJackson2YamlHttpMessageConverter} with a + * custom {@link ObjectMapper} (must be configured with a {@code YAMLFactory} + * instance). + *
You can use {@link Jackson2ObjectMapperBuilder} to build it easily.
+ * @see Jackson2ObjectMapperBuilder#yaml()
+ */
+ public MappingJackson2YamlHttpMessageConverter(ObjectMapper objectMapper) {
+ super(objectMapper, MediaType.APPLICATION_YAML);
+ Assert.isInstanceOf(YAMLFactory.class, objectMapper.getFactory(), "YAMLFactory required");
+ }
+
+
+ /**
+ * {@inheritDoc}
+ * The {@code ObjectMapper} must be configured with a {@code YAMLFactory} instance.
+ */
+ @Override
+ public void setObjectMapper(ObjectMapper objectMapper) {
+ Assert.isInstanceOf(YAMLFactory.class, objectMapper.getFactory(), "YAMLFactory required");
+ super.setObjectMapper(objectMapper);
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java b/spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java
new file mode 100644
index 000000000000..18c07e5214c0
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/yaml/package-info.java
@@ -0,0 +1,9 @@
+/**
+ * Provides an HttpMessageConverter for the Yaml data format.
+ */
+@NonNullApi
+@NonNullFields
+package org.springframework.http.converter.yaml;
+
+import org.springframework.lang.NonNullApi;
+import org.springframework.lang.NonNullFields;
diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java
index 82e2a9f219ec..e2ae3b686717 100644
--- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java
+++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClientBuilder.java
@@ -48,6 +48,7 @@
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
+import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -60,6 +61,7 @@
* Default implementation of {@link RestClient.Builder}.
*
* @author Arjen Poutsma
+ * @author Hyoungjune Kim
* @since 6.1
*/
final class DefaultRestClientBuilder implements RestClient.Builder {
@@ -86,6 +88,8 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
private static final boolean jackson2CborPresent;
+ private static final boolean jackson2YamlPresent;
+
static {
ClassLoader loader = DefaultRestClientBuilder.class.getClassLoader();
@@ -101,6 +105,7 @@ final class DefaultRestClientBuilder implements RestClient.Builder {
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", loader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", loader);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", loader);
+ jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", loader);
}
@Nullable
@@ -394,6 +399,9 @@ else if (jsonbPresent) {
if (jackson2CborPresent) {
this.messageConverters.add(new MappingJackson2CborHttpMessageConverter());
}
+ if (jackson2YamlPresent) {
+ this.messageConverters.add(new MappingJackson2YamlHttpMessageConverter());
+ }
}
return this.messageConverters;
}
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
index 78cace8148ef..343b42c5f2ad 100644
--- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
+++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
@@ -66,6 +66,7 @@
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
+import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -108,6 +109,7 @@
* @author Juergen Hoeller
* @author Sam Brannen
* @author Sebastien Deleuze
+ * @author Hyoungjune Kim
* @since 3.0
* @see HttpMessageConverter
* @see RequestCallback
@@ -128,6 +130,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
private static final boolean jackson2CborPresent;
+ private static final boolean jackson2YamlPresent;
+
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
@@ -149,6 +153,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
+ jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader);
@@ -222,6 +227,10 @@ else if (kotlinSerializationCborPresent) {
this.messageConverters.add(new KotlinSerializationCborHttpMessageConverter());
}
+ if (jackson2YamlPresent) {
+ this.messageConverters.add(new MappingJackson2YamlHttpMessageConverter());
+ }
+
updateErrorHandlerConverters();
this.uriTemplateHandler = initUriTemplateHandler();
}
diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java
index a6fc0b36f683..b8c927bac516 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java
@@ -79,6 +79,7 @@
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import kotlin.ranges.IntRange;
import org.junit.jupiter.api.Test;
@@ -95,6 +96,7 @@
*
* @author Sebastien Deleuze
* @author Eddú Meléndez
+ * @author Hyoungjune Kim
*/
@SuppressWarnings("deprecation")
class Jackson2ObjectMapperBuilderTests {
@@ -588,6 +590,13 @@ void factory() {
assertThat(objectMapper.getFactory().getClass()).isEqualTo(SmileFactory.class);
}
+ @Test
+ void yaml() {
+ ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.yaml().build();
+ assertThat(objectMapper).isNotNull();
+ assertThat(objectMapper.getFactory().getClass()).isEqualTo(YAMLFactory.class);
+ }
+
@Test
void visibility() throws JsonProcessingException {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
diff --git a/spring-webmvc/spring-webmvc.gradle b/spring-webmvc/spring-webmvc.gradle
index 30e75678213d..44661dd02cf3 100644
--- a/spring-webmvc/spring-webmvc.gradle
+++ b/spring-webmvc/spring-webmvc.gradle
@@ -19,6 +19,7 @@ dependencies {
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile")
optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
+ optional("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
optional("com.github.librepdf:openpdf")
optional("com.rometools:rome")
optional("io.micrometer:context-propagation")
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java
index b48e06b26241..37daf14a1cc9 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -21,6 +21,7 @@
import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
import com.fasterxml.jackson.dataformat.smile.SmileFactory;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import org.w3c.dom.Element;
import org.springframework.beans.factory.FactoryBean;
@@ -55,6 +56,7 @@
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
+import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -148,6 +150,7 @@
* @author Rossen Stoyanchev
* @author Brian Clozel
* @author Agim Emruli
+ * @author Hyoungjune Kim
* @since 3.0
*/
class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
@@ -173,6 +176,8 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
private static final boolean jackson2CborPresent;
+ private static final boolean jackson2YamlPresent;
+
private static final boolean gsonPresent;
static {
@@ -185,6 +190,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
+ jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
}
@@ -463,6 +469,9 @@ private Properties getDefaultMediaTypes() {
if (jackson2CborPresent) {
defaultMediaTypes.put("cbor", MediaType.APPLICATION_CBOR_VALUE);
}
+ if (jackson2YamlPresent) {
+ defaultMediaTypes.put("yaml", MediaType.APPLICATION_YAML_VALE);
+ }
return defaultMediaTypes;
}
@@ -614,6 +623,14 @@ else if (gsonPresent) {
jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
messageConverters.add(jacksonConverterDef);
}
+ if(jackson2YamlPresent) {
+ Class> type = MappingJackson2YamlHttpMessageConverter.class;
+ RootBeanDefinition jacksonConverterDef = createConverterDefinition(type, source);
+ GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source);
+ jacksonFactoryDef.getPropertyValues().add("factory", new RootBeanDefinition(YAMLFactory.class));
+ jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef);
+ messageConverters.add(jacksonConverterDef);
+ }
}
return messageConverters;
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
index 0f210c42c558..5585413091bd 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2024 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.
@@ -58,6 +58,7 @@
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
+import org.springframework.http.converter.yaml.MappingJackson2YamlHttpMessageConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
@@ -182,6 +183,7 @@
* @author Rossen Stoyanchev
* @author Brian Clozel
* @author Sebastien Deleuze
+ * @author Hyoungjune Kim
* @since 3.1
* @see EnableWebMvc
* @see WebMvcConfigurer
@@ -200,6 +202,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
private static final boolean jackson2CborPresent;
+ private static final boolean jackson2YamlPresent;
+
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
@@ -219,6 +223,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
+ jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader);
@@ -463,6 +468,9 @@ protected Map