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}