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<Integer></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