1 package com.fasterxml.jackson.databind.objectid; 2 3 import java.util.HashMap; 4 import java.util.Iterator; 5 import java.util.List; 6 import java.util.Map; 7 import java.util.Map.Entry; 8 9 import com.fasterxml.jackson.annotation.JsonAnySetter; 10 import com.fasterxml.jackson.annotation.JsonIdentityInfo; 11 import com.fasterxml.jackson.annotation.JsonIdentityReference; 12 import com.fasterxml.jackson.annotation.ObjectIdGenerator.IdKey; 13 import com.fasterxml.jackson.annotation.ObjectIdGenerators; 14 import com.fasterxml.jackson.annotation.ObjectIdResolver; 15 import com.fasterxml.jackson.databind.BaseMapTest; 16 import com.fasterxml.jackson.databind.DeserializationContext; 17 import com.fasterxml.jackson.databind.DeserializationFeature; 18 import com.fasterxml.jackson.databind.ObjectMapper; 19 import com.fasterxml.jackson.databind.cfg.ContextAttributes; 20 import com.fasterxml.jackson.databind.deser.UnresolvedForwardReference; 21 import com.fasterxml.jackson.databind.deser.UnresolvedId; 22 import com.fasterxml.jackson.databind.objectid.TestObjectId.Company; 23 import com.fasterxml.jackson.databind.objectid.TestObjectId.Employee; 24 25 /** 26 * Unit test to verify handling of Object Id deserialization 27 */ 28 public class TestObjectIdDeserialization extends BaseMapTest 29 { 30 private static final String POOL_KEY = "POOL"; 31 32 // // Classes for external id use 33 34 @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="id") 35 static class Identifiable 36 { 37 public int value; 38 39 public Identifiable next; 40 Identifiable()41 public Identifiable() { this(0); } Identifiable(int v)42 public Identifiable(int v) { 43 value = v; 44 } 45 } 46 47 @JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="#") 48 static class UUIDNode 49 { 50 public int value; 51 public UUIDNode parent; 52 public UUIDNode first; 53 public UUIDNode second; 54 UUIDNode()55 public UUIDNode() { this(0); } UUIDNode(int v)56 public UUIDNode(int v) { value = v; } 57 } 58 59 // // Classes for external id from property annotations: 60 61 static class IdWrapper 62 { 63 @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@id") 64 public ValueNode node; 65 IdWrapper()66 public IdWrapper() { } IdWrapper(int v)67 public IdWrapper(int v) { 68 node = new ValueNode(v); 69 } 70 } 71 72 static class ValueNode { 73 public int value; 74 public IdWrapper next; 75 ValueNode()76 public ValueNode() { this(0); } ValueNode(int v)77 public ValueNode(int v) { value = v; } 78 } 79 80 // // Classes for external id use 81 82 @JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="customId") 83 static class IdentifiableCustom 84 { 85 public int value; 86 87 public int customId; 88 89 public IdentifiableCustom next; 90 IdentifiableCustom()91 public IdentifiableCustom() { this(-1, 0); } IdentifiableCustom(int i, int v)92 public IdentifiableCustom(int i, int v) { 93 customId = i; 94 value = v; 95 } 96 } 97 98 static class IdWrapperExt 99 { 100 @JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, 101 property="customId") 102 public ValueNodeExt node; 103 IdWrapperExt()104 public IdWrapperExt() { } IdWrapperExt(int v)105 public IdWrapperExt(int v) { 106 node = new ValueNodeExt(v); 107 } 108 } 109 110 static class ValueNodeExt 111 { 112 public int value; 113 protected int customId; 114 public IdWrapperExt next; 115 ValueNodeExt()116 public ValueNodeExt() { this(0); } ValueNodeExt(int v)117 public ValueNodeExt(int v) { value = v; } 118 setCustomId(int i)119 public void setCustomId(int i) { 120 customId = i; 121 } 122 } 123 124 static class MappedCompany { 125 public Map<Integer, Employee> employees; 126 } 127 128 @JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class) 129 static class AnySetterObjectId { 130 protected Map<String, AnySetterObjectId> values = new HashMap<String, AnySetterObjectId>(); 131 132 @JsonAnySetter anySet(String field, AnySetterObjectId value)133 public void anySet(String field, AnySetterObjectId value) { 134 // Ensure that it is never called with null because of unresolved reference. 135 assertNotNull(value); 136 values.put(field, value); 137 } 138 } 139 140 static class CustomResolutionWrapper { 141 public List<WithCustomResolution> data; 142 } 143 144 @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", resolver = PoolResolver.class) 145 @JsonIdentityReference(alwaysAsId = true) // #524 146 static class WithCustomResolution { 147 public int id; 148 public int data; 149 WithCustomResolution(int id, int data)150 public WithCustomResolution(int id, int data) 151 { 152 this.id = id; 153 this.data = data; 154 } 155 } 156 157 public static class PoolResolver implements ObjectIdResolver { 158 private Map<Object,WithCustomResolution> _pool; 159 PoolResolver()160 public PoolResolver() {} PoolResolver(Map<Object,WithCustomResolution> pool)161 public PoolResolver(Map<Object,WithCustomResolution> pool){ _pool = pool; } 162 163 @Override bindItem(IdKey id, Object pojo)164 public void bindItem(IdKey id, Object pojo){ } 165 166 @Override resolveId(IdKey id)167 public Object resolveId(IdKey id){ return _pool.get(id.key); } 168 169 @Override canUseFor(ObjectIdResolver resolverType)170 public boolean canUseFor(ObjectIdResolver resolverType) 171 { 172 return resolverType.getClass() == getClass() && _pool != null && !_pool.isEmpty(); 173 } 174 175 @Override newForDeserialization(Object c)176 public ObjectIdResolver newForDeserialization(Object c) 177 { 178 DeserializationContext context = (DeserializationContext)c; 179 @SuppressWarnings("unchecked") 180 Map<Object,WithCustomResolution> pool = (Map<Object,WithCustomResolution>)context.getAttribute(POOL_KEY); 181 return new PoolResolver(pool); 182 } 183 } 184 185 /* 186 /***************************************************** 187 /* Unit tests, external id deserialization 188 /***************************************************** 189 */ 190 191 private final ObjectMapper MAPPER = new ObjectMapper(); 192 193 private final static String EXP_SIMPLE_INT_CLASS = "{\"id\":1,\"value\":13,\"next\":1}"; 194 testSimpleDeserializationClass()195 public void testSimpleDeserializationClass() throws Exception 196 { 197 // then bring back... 198 Identifiable result = MAPPER.readValue(EXP_SIMPLE_INT_CLASS, Identifiable.class); 199 assertEquals(13, result.value); 200 assertSame(result, result.next); 201 } 202 203 // Should be ok NOT to have Object id, as well testMissingObjectId()204 public void testMissingObjectId() throws Exception 205 { 206 Identifiable result = MAPPER.readValue(aposToQuotes("{'value':28, 'next':{'value':29}}"), 207 Identifiable.class); 208 assertNotNull(result); 209 assertEquals(28, result.value); 210 assertNotNull(result.next); 211 assertEquals(29, result.next.value); 212 } 213 testSimpleUUIDForClassRoundTrip()214 public void testSimpleUUIDForClassRoundTrip() throws Exception 215 { 216 UUIDNode root = new UUIDNode(1); 217 UUIDNode child1 = new UUIDNode(2); 218 UUIDNode child2 = new UUIDNode(3); 219 root.first = child1; 220 root.second = child2; 221 child1.parent = root; 222 child2.parent = root; 223 child1.first = child2; 224 225 String json = MAPPER.writeValueAsString(root); 226 227 // and should come back the same too... 228 UUIDNode result = MAPPER.readValue(json, UUIDNode.class); 229 assertEquals(1, result.value); 230 UUIDNode result2 = result.first; 231 UUIDNode result3 = result.second; 232 assertNotNull(result2); 233 assertNotNull(result3); 234 assertEquals(2, result2.value); 235 assertEquals(3, result3.value); 236 237 assertSame(result, result2.parent); 238 assertSame(result, result3.parent); 239 assertSame(result3, result2.first); 240 } 241 242 // Bit more complex, due to extra wrapping etc: 243 private final static String EXP_SIMPLE_INT_PROP = "{\"node\":{\"@id\":1,\"value\":7,\"next\":{\"node\":1}}}"; 244 testSimpleDeserializationProperty()245 public void testSimpleDeserializationProperty() throws Exception 246 { 247 IdWrapper result = MAPPER.readValue(EXP_SIMPLE_INT_PROP, IdWrapper.class); 248 assertEquals(7, result.node.value); 249 assertSame(result.node, result.node.next.node); 250 } 251 252 // Another test to ensure ordering is not required (i.e. can do front references) testSimpleDeserWithForwardRefs()253 public void testSimpleDeserWithForwardRefs() throws Exception 254 { 255 IdWrapper result = MAPPER.readValue("{\"node\":{\"value\":7,\"next\":{\"node\":1}, \"@id\":1}}" 256 ,IdWrapper.class); 257 assertEquals(7, result.node.value); 258 assertSame(result.node, result.node.next.node); 259 } 260 testForwardReference()261 public void testForwardReference() 262 throws Exception 263 { 264 String json = "{\"employees\":[" 265 + "{\"id\":1,\"name\":\"First\",\"manager\":2,\"reports\":[]}," 266 + "{\"id\":2,\"name\":\"Second\",\"manager\":null,\"reports\":[1]}" 267 + "]}"; 268 Company company = MAPPER.readValue(json, Company.class); 269 assertEquals(2, company.employees.size()); 270 Employee firstEmployee = company.employees.get(0); 271 Employee secondEmployee = company.employees.get(1); 272 assertEquals(1, firstEmployee.id); 273 assertEquals(2, secondEmployee.id); 274 assertEquals(secondEmployee, firstEmployee.manager); // Ensure that forward reference was properly resolved. 275 assertEquals(firstEmployee, secondEmployee.reports.get(0)); // And that back reference is also properly resolved. 276 } 277 testForwardReferenceInCollection()278 public void testForwardReferenceInCollection() 279 throws Exception 280 { 281 String json = "{\"employees\":[" 282 + "{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]}," 283 + "{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}" 284 + "]}"; 285 Company company = MAPPER.readValue(json, Company.class); 286 assertEquals(2, company.employees.size()); 287 Employee firstEmployee = company.employees.get(0); 288 Employee secondEmployee = company.employees.get(1); 289 assertEmployees(firstEmployee, secondEmployee); 290 } 291 testForwardReferenceInMap()292 public void testForwardReferenceInMap() 293 throws Exception 294 { 295 String json = "{\"employees\":{" 296 + "\"1\":{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]}," 297 + "\"2\": 2," 298 + "\"3\":{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}" 299 + "}}"; 300 MappedCompany company = MAPPER.readValue(json, MappedCompany.class); 301 assertEquals(3, company.employees.size()); 302 Employee firstEmployee = company.employees.get(1); 303 Employee secondEmployee = company.employees.get(3); 304 assertEmployees(firstEmployee, secondEmployee); 305 } 306 assertEmployees(Employee firstEmployee, Employee secondEmployee)307 private void assertEmployees(Employee firstEmployee, Employee secondEmployee) 308 { 309 assertEquals(1, firstEmployee.id); 310 assertEquals(2, secondEmployee.id); 311 assertEquals(1, firstEmployee.reports.size()); 312 assertSame(secondEmployee, firstEmployee.reports.get(0)); // Ensure that forward reference was properly resolved and in order. 313 assertSame(firstEmployee, secondEmployee.manager); // And that back reference is also properly resolved. 314 } 315 testForwardReferenceAnySetterCombo()316 public void testForwardReferenceAnySetterCombo() throws Exception { 317 String json = "{\"@id\":1, \"foo\":2, \"bar\":{\"@id\":2, \"foo\":1}}"; 318 AnySetterObjectId value = MAPPER.readValue(json, AnySetterObjectId.class); 319 assertSame(value.values.get("bar"), value.values.get("foo")); 320 } 321 testUnresolvedForwardReference()322 public void testUnresolvedForwardReference() 323 throws Exception 324 { 325 String json = "{\"employees\":[" 326 + "{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[3]}," 327 + "{\"id\":2,\"name\":\"Second\",\"manager\":3,\"reports\":[]}" 328 + "]}"; 329 try { 330 MAPPER.readValue(json, Company.class); 331 fail("Should have thrown."); 332 } catch (UnresolvedForwardReference exception) { 333 // Expected 334 List<UnresolvedId> unresolvedIds = exception.getUnresolvedIds(); 335 assertEquals(2, unresolvedIds.size()); 336 UnresolvedId firstUnresolvedId = unresolvedIds.get(0); 337 assertEquals(3, firstUnresolvedId.getId()); 338 assertEquals(Employee.class, firstUnresolvedId.getType()); 339 UnresolvedId secondUnresolvedId = unresolvedIds.get(1); 340 assertEquals(firstUnresolvedId.getId(), secondUnresolvedId.getId()); 341 assertEquals(Employee.class, secondUnresolvedId.getType()); 342 } 343 } 344 345 // [databind#299]: Allow unresolved ids to become nulls testUnresolvableAsNull()346 public void testUnresolvableAsNull() throws Exception 347 { 348 IdWrapper w = MAPPER.readerFor(IdWrapper.class) 349 .without(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS) 350 .readValue(aposToQuotes("{'node':123}")); 351 assertNotNull(w); 352 assertNull(w.node); 353 } 354 testKeepCollectionOrdering()355 public void testKeepCollectionOrdering() throws Exception 356 { 357 String json = "{\"employees\":[2,1," 358 + "{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]}," 359 + "{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}" 360 + "]}"; 361 Company company = MAPPER.readValue(json, Company.class); 362 assertEquals(4, company.employees.size()); 363 // Deser must keep object ordering. 364 Employee firstEmployee = company.employees.get(1); 365 Employee secondEmployee = company.employees.get(0); 366 assertSame(firstEmployee, company.employees.get(2)); 367 assertSame(secondEmployee, company.employees.get(3)); 368 assertEmployees(firstEmployee, secondEmployee); 369 } 370 testKeepMapOrdering()371 public void testKeepMapOrdering() 372 throws Exception 373 { 374 String json = "{\"employees\":{" 375 + "\"1\":2, \"2\":1," 376 + "\"3\":{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]}," 377 + "\"4\":{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}" 378 + "}}"; 379 MappedCompany company = MAPPER.readValue(json, MappedCompany.class); 380 assertEquals(4, company.employees.size()); 381 Employee firstEmployee = company.employees.get(2); 382 Employee secondEmployee = company.employees.get(1); 383 assertEmployees(firstEmployee, secondEmployee); 384 // Deser must keep object ordering. Not sure if it's really important for maps, 385 // but since default map is LinkedHashMap might as well ensure it does... 386 Iterator<Entry<Integer,Employee>> iterator = company.employees.entrySet().iterator(); 387 assertSame(secondEmployee, iterator.next().getValue()); 388 assertSame(firstEmployee, iterator.next().getValue()); 389 assertSame(firstEmployee, iterator.next().getValue()); 390 assertSame(secondEmployee, iterator.next().getValue()); 391 } 392 393 /* 394 /***************************************************** 395 /* Unit tests, custom (property-based) id deserialization 396 /***************************************************** 397 */ 398 399 private final static String EXP_CUSTOM_VIA_CLASS = "{\"customId\":123,\"value\":-900,\"next\":123}"; 400 testCustomDeserializationClass()401 public void testCustomDeserializationClass() throws Exception 402 { 403 // then bring back... 404 IdentifiableCustom result = MAPPER.readValue(EXP_CUSTOM_VIA_CLASS, IdentifiableCustom.class); 405 assertEquals(-900, result.value); 406 assertSame(result, result.next); 407 } 408 409 private final static String EXP_CUSTOM_VIA_PROP = "{\"node\":{\"customId\":3,\"value\":99,\"next\":{\"node\":3}}}"; 410 testCustomDeserializationProperty()411 public void testCustomDeserializationProperty() throws Exception 412 { 413 // then bring back... 414 IdWrapperExt result = MAPPER.readValue(EXP_CUSTOM_VIA_PROP, IdWrapperExt.class); 415 assertEquals(99, result.node.value); 416 assertSame(result.node, result.node.next.node); 417 assertEquals(3, result.node.customId); 418 } 419 420 /* 421 /***************************************************** 422 /* Unit tests, custom id resolver 423 /***************************************************** 424 */ 425 testCustomPoolResolver()426 public void testCustomPoolResolver() throws Exception 427 { 428 Map<Object,WithCustomResolution> pool = new HashMap<Object,WithCustomResolution>(); 429 pool.put(1, new WithCustomResolution(1, 1)); 430 pool.put(2, new WithCustomResolution(2, 2)); 431 pool.put(3, new WithCustomResolution(3, 3)); 432 pool.put(4, new WithCustomResolution(4, 4)); 433 pool.put(5, new WithCustomResolution(5, 5)); 434 ContextAttributes attrs = MAPPER.getDeserializationConfig().getAttributes().withSharedAttribute(POOL_KEY, pool); 435 String content = "{\"data\":[1,2,3,4,5]}"; 436 CustomResolutionWrapper wrapper = MAPPER.readerFor(CustomResolutionWrapper.class).with(attrs).readValue(content); 437 assertFalse(wrapper.data.isEmpty()); 438 for (WithCustomResolution ob : wrapper.data) { 439 assertSame(pool.get(ob.id), ob); 440 } 441 } 442 443 /* 444 /***************************************************** 445 /* Unit tests, missing/null Object id [databind#742] 446 /***************************************************** 447 */ 448 449 /* 450 private final static String EXP_SIMPLE_INT_CLASS = "{\"id\":1,\"value\":13,\"next\":1}"; 451 @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="id") 452 static class Identifiable 453 { 454 public int value; 455 456 public Identifiable next; 457 } 458 */ 459 testNullObjectId()460 public void testNullObjectId() throws Exception 461 { 462 // Ok, so missing Object Id is ok, but so is null. 463 464 Identifiable value = MAPPER.readValue 465 (aposToQuotes("{'value':3, 'next':null, 'id':null}"), Identifiable.class); 466 assertNotNull(value); 467 assertEquals(3, value.value); 468 } 469 } 470