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