View Javadoc
1   package top.infra.jackson2;
2   
3   import static java.lang.Boolean.FALSE;
4   import static java.lang.Boolean.TRUE;
5   
6   import com.fasterxml.jackson.core.JsonGenerator;
7   import com.fasterxml.jackson.core.JsonParser;
8   import com.fasterxml.jackson.databind.DeserializationFeature;
9   import com.fasterxml.jackson.databind.MapperFeature;
10  import com.fasterxml.jackson.databind.Module;
11  import com.fasterxml.jackson.databind.ObjectMapper;
12  import com.fasterxml.jackson.databind.SerializationFeature;
13  
14  import org.springframework.beans.BeanUtils;
15  import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
16  import org.springframework.util.ClassUtils;
17  
18  import java.text.DateFormat;
19  import java.text.SimpleDateFormat;
20  import java.util.List;
21  import java.util.TimeZone;
22  
23  /**
24   * see: {@link org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration}.
25   * see: org.springframework.hateoas.config.HypermediaSupportBeanDefinitionRegistrar
26   * see: org.springframework.hateoas.config.EnableHypermediaSupport
27   */
28  public class DefaultJackson2Customizer implements Jackson2Customizer {
29  
30      @SuppressWarnings("unchecked")
31      @Override
32      public void customize(final Jackson2Properties properties, final ObjectMapper mapper) {
33          mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY,
34              this.feature(properties, DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, TRUE));
35  
36          mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
37              this.feature(properties, DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, FALSE));
38  
39          mapper.configure(JsonGenerator.Feature.QUOTE_NON_NUMERIC_NUMBERS,
40              this.feature(properties, JsonGenerator.Feature.QUOTE_NON_NUMERIC_NUMBERS, FALSE));
41  
42          mapper.configure(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS,
43              this.feature(properties, JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS, TRUE));
44  
45          mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES,
46              this.feature(properties, JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, TRUE));
47  
48          // see: https://github.com/FasterXML/jackson-databind/issues/1218
49          // since jackson2 2.9.x
50          //mapper.configure(MapperFeature.INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES,
51          //    this.feature(properties, MapperFeature.INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES, FALSE));
52  
53          mapper.configure(SerializationFeature.INDENT_OUTPUT,
54              this.feature(properties, SerializationFeature.INDENT_OUTPUT, FALSE));
55  
56          mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
57              this.feature(properties, SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, FALSE));
58          //WRITE_DATES_WITH_ZONE_ID
59  
60          mapper.setTimeZone(this.timeZone(properties));
61          this.configureDateFormat(properties, mapper);
62  
63          // Jackson2ObjectMapperBuilder.registerWellKnownModulesIfAvailable
64          new Jackson2ObjectMapperBuilder().configure(mapper);
65  
66          final ClassLoader moduleClassLoader = getClass().getClassLoader();
67          if (isGuavaPresent(moduleClassLoader)) {
68              try {
69                  final Class<? extends Module> guavaModuleClass = (Class<? extends Module>)
70                      ClassUtils.forName("com.fasterxml.jackson.datatype.guava.GuavaModule", moduleClassLoader);
71                  final Module guavaModule = BeanUtils.instantiateClass(guavaModuleClass);
72                  mapper.registerModule(guavaModule);
73              } catch (final ClassNotFoundException ex) {
74                  // jackson-datatype-guava or guava not available
75              }
76          }
77      }
78  
79      @SuppressWarnings("unchecked")
80      @Override
81      public void customize(final Jackson2Properties properties, final Jackson2ObjectMapperBuilder builder) {
82          this.feature(builder, DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY,
83              this.feature(properties, DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, TRUE));
84  
85          this.feature(builder, DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
86              this.feature(properties, DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, FALSE));
87  
88          this.feature(builder, JsonGenerator.Feature.QUOTE_NON_NUMERIC_NUMBERS,
89              this.feature(properties, JsonGenerator.Feature.QUOTE_NON_NUMERIC_NUMBERS, FALSE));
90  
91          this.feature(builder, JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS,
92              this.feature(properties, JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS, TRUE));
93  
94          this.feature(builder, JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES,
95              this.feature(properties, JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, TRUE));
96  
97          // see: https://github.com/FasterXML/jackson-databind/issues/1218
98          // since jackson2 2.9.x
99          //this.feature(builder, MapperFeature.INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES,
100         //    this.feature(properties, MapperFeature.INFER_CREATOR_FROM_CONSTRUCTOR_PROPERTIES, FALSE));
101 
102         this.feature(builder, SerializationFeature.INDENT_OUTPUT,
103             this.feature(properties, SerializationFeature.INDENT_OUTPUT, FALSE));
104 
105         this.feature(builder, SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
106             this.feature(properties, SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, FALSE));
107         //WRITE_DATES_WITH_ZONE_ID
108 
109         builder.timeZone(this.timeZone(properties));
110         properties.setDateFormat(this.dateFormat(properties));
111 
112         final ClassLoader moduleClassLoader = this.moduleClassLoader(builder);
113         if (isGuavaPresent(moduleClassLoader)) {
114             try {
115                 final Class<? extends Module> guavaModuleClass = (Class<? extends Module>)
116                     ClassUtils.forName("com.fasterxml.jackson.datatype.guava.GuavaModule", moduleClassLoader);
117                 final Module guavaModule = BeanUtils.instantiateClass(guavaModuleClass);
118                 final List<Module> modules = this.modules(builder);
119                 modules.add(guavaModule);
120                 builder.modulesToInstall(modules.toArray(new Module[0]));
121             } catch (final ClassNotFoundException ex) {
122                 // jackson-datatype-guava or guava not available
123             }
124         }
125     }
126 
127     public boolean isGuavaPresent(final ClassLoader classLoader) {
128         return ClassUtils.isPresent("com.google.common.collect.Multimap", classLoader)
129             && ClassUtils.isPresent("com.fasterxml.jackson.datatype.guava.GuavaModule", classLoader);
130     }
131 
132     /**
133      * see: {@link org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration}
134      * Jackson2ObjectMapperBuilderCustomizerConfiguration#configureDateFormat
135      */
136     private void configureDateFormat(final Jackson2Properties properties, final ObjectMapper mapper) {
137         // We support a fully qualified class name extending DateFormat or a date
138         // pattern string value
139         String dateFormat = properties.getDateFormat();
140         if (dateFormat != null) {
141             try {
142                 Class<?> dateFormatClass = ClassUtils.forName(dateFormat, null);
143                 mapper.setDateFormat((DateFormat) BeanUtils.instantiateClass(dateFormatClass));
144             } catch (ClassNotFoundException ex) {
145                 SimpleDateFormat simpleDateFormat = new SimpleDateFormat(
146                     dateFormat);
147                 // Since Jackson 2.6.3 we always need to set a TimeZone (see
148                 // gh-4170). If none in our properties fallback to the Jackson's
149                 // default
150                 TimeZone timeZone = properties.getTimeZone();
151                 if (timeZone == null) {
152                     timeZone = new ObjectMapper().getSerializationConfig()
153                         .getTimeZone();
154                 }
155                 simpleDateFormat.setTimeZone(timeZone);
156                 mapper.setDateFormat(simpleDateFormat);
157             }
158         }
159     }
160 
161     private void feature(final Jackson2ObjectMapperBuilder builder, final Object feature, final Boolean enabled) {
162         if (enabled) {
163             builder.featuresToEnable(feature);
164         } else {
165             builder.featuresToDisable(feature);
166         }
167     }
168 
169     Boolean feature(final Jackson2Properties properties, final DeserializationFeature feature, final Boolean defaultValue) {
170         return properties.getDeserialization().getOrDefault(feature, defaultValue);
171     }
172 
173     Boolean feature(final Jackson2Properties properties, final JsonGenerator.Feature feature, final Boolean defaultValue) {
174         return properties.getGenerator().getOrDefault(feature, defaultValue);
175     }
176 
177     Boolean feature(final Jackson2Properties properties, final JsonParser.Feature feature, final Boolean defaultValue) {
178         return properties.getParser().getOrDefault(feature, defaultValue);
179     }
180 
181     Boolean feature(final Jackson2Properties properties, final MapperFeature feature, final Boolean defaultValue) {
182         return properties.getMapper().getOrDefault(feature, defaultValue);
183     }
184 
185     Boolean feature(final Jackson2Properties properties, final SerializationFeature feature, final Boolean defaultValue) {
186         return properties.getSerialization().getOrDefault(feature, defaultValue);
187     }
188 }