package com.fasterxml.jackson.databind.objectid; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.JsonTypeInfo.As; import com.fasterxml.jackson.annotation.JsonTypeInfo.Id; import com.fasterxml.jackson.annotation.ObjectIdGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.BaseMapTest; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import java.io.IOException; /** * Unit test(s) for [databind#622], supporting non-scalar-Object-ids, * to support things like JSOG. */ public class JSOGDeserialize622Test extends BaseMapTest { /** the key of the property that holds the ref */ public static final String REF_KEY = "@ref"; /** * JSON input */ private static final String EXP_EXAMPLE_JSOG = aposToQuotes( "{'@id':'1','foo':66,'next':{'"+REF_KEY+"':'1'}}"); /** * Customer IdGenerator */ static class JSOGGenerator extends ObjectIdGenerator<JSOGRef> { private static final long serialVersionUID = 1L; protected transient int _nextValue; protected final Class<?> _scope; protected JSOGGenerator() { this(null, -1); } protected JSOGGenerator(Class<?> scope, int nextValue) { _scope = scope; _nextValue = nextValue; } @Override public Class<?> getScope() { return _scope; } @Override public boolean canUseFor(ObjectIdGenerator<?> gen) { return (gen.getClass() == getClass()) && (gen.getScope() == _scope); } @Override public ObjectIdGenerator<JSOGRef> forScope(Class<?> scope) { return (_scope == scope) ? this : new JSOGGenerator(scope, _nextValue); } @Override public ObjectIdGenerator<JSOGRef> newForSerialization(Object context) { return new JSOGGenerator(_scope, 1); } @Override public com.fasterxml.jackson.annotation.ObjectIdGenerator.IdKey key(Object key) { return new IdKey(getClass(), _scope, key); } // important: otherwise won't get proper handling @Override public boolean maySerializeAsObject() { return true; } // ditto: needed for handling Object-valued Object references @Override public boolean isValidReferencePropertyName(String name, Object parser) { return REF_KEY.equals(name); } @Override public JSOGRef generateId(Object forPojo) { int id = _nextValue; ++_nextValue; return new JSOGRef(id); } } /** * The reference deserializer */ static class JSOGRefDeserializer extends JsonDeserializer<JSOGRef> { @Override public JSOGRef deserialize(JsonParser p, DeserializationContext ctx) throws IOException { JsonNode node = p.readValueAsTree(); if (node.isTextual()) { return new JSOGRef(node.asInt()); } JsonNode n = node.get(REF_KEY); if (n == null) { throw new JsonMappingException(p, "Could not find key '"+REF_KEY +"' from ("+node.getClass().getName()+"): "+node); } return new JSOGRef(n.asInt()); } } /** * The reference object */ @JsonDeserialize(using=JSOGRefDeserializer.class) static class JSOGRef { @JsonProperty(REF_KEY) public int ref; public JSOGRef() { } public JSOGRef(int val) { ref = val; } @Override public String toString() { return "[JSOGRef#"+ref+"]"; } @Override public int hashCode() { return ref; } @Override public boolean equals(Object other) { return (other instanceof JSOGRef) && ((JSOGRef) other).ref == this.ref; } } /** * Example class using JSOGGenerator */ @JsonIdentityInfo(generator=JSOGGenerator.class, property="@id") public static class IdentifiableExampleJSOG { public int foo; public IdentifiableExampleJSOG next; protected IdentifiableExampleJSOG() { } public IdentifiableExampleJSOG(int v) { foo = v; } } public static class JSOGWrapper { public int value; @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS) public Object jsog; JSOGWrapper() { } public JSOGWrapper(int v) { value = v; } } // For [databind#669] @JsonIdentityInfo(generator=JSOGGenerator.class) @JsonTypeInfo(use=Id.CLASS, include= As.PROPERTY, property="@class") public static class Inner { public String bar; protected Inner() {} public Inner(String bar) { this.bar = bar; } } public static class SubInner extends Inner { public String extra; protected SubInner() {} public SubInner(String bar, String extra) { super(bar); this.extra = extra; } } @JsonIdentityInfo(generator=JSOGGenerator.class) public static class Outer { public String foo; public Inner inner1; public Inner inner2; } /* /********************************************************************** /* Test methods /********************************************************************** */ private final ObjectMapper MAPPER = new ObjectMapper(); // Basic for [databind#622] public void testStructJSOGRef() throws Exception { IdentifiableExampleJSOG result = MAPPER.readValue(EXP_EXAMPLE_JSOG, IdentifiableExampleJSOG.class); assertEquals(66, result.foo); assertSame(result, result.next); } // polymorphic alternative for [databind#622] public void testPolymorphicRoundTrip() throws Exception { JSOGWrapper w = new JSOGWrapper(15); // create a nice little loop IdentifiableExampleJSOG ex = new IdentifiableExampleJSOG(123); ex.next = ex; w.jsog = ex; String json = MAPPER.writeValueAsString(w); JSOGWrapper out = MAPPER.readValue(json, JSOGWrapper.class); assertNotNull(out); assertEquals(15, out.value); assertTrue(out.jsog instanceof IdentifiableExampleJSOG); IdentifiableExampleJSOG jsog = (IdentifiableExampleJSOG) out.jsog; assertEquals(123, jsog.foo); assertSame(jsog, jsog.next); } // polymorphic alternative for [databind#669] public void testAlterativePolymorphicRoundTrip669() throws Exception { Outer outer = new Outer(); outer.foo = "foo"; outer.inner1 = outer.inner2 = new SubInner("bar", "extra"); String jsog = MAPPER.writeValueAsString(outer); Outer back = MAPPER.readValue(jsog, Outer.class); assertSame(back.inner1, back.inner2); } }