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.annotation.Nullable;
17  
18  import java.io.IOException;
19  
20  import java.util.Iterator;
21  
22  import com.addthis.codec.plugins.PluginMap;
23  
24  import com.fasterxml.jackson.annotation.JsonTypeInfo;
25  import com.fasterxml.jackson.core.JsonLocation;
26  import com.fasterxml.jackson.core.JsonParser;
27  import com.fasterxml.jackson.core.JsonToken;
28  import com.fasterxml.jackson.core.ObjectCodec;
29  import com.fasterxml.jackson.databind.BeanProperty;
30  import com.fasterxml.jackson.databind.DeserializationContext;
31  import com.fasterxml.jackson.databind.JavaType;
32  import com.fasterxml.jackson.databind.JsonDeserializer;
33  import com.fasterxml.jackson.databind.JsonMappingException;
34  import com.fasterxml.jackson.databind.JsonNode;
35  import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
36  import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
37  import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer;
38  import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
39  import com.fasterxml.jackson.databind.jsontype.impl.TypeDeserializerBase;
40  import com.fasterxml.jackson.databind.node.ArrayNode;
41  import com.fasterxml.jackson.databind.node.ObjectNode;
42  import com.typesafe.config.Config;
43  import com.typesafe.config.ConfigObject;
44  
45  import static com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath;
46  
47  public class CodecTypeDeserializer extends TypeDeserializerBase {
48      private final PluginMap pluginMap;
49      private final JsonTypeInfo.As inludeAs;
50      private final CodecTypeIdResolver idRes;
51  
52      protected CodecTypeDeserializer(PluginMap pluginMap, JsonTypeInfo.As inludeAs,
53                                      JavaType baseType, CodecTypeIdResolver idRes,
54                                      String typePropertyName, boolean typeIdVisible,
55                                      Class<?> defaultImpl) {
56          super(baseType, idRes, typePropertyName, typeIdVisible, defaultImpl);
57          this.pluginMap = pluginMap;
58          this.inludeAs = inludeAs;
59          this.idRes = idRes;
60      }
61  
62      protected CodecTypeDeserializer(CodecTypeDeserializer src, BeanProperty property) {
63          super(src, property);
64          this.pluginMap = src.pluginMap;
65          this.inludeAs = src.inludeAs;
66          this.idRes = src.idRes;
67      }
68  
69      @Override public CodecTypeDeserializer forProperty(BeanProperty prop) {
70          return (prop == _property) ? this : new CodecTypeDeserializer(this, prop);
71      }
72  
73      @Override
74      public JsonTypeInfo.As getTypeInclusion() {
75          return inludeAs;
76      }
77  
78      // based on methods/ comments from other TypeDeserializers, these methods cannot be trusted
79      // to actually reflect the current json type. so reroute all to the switch and check ourselves.
80  
81      @Override
82      public Object deserializeTypedFromObject(JsonParser jp, DeserializationContext ctxt) throws IOException {
83          return deserializeTypedFromAny(jp, ctxt);
84      }
85  
86      @Override
87      public Object deserializeTypedFromArray(JsonParser jp, DeserializationContext ctxt) throws IOException {
88          return deserializeTypedFromAny(jp, ctxt);
89      }
90  
91      @Override
92      public Object deserializeTypedFromScalar(JsonParser jp, DeserializationContext ctxt) throws IOException {
93          return deserializeTypedFromAny(jp, ctxt);
94      }
95  
96      @Override
97      public Object deserializeTypedFromAny(JsonParser jp, DeserializationContext ctxt) throws IOException {
98          // a jackson thing we might as well include
99          if (jp.canReadTypeId()) {
100             Object typeId = jp.getTypeId();
101             if (typeId != null) {
102                 return _deserializeWithNativeTypeId(jp, ctxt, typeId);
103             }
104         }
105         // can use this to approximate error location if a sub-method throws an exception
106         JsonLocation currentLocation = jp.getTokenLocation();
107         JsonNode jsonNode;
108         // empty objects can appear with END_OBJECT. that has special handling lots of places, but not in readTree
109         if (jp.getCurrentToken() == JsonToken.END_OBJECT) {
110             jsonNode = ctxt.getNodeFactory().objectNode();
111         } else {
112             jsonNode = jp.readValueAsTree();
113         }
114         ObjectCodec objectCodec = jp.getCodec();
115 
116         try {
117             Object bean = null;
118             // _array handler
119             if (jsonNode.isArray()) {
120                 bean = _deserializeTypedFromArray((ArrayNode) jsonNode, objectCodec, ctxt);
121                 // object handler
122             } else if (jsonNode.isObject()) {
123                 bean = _deserializeTypedFromObject((ObjectNode) jsonNode, objectCodec, ctxt);
124             }
125             if (bean != null) {
126                 return bean;
127             } else {
128                 JsonDeserializer<Object> deser = _findDeserializer(ctxt, null);
129                 JsonParser treeParser = jp.getCodec().treeAsTokens(jsonNode);
130                 treeParser.nextToken();
131                 return deser.deserialize(treeParser, ctxt);
132             }
133         } catch (JsonMappingException ex) {
134             throw Jackson.maybeImproveLocation(currentLocation, ex);
135         }
136     }
137 
138     @Nullable public Object _deserializeTypedFromObject(ObjectNode objectNode,
139                                                         ObjectCodec objectCodec,
140                                                         DeserializationContext ctxt) throws IOException {
141         if (objectNode.hasNonNull(_typePropertyName)) {
142             return _deserializeObjectFromProperty(objectNode, objectCodec, ctxt);
143         }
144         String singleKeyName = getSingleKeyIfPresent(objectNode);
145         if (singleKeyName != null) {
146             Object bean = _deserializeObjectFromSingleKey(objectNode, singleKeyName, objectCodec, ctxt);
147             if (bean != null) {
148                 return bean;
149             }
150         }
151         Object bean = _deserializeObjectFromInlinedType(objectNode, objectCodec, ctxt);
152         if (bean != null) {
153             return bean;
154         }
155         if (idRes.isValidTypeId("_default")) {
156             ConfigObject aliasDefaults = pluginMap.aliasDefaults("_default");
157             JsonDeserializer<Object> deser = _findDeserializer(ctxt, "_default");
158             boolean unwrapPrimary = handleDefaultsAndImplicitPrimary(objectNode, aliasDefaults, deser, ctxt);
159             try {
160                 JsonParser treeParser = objectCodec.treeAsTokens(objectNode);
161                 treeParser.nextToken();
162                 bean = deser.deserialize(treeParser, ctxt);
163             } catch (IOException cause) {
164                 if (unwrapPrimary) {
165                     throw Jackson.maybeUnwrapPath((String) aliasDefaults.get("_primary").unwrapped(), cause);
166                 } else {
167                     throw cause;
168                 }
169             }
170         }
171         return bean;
172     }
173 
174     // returns non-null if there is only one key or only one non-meta-data key. ie. (_class, some-key) or (_some-key)
175     @Nullable private String getSingleKeyIfPresent(ObjectNode objectNode) {
176         Iterator<String> fieldNames = objectNode.fieldNames();
177         String singleKey = null;
178         while (fieldNames.hasNext()) {
179             String fieldName = fieldNames.next();
180             if ((singleKey == null) || (singleKey.charAt(0) == '_')) {
181                 singleKey = fieldName;
182             } else if (fieldName.charAt(0) != '_') {
183                 return null; // more than one key found
184             }
185         }
186         return singleKey;
187     }
188 
189     @Nullable private Object _deserializeObjectFromInlinedType(ObjectNode objectNode,
190                                                                ObjectCodec objectCodec,
191                                                                DeserializationContext ctxt) throws IOException {
192         String matched = null;
193         for (String alias : pluginMap.inlinedAliases()) {
194             if (objectNode.get(alias) != null) {
195                 if (matched != null) {
196                     String message = String.format(
197                             "no type specified, more than one key, and both %s and %s match for inlined types.",
198                             matched, alias);
199                     JsonMappingException exception = ctxt.instantiationException(_baseType.getRawClass(), message);
200                     exception.prependPath(_baseType, matched);
201                     throw exception;
202                 }
203                 matched = alias;
204             }
205         }
206         if (matched != null) {
207             ConfigObject aliasDefaults = pluginMap.aliasDefaults(matched);
208             JsonNode configValue = objectNode.get(matched);
209             String primaryField = (String) aliasDefaults.get("_primary").unwrapped();
210             objectNode.remove(matched);
211             Jackson.setAt(objectNode, configValue, primaryField);
212             Jackson.merge(objectNode, Jackson.configConverter(aliasDefaults));
213             if (_typeIdVisible) {
214                 objectNode.put(_typePropertyName, matched);
215             }
216             try {
217                 JsonDeserializer<Object> deser = _findDeserializer(ctxt, matched);
218                 JsonParser treeParser = objectCodec.treeAsTokens(objectNode);
219                 treeParser.nextToken();
220                 return deser.deserialize(treeParser, ctxt);
221             } catch (IOException cause) {
222                 IOException unwrapped = Jackson.maybeUnwrapPath(primaryField, cause);
223                 if (unwrapped != cause) {
224                     throw wrapWithPath(unwrapped, idRes.typeFromId(matched), matched);
225                 } else {
226                     throw unwrapped;
227                 }
228             }
229         } else {
230             return null;
231         }
232     }
233 
234     @Nullable private Object _deserializeObjectFromSingleKey(ObjectNode objectNode,
235                                                              String singleKeyName,
236                                                              ObjectCodec objectCodec,
237                                                              DeserializationContext ctxt) throws IOException {
238         if (idRes.isValidTypeId(singleKeyName)) {
239             ConfigObject aliasDefaults = pluginMap.aliasDefaults(singleKeyName);
240             String primaryField;
241             if (aliasDefaults.containsKey("_primary")) {
242                 primaryField = (String) aliasDefaults.get("_primary").unwrapped();
243             } else {
244                 primaryField = null;
245             }
246             boolean unwrapPrimary = false;
247             try {
248                 JsonNode singleKeyValue = objectNode.get(singleKeyName);
249                 JsonDeserializer<Object> deser = _findDeserializer(ctxt, singleKeyName);
250                 if (!singleKeyValue.isObject()) {
251                     // if value is not an object, try supporting _primary syntax to derive one
252                     if (primaryField != null) {
253                         ObjectNode singleKeyObject = (ObjectNode) objectCodec.createObjectNode();
254                         Jackson.setAt(singleKeyObject, singleKeyValue, primaryField);
255                         Jackson.merge(singleKeyObject, Jackson.configConverter(aliasDefaults));
256                         singleKeyValue = singleKeyObject;
257                         unwrapPrimary = true;
258                     } // else let the downstream serializer try to handle it or complain
259                 } else {
260                     ObjectNode singleKeyObject = (ObjectNode) singleKeyValue;
261                     unwrapPrimary = handleDefaultsAndImplicitPrimary(singleKeyObject, aliasDefaults, deser, ctxt);
262                 }
263                 if (_typeIdVisible && singleKeyValue.isObject()) {
264                     ((ObjectNode) singleKeyValue).put(_typePropertyName, singleKeyName);
265                 }
266                 JsonParser treeParser = objectCodec.treeAsTokens(singleKeyValue);
267                 treeParser.nextToken();
268                 return deser.deserialize(treeParser, ctxt);
269             } catch (IOException cause) {
270                 if (unwrapPrimary) {
271                     cause = Jackson.maybeUnwrapPath(primaryField, cause);
272                 }
273                 throw wrapWithPath(cause, idRes.typeFromId(singleKeyName), singleKeyName);
274             } catch (Throwable cause) {
275                 throw wrapWithPath(cause, idRes.typeFromId(singleKeyName), singleKeyName);
276             }
277         }
278         return null;
279     }
280 
281     private Object _deserializeObjectFromProperty(ObjectNode objectNode,
282                                                   ObjectCodec objectCodec,
283                                                   DeserializationContext ctxt) throws IOException {
284         String type = objectNode.get(_typePropertyName).asText();
285         if (!_typeIdVisible) {
286             objectNode.remove(_typePropertyName);
287         }
288         JsonDeserializer<Object> deser;
289         try {
290             deser = _findDeserializer(ctxt, type);
291         } catch (Throwable cause) {
292             throw wrapWithPath(cause, Class.class, _typePropertyName);
293         }
294         ConfigObject aliasDefaults = pluginMap.aliasDefaults(type);
295         String primaryField;
296         if (aliasDefaults.containsKey("_primary")) {
297             primaryField = (String) aliasDefaults.get("_primary").unwrapped();
298         } else {
299             primaryField = null;
300         }
301         boolean unwrapPrimary = handleDefaultsAndImplicitPrimary(objectNode, aliasDefaults, deser, ctxt);
302         try {
303             JsonParser treeParser = objectCodec.treeAsTokens(objectNode);
304             treeParser.nextToken();
305             return deser.deserialize(treeParser, ctxt);
306         } catch (IOException cause) {
307             if (unwrapPrimary) {
308                 throw Jackson.maybeUnwrapPath(primaryField, cause);
309             } else {
310                 throw cause;
311             }
312         }
313     }
314 
315     @Nullable private Object _deserializeTypedFromArray(ArrayNode arrayNode,
316                                                         ObjectCodec objectCodec,
317                                                         DeserializationContext ctxt) throws IOException {
318         if (idRes.isValidTypeId("_array")) {
319             Config aliasDefaults = pluginMap.aliasDefaults("_array").toConfig();
320             String arrayField = aliasDefaults.getString("_primary");
321             try {
322                 ObjectNode objectFieldValues = (ObjectNode) objectCodec.createObjectNode();
323                 Jackson.setAt(objectFieldValues, arrayNode, arrayField);
324                 ObjectNode aliasFieldDefaults = Jackson.configConverter(aliasDefaults.root());
325                 Jackson.merge(objectFieldValues, aliasFieldDefaults);
326                 JsonDeserializer<Object> deser = _findDeserializer(ctxt, "_array");
327                 JsonParser treeParser = objectCodec.treeAsTokens(objectFieldValues);
328                 treeParser.nextToken();
329                 return deser.deserialize(treeParser, ctxt);
330             } catch (IOException ex) {
331                 throw Jackson.maybeUnwrapPath(arrayField, ex);
332             }
333         } else {
334             return null;
335         }
336     }
337 
338     private boolean handleDefaultsAndImplicitPrimary(ObjectNode fieldValues,
339                                                      ConfigObject aliasDefaults,
340                                                      JsonDeserializer<?> deserializer,
341                                                      DeserializationContext ctxt) throws JsonMappingException {
342         if (!aliasDefaults.isEmpty()) {
343             if (deserializer instanceof DelegatingDeserializer) {
344                 deserializer = ((DelegatingDeserializer) deserializer).getDelegatee();
345             }
346             if ((deserializer instanceof BeanDeserializerBase) && (aliasDefaults.get("_primary") != null)) {
347                 BeanDeserializerBase beanDeserializer = (BeanDeserializerBase) deserializer;
348                 String primaryField = (String) aliasDefaults.get("_primary").unwrapped();
349                 if (!fieldValues.has(primaryField)) {
350                     // user has not explicitly set a value where _primary points, see if _primary is a plugin type
351                     SettableBeanProperty primaryProperty = beanDeserializer.findProperty(primaryField);
352                     if ((primaryProperty != null) && primaryProperty.hasValueTypeDeserializer()) {
353                         TypeDeserializer primaryTypeDeserializer = primaryProperty.getValueTypeDeserializer();
354                         if (primaryTypeDeserializer instanceof CodecTypeDeserializer) {
355                             CodecTypeIdResolver primaryPropertyTypeIdResolver =
356                                     ((CodecTypeDeserializer) primaryTypeDeserializer).idRes;
357                             String possibleInlinedPrimary = null;
358                             Iterator<String> fieldNames = fieldValues.fieldNames();
359                             while (fieldNames.hasNext()) {
360                                 String fieldName = fieldNames.next();
361                                 if ((fieldName.charAt(0) != '_') && !beanDeserializer.hasProperty(fieldName)) {
362                                     if (primaryPropertyTypeIdResolver.isValidTypeId(fieldName)) {
363                                         if (possibleInlinedPrimary == null) {
364                                             possibleInlinedPrimary = fieldName;
365                                         } else {
366                                             String message = String.format(
367                                                     "%s and %s are both otherwise unknown properties that "
368                                                     + "could be types for the _primary property %s whose category is "
369                                                     + "%s. This is too ambiguous to resolve.",
370                                                     possibleInlinedPrimary, fieldName, primaryField,
371                                                     ((CodecTypeDeserializer) primaryTypeDeserializer)
372                                                             .pluginMap.category());
373                                             JsonMappingException ex =
374                                                     ctxt.instantiationException(_baseType.getRawClass(), message);
375                                             ex.prependPath(beanDeserializer.getValueType(), fieldName);
376                                             throw ex;
377                                         }
378                                     }
379                                 }
380                             }
381                             // did we find a good candidate?
382                             if (possibleInlinedPrimary != null) {
383                                 // then wrap the value with its key (its type), and stash it in our primary field
384                                 JsonNode inlinedPrimaryValue = fieldValues.remove(possibleInlinedPrimary);
385                                 fieldValues.with(primaryField).set(possibleInlinedPrimary, inlinedPrimaryValue);
386                                 Jackson.merge(fieldValues, Jackson.configConverter(aliasDefaults));
387                                 return true;
388                             }
389                         }
390                     }
391                 }
392             }
393             // merge alias defaults here since we check for empty etc anyway
394             Jackson.merge(fieldValues, Jackson.configConverter(aliasDefaults));
395         }
396         return false;
397     }
398 }