diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java index 8ac3bb8fba5..bc86a82c676 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfiguration.java @@ -16,11 +16,15 @@ package org.springframework.boot.autoconfigure.jackson; +import java.lang.reflect.Field; +import java.text.DateFormat; +import java.text.SimpleDateFormat; import java.util.Collection; import java.util.Map.Entry; import javax.annotation.PostConstruct; +import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -33,6 +37,8 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; @@ -40,6 +46,7 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.joda.JodaModule; import com.fasterxml.jackson.datatype.jsr310.JSR310Module; @@ -57,6 +64,7 @@ import com.fasterxml.jackson.datatype.jsr310.JSR310Module; * * @author Oliver Gierke * @author Andy Wilkinson + * @author Marcel Overdijk * @since 1.1.0 */ @Configuration @@ -107,9 +115,65 @@ public class JacksonAutoConfiguration { configureParserFeatures(objectMapper); configureGeneratorFeatures(objectMapper); + configureDateFormat(objectMapper); + configurePropertyNamingStrategy(objectMapper); + return objectMapper; } + private void configurePropertyNamingStrategy(ObjectMapper objectMapper) { + // We support a fully qualified class name extending Jackson's + // PropertyNamingStrategy or a string value corresponding to the constant + // names in PropertyNamingStrategy which hold default provided implementations + String propertyNamingStrategy = this.jacksonProperties + .getPropertyNamingStrategy(); + if (propertyNamingStrategy != null) { + try { + Class clazz = ClassUtils.forName(propertyNamingStrategy, null); + objectMapper + .setPropertyNamingStrategy((PropertyNamingStrategy) BeanUtils + .instantiateClass(clazz)); + } + catch (ClassNotFoundException e) { + // Find the field (this way we automatically support new constants + // that may be added by Jackson in the future) + Field field = ReflectionUtils.findField(PropertyNamingStrategy.class, + propertyNamingStrategy, PropertyNamingStrategy.class); + if (field != null) { + try { + objectMapper + .setPropertyNamingStrategy((PropertyNamingStrategy) field + .get(null)); + } + catch (Exception ex) { + throw new IllegalStateException(ex); + } + } + else { + throw new IllegalArgumentException("Constant named '" + + propertyNamingStrategy + "' not found on " + + PropertyNamingStrategy.class.getName()); + } + } + } + } + + private void configureDateFormat(ObjectMapper objectMapper) { + // We support a fully qualified class name extending DateFormat or a date + // pattern string value + String dateFormat = this.jacksonProperties.getDateFormat(); + if (dateFormat != null) { + try { + Class clazz = ClassUtils.forName(dateFormat, null); + objectMapper.setDateFormat((DateFormat) BeanUtils + .instantiateClass(clazz)); + } + catch (ClassNotFoundException e) { + objectMapper.setDateFormat(new SimpleDateFormat(dateFormat)); + } + } + } + private void configureDeserializationFeatures(ObjectMapper objectMapper) { for (Entry entry : this.jacksonProperties .getDeserialization().entrySet()) { diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java index 16c1556cf4a..b2169280979 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java @@ -31,11 +31,16 @@ import com.fasterxml.jackson.databind.SerializationFeature; * Configuration properties to configure Jackson * * @author Andy Wilkinson + * @author Marcel Overdijk * @since 1.2.0 */ @ConfigurationProperties(prefix = "spring.jackson") public class JacksonProperties { + private String dateFormat; + + private String propertyNamingStrategy; + private Map serialization = new HashMap(); private Map deserialization = new HashMap(); @@ -46,6 +51,22 @@ public class JacksonProperties { private Map generator = new HashMap(); + public String getDateFormat() { + return this.dateFormat; + } + + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + + public String getPropertyNamingStrategy() { + return this.propertyNamingStrategy; + } + + public void setPropertyNamingStrategy(String propertyNamingStrategy) { + this.propertyNamingStrategy = propertyNamingStrategy; + } + public Map getSerialization() { return this.serialization; } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java index e0aa296634d..4b5ba52888f 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jackson/JacksonAutoConfigurationTests.java @@ -17,8 +17,11 @@ package org.springframework.boot.autoconfigure.jackson; import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Map; +import org.joda.time.DateTime; import org.joda.time.LocalDateTime; import org.junit.After; import org.junit.Before; @@ -60,6 +63,8 @@ import static org.mockito.Mockito.verify; * * @author Dave Syer * @author Oliver Gierke + * @author Andy Wilkinson + * @author Marcel Overdijk */ public class JacksonAutoConfigurationTests { @@ -108,6 +113,110 @@ public class JacksonAutoConfigurationTests { assertEquals("{\"foo\":\"bar\"}", mapper.writeValueAsString(new Foo())); } + /* + * ObjectMapper does not contain method to get the date format of the mapper. See + * https://github.com/FasterXML/jackson-databind/issues/559 If such a method will be + * provided below tests can be simplified. + */ + + @Test + public void noCustomDateFormat() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + Date date = new DateTime(1988, 6, 25, 20, 30).toDate(); + assertEquals(String.valueOf(date.getTime()), mapper.writeValueAsString(date)); + } + + @Test + public void customDateFormat() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.date-format:yyyyMMddHHmmss"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + Date date = new DateTime(1988, 6, 25, 20, 30).toDate(); + assertEquals("\"19880625203000\"", mapper.writeValueAsString(date)); + } + + @Test + public void customDateFormatClass() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils + .addEnvironment( + this.context, + "spring.jackson.date-format:org.springframework.boot.autoconfigure.jackson.JacksonAutoConfigurationTests.MyDateFormat"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + Date date = new DateTime(1988, 6, 25, 20, 30).toDate(); + assertEquals("\"1988-06-25 20:30:00\"", mapper.writeValueAsString(date)); + } + + public static class MyDateFormat extends SimpleDateFormat { + + public MyDateFormat() { + super("yyyy-MM-dd HH:mm:ss"); + } + } + + /* + * ObjectMapper does not contain method to get the property naming strategy of the + * mapper. See https://github.com/FasterXML/jackson-databind/issues/559 If such a + * method will be provided below tests can be simplified. + */ + + @Test + public void noCustomPropertyNamingStrategy() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertEquals("{\"propertyName\":null}", mapper.writeValueAsString(new Bar())); + } + + @Test + public void customPropertyNamingStrategyCamelCaseToLowerCaseWithUnderscores() + throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils + .addEnvironment(this.context, + "spring.jackson.property-naming-strategy:CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertEquals("{\"property_name\":null}", mapper.writeValueAsString(new Bar())); + } + + @Test + public void customPropertyNamingStrategyPascalCaseToCamelCase() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.property-naming-strategy:PASCAL_CASE_TO_CAMEL_CASE"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertEquals("{\"PropertyName\":null}", mapper.writeValueAsString(new Bar())); + } + + @Test + public void customPropertyNamingStrategyLowerCase() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils.addEnvironment(this.context, + "spring.jackson.property-naming-strategy:LOWER_CASE"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertEquals("{\"propertyname\":null}", mapper.writeValueAsString(new Bar())); + } + + @Test + public void customPropertyNamingStrategyClass() throws Exception { + this.context.register(JacksonAutoConfiguration.class); + EnvironmentTestUtils + .addEnvironment( + this.context, + "spring.jackson.property-naming-strategy:com.fasterxml.jackson.databind.PropertyNamingStrategy.LowerCaseWithUnderscoresStrategy"); + this.context.refresh(); + ObjectMapper mapper = this.context.getBean(ObjectMapper.class); + assertEquals("{\"property_name\":null}", mapper.writeValueAsString(new Bar())); + } + @Test public void enableSerializationFeature() throws Exception { this.context.register(JacksonAutoConfiguration.class); @@ -297,4 +406,16 @@ public class JacksonAutoConfigurationTests { } + protected static class Bar { + + private String propertyName; + + public String getPropertyName() { + return this.propertyName; + } + + public void setPropertyName(String propertyName) { + this.propertyName = propertyName; + } + } } diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 8f4d8ab8771..0bc598304c7 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -94,6 +94,8 @@ content into your application; rather pick only the properties that you need. spring.resources.add-mappings=true # if default mappings should be added # JACKSON ({sc-spring-boot-autoconfigure}}/jackson/JacksonProperties.{sc-ext}[JacksonProperties]) + spring.jackson.date-format= # Date format string (e.g. yyyy-MM-dd HH:mm:ss), or a fully-qualified date format class name (e.g. com.fasterxml.jackson.databind.util.ISO8601DateFormat) + spring.jackson.property-naming-strategy= # One of the constants on Jackson's PropertyNamingStrategy (e.g. CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES) or the fully-qualified class name of a PropertyNamingStrategy subclass spring.jackson.deserialization.*= # see Jackson's DeserializationFeature spring.jackson.generator.*= # see Jackson's JsonGenerator.Feature spring.jackson.mapper.*= # see Jackson's MapperFeature