• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.fasterxml.jackson.databind.deser.impl;
2 
3 import java.io.IOException;
4 import java.util.*;
5 
6 import com.fasterxml.jackson.core.*;
7 import com.fasterxml.jackson.databind.*;
8 import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
9 import com.fasterxml.jackson.databind.jsontype.TypeDeserializer;
10 import com.fasterxml.jackson.databind.util.TokenBuffer;
11 
12 /**
13  * Helper class that is used to flatten JSON structure when using
14  * "external type id" (see {@link com.fasterxml.jackson.annotation.JsonTypeInfo.As#EXTERNAL_PROPERTY}).
15  * This is needed to store temporary state and buffer tokens, as the structure is
16  * rearranged a bit so that actual type deserializer can resolve type and
17  * finalize deserialization.
18  */
19 public class ExternalTypeHandler
20 {
21     private final JavaType _beanType;
22 
23     private final ExtTypedProperty[] _properties;
24 
25     /**
26      * Mapping from external property ids to one or more indexes;
27      * in most cases single index as <code>Integer</code>, but
28      * occasionally same name maps to multiple ones: if so,
29      * <code>List&lt;Integer&gt;</code>.
30      */
31     private final Map<String, Object> _nameToPropertyIndex;
32 
33     private final String[] _typeIds;
34     private final TokenBuffer[] _tokens;
35 
ExternalTypeHandler(JavaType beanType, ExtTypedProperty[] properties, Map<String, Object> nameToPropertyIndex, String[] typeIds, TokenBuffer[] tokens)36     protected ExternalTypeHandler(JavaType beanType,
37             ExtTypedProperty[] properties,
38             Map<String, Object> nameToPropertyIndex,
39             String[] typeIds, TokenBuffer[] tokens)
40     {
41         _beanType = beanType;
42         _properties = properties;
43         _nameToPropertyIndex = nameToPropertyIndex;
44         _typeIds = typeIds;
45         _tokens = tokens;
46     }
47 
ExternalTypeHandler(ExternalTypeHandler h)48     protected ExternalTypeHandler(ExternalTypeHandler h)
49     {
50         _beanType = h._beanType;
51         _properties = h._properties;
52         _nameToPropertyIndex = h._nameToPropertyIndex;
53         int len = _properties.length;
54         _typeIds = new String[len];
55         _tokens = new TokenBuffer[len];
56     }
57 
58     /**
59      * @since 2.9
60      */
builder(JavaType beanType)61     public static Builder builder(JavaType beanType) {
62         return new Builder(beanType);
63     }
64 
65     /**
66      * Method called to start collection process by creating non-blueprint
67      * instances.
68      */
start()69     public ExternalTypeHandler start() {
70         return new ExternalTypeHandler(this);
71     }
72 
73     /**
74      * Method called to see if given property/value pair is an external type
75      * id; and if so handle it. This is <b>only</b> to be called in case
76      * containing POJO has similarly named property as the external type id AND
77      * value is of scalar type:
78      * otherwise {@link #handlePropertyValue} should be called instead.
79      */
80     @SuppressWarnings("unchecked")
handleTypePropertyValue(JsonParser p, DeserializationContext ctxt, String propName, Object bean)81     public boolean handleTypePropertyValue(JsonParser p, DeserializationContext ctxt,
82             String propName, Object bean)
83         throws IOException
84     {
85         Object ob = _nameToPropertyIndex.get(propName);
86         if (ob == null) {
87             return false;
88         }
89         final String typeId = p.getText();
90         // 28-Nov-2016, tatu: For [databind#291], need separate handling
91         if (ob instanceof List<?>) {
92             boolean result = false;
93             for (Integer index : (List<Integer>) ob) {
94                 if (_handleTypePropertyValue(p, ctxt, propName, bean,
95                         typeId, index.intValue())) {
96                     result = true;
97                 }
98             }
99             return result;
100         }
101         return _handleTypePropertyValue(p, ctxt, propName, bean,
102                 typeId, ((Integer) ob).intValue());
103     }
104 
_handleTypePropertyValue(JsonParser p, DeserializationContext ctxt, String propName, Object bean, String typeId, int index)105     private final boolean _handleTypePropertyValue(JsonParser p, DeserializationContext ctxt,
106             String propName, Object bean, String typeId, int index)
107         throws IOException
108     {
109         ExtTypedProperty prop = _properties[index];
110         if (!prop.hasTypePropertyName(propName)) { // when could/should this ever happen?
111             return false;
112         }
113         // note: can NOT skip child values (should always be String anyway)
114         boolean canDeserialize = (bean != null) && (_tokens[index] != null);
115         // Minor optimization: deserialize properties as soon as we have all we need:
116         if (canDeserialize) {
117             _deserializeAndSet(p, ctxt, bean, index, typeId);
118             // clear stored data, to avoid deserializing+setting twice:
119             _tokens[index] = null;
120         } else {
121             _typeIds[index] = typeId;
122         }
123         return true;
124     }
125 
126     /**
127      * Method called to ask handler to handle value of given property,
128      * at point where parser points to the first token of the value.
129      * Handling can mean either resolving type id it contains (if it matches type
130      * property name), or by buffering the value for further use.
131      *
132      * @return True, if the given property was properly handled
133      */
134     @SuppressWarnings("unchecked")
handlePropertyValue(JsonParser p, DeserializationContext ctxt, String propName, Object bean)135     public boolean handlePropertyValue(JsonParser p, DeserializationContext ctxt,
136             String propName, Object bean) throws IOException
137     {
138         Object ob = _nameToPropertyIndex.get(propName);
139         if (ob == null) {
140             return false;
141         }
142         // 28-Nov-2016, tatu: For [databind#291], need separate handling
143         if (ob instanceof List<?>) {
144             Iterator<Integer> it = ((List<Integer>) ob).iterator();
145             Integer index = it.next();
146 
147             ExtTypedProperty prop = _properties[index];
148             // For now, let's assume it's same type (either type id OR value)
149             // for all mappings, so we'll only check first one
150             if (prop.hasTypePropertyName(propName)) {
151                 String typeId = p.getText();
152                 p.skipChildren();
153                 _typeIds[index] = typeId;
154                 while (it.hasNext()) {
155                     _typeIds[it.next()] = typeId;
156                 }
157             } else {
158                 @SuppressWarnings("resource")
159                 TokenBuffer tokens = new TokenBuffer(p, ctxt);
160                 tokens.copyCurrentStructure(p);
161                 _tokens[index] = tokens;
162                 while (it.hasNext()) {
163                     _tokens[it.next()] = tokens;
164                 }
165             }
166             return true;
167         }
168 
169         // Otherwise only maps to a single value, in which case we can
170         // handle things in bit more optimal way...
171         int index = ((Integer) ob).intValue();
172         ExtTypedProperty prop = _properties[index];
173         boolean canDeserialize;
174         if (prop.hasTypePropertyName(propName)) {
175             _typeIds[index] = p.getText();
176             p.skipChildren();
177             canDeserialize = (bean != null) && (_tokens[index] != null);
178         } else {
179             @SuppressWarnings("resource")
180             TokenBuffer tokens = new TokenBuffer(p, ctxt);
181             tokens.copyCurrentStructure(p);
182             _tokens[index] = tokens;
183             canDeserialize = (bean != null) && (_typeIds[index] != null);
184         }
185         // Minor optimization: let's deserialize properties as soon as
186         // we have all pertinent information:
187         if (canDeserialize) {
188             String typeId = _typeIds[index];
189             // clear stored data, to avoid deserializing+setting twice:
190             _typeIds[index] = null;
191             _deserializeAndSet(p, ctxt, bean, index, typeId);
192             _tokens[index] = null;
193         }
194         return true;
195     }
196 
197     /**
198      * Method called after JSON Object closes, and has to ensure that all external
199      * type ids have been handled.
200      */
201     @SuppressWarnings("resource")
complete(JsonParser p, DeserializationContext ctxt, Object bean)202     public Object complete(JsonParser p, DeserializationContext ctxt, Object bean)
203         throws IOException
204     {
205         for (int i = 0, len = _properties.length; i < len; ++i) {
206             String typeId = _typeIds[i];
207             if (typeId == null) {
208                 TokenBuffer tokens = _tokens[i];
209                 // let's allow missing both type and property (may already have been set, too)
210                 // but not just one
211                 if (tokens == null) {
212                     continue;
213                 }
214                 // [databind#118]: Need to mind natural types, for which no type id
215                 // will be included.
216                 JsonToken t = tokens.firstToken();
217                 if (t.isScalarValue()) { // can't be null as we never store empty buffers
218                     JsonParser buffered = tokens.asParser(p);
219                     buffered.nextToken();
220                     SettableBeanProperty extProp = _properties[i].getProperty();
221                     Object result = TypeDeserializer.deserializeIfNatural(buffered, ctxt, extProp.getType());
222                     if (result != null) {
223                         extProp.set(bean, result);
224                         continue;
225                     }
226                     // 26-Oct-2012, tatu: As per [databind#94], must allow use of 'defaultImpl'
227                     if (!_properties[i].hasDefaultType()) {
228                         ctxt.reportPropertyInputMismatch(bean.getClass(), extProp.getName(),
229                                 "Missing external type id property '%s'",
230                                 _properties[i].getTypePropertyName());
231                     } else  {
232                         typeId = _properties[i].getDefaultTypeId();
233                     }
234                 }
235             } else if (_tokens[i] == null) {
236                 SettableBeanProperty prop = _properties[i].getProperty();
237 
238                 if(prop.isRequired() ||
239                         ctxt.isEnabled(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY)) {
240                     ctxt.reportPropertyInputMismatch(bean.getClass(), prop.getName(),
241                             "Missing property '%s' for external type id '%s'",
242                             prop.getName(), _properties[i].getTypePropertyName());
243                 }
244                 return bean;
245             }
246             _deserializeAndSet(p, ctxt, bean, i, typeId);
247         }
248         return bean;
249     }
250 
251     /**
252      * Variant called when creation of the POJO involves buffering of creator properties
253      * as well as property-based creator.
254      */
complete(JsonParser p, DeserializationContext ctxt, PropertyValueBuffer buffer, PropertyBasedCreator creator)255     public Object complete(JsonParser p, DeserializationContext ctxt,
256             PropertyValueBuffer buffer, PropertyBasedCreator creator)
257         throws IOException
258     {
259         // first things first: deserialize all data buffered:
260         final int len = _properties.length;
261         Object[] values = new Object[len];
262         for (int i = 0; i < len; ++i) {
263             String typeId = _typeIds[i];
264             final ExtTypedProperty extProp = _properties[i];
265             if (typeId == null) {
266                 // let's allow missing both type and property (may already have been set, too)
267                 if (_tokens[i] == null) {
268                     continue;
269                 }
270                 // but not just one
271                 // 26-Oct-2012, tatu: As per [databind#94], must allow use of 'defaultImpl'
272                 if (!extProp.hasDefaultType()) {
273                     ctxt.reportPropertyInputMismatch(_beanType, extProp.getProperty().getName(),
274                             "Missing external type id property '%s'",
275                             extProp.getTypePropertyName());
276                 } else {
277                     typeId = extProp.getDefaultTypeId();
278                 }
279             } else if (_tokens[i] == null) {
280                 SettableBeanProperty prop = extProp.getProperty();
281                 if (prop.isRequired() ||
282                         ctxt.isEnabled(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY)) {
283                     ctxt.reportPropertyInputMismatch(_beanType, prop.getName(),
284                             "Missing property '%s' for external type id '%s'",
285                             prop.getName(), _properties[i].getTypePropertyName());
286                 }
287             }
288             if (_tokens[i] != null) {
289                 values[i] = _deserialize(p, ctxt, i, typeId);
290             }
291 
292             final SettableBeanProperty prop = extProp.getProperty();
293             // also: if it's creator prop, fill in
294             if (prop.getCreatorIndex() >= 0) {
295                 buffer.assignParameter(prop, values[i]);
296 
297                 // [databind#999] And maybe there's creator property for type id too?
298                 SettableBeanProperty typeProp = extProp.getTypeProperty();
299                 // for now, should only be needed for creator properties, too
300                 if ((typeProp != null) && (typeProp.getCreatorIndex() >= 0)) {
301                     // 31-May-2018, tatu: [databind#1328] if id is NOT plain `String`, need to
302                     //    apply deserializer... fun fun.
303                     final Object v;
304                     if (typeProp.getType().hasRawClass(String.class)) {
305                         v = typeId;
306                     } else {
307                         TokenBuffer tb = new TokenBuffer(p, ctxt);
308                         tb.writeString(typeId);
309                         v = typeProp.getValueDeserializer().deserialize(tb.asParserOnFirstToken(), ctxt);
310                         tb.close();
311                     }
312                     buffer.assignParameter(typeProp, v);
313                 }
314             }
315         }
316         Object bean = creator.build(ctxt, buffer);
317         // third: assign non-creator properties
318         for (int i = 0; i < len; ++i) {
319             SettableBeanProperty prop = _properties[i].getProperty();
320             if (prop.getCreatorIndex() < 0) {
321                 prop.set(bean, values[i]);
322             }
323         }
324         return bean;
325     }
326 
327     @SuppressWarnings("resource")
_deserialize(JsonParser p, DeserializationContext ctxt, int index, String typeId)328     protected final Object _deserialize(JsonParser p, DeserializationContext ctxt,
329             int index, String typeId) throws IOException
330     {
331         JsonParser p2 = _tokens[index].asParser(p);
332         JsonToken t = p2.nextToken();
333         // 29-Sep-2015, tatu: As per [databind#942], nulls need special support
334         if (t == JsonToken.VALUE_NULL) {
335             return null;
336         }
337         TokenBuffer merged = new TokenBuffer(p, ctxt);
338         merged.writeStartArray();
339         merged.writeString(typeId);
340         merged.copyCurrentStructure(p2);
341         merged.writeEndArray();
342 
343         // needs to point to START_OBJECT (or whatever first token is)
344         JsonParser mp = merged.asParser(p);
345         mp.nextToken();
346         return _properties[index].getProperty().deserialize(mp, ctxt);
347     }
348 
349     @SuppressWarnings("resource")
_deserializeAndSet(JsonParser p, DeserializationContext ctxt, Object bean, int index, String typeId)350     protected final void _deserializeAndSet(JsonParser p, DeserializationContext ctxt,
351             Object bean, int index, String typeId) throws IOException
352     {
353         // Ok: time to mix type id, value; and we will actually use "wrapper-array"
354         // style to ensure we can handle all kinds of JSON constructs.
355         JsonParser p2 = _tokens[index].asParser(p);
356         JsonToken t = p2.nextToken();
357         // 29-Sep-2015, tatu: As per [databind#942], nulls need special support
358         if (t == JsonToken.VALUE_NULL) {
359             _properties[index].getProperty().set(bean, null);
360             return;
361         }
362         TokenBuffer merged = new TokenBuffer(p, ctxt);
363         merged.writeStartArray();
364         merged.writeString(typeId);
365 
366         merged.copyCurrentStructure(p2);
367         merged.writeEndArray();
368         // needs to point to START_OBJECT (or whatever first token is)
369         JsonParser mp = merged.asParser(p);
370         mp.nextToken();
371         _properties[index].getProperty().deserializeAndSet(mp, ctxt, bean);
372     }
373 
374     /*
375     /**********************************************************
376     /* Helper classes
377     /**********************************************************
378      */
379 
380     public static class Builder
381     {
382         private final JavaType _beanType;
383 
384         private final List<ExtTypedProperty> _properties = new ArrayList<>();
385         private final Map<String, Object> _nameToPropertyIndex = new HashMap<>();
386 
Builder(JavaType t)387         protected Builder(JavaType t) {
388             _beanType = t;
389         }
390 
addExternal(SettableBeanProperty property, TypeDeserializer typeDeser)391         public void addExternal(SettableBeanProperty property, TypeDeserializer typeDeser)
392         {
393             Integer index = _properties.size();
394             _properties.add(new ExtTypedProperty(property, typeDeser));
395             _addPropertyIndex(property.getName(), index);
396             _addPropertyIndex(typeDeser.getPropertyName(), index);
397         }
398 
_addPropertyIndex(String name, Integer index)399         private void _addPropertyIndex(String name, Integer index) {
400             Object ob = _nameToPropertyIndex.get(name);
401             if (ob == null) {
402                 _nameToPropertyIndex.put(name, index);
403             } else if (ob instanceof List<?>) {
404                 @SuppressWarnings("unchecked")
405                 List<Object> list = (List<Object>) ob;
406                 list.add(index);
407             } else {
408                 List<Object> list = new LinkedList<>();
409                 list.add(ob);
410                 list.add(index);
411                 _nameToPropertyIndex.put(name, list);
412             }
413         }
414 
415         /**
416          * Method called after all external properties have been assigned, to further
417          * link property with polymorphic value with possible property for type id
418          * itself. This is needed to support type ids as Creator properties.
419          *
420          * @since 2.8
421          */
build(BeanPropertyMap otherProps)422         public ExternalTypeHandler build(BeanPropertyMap otherProps) {
423             // 21-Jun-2016, tatu: as per [databind#999], may need to link type id property also
424             final int len = _properties.size();
425             ExtTypedProperty[] extProps = new ExtTypedProperty[len];
426             for (int i = 0; i < len; ++i) {
427                 ExtTypedProperty extProp = _properties.get(i);
428                 String typePropId = extProp.getTypePropertyName();
429                 SettableBeanProperty typeProp = otherProps.find(typePropId);
430                 if (typeProp != null) {
431                     extProp.linkTypeProperty(typeProp);
432                 }
433                 extProps[i] = extProp;
434             }
435             return new ExternalTypeHandler(_beanType, extProps, _nameToPropertyIndex,
436                     null, null);
437         }
438     }
439 
440     private final static class ExtTypedProperty
441     {
442         private final SettableBeanProperty _property;
443         private final TypeDeserializer _typeDeserializer;
444         private final String _typePropertyName;
445 
446         /**
447          * @since 2.8
448          */
449         private SettableBeanProperty _typeProperty;
450 
ExtTypedProperty(SettableBeanProperty property, TypeDeserializer typeDeser)451         public ExtTypedProperty(SettableBeanProperty property, TypeDeserializer typeDeser)
452         {
453             _property = property;
454             _typeDeserializer = typeDeser;
455             _typePropertyName = typeDeser.getPropertyName();
456         }
457 
458         /**
459          * @since 2.8
460          */
linkTypeProperty(SettableBeanProperty p)461         public void linkTypeProperty(SettableBeanProperty p) {
462             _typeProperty = p;
463         }
464 
hasTypePropertyName(String n)465         public boolean hasTypePropertyName(String n) {
466             return n.equals(_typePropertyName);
467         }
468 
hasDefaultType()469         public boolean hasDefaultType() {
470             return _typeDeserializer.getDefaultImpl() != null;
471         }
472 
473         /**
474          * Specialized called when we need to expose type id of `defaultImpl` when
475          * serializing: we may need to expose it for assignment to a property, or
476          * it may be requested as visible for some other reason.
477          */
getDefaultTypeId()478         public String getDefaultTypeId() {
479             Class<?> defaultType = _typeDeserializer.getDefaultImpl();
480             if (defaultType == null) {
481                 return null;
482             }
483             return _typeDeserializer.getTypeIdResolver().idFromValueAndType(null, defaultType);
484         }
485 
getTypePropertyName()486         public String getTypePropertyName() { return _typePropertyName; }
487 
getProperty()488         public SettableBeanProperty getProperty() {
489             return _property;
490         }
491 
492         /**
493          * @since 2.8
494          */
getTypeProperty()495         public SettableBeanProperty getTypeProperty() {
496             return _typeProperty;
497         }
498     }
499 }
500