View Javadoc
1   /*
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   */
14  package com.addthis.codec.jackson;
15  
16  import javax.validation.Validator;
17  
18  import java.io.IOException;
19  
20  import java.util.Iterator;
21  import java.util.List;
22  
23  import com.addthis.codec.plugins.PluginRegistry;
24  import com.addthis.codec.utils.ExecutorsModule;
25  
26  import com.google.common.annotations.Beta;
27  import com.google.common.base.Splitter;
28  import com.google.common.collect.Lists;
29  
30  import com.fasterxml.jackson.annotation.JsonInclude;
31  import com.fasterxml.jackson.core.JsonLocation;
32  import com.fasterxml.jackson.core.JsonParser;
33  import com.fasterxml.jackson.databind.JavaType;
34  import com.fasterxml.jackson.databind.JsonMappingException;
35  import com.fasterxml.jackson.databind.JsonNode;
36  import com.fasterxml.jackson.databind.ObjectMapper;
37  import com.fasterxml.jackson.databind.exc.PropertyBindingException;
38  import com.fasterxml.jackson.databind.node.ObjectNode;
39  import com.fasterxml.jackson.datatype.guava.GuavaModule;
40  import com.fasterxml.jackson.datatype.jdk7.Jdk7Module;
41  import com.fasterxml.jackson.datatype.joda.JodaModule;
42  import com.typesafe.config.Config;
43  import com.typesafe.config.ConfigList;
44  import com.typesafe.config.ConfigObject;
45  import com.typesafe.config.ConfigOrigin;
46  import com.typesafe.config.ConfigValue;
47  
48  import static com.fasterxml.jackson.databind.DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY;
49  import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_READING_DUP_TREE_KEY;
50  import static com.fasterxml.jackson.databind.MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS;
51  import static com.fasterxml.jackson.databind.MapperFeature.INFER_PROPERTY_MUTATORS;
52  import static com.fasterxml.jackson.databind.MapperFeature.USE_GETTERS_AS_SETTERS;
53  
54  /**
55   * Utility methods for interacting with the jackson library and creating more customized
56   * object mappers.
57   */
58  @Beta
59  public final class Jackson {
60      private Jackson() {}
61  
62      public static final ObjectMapper SIMPLE_MAPPER = new ObjectMapper();
63      public static final JavaType NODE_TYPE = SIMPLE_MAPPER.constructType(JsonNode.class);
64  
65      public static ObjectMapper defaultMapper() {
66          return DefaultCodecJackson.DEFAULT_MAPPER;
67      }
68  
69      public static CodecJackson defaultCodec() {
70          return DefaultCodecJackson.DEFAULT_CODEC;
71      }
72  
73      public static Validator defaultValidator() {
74          return DefaultCodecJackson.DEFAULT_VALIDATOR;
75      }
76  
77      public static ObjectMapper newObjectMapper(PluginRegistry pluginRegistry) {
78          CodecModule codecModule = new CodecModule(pluginRegistry);
79          Config globalConfig = pluginRegistry.config();
80          ObjectMapper objectMapper = new ObjectMapper();
81          toggleObjectMapperOptions(objectMapper);
82          objectMapper.registerModule(codecModule);
83          registerExtraModules(objectMapper);
84          allowCommentsAndUnquotedFields(objectMapper);
85          objectMapper.setInjectableValues(new EmptyInjectableValues());
86          return objectMapper;
87      }
88  
89      public static ObjectMapper registerExtraModules(ObjectMapper objectMapper) {
90          objectMapper.registerModule(new GuavaModule());
91          objectMapper.registerModule(new Jdk7Module());
92          objectMapper.registerModule(new JodaModule());
93          objectMapper.registerModule(new ExecutorsModule());
94          return objectMapper;
95      }
96  
97      public static ObjectMapper allowCommentsAndUnquotedFields(ObjectMapper objectMapper) {
98          objectMapper.configure(JsonParser.Feature.ALLOW_COMMENTS, true);
99          objectMapper.configure(JsonParser.Feature.ALLOW_YAML_COMMENTS, true);
100         objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
101         return objectMapper;
102     }
103 
104     public static ObjectMapper toggleObjectMapperOptions(ObjectMapper objectMapper) {
105         // potentially useful features, but disabled by default to maintain existing behavior
106 
107         // ignore final fields
108         objectMapper.disable(ALLOW_FINAL_FIELDS_AS_MUTATORS);
109         // do not try to modify existing containers
110         objectMapper.disable(USE_GETTERS_AS_SETTERS);
111         // public getters do not automaticaly imply codec should try to write to it
112         objectMapper.disable(INFER_PROPERTY_MUTATORS);
113 
114         // more aggressive failure detection
115         objectMapper.enable(FAIL_ON_READING_DUP_TREE_KEY);
116 
117         // essentially auto-collection everywhere, but that seems fine and this is easy
118         objectMapper.enable(ACCEPT_SINGLE_VALUE_AS_ARRAY);
119 
120         // don't write out null fields
121         objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
122         return objectMapper;
123     }
124 
125     public static JsonNode configConverter(ConfigValue source) {
126         return SIMPLE_MAPPER.convertValue(source.unwrapped(), NODE_TYPE);
127     }
128 
129     public static ObjectNode configConverter(ConfigObject source) {
130         return SIMPLE_MAPPER.convertValue(source.unwrapped(), NODE_TYPE);
131     }
132 
133     public static void merge(ObjectNode primary, ObjectNode backup) {
134         Iterator<String> fieldNames = backup.fieldNames();
135         while (fieldNames.hasNext()) {
136             String fieldName = fieldNames.next();
137             JsonNode primaryValue = primary.get(fieldName);
138             if (primaryValue == null) {
139                 JsonNode backupValue = backup.get(fieldName).deepCopy();
140                 primary.set(fieldName, backupValue);
141             } else if (primaryValue.isObject()) {
142                 JsonNode backupValue = backup.get(fieldName);
143                 if (backupValue.isObject()) {
144                     merge((ObjectNode) primaryValue, (ObjectNode) backupValue.deepCopy());
145                 }
146             }
147         }
148     }
149 
150     private static final Splitter dotSplitter = Splitter.on('.');
151 
152     public static void setAt(ObjectNode root, JsonNode value, String path) {
153         if (path.indexOf('.') >= 0) {
154             Iterator<String> pathIterator = dotSplitter.split(path).iterator();
155             while (pathIterator.hasNext()) {
156                 String nextPart = pathIterator.next();
157                 if (pathIterator.hasNext()) {
158                     root = root.with(nextPart);
159                 } else {
160                     root.set(nextPart, value);
161                 }
162             }
163         } else {
164             root.set(path, value);
165         }
166     }
167 
168     public static IOException maybeUnwrapPath(String pathToSkip, IOException cause) {
169         if ((pathToSkip != null) && (cause instanceof JsonMappingException)) {
170             JsonMappingException mappingException = (JsonMappingException) cause;
171             List<JsonMappingException.Reference> paths = mappingException.getPath();
172             if (!paths.isEmpty()) {
173                 Iterator<String> pathIterator = dotSplitter.split(pathToSkip).iterator();
174                 Iterator<JsonMappingException.Reference> refIterator = paths.iterator();
175                 while (pathIterator.hasNext()) {
176                     String pathToSkipPart = pathIterator.next();
177                     if (!refIterator.hasNext()) {
178                         return cause;
179                     }
180                     String nextRefField = refIterator.next().getFieldName();
181                     if (!pathToSkipPart.equals(nextRefField)) {
182                         return cause;
183                     }
184                 }
185                 JsonMappingException unwrapped = new JsonMappingException(rootMessage(mappingException),
186                                                                           mappingException.getLocation(),
187                                                                           mappingException.getCause());
188                 if (refIterator.hasNext()) {
189                     List<JsonMappingException.Reference> remainingRefs = Lists.newArrayList(refIterator);
190                     for (JsonMappingException.Reference reference : Lists.reverse(remainingRefs)) {
191                         unwrapped.prependPath(reference);
192                     }
193                 }
194                 return unwrapped;
195             }
196         }
197         return cause;
198     }
199 
200     public static boolean isRealLocation(JsonLocation jsonLocation) {
201         return (jsonLocation != null) && (jsonLocation != JsonLocation.NA);
202     }
203 
204     public static JsonMappingException maybeImproveLocation(JsonLocation wrapLoc, JsonMappingException cause) {
205         JsonLocation exLoc = cause.getLocation();
206         if (isRealLocation(wrapLoc) && !isRealLocation(exLoc)) {
207             if (wrapLoc.getSourceRef() instanceof ConfigValue) {
208                 ConfigValue locRef = (ConfigValue) wrapLoc.getSourceRef();
209                 List<JsonMappingException.Reference> paths = cause.getPath();
210                 for (JsonMappingException.Reference path : paths) {
211                     if (locRef instanceof ConfigObject) {
212                         String fieldName = path.getFieldName();
213                         ConfigObject locRefObject = (ConfigObject) locRef;
214                         if (locRefObject.containsKey(fieldName)) {
215                             locRef = locRefObject.get(fieldName);
216                         } else {
217                             break;
218                         }
219                     } else if (locRef instanceof ConfigList) {
220                         int fieldIndex = path.getIndex();
221                         ConfigList locRefList = (ConfigList) locRef;
222                         if ((fieldIndex >= 0) && (locRefList.size() > fieldIndex)) {
223                             locRef = locRefList.get(fieldIndex);
224                         } else {
225                             break;
226                         }
227                     } else {
228                         break;
229                     }
230                 }
231                 if (locRef != wrapLoc.getSourceRef()) {
232                     wrapLoc = fromConfigValue(locRef);
233                 }
234             }
235             List<JsonMappingException.Reference> paths = Lists.reverse(cause.getPath());
236             if (!paths.isEmpty()) {
237                 JsonMappingException withLoc = new JsonMappingException(rootMessage(cause), wrapLoc, cause);
238                 for (JsonMappingException.Reference path : paths) {
239                     withLoc.prependPath(path);
240                 }
241                 return withLoc;
242             } else {
243                 return new JsonMappingException(rootMessage(cause), wrapLoc, cause);
244             }
245         }
246         return cause;
247     }
248 
249     public static String rootMessage(JsonMappingException ex) {
250         String rootMessage = ex.getOriginalMessage();
251         if (ex instanceof PropertyBindingException) {
252             String suffix = ((PropertyBindingException) ex).getMessageSuffix();
253             if (rootMessage == null) {
254                 return suffix;
255             } else if (suffix != null) {
256                 return rootMessage + suffix;
257             }
258         }
259         return rootMessage;
260     }
261 
262     public static JsonLocation fromConfigValue(ConfigValue configValue) {
263         ConfigOrigin configOrigin = configValue.origin();
264         return new JsonLocation(configValue, -1, configOrigin.lineNumber(), -1);
265     }
266 
267     public static JsonNode pathAt(ObjectNode root, String path) {
268         if (path.indexOf('.') >= 0) {
269             JsonNode returnNode = root;
270             for (String nextPart : dotSplitter.split(path)) {
271                 returnNode = returnNode.path(nextPart);
272             }
273             return returnNode;
274         } else {
275             return root.path(path);
276         }
277     }
278 }