001/* 002 * Copyright 2012-2016 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.autoconfigure.jackson; 018 019import java.lang.reflect.Field; 020import java.text.DateFormat; 021import java.text.SimpleDateFormat; 022import java.util.Collection; 023import java.util.List; 024import java.util.Locale; 025import java.util.Map; 026import java.util.Map.Entry; 027import java.util.TimeZone; 028 029import com.fasterxml.jackson.annotation.JsonCreator; 030import com.fasterxml.jackson.databind.Module; 031import com.fasterxml.jackson.databind.ObjectMapper; 032import com.fasterxml.jackson.databind.PropertyNamingStrategy; 033import com.fasterxml.jackson.databind.module.SimpleModule; 034import com.fasterxml.jackson.datatype.joda.cfg.JacksonJodaDateFormat; 035import com.fasterxml.jackson.datatype.joda.ser.DateTimeSerializer; 036import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; 037import org.apache.commons.logging.Log; 038import org.apache.commons.logging.LogFactory; 039import org.joda.time.DateTime; 040import org.joda.time.format.DateTimeFormat; 041 042import org.springframework.beans.BeanUtils; 043import org.springframework.beans.factory.BeanFactoryUtils; 044import org.springframework.beans.factory.ListableBeanFactory; 045import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 046import org.springframework.boot.autoconfigure.condition.ConditionalOnJava; 047import org.springframework.boot.autoconfigure.condition.ConditionalOnJava.JavaVersion; 048import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 049import org.springframework.boot.context.properties.EnableConfigurationProperties; 050import org.springframework.boot.jackson.JsonComponentModule; 051import org.springframework.context.ApplicationContext; 052import org.springframework.context.annotation.Bean; 053import org.springframework.context.annotation.Configuration; 054import org.springframework.context.annotation.Primary; 055import org.springframework.core.Ordered; 056import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 057import org.springframework.util.Assert; 058import org.springframework.util.ClassUtils; 059import org.springframework.util.ReflectionUtils; 060 061/** 062 * Auto configuration for Jackson. The following auto-configuration will get applied: 063 * <ul> 064 * <li>an {@link ObjectMapper} in case none is already configured.</li> 065 * <li>a {@link Jackson2ObjectMapperBuilder} in case none is already configured.</li> 066 * <li>auto-registration for all {@link Module} beans with all {@link ObjectMapper} beans 067 * (including the defaulted ones).</li> 068 * </ul> 069 * 070 * @author Oliver Gierke 071 * @author Andy Wilkinson 072 * @author Marcel Overdijk 073 * @author Sebastien Deleuze 074 * @author Johannes Edmeier 075 * @since 1.1.0 076 */ 077@Configuration 078@ConditionalOnClass(ObjectMapper.class) 079public class JacksonAutoConfiguration { 080 081 @Bean 082 public JsonComponentModule jsonComponentModule() { 083 return new JsonComponentModule(); 084 } 085 086 @Configuration 087 @ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class }) 088 static class JacksonObjectMapperConfiguration { 089 090 @Bean 091 @Primary 092 @ConditionalOnMissingBean(ObjectMapper.class) 093 public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { 094 return builder.createXmlMapper(false).build(); 095 } 096 097 } 098 099 @Configuration 100 @ConditionalOnClass({ Jackson2ObjectMapperBuilder.class, DateTime.class, 101 DateTimeSerializer.class, JacksonJodaDateFormat.class }) 102 static class JodaDateTimeJacksonConfiguration { 103 104 private static final Log logger = LogFactory 105 .getLog(JodaDateTimeJacksonConfiguration.class); 106 107 private final JacksonProperties jacksonProperties; 108 109 JodaDateTimeJacksonConfiguration(JacksonProperties jacksonProperties) { 110 this.jacksonProperties = jacksonProperties; 111 } 112 113 @Bean 114 public SimpleModule jodaDateTimeSerializationModule() { 115 SimpleModule module = new SimpleModule(); 116 JacksonJodaDateFormat jacksonJodaFormat = getJacksonJodaDateFormat(); 117 if (jacksonJodaFormat != null) { 118 module.addSerializer(DateTime.class, 119 new DateTimeSerializer(jacksonJodaFormat)); 120 } 121 return module; 122 } 123 124 private JacksonJodaDateFormat getJacksonJodaDateFormat() { 125 if (this.jacksonProperties.getJodaDateTimeFormat() != null) { 126 return new JacksonJodaDateFormat(DateTimeFormat 127 .forPattern(this.jacksonProperties.getJodaDateTimeFormat()) 128 .withZoneUTC()); 129 } 130 if (this.jacksonProperties.getDateFormat() != null) { 131 try { 132 return new JacksonJodaDateFormat(DateTimeFormat 133 .forPattern(this.jacksonProperties.getDateFormat()) 134 .withZoneUTC()); 135 } 136 catch (IllegalArgumentException ex) { 137 if (logger.isWarnEnabled()) { 138 logger.warn("spring.jackson.date-format could not be used to " 139 + "configure formatting of Joda's DateTime. You may want " 140 + "to configure spring.jackson.joda-date-time-format as " 141 + "well."); 142 } 143 } 144 } 145 return null; 146 } 147 148 } 149 150 @Configuration 151 @ConditionalOnJava(JavaVersion.EIGHT) 152 @ConditionalOnClass(ParameterNamesModule.class) 153 static class ParameterNamesModuleConfiguration { 154 155 @Bean 156 @ConditionalOnMissingBean(ParameterNamesModule.class) 157 public ParameterNamesModule parameterNamesModule() { 158 return new ParameterNamesModule(JsonCreator.Mode.DEFAULT); 159 } 160 161 } 162 163 @Configuration 164 @ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class }) 165 static class JacksonObjectMapperBuilderConfiguration { 166 167 private final ApplicationContext applicationContext; 168 169 JacksonObjectMapperBuilderConfiguration(ApplicationContext applicationContext) { 170 this.applicationContext = applicationContext; 171 } 172 173 @Bean 174 @ConditionalOnMissingBean(Jackson2ObjectMapperBuilder.class) 175 public Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder( 176 List<Jackson2ObjectMapperBuilderCustomizer> customizers) { 177 Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); 178 builder.applicationContext(this.applicationContext); 179 customize(builder, customizers); 180 return builder; 181 } 182 183 private void customize(Jackson2ObjectMapperBuilder builder, 184 List<Jackson2ObjectMapperBuilderCustomizer> customizers) { 185 for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) { 186 customizer.customize(builder); 187 } 188 } 189 190 } 191 192 @Configuration 193 @ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class }) 194 @EnableConfigurationProperties(JacksonProperties.class) 195 static class Jackson2ObjectMapperBuilderCustomizerConfiguration { 196 197 @Bean 198 public StandardJackson2ObjectMapperBuilderCustomizer standardJacksonObjectMapperBuilderCustomizer( 199 ApplicationContext applicationContext, 200 JacksonProperties jacksonProperties) { 201 return new StandardJackson2ObjectMapperBuilderCustomizer(applicationContext, 202 jacksonProperties); 203 } 204 205 private static final class StandardJackson2ObjectMapperBuilderCustomizer 206 implements Jackson2ObjectMapperBuilderCustomizer, Ordered { 207 208 private final ApplicationContext applicationContext; 209 210 private final JacksonProperties jacksonProperties; 211 212 StandardJackson2ObjectMapperBuilderCustomizer( 213 ApplicationContext applicationContext, 214 JacksonProperties jacksonProperties) { 215 this.applicationContext = applicationContext; 216 this.jacksonProperties = jacksonProperties; 217 } 218 219 @Override 220 public int getOrder() { 221 return 0; 222 } 223 224 @Override 225 public void customize(Jackson2ObjectMapperBuilder builder) { 226 227 if (this.jacksonProperties.getDefaultPropertyInclusion() != null) { 228 builder.serializationInclusion( 229 this.jacksonProperties.getDefaultPropertyInclusion()); 230 } 231 if (this.jacksonProperties.getTimeZone() != null) { 232 builder.timeZone(this.jacksonProperties.getTimeZone()); 233 } 234 configureFeatures(builder, this.jacksonProperties.getDeserialization()); 235 configureFeatures(builder, this.jacksonProperties.getSerialization()); 236 configureFeatures(builder, this.jacksonProperties.getMapper()); 237 configureFeatures(builder, this.jacksonProperties.getParser()); 238 configureFeatures(builder, this.jacksonProperties.getGenerator()); 239 configureDateFormat(builder); 240 configurePropertyNamingStrategy(builder); 241 configureModules(builder); 242 configureLocale(builder); 243 } 244 245 private void configureFeatures(Jackson2ObjectMapperBuilder builder, 246 Map<?, Boolean> features) { 247 for (Entry<?, Boolean> entry : features.entrySet()) { 248 if (entry.getValue() != null && entry.getValue()) { 249 builder.featuresToEnable(entry.getKey()); 250 } 251 else { 252 builder.featuresToDisable(entry.getKey()); 253 } 254 } 255 } 256 257 private void configureDateFormat(Jackson2ObjectMapperBuilder builder) { 258 // We support a fully qualified class name extending DateFormat or a date 259 // pattern string value 260 String dateFormat = this.jacksonProperties.getDateFormat(); 261 if (dateFormat != null) { 262 try { 263 Class<?> dateFormatClass = ClassUtils.forName(dateFormat, null); 264 builder.dateFormat( 265 (DateFormat) BeanUtils.instantiateClass(dateFormatClass)); 266 } 267 catch (ClassNotFoundException ex) { 268 SimpleDateFormat simpleDateFormat = new SimpleDateFormat( 269 dateFormat); 270 // Since Jackson 2.6.3 we always need to set a TimeZone (see 271 // gh-4170). If none in our properties fallback to the Jackson's 272 // default 273 TimeZone timeZone = this.jacksonProperties.getTimeZone(); 274 if (timeZone == null) { 275 timeZone = new ObjectMapper().getSerializationConfig() 276 .getTimeZone(); 277 } 278 simpleDateFormat.setTimeZone(timeZone); 279 builder.dateFormat(simpleDateFormat); 280 } 281 } 282 } 283 284 private void configurePropertyNamingStrategy( 285 Jackson2ObjectMapperBuilder builder) { 286 // We support a fully qualified class name extending Jackson's 287 // PropertyNamingStrategy or a string value corresponding to the constant 288 // names in PropertyNamingStrategy which hold default provided 289 // implementations 290 String strategy = this.jacksonProperties.getPropertyNamingStrategy(); 291 if (strategy != null) { 292 try { 293 configurePropertyNamingStrategyClass(builder, 294 ClassUtils.forName(strategy, null)); 295 } 296 catch (ClassNotFoundException ex) { 297 configurePropertyNamingStrategyField(builder, strategy); 298 } 299 } 300 } 301 302 private void configurePropertyNamingStrategyClass( 303 Jackson2ObjectMapperBuilder builder, 304 Class<?> propertyNamingStrategyClass) { 305 builder.propertyNamingStrategy((PropertyNamingStrategy) BeanUtils 306 .instantiateClass(propertyNamingStrategyClass)); 307 } 308 309 private void configurePropertyNamingStrategyField( 310 Jackson2ObjectMapperBuilder builder, String fieldName) { 311 // Find the field (this way we automatically support new constants 312 // that may be added by Jackson in the future) 313 Field field = ReflectionUtils.findField(PropertyNamingStrategy.class, 314 fieldName, PropertyNamingStrategy.class); 315 Assert.notNull(field, "Constant named '" + fieldName + "' not found on " 316 + PropertyNamingStrategy.class.getName()); 317 try { 318 builder.propertyNamingStrategy( 319 (PropertyNamingStrategy) field.get(null)); 320 } 321 catch (Exception ex) { 322 throw new IllegalStateException(ex); 323 } 324 } 325 326 private void configureModules(Jackson2ObjectMapperBuilder builder) { 327 Collection<Module> moduleBeans = getBeans(this.applicationContext, 328 Module.class); 329 builder.modulesToInstall( 330 moduleBeans.toArray(new Module[moduleBeans.size()])); 331 } 332 333 private void configureLocale(Jackson2ObjectMapperBuilder builder) { 334 Locale locale = this.jacksonProperties.getLocale(); 335 if (locale != null) { 336 builder.locale(locale); 337 } 338 } 339 340 private static <T> Collection<T> getBeans(ListableBeanFactory beanFactory, 341 Class<T> type) { 342 return BeanFactoryUtils.beansOfTypeIncludingAncestors(beanFactory, type) 343 .values(); 344 } 345 346 } 347 348 } 349 350}