package com.fasterxml.jackson.databind.objectid;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.JsonIdentityReference;
import com.fasterxml.jackson.annotation.ObjectIdGenerator.IdKey;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.annotation.ObjectIdResolver;
import com.fasterxml.jackson.databind.BaseMapTest;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.cfg.ContextAttributes;
import com.fasterxml.jackson.databind.deser.UnresolvedForwardReference;
import com.fasterxml.jackson.databind.deser.UnresolvedId;
import com.fasterxml.jackson.databind.objectid.TestObjectId.Company;
import com.fasterxml.jackson.databind.objectid.TestObjectId.Employee;
/**
* Unit test to verify handling of Object Id deserialization
*/
public class TestObjectIdDeserialization extends BaseMapTest
{
private static final String POOL_KEY = "POOL";
// // Classes for external id use
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="id")
static class Identifiable
{
public int value;
public Identifiable next;
public Identifiable() { this(0); }
public Identifiable(int v) {
value = v;
}
}
@JsonIdentityInfo(generator=ObjectIdGenerators.UUIDGenerator.class, property="#")
static class UUIDNode
{
public int value;
public UUIDNode parent;
public UUIDNode first;
public UUIDNode second;
public UUIDNode() { this(0); }
public UUIDNode(int v) { value = v; }
}
// // Classes for external id from property annotations:
static class IdWrapper
{
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="@id")
public ValueNode node;
public IdWrapper() { }
public IdWrapper(int v) {
node = new ValueNode(v);
}
}
static class ValueNode {
public int value;
public IdWrapper next;
public ValueNode() { this(0); }
public ValueNode(int v) { value = v; }
}
// // Classes for external id use
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class, property="customId")
static class IdentifiableCustom
{
public int value;
public int customId;
public IdentifiableCustom next;
public IdentifiableCustom() { this(-1, 0); }
public IdentifiableCustom(int i, int v) {
customId = i;
value = v;
}
}
static class IdWrapperExt
{
@JsonIdentityInfo(generator=ObjectIdGenerators.PropertyGenerator.class,
property="customId")
public ValueNodeExt node;
public IdWrapperExt() { }
public IdWrapperExt(int v) {
node = new ValueNodeExt(v);
}
}
static class ValueNodeExt
{
public int value;
protected int customId;
public IdWrapperExt next;
public ValueNodeExt() { this(0); }
public ValueNodeExt(int v) { value = v; }
public void setCustomId(int i) {
customId = i;
}
}
static class MappedCompany {
public Map<Integer, Employee> employees;
}
@JsonIdentityInfo(generator = ObjectIdGenerators.IntSequenceGenerator.class)
static class AnySetterObjectId {
protected Map<String, AnySetterObjectId> values = new HashMap<String, AnySetterObjectId>();
@JsonAnySetter
public void anySet(String field, AnySetterObjectId value) {
// Ensure that it is never called with null because of unresolved reference.
assertNotNull(value);
values.put(field, value);
}
}
static class CustomResolutionWrapper {
public List<WithCustomResolution> data;
}
@JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id", resolver = PoolResolver.class)
@JsonIdentityReference(alwaysAsId = true) // #524
static class WithCustomResolution {
public int id;
public int data;
public WithCustomResolution(int id, int data)
{
this.id = id;
this.data = data;
}
}
public static class PoolResolver implements ObjectIdResolver {
private Map<Object,WithCustomResolution> _pool;
public PoolResolver() {}
public PoolResolver(Map<Object,WithCustomResolution> pool){ _pool = pool; }
@Override
public void bindItem(IdKey id, Object pojo){ }
@Override
public Object resolveId(IdKey id){ return _pool.get(id.key); }
@Override
public boolean canUseFor(ObjectIdResolver resolverType)
{
return resolverType.getClass() == getClass() && _pool != null && !_pool.isEmpty();
}
@Override
public ObjectIdResolver newForDeserialization(Object c)
{
DeserializationContext context = (DeserializationContext)c;
@SuppressWarnings("unchecked")
Map<Object,WithCustomResolution> pool = (Map<Object,WithCustomResolution>)context.getAttribute(POOL_KEY);
return new PoolResolver(pool);
}
}
/*
/*****************************************************
/* Unit tests, external id deserialization
/*****************************************************
*/
private final ObjectMapper MAPPER = new ObjectMapper();
private final static String EXP_SIMPLE_INT_CLASS = "{\"id\":1,\"value\":13,\"next\":1}";
public void testSimpleDeserializationClass() throws Exception
{
// then bring back...
Identifiable result = MAPPER.readValue(EXP_SIMPLE_INT_CLASS, Identifiable.class);
assertEquals(13, result.value);
assertSame(result, result.next);
}
// Should be ok NOT to have Object id, as well
public void testMissingObjectId() throws Exception
{
Identifiable result = MAPPER.readValue(aposToQuotes("{'value':28, 'next':{'value':29}}"),
Identifiable.class);
assertNotNull(result);
assertEquals(28, result.value);
assertNotNull(result.next);
assertEquals(29, result.next.value);
}
public void testSimpleUUIDForClassRoundTrip() throws Exception
{
UUIDNode root = new UUIDNode(1);
UUIDNode child1 = new UUIDNode(2);
UUIDNode child2 = new UUIDNode(3);
root.first = child1;
root.second = child2;
child1.parent = root;
child2.parent = root;
child1.first = child2;
String json = MAPPER.writeValueAsString(root);
// and should come back the same too...
UUIDNode result = MAPPER.readValue(json, UUIDNode.class);
assertEquals(1, result.value);
UUIDNode result2 = result.first;
UUIDNode result3 = result.second;
assertNotNull(result2);
assertNotNull(result3);
assertEquals(2, result2.value);
assertEquals(3, result3.value);
assertSame(result, result2.parent);
assertSame(result, result3.parent);
assertSame(result3, result2.first);
}
// Bit more complex, due to extra wrapping etc:
private final static String EXP_SIMPLE_INT_PROP = "{\"node\":{\"@id\":1,\"value\":7,\"next\":{\"node\":1}}}";
public void testSimpleDeserializationProperty() throws Exception
{
IdWrapper result = MAPPER.readValue(EXP_SIMPLE_INT_PROP, IdWrapper.class);
assertEquals(7, result.node.value);
assertSame(result.node, result.node.next.node);
}
// Another test to ensure ordering is not required (i.e. can do front references)
public void testSimpleDeserWithForwardRefs() throws Exception
{
IdWrapper result = MAPPER.readValue("{\"node\":{\"value\":7,\"next\":{\"node\":1}, \"@id\":1}}"
,IdWrapper.class);
assertEquals(7, result.node.value);
assertSame(result.node, result.node.next.node);
}
public void testForwardReference()
throws Exception
{
String json = "{\"employees\":["
+ "{\"id\":1,\"name\":\"First\",\"manager\":2,\"reports\":[]},"
+ "{\"id\":2,\"name\":\"Second\",\"manager\":null,\"reports\":[1]}"
+ "]}";
Company company = MAPPER.readValue(json, Company.class);
assertEquals(2, company.employees.size());
Employee firstEmployee = company.employees.get(0);
Employee secondEmployee = company.employees.get(1);
assertEquals(1, firstEmployee.id);
assertEquals(2, secondEmployee.id);
assertEquals(secondEmployee, firstEmployee.manager); // Ensure that forward reference was properly resolved.
assertEquals(firstEmployee, secondEmployee.reports.get(0)); // And that back reference is also properly resolved.
}
public void testForwardReferenceInCollection()
throws Exception
{
String json = "{\"employees\":["
+ "{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]},"
+ "{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}"
+ "]}";
Company company = MAPPER.readValue(json, Company.class);
assertEquals(2, company.employees.size());
Employee firstEmployee = company.employees.get(0);
Employee secondEmployee = company.employees.get(1);
assertEmployees(firstEmployee, secondEmployee);
}
public void testForwardReferenceInMap()
throws Exception
{
String json = "{\"employees\":{"
+ "\"1\":{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]},"
+ "\"2\": 2,"
+ "\"3\":{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}"
+ "}}";
MappedCompany company = MAPPER.readValue(json, MappedCompany.class);
assertEquals(3, company.employees.size());
Employee firstEmployee = company.employees.get(1);
Employee secondEmployee = company.employees.get(3);
assertEmployees(firstEmployee, secondEmployee);
}
private void assertEmployees(Employee firstEmployee, Employee secondEmployee)
{
assertEquals(1, firstEmployee.id);
assertEquals(2, secondEmployee.id);
assertEquals(1, firstEmployee.reports.size());
assertSame(secondEmployee, firstEmployee.reports.get(0)); // Ensure that forward reference was properly resolved and in order.
assertSame(firstEmployee, secondEmployee.manager); // And that back reference is also properly resolved.
}
public void testForwardReferenceAnySetterCombo() throws Exception {
String json = "{\"@id\":1, \"foo\":2, \"bar\":{\"@id\":2, \"foo\":1}}";
AnySetterObjectId value = MAPPER.readValue(json, AnySetterObjectId.class);
assertSame(value.values.get("bar"), value.values.get("foo"));
}
public void testUnresolvedForwardReference()
throws Exception
{
String json = "{\"employees\":["
+ "{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[3]},"
+ "{\"id\":2,\"name\":\"Second\",\"manager\":3,\"reports\":[]}"
+ "]}";
try {
MAPPER.readValue(json, Company.class);
fail("Should have thrown.");
} catch (UnresolvedForwardReference exception) {
// Expected
List<UnresolvedId> unresolvedIds = exception.getUnresolvedIds();
assertEquals(2, unresolvedIds.size());
UnresolvedId firstUnresolvedId = unresolvedIds.get(0);
assertEquals(3, firstUnresolvedId.getId());
assertEquals(Employee.class, firstUnresolvedId.getType());
UnresolvedId secondUnresolvedId = unresolvedIds.get(1);
assertEquals(firstUnresolvedId.getId(), secondUnresolvedId.getId());
assertEquals(Employee.class, secondUnresolvedId.getType());
}
}
// [databind#299]: Allow unresolved ids to become nulls
public void testUnresolvableAsNull() throws Exception
{
IdWrapper w = MAPPER.readerFor(IdWrapper.class)
.without(DeserializationFeature.FAIL_ON_UNRESOLVED_OBJECT_IDS)
.readValue(aposToQuotes("{'node':123}"));
assertNotNull(w);
assertNull(w.node);
}
public void testKeepCollectionOrdering() throws Exception
{
String json = "{\"employees\":[2,1,"
+ "{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]},"
+ "{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}"
+ "]}";
Company company = MAPPER.readValue(json, Company.class);
assertEquals(4, company.employees.size());
// Deser must keep object ordering.
Employee firstEmployee = company.employees.get(1);
Employee secondEmployee = company.employees.get(0);
assertSame(firstEmployee, company.employees.get(2));
assertSame(secondEmployee, company.employees.get(3));
assertEmployees(firstEmployee, secondEmployee);
}
public void testKeepMapOrdering()
throws Exception
{
String json = "{\"employees\":{"
+ "\"1\":2, \"2\":1,"
+ "\"3\":{\"id\":1,\"name\":\"First\",\"manager\":null,\"reports\":[2]},"
+ "\"4\":{\"id\":2,\"name\":\"Second\",\"manager\":1,\"reports\":[]}"
+ "}}";
MappedCompany company = MAPPER.readValue(json, MappedCompany.class);
assertEquals(4, company.employees.size());
Employee firstEmployee = company.employees.get(2);
Employee secondEmployee = company.employees.get(1);
assertEmployees(firstEmployee, secondEmployee);
// Deser must keep object ordering. Not sure if it's really important for maps,
// but since default map is LinkedHashMap might as well ensure it does...
Iterator<Entry<Integer,Employee>> iterator = company.employees.entrySet().iterator();
assertSame(secondEmployee, iterator.next().getValue());
assertSame(firstEmployee, iterator.next().getValue());
assertSame(firstEmployee, iterator.next().getValue());
assertSame(secondEmployee, iterator.next().getValue());
}
/*
/*****************************************************
/* Unit tests, custom (property-based) id deserialization
/*****************************************************
*/
private final static String EXP_CUSTOM_VIA_CLASS = "{\"customId\":123,\"value\":-900,\"next\":123}";
public void testCustomDeserializationClass() throws Exception
{
// then bring back...
IdentifiableCustom result = MAPPER.readValue(EXP_CUSTOM_VIA_CLASS, IdentifiableCustom.class);
assertEquals(-900, result.value);
assertSame(result, result.next);
}
private final static String EXP_CUSTOM_VIA_PROP = "{\"node\":{\"customId\":3,\"value\":99,\"next\":{\"node\":3}}}";
public void testCustomDeserializationProperty() throws Exception
{
// then bring back...
IdWrapperExt result = MAPPER.readValue(EXP_CUSTOM_VIA_PROP, IdWrapperExt.class);
assertEquals(99, result.node.value);
assertSame(result.node, result.node.next.node);
assertEquals(3, result.node.customId);
}
/*
/*****************************************************
/* Unit tests, custom id resolver
/*****************************************************
*/
public void testCustomPoolResolver() throws Exception
{
Map<Object,WithCustomResolution> pool = new HashMap<Object,WithCustomResolution>();
pool.put(1, new WithCustomResolution(1, 1));
pool.put(2, new WithCustomResolution(2, 2));
pool.put(3, new WithCustomResolution(3, 3));
pool.put(4, new WithCustomResolution(4, 4));
pool.put(5, new WithCustomResolution(5, 5));
ContextAttributes attrs = MAPPER.getDeserializationConfig().getAttributes().withSharedAttribute(POOL_KEY, pool);
String content = "{\"data\":[1,2,3,4,5]}";
CustomResolutionWrapper wrapper = MAPPER.readerFor(CustomResolutionWrapper.class).with(attrs).readValue(content);
assertFalse(wrapper.data.isEmpty());
for (WithCustomResolution ob : wrapper.data) {
assertSame(pool.get(ob.id), ob);
}
}
/*
/*****************************************************
/* Unit tests, missing/null Object id [databind#742]
/*****************************************************
*/
/*
private final static String EXP_SIMPLE_INT_CLASS = "{\"id\":1,\"value\":13,\"next\":1}";
@JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="id")
static class Identifiable
{
public int value;
public Identifiable next;
}
*/
public void testNullObjectId() throws Exception
{
// Ok, so missing Object Id is ok, but so is null.
Identifiable value = MAPPER.readValue
(aposToQuotes("{'value':3, 'next':null, 'id':null}"), Identifiable.class);
assertNotNull(value);
assertEquals(3, value.value);
}
}