1
2
3
4
5
6
7
8
9
10
11
12
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
79
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
99 if (jp.canReadTypeId()) {
100 Object typeId = jp.getTypeId();
101 if (typeId != null) {
102 return _deserializeWithNativeTypeId(jp, ctxt, typeId);
103 }
104 }
105
106 JsonLocation currentLocation = jp.getTokenLocation();
107 JsonNode jsonNode;
108
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
119 if (jsonNode.isArray()) {
120 bean = _deserializeTypedFromArray((ArrayNode) jsonNode, objectCodec, ctxt);
121
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
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;
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
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 }
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
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
382 if (possibleInlinedPrimary != null) {
383
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
394 Jackson.merge(fieldValues, Jackson.configConverter(aliasDefaults));
395 }
396 return false;
397 }
398 }