diff --git a/src/main/java/tools/jackson/databind/JRefModule.java b/src/main/java/tools/jackson/databind/JRefModule.java new file mode 100644 index 0000000000..4a578fda2b --- /dev/null +++ b/src/main/java/tools/jackson/databind/JRefModule.java @@ -0,0 +1,338 @@ +package tools.jackson.databind; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.HashMap; +import java.util.Map; + +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonPointer; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.BeanDescription.Supplier; +import tools.jackson.databind.deser.ValueDeserializerModifier; +import tools.jackson.databind.deser.std.DelegatingDeserializer; +import tools.jackson.databind.jsontype.TypeDeserializer; +import tools.jackson.databind.jsontype.TypeSerializer; +import tools.jackson.databind.module.SimpleModule; +import tools.jackson.databind.node.TreeTraversingParser; +import tools.jackson.databind.ser.ValueSerializerModifier; +import tools.jackson.databind.ser.std.DelegatingSerializer; +import tools.jackson.databind.type.ArrayType; +import tools.jackson.databind.type.CollectionLikeType; +import tools.jackson.databind.type.CollectionType; +import tools.jackson.databind.type.MapLikeType; +import tools.jackson.databind.type.MapType; + +/** + * This Jackson 3 module extends Jackson's {@link ObjectMapper} serialization + * and de-serialization with a reference type (JRef). JRefs allow for circular + * references to be serialized as JSON or for multiple references to a target + * json object to be dealt with more efficiently than producing copies of that + * target object. This is especially useful for complex data structures such as + * trees and other object graphs where potentially many references to a single + * object may need to be serialized and de-serialized. + *

+ *

+ * References take the serialized form of { "$ref": "#/path/to/target" } where + * the URL fragment (starts with the '#' uri fragment identifier) is a JSON + * Pointer rfc-6901 + * locating a path local to the current document. + *

+ *

+ * The JRef + * specification can be used to reference locations in external documents as + * well (the JSON pointer specifies a full URI rather than just the fragment), + * but this implementation only supports references to objects in the current + * document. See the JRef specification and + * @hyperjump/browser for + * an implementation supporting such external references. + * + **/ +public class JRefModule extends SimpleModule { + + private static final long serialVersionUID = 1L; + public static final String JREF_NAME = "$ref"; + public static final String HASH = "#"; + + public JRefModule() { + super("JRefModule"); + } + + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + context.addDeserializerModifier(new JRefValueDeserializerModifier()); + context.addSerializerModifier(new JRefValueSerializerModifier()); + } + + public class JRefValueSerializerModifier extends ValueSerializerModifier { + + private static final long serialVersionUID = 1L; + + static final String PTR_MAP_ATTR = JRefValueSerializerModifier.class.getName() + ".ptrMap"; + + @FunctionalInterface + interface Serializer { + void serialize() throws RuntimeException; + } + + class JRefValueSerializer extends DelegatingSerializer { + + JRefValueSerializer(ValueSerializer delegatee) { + super(delegatee); + } + + void jrefSerialize(Object value, JsonGenerator gen, SerializationContext ctxt, Serializer serializer) { + @SuppressWarnings("unchecked") + Map valueToPtrMap = (Map) ctxt.getAttribute(PTR_MAP_ATTR); + // if it doesn't exist, then create and add as context attribute + if (valueToPtrMap == null) { + valueToPtrMap = new HashMap<>(); + ctxt.setAttribute(PTR_MAP_ATTR, valueToPtrMap); + } + JsonPointer ptr = valueToPtrMap.get(value); + if (ptr != null) { + // If JsonPointer found for value id, write it out and we're done! + gen.writeStartObject(); + gen.writeStringProperty(JREF_NAME, "#" + ptr.toString()); + gen.writeEndObject(); + } else { + // serialize the value with delegate + serializer.serialize(); + // put the object -> ptr into for possible reference usage + valueToPtrMap.put(value, JsonPointer.forPath(gen.streamWriteContext(), false)); + } + } + + @Override + public void serializeWithType(Object value, JsonGenerator gen, SerializationContext ctxt, + TypeSerializer typeSer) { + jrefSerialize(value, gen, ctxt, () -> super.serializeWithType(value, gen, ctxt, typeSer)); + } + + @Override + public void serialize(Object value, JsonGenerator gen, SerializationContext ctxt) { + jrefSerialize(value, gen, ctxt, () -> super.serialize(value, gen, ctxt)); + } + + @Override + public ValueSerializer newDelegatingInstance(ValueSerializer delegatee) { + return new JRefValueSerializer(delegatee); + } + + } + + @Override + public ValueSerializer modifySerializer(SerializationConfig config, Supplier beanDesc, + ValueSerializer serializer) { + return new JRefValueSerializer(serializer); + } + + @Override + public ValueSerializer modifyArraySerializer(SerializationConfig config, ArrayType valueType, + Supplier beanDesc, ValueSerializer serializer) { + return new JRefValueSerializer(serializer); + } + + @Override + public ValueSerializer modifyCollectionSerializer(SerializationConfig config, CollectionType valueType, + Supplier beanDesc, ValueSerializer serializer) { + return new JRefValueSerializer(serializer); + } + + @Override + public ValueSerializer modifyCollectionLikeSerializer(SerializationConfig config, + CollectionLikeType valueType, Supplier beanDesc, ValueSerializer serializer) { + return new JRefValueSerializer(serializer); + } + + @Override + public ValueSerializer modifyMapSerializer(SerializationConfig config, MapType valueType, Supplier beanDesc, + ValueSerializer serializer) { + return new JRefValueSerializer(serializer); + } + + @Override + public ValueSerializer modifyMapLikeSerializer(SerializationConfig config, MapLikeType valueType, + Supplier beanDesc, ValueSerializer serializer) { + return new JRefValueSerializer(serializer); + } + + @Override + public ValueSerializer modifyEnumSerializer(SerializationConfig config, JavaType valueType, + Supplier beanDesc, ValueSerializer serializer) { + return new JRefValueSerializer(serializer); + } + } + + public class JRefValueDeserializerModifier extends ValueDeserializerModifier { + + private static final long serialVersionUID = 1L; + + static final String STACK_ATTR = JRefValueDeserializerModifier.class.getName() + ".callStack"; + static final String OBJECT_PTR_MAP_ATTR = JRefValueDeserializerModifier.class.getName() + ".objectPtrMap"; + + @FunctionalInterface + interface Deserializer { + Object deserialize(JsonParser p) throws RuntimeException; + } + + class JRefValueDeserializer extends DelegatingDeserializer { + + JRefValueDeserializer(ValueDeserializer src) { + super(src); + } + + Object jrefDeserialize(JsonParser p, DeserializationContext ctxt, Deserializer deserializer) { + @SuppressWarnings("unchecked") + Deque ptrStack = (Deque) ctxt.getAttribute(STACK_ATTR); + if (ptrStack == null) { + // Create on first access + ptrStack = new ArrayDeque<>(); + ctxt.setAttribute(STACK_ATTR, ptrStack); + } + JsonPointer parentPtr = ptrStack.peek(); + if (parentPtr == null) { + // use empty + parentPtr = JsonPointer.empty(); + } + JsonPointer ctxtPtr = JsonPointer.forPath(p.streamReadContext(), false); + // build currPtr from context and parent + JsonPointer currPtr = ctxtPtr.toString().startsWith(parentPtr.toString()) ? ctxtPtr + : parentPtr.append(ctxtPtr); + ptrStack.push(currPtr); + Object result = null; + if (p.currentToken() == JsonToken.START_OBJECT) { + JsonNode node = ctxt.readTree(p); + // Look for "$ref" property + JsonNode jrefValue = node.asObject().get(JREF_NAME); + if (jrefValue != null) { + String pathWithHashExpected = jrefValue.asString(); + // Must start with # (local-only json pointers) + if (!pathWithHashExpected.startsWith(HASH)) { + throw DatabindException.from(p, + String.format("JsonPointer value=%s must start with '#' character (local only)", + pathWithHashExpected)); + } + // Remove hash prefix (local only) + String path = pathWithHashExpected.substring(1); + try { + // create JsonPointer from jref path + JsonPointer pathPtr = JsonPointer.valueOf(path); + if (pathPtr.equals(JsonPointer.empty())) { + throw DatabindException.from(p, "JsonPointer cannot be empty"); + } + @SuppressWarnings("unchecked") + Map resultsMap = (Map) ctxt + .getAttribute(OBJECT_PTR_MAP_ATTR); + if (resultsMap != null) { + // lookup previous result with ptr + Object previousResult = resultsMap.get(pathPtr); + if (previousResult == null) { + throw DatabindException.from(p, + String.format("Could not find previous value for JsonPointer=%s", pathPtr)); + } + // result found + result = previousResult; + } else { + throw DatabindException.from(p, + String.format("No previous values present for JsonPointer=%", pathPtr)); + } + } catch (IllegalArgumentException e) { + throw DatabindException.from(p, String.format("Illegal JsonPointer=%s", path), e); + } + } + // If we have not found result via jref, then reset parser to + // TreeTraversingParser + if (result == null) { + p = new TreeTraversingParser(node); + if (p.currentToken() != JsonToken.END_OBJECT) { + p.nextToken(); + } + } + } + // call delegate deserializer if no result yet + if (result == null) { + // If jref result not found, delegate serialization by calling super class + result = deserializer.deserialize(p); + if (result != null) { + @SuppressWarnings("unchecked") + Map resultsMap = (Map) ctxt + .getAttribute(OBJECT_PTR_MAP_ATTR); + if (resultsMap == null) { + resultsMap = new HashMap<>(); + ctxt.setAttribute(OBJECT_PTR_MAP_ATTR, resultsMap); + } + resultsMap.put(currPtr, result); + } + } + ptrStack.pollFirst(); + return result; + } + + @Override + public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, + TypeDeserializer typeDeserializer) throws JacksonException { + return jrefDeserialize(p, ctxt, (p1) -> super.deserializeWithType(p1, ctxt, typeDeserializer)); + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + return jrefDeserialize(p, ctxt, (p1) -> super.deserialize(p1, ctxt)); + } + + @Override + protected ValueDeserializer newDelegatingInstance(ValueDeserializer newDelegatee) { + return new JRefValueDeserializer(newDelegatee); + } + + } + + @Override + public ValueDeserializer modifyArrayDeserializer(DeserializationConfig config, ArrayType valueType, + Supplier beanDescRef, ValueDeserializer deserializer) { + return new JRefValueDeserializer(deserializer); + } + + @Override + public ValueDeserializer modifyCollectionDeserializer(DeserializationConfig config, CollectionType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return new JRefValueDeserializer(deserializer); + } + + @Override + public ValueDeserializer modifyEnumDeserializer(DeserializationConfig config, JavaType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return new JRefValueDeserializer(deserializer); + } + + @Override + public ValueDeserializer modifyCollectionLikeDeserializer(DeserializationConfig config, + CollectionLikeType type, Supplier beanDescRef, ValueDeserializer deserializer) { + return new JRefValueDeserializer(deserializer); + } + + @Override + public ValueDeserializer modifyDeserializer(DeserializationConfig config, Supplier beanDescRef, + ValueDeserializer deserializer) { + return new JRefValueDeserializer(deserializer); + } + + @Override + public ValueDeserializer modifyMapDeserializer(DeserializationConfig config, MapType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return new JRefValueDeserializer(deserializer); + } + + @Override + public ValueDeserializer modifyMapLikeDeserializer(DeserializationConfig config, MapLikeType type, + Supplier beanDescRef, ValueDeserializer deserializer) { + return new JRefValueDeserializer(deserializer); + } + + } + +} diff --git a/src/test/java/tools/jackson/databind/jref/JRefAbstractTest.java b/src/test/java/tools/jackson/databind/jref/JRefAbstractTest.java new file mode 100644 index 0000000000..f8c529fa82 --- /dev/null +++ b/src/test/java/tools/jackson/databind/jref/JRefAbstractTest.java @@ -0,0 +1,38 @@ +package tools.jackson.databind.jref; + +import static org.junit.Assert.assertEquals; +import static tools.jackson.databind.testutil.DatabindTestUtil.jsonMapperBuilder; + +import java.util.regex.Pattern; + +import tools.jackson.databind.JRefModule; +import tools.jackson.databind.ObjectMapper; + +public class JRefAbstractTest { + + protected ObjectMapper buildObjectMapperWithJRefSupport() { + return jsonMapperBuilder().addModule(new JRefModule()).build(); + } + + protected ObjectMapper buildObjectMapperWithoutJRefSupport() { + return jsonMapperBuilder().build(); + } + + static long countMatches(String text, String target) { + if (text == null || target == null || target.isEmpty()) return 0; + String quotedTarget = Pattern.quote(target); + + return Pattern.compile(quotedTarget) + .matcher(text) + .results() + .count(); + } + + protected void assertJRefCount(String input, long expectedJRefs) { + assertEquals(expectedJRefs, countMatches(input,"$ref")); + } + + void trace(String method, String s) { + System.out.println(method+"."+s); + } +} diff --git a/src/test/java/tools/jackson/databind/jref/JRefArrayTest.java b/src/test/java/tools/jackson/databind/jref/JRefArrayTest.java new file mode 100644 index 0000000000..b9885dca47 --- /dev/null +++ b/src/test/java/tools/jackson/databind/jref/JRefArrayTest.java @@ -0,0 +1,196 @@ +package tools.jackson.databind.jref; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; + +public class JRefArrayTest extends JRefAbstractTest { + + @Test + void testObjectArrayRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + Object o1 = new Object(); + Object o2 = o1; + Object[] arr = new Object[] { o1, o2 }; + String out = mapper.writeValueAsString(arr); + assertJRefCount(out, 1); + trace("testObjectArrayRef jrefserialized=",out); + Object[] oa = mapper.readValue(out, Object[].class); + assertTrue(oa[0] instanceof Map); + assertTrue(oa[1] instanceof Map); + assertEquals(oa[0],oa[1]); + } + + @Test + void testStringArrayRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String o1 = new String("one"); + String o2 = o1; + String[] arr = new String[] { o1, o2 }; + String out = mapper.writeValueAsString(arr); + assertJRefCount(out, 1); + trace("testStringArrayRef jrefserialized=",out); + String[] oa = mapper.readValue(out, String[].class); + assertTrue(oa[0] instanceof String); + assertTrue(oa[1] instanceof String); + assertEquals(oa[0],oa[1]); + } + + @Test + void test2DObjectArrayRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + Object o1 = new Object(); + Object o2 = o1; + Object[] arr1 = new Object[] { o1, o2 }; + Object[] arr2 = arr1; + String out = mapper.writeValueAsString(new Object[][] { arr1, arr2 }); + assertJRefCount(out, 2); + trace("test2DObjectArrayRef jrefserialized=",out); + Object[][] oa = mapper.readValue(out, Object[][].class); + assertTrue(oa[0][0] instanceof Map); + assertTrue(oa[0][1] instanceof Map); + assertEquals(oa[0], oa[1]); + assertArrayEquals(oa[0], oa[1]); + } + + @Test + void test2DStringArrayRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String o1 = new String("one"); + String o2 = o1; + String[] arr1 = new String[] { o1, o2 }; + String[] arr2 = arr1; + String out = mapper.writeValueAsString(new String[][] { arr1, arr2 }); + assertJRefCount(out, 2); + trace("test2DStringArrayRef jrefserialized=",out); + String[][] oa = mapper.readValue(out, String[][].class); + assertTrue(oa[0][0] instanceof String); + assertTrue(oa[0][1] instanceof String); + assertEquals(oa[0], oa[1]); + assertArrayEquals(oa[0], oa[1]); + } + + @Test + void testIntegerArrayRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + Integer o1 = Integer.valueOf(100); + Integer o2 = o1; + Integer[] arr = new Integer[] { o1, o2 }; + String out = mapper.writeValueAsString(arr); + assertJRefCount(out, 1); + trace("testIntegerArrayRef jrefserialized=",out); + Integer[] oa = mapper.readValue(out, Integer[].class); + assertTrue(oa[0] instanceof Integer); + assertTrue(oa[1] instanceof Integer); + assertEquals(oa[0],oa[1]); + } + + @Test + void test2DIntegerArrayRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + Integer o1 = Integer.valueOf(5); + Integer o2 = o1; + Integer[] arr1 = new Integer[] { o1, o2 }; + Integer[] arr2 = arr1; + String out = mapper.writeValueAsString(new Integer[][] { arr1, arr2 }); + assertJRefCount(out, 2); + trace("test2DStringArrayRef jrefserialized=",out); + Integer[][] oa = mapper.readValue(out, Integer[][].class); + assertTrue(oa[0][0] instanceof Integer); + assertTrue(oa[0][1] instanceof Integer); + assertEquals(oa[0], oa[1]); + assertArrayEquals(oa[0], oa[1]); + } + + @Test + void test3DObjectArrayRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + Object o1 = new Object(); + Object o2 = o1; + Object[] arr1 = new Object[] { o1, o2 }; + Object[][] arr2d = new Object[][] { arr1, arr1 }; + Object[][][] arr3d = new Object[][][] { arr2d, arr2d }; + + String out = mapper.writeValueAsString(arr3d); + assertJRefCount(out, 3); + trace("test3DObjectArrayRef jrefserialized=", out); + + Object[][][] oa = mapper.readValue(out, Object[][][].class); + assertEquals(oa[0], oa[1]); + assertEquals(oa[0][0], oa[0][1]); + assertEquals(oa[0][0][0], oa[0][0][1]); + assertTrue(oa[0][0][0] instanceof Map); + } + + @Test + void test3DStringArrayRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String s1 = new String("test"); + String[] arr1 = new String[] { s1, s1 }; + String[][] arr2d = new String[][] { arr1, arr1 }; + String[][][] arr3d = new String[][][] { arr2d, arr2d }; + + String out = mapper.writeValueAsString(arr3d); + assertJRefCount(out, 3); + trace("test3DStringArrayRef jrefserialized=", out); + + String[][][] oa = mapper.readValue(out, String[][][].class); + assertArrayEquals(oa[0], oa[1]); + assertArrayEquals(oa[0][0], oa[0][1]); + assertEquals(oa[0][0][0], oa[0][0][1]); + assertTrue(oa[0][0][0] instanceof String); + } + + @Test + void test4DObjectArrayRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + Object o1 = new Object(); + Object[] arr1 = new Object[] { o1 }; + Object[][] arr2 = new Object[][] { arr1 }; + Object[][][] arr3 = new Object[][][] { arr2 }; + Object[][][][] arr4 = new Object[][][][] { arr3, arr3 }; + + String out = mapper.writeValueAsString(arr4); + assertJRefCount(out, 1); + trace("test4DObjectArrayRef jrefserialized=", out); + + Object[][][][] oa = mapper.readValue(out, Object[][][][].class); + assertEquals(oa[0], oa[1]); + assertEquals(oa[0][0], oa[1][0]); + assertEquals(oa[0][0][0], oa[1][0][0]); + assertTrue(oa[0][0][0][0] instanceof Map); + } + + @Test + void test4DStringArrayRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String s1 = new String("deep"); + String s2 = new String("thoughts"); + String[] arr1 = new String[] { s1, s2, s2 }; + String[] arr1a = arr1; + String[][] arr2 = new String[][] { arr1, arr1a }; + String[][] arr2a = arr2; + String[][][] arr3 = new String[][][] { arr2, arr2a }; + String[][][] arr3a = arr3; + String[][][][] arr4 = new String[][][][] { arr3, arr3a, arr3 }; + + String out = mapper.writeValueAsString(arr4); + assertJRefCount(out, 5); + trace("test4DStringArrayRef jrefserialized=", out); + + String[][][][] oa = mapper.readValue(out, String[][][][].class); + assertArrayEquals(oa[0], oa[1]); + assertArrayEquals(oa[0], oa[2]); + assertArrayEquals(oa[0][0], oa[0][1]); + assertArrayEquals(oa[0][0][0], oa[0][0][1]); + assertEquals(oa[0][0][0][0], "deep"); + assertEquals(oa[0][0][0][1], "thoughts"); + + } +} diff --git a/src/test/java/tools/jackson/databind/jref/JRefBeanNonPublicMemberDeserializerTest.java b/src/test/java/tools/jackson/databind/jref/JRefBeanNonPublicMemberDeserializerTest.java new file mode 100644 index 0000000000..b78ffa1cbf --- /dev/null +++ b/src/test/java/tools/jackson/databind/jref/JRefBeanNonPublicMemberDeserializerTest.java @@ -0,0 +1,288 @@ +package tools.jackson.databind.jref; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +import tools.jackson.databind.ObjectMapper; + +public class JRefBeanNonPublicMemberDeserializerTest extends JRefAbstractTest { + + @Test + public void testMapRefNoValueType() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + Object v1 = new String("val1"); + String k2 = new String("second"); + // second value refs first + Object v2 = v1; + Map mi = Map.of(k1,v1,k2,v2); + String out = mapper.writeValueAsString(mi); + trace("testMapRefNoValueType jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + + @Test + public void testMapNoRefNoValueType() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + Object v1 = new String("val1"); + String k2 = new String("second"); + // second value refs first + Object v2 = new String("val1"); + Map mi = Map.of(k1,v1,k2,v2); + String out = mapper.writeValueAsString(mi); + trace("testMapNoRefNoValueType jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + @Test + public void testStringListTwoValue() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // These are two separate instances, with same string underneath + String s1 = new String("one"); + String s2 = new String("one"); + List sl = List.of(s1, s2); + String out = mapper.writeValueAsString(sl); + trace("testStringListTwoValue jrefserialized=",out); + List result = mapper.readValue(out, List.class); + assertEquals(result.get(0), s1); + assertEquals(result.get(1), s2); + } + + @Test + public void testStringListOneValue() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String s1 = new String("one"); + // s2 is reference to s1 + String s2 = s1; + List sl = List.of(s1, s2); + String out = mapper.writeValueAsString(sl); + trace("testStringListOneValue jrefserialized=",out); + List result = mapper.readValue(out, List.class); + assertEquals(result.get(0), s1); + assertEquals(result.get(1), s2); + } + + @Test + public void testIntList() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + List l = List.of(10,10); + String input = mapper.writeValueAsString(l); + trace("testIntegerList",input); + List result = mapper.readValue(input, List.class); + assertEquals(l, result); + + } + + static class IntType { + @JsonProperty + int i; + } + + static class IntItems { + @JsonProperty + int j; + @JsonProperty + List items; + } + + @Test + public void testIntItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"j\": 20, \"items\":[ { \"i\": 10}, { \"i\": { \"$ref\": \"#/items/0/i\" }}, { \"i\": { \"$ref\": \"#/j\" }}]}"; + IntItems result = mapper.readValue(input, IntItems.class); + assertEquals(result.items.get(0).i, result.items.get(1).i); + assertEquals(result.j, result.items.get(2).i); + } + + static class StringItems { + @JsonProperty + List items; + @JsonProperty + String second; + } + + @Test + public void testStringItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[\"hello\", { \"$ref\": \"#/items/0\" }], \"second\": { \"$ref\": \"#/items/0\" }}"; + StringItems result = mapper.readValue(input, StringItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + assertEquals(result.items.get(0), result.second); + } + + static class IntegerItems { + @JsonProperty + List items; + } + + @Test + public void testIntegerItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5, { \"$ref\": \"#/items/0\" }]}"; + IntegerItems result = mapper.readValue(input, IntegerItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class DoubleItems { + @JsonProperty + List items; + } + + @Test + public void testDoubleItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5.0, { \"$ref\": \"#/items/0\" }]}"; + DoubleItems result = mapper.readValue(input, DoubleItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class FloatItems { + @JsonProperty + List items; + } + + @Test + public void testFloatItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5.0, { \"$ref\": \"#/items/0\" }]}"; + FloatItems result = mapper.readValue(input, FloatItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class BooleanItems { + @JsonProperty + List items; + } + + @Test + public void testBooleanItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[true, { \"$ref\": \"#/items/0\" }]}"; + BooleanItems result = mapper.readValue(input, BooleanItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + @JsonInclude(JsonInclude.Include.NON_NULL) + static class Human { + @JsonProperty + String name; + @JsonProperty + Human parent; + @JsonProperty + Map props; + @JsonProperty + Human o; + @JsonProperty + String otherName; + @JsonProperty + Map moreProps; + + public Human() { + } + + @Override + public String toString() { + return "Human[name=" + name + ", parent=" + parent + ", props=" + props + ", o=" + this.o + "]"; + } + + } + + static class Message { + @JsonProperty + List items; + + public Message(List items) { + this.items = items; + } + + @Override + public String toString() { + return "Message[items=" + items + "]"; + } + } + + @Test + public void testStringItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 }, \"otherName\": { \"$ref\": \"#/items/0/name\" } }]}"; + + Message msg = mapper.readValue(message, Message.class); + assertEquals(msg.items.get(0).name, msg.items.get(0).otherName); + } + + @Test + public void testCollectionStringKeyItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"$ref\": \"#/items/0\" }]}"; + + Message msg = mapper.readValue(message, Message.class); + assertEquals(msg.items.get(0), msg.items.get(1)); + } + + @Test + public void testCollectionObjectKeyItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"name\": \"wendy\", \"parent\": null, \"moreProps\": { \"$ref\": \"#/items/0/props\" }}]}"; + + Message msg = mapper.readValue(message, Message.class); + assertEquals(msg.items.get(0).props, msg.items.get(1).moreProps); + } + + @Test + public void testJRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + + Map m1 = Map.of("s1", 1); + Human sam = new Human(); + sam.name = "sam"; + sam.props = m1; + Map m2 = Map.of("q","r","p",sam); + Human wendy = new Human(); + wendy.name = "wendy"; + wendy.parent = sam; + wendy.props = m2; + Human rick = new Human(); + rick.name = "rick"; + rick.parent = sam; + rick.o = sam; + + // wendy and rick are the 2 items in message + Message mess = new Message(List.of(wendy, rick)); + + String gen = mapper.writeValueAsString(mess); + System.out.println("gen="+gen); + /* + String message = "{\r\n" + " \"items\" : [ {\r\n" + " \"name\" : \"wendy\",\r\n" + " \"parent\" : {\r\n" + + " \"name\" : \"sam\",\r\n" + " \"parent\" : null,\r\n" + " \"props\" : {\r\n" + + " \"s1\" : 1\r\n" + " }\r\n" + " },\r\n" + " \"props\" : {\r\n" + + " \"q\" : \"r\",\r\n" + " \"p\" : { \"$ref\" : \"#/items/0/parent\" }" + " }\r\n" + + " }, {\r\n" + " \"name\" : \"rick\",\r\n" + " \"parent\" : {\r\n" + + " \"$ref\" : \"#/items/0/parent\"\r\n" + " },\r\n" + + " \"o\" : { \"$ref\" : \"#/items/0/parent\" }\r\n" + " } ]\r\n" + "}"; + */ + // Now read + Message msg = mapper.readValue(gen, Message.class); + // Compare with structure expected + assertEquals(msg.items.size(), 2); + assertEquals(msg.items.get(0).parent, msg.items.get(0).props.get("p")); + assertEquals(msg.items.get(0).parent, msg.items.get(1).parent); + } + +} diff --git a/src/test/java/tools/jackson/databind/jref/JRefBeanPublicMemberTest.java b/src/test/java/tools/jackson/databind/jref/JRefBeanPublicMemberTest.java new file mode 100644 index 0000000000..430b24fa96 --- /dev/null +++ b/src/test/java/tools/jackson/databind/jref/JRefBeanPublicMemberTest.java @@ -0,0 +1,182 @@ +package tools.jackson.databind.jref; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; + +public class JRefBeanPublicMemberTest extends JRefAbstractTest { + + static class StringItems { + public List items; + public String second; + } + + @Test + public void testStringItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[\"hello\", { \"$ref\": \"#/items/0\" }], \"second\": { \"$ref\": \"#/items/0\" }}"; + StringItems result = mapper.readValue(input, StringItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + assertEquals(result.items.get(0), result.second); + } + + static class IntegerItems { + public List items; + } + + @Test + public void testIntegerItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5, { \"$ref\": \"#/items/0\" }]}"; + IntegerItems result = mapper.readValue(input, IntegerItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class DoubleItems { + public List items; + } + + @Test + public void testDoubleItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5.0, { \"$ref\": \"#/items/0\" }]}"; + DoubleItems result = mapper.readValue(input, DoubleItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class FloatItems { + public List items; + } + + @Test + public void testFloatItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[5.0, { \"$ref\": \"#/items/0\" }]}"; + FloatItems result = mapper.readValue(input, FloatItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class BooleanItems { + public List items; + } + + @Test + public void testBooleanItems() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String input = "{\"items\":[true, { \"$ref\": \"#/items/0\" }]}"; + BooleanItems result = mapper.readValue(input, BooleanItems.class); + assertEquals(result.items.get(0), result.items.get(1)); + } + + static class Human { + public String name; + public Human parent; + public Map props; + public Human o; + public String otherName; + public Map moreProps; + + public Human() { + } + + @Override + public String toString() { + return "Human[name=" + name + ", parent=" + parent + ", props=" + props + ", o=" + this.o + "]"; + } + + } + + static class Message { + public List items; + + public Message(List items) { + this.items = items; + } + + @Override + public String toString() { + return "Message[items=" + items + "]"; + } + } + + @Test + public void testStringItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 }, \"otherName\": { \"$ref\": \"#/items/0/name\" } }]}"; + + Message msg = mapper.readValue(message, Message.class); + assertEquals(msg.items.get(0).name, msg.items.get(0).otherName); + } + + @Test + public void testCollectionStringKeyItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"$ref\": \"#/items/0\" }]}"; + + Message msg = mapper.readValue(message, Message.class); + assertEquals(msg.items.get(0), msg.items.get(1)); + } + + @Test + public void testCollectionObjectKeyItemPath() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + // Input has first item in Message.items list fully defined, and second item + // jrefs to first item + String message = "{\"items\": [{ \"name\": \"sam\", \"parent\": null, \"props\": { \"p\": 1 } }, { \"name\": \"wendy\", \"parent\": null, \"moreProps\": { \"$ref\": \"#/items/0/props\" }}]}"; + + Message msg = mapper.readValue(message, Message.class); + assertEquals(msg.items.get(0).props, msg.items.get(1).moreProps); + } + + @Test + public void testJRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + + Map m1 = Map.of("s1", 1); + Human sam = new Human(); + sam.name = "sam"; + sam.props = m1; + Map m2 = Map.of("q", "r", "p", sam); + Human wendy = new Human(); + wendy.name = "wendy"; + wendy.parent = sam; + wendy.props = m2; + Human rick = new Human(); + rick.name = "rick"; + rick.parent = sam; + rick.o = sam; + + // wendy and rick are the 2 items in message + Message mess = new Message(List.of(wendy, rick)); + + String gen = mapper.writeValueAsString(mess); + System.out.println("gen=" + gen); + /* + * String message = "{\r\n" + " \"items\" : [ {\r\n" + + * " \"name\" : \"wendy\",\r\n" + " \"parent\" : {\r\n" + + * " \"name\" : \"sam\",\r\n" + " \"parent\" : null,\r\n" + + * " \"props\" : {\r\n" + " \"s1\" : 1\r\n" + " }\r\n" + + * " },\r\n" + " \"props\" : {\r\n" + " \"q\" : \"r\",\r\n" + + * " \"p\" : { \"$ref\" : \"#/items/0/parent\" }" + " }\r\n" + + * " }, {\r\n" + " \"name\" : \"rick\",\r\n" + " \"parent\" : {\r\n" + + * " \"$ref\" : \"#/items/0/parent\"\r\n" + " },\r\n" + + * " \"o\" : { \"$ref\" : \"#/items/0/parent\" }\r\n" + " } ]\r\n" + "}"; + */ + // Now read + Message msg = mapper.readValue(gen, Message.class); + // Compare with structure expected + assertEquals(msg.items.size(), 2); + assertEquals(msg.items.get(0).parent, msg.items.get(0).props.get("p")); + assertEquals(msg.items.get(0).parent, msg.items.get(1).parent); + } + +} diff --git a/src/test/java/tools/jackson/databind/jref/JRefCircularAndNestedReferenceTests.java b/src/test/java/tools/jackson/databind/jref/JRefCircularAndNestedReferenceTests.java new file mode 100644 index 0000000000..0e33b3de3f --- /dev/null +++ b/src/test/java/tools/jackson/databind/jref/JRefCircularAndNestedReferenceTests.java @@ -0,0 +1,139 @@ +package tools.jackson.databind.jref; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.exc.StreamConstraintsException; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.ObjectMapper; + +public class JRefCircularAndNestedReferenceTests extends JRefAbstractTest { + + static class Node { + public String name; + public Node child; + public Node sibling; + public List neighbors = new ArrayList<>(); + + public Node() {} + + public Node(String name) { + this.name = name; + } + } + + static class Graph { + public Node root; + public List allNodes = new ArrayList<>(); + } + + @Test + public void testNestedCircularDeserialization() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + + // JSON representing a circular structure: root -> child -> child (ref to root) + String json = "{" + + " \"root\": {" + + " \"name\": \"parent\"," + + " \"child\": {" + + " \"name\": \"child\"," + + " \"child\": { \"$ref\": \"#/root\" }" + + " }" + + " }" + + "}"; + + try { + mapper.readValue(json, Graph.class); + fail(); + } catch (DatabindException e) { + // this should be thrown by deserialization + // so we pass + } + + } + + @Test + public void testSiblingAndNeighborDeserialization() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + + // JSON representing nodes where neighbors refer back to previous nodes in a list + String json = "{" + + " \"allNodes\": [" + + " { \"name\": \"node0\" }," + + " { \"name\": \"node1\", \"sibling\": { \"$ref\": \"#/allNodes/0\" } }," + + " { \"name\": \"node2\", \"neighbors\": [ { \"$ref\": \"#/allNodes/0\" }, { \"$ref\": \"#/allNodes/1\" } ] }" + + " ]" + + "}"; + + Graph graph = mapper.readValue(json, Graph.class); + + assertEquals(3, graph.allNodes.size()); + Node n0 = graph.allNodes.get(0); + Node n1 = graph.allNodes.get(1); + Node n2 = graph.allNodes.get(2); + + assertEquals("node0", n0.name); + assertEquals("node1", n1.name); + assertEquals("node2", n2.name); + + assertSame(n0, n1.sibling); + assertEquals(2, n2.neighbors.size()); + assertSame(n0, n2.neighbors.get(0)); + assertSame(n1, n2.neighbors.get(1)); + } + + @Test + public void testCircularStructureSerialization() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + + Node root = new Node("root"); + Node child = new Node("child"); + root.child = child; + child.child = root; // Circular + + Graph graph = new Graph(); + graph.root = root; + graph.allNodes.add(root); + graph.allNodes.add(child); + + try { + mapper.writeValueAsString(graph); + fail(); + } catch (StreamConstraintsException e) { + // this should be thrown by serialization + // so we pass + } + } + + @Test + public void testNestedPathDeserialization() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + + // Test resolving a path that goes through multiple levels of objects and arrays + String json = "{" + + " \"root\": {" + + " \"neighbors\": [" + + " { \"name\": \"neighbor0\", \"child\": { \"name\": \"inner\" } }" + + " ]" + + " }," + + " \"allNodes\": [" + + " { \"$ref\": \"#/root/neighbors/name/child/name\" }" + + " ]" + + "}"; + + try { + mapper.readValue(json, Graph.class); + fail(); + } catch (DatabindException e) { + // this should be thrown by deserialization + // so we pass + System.out.println(e); + } + } +} diff --git a/src/test/java/tools/jackson/databind/jref/JRefMapTest.java b/src/test/java/tools/jackson/databind/jref/JRefMapTest.java new file mode 100644 index 0000000000..12266f273e --- /dev/null +++ b/src/test/java/tools/jackson/databind/jref/JRefMapTest.java @@ -0,0 +1,186 @@ +package tools.jackson.databind.jref; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; + +public class JRefMapTest extends JRefAbstractTest { + + @Test + public void testMapKeyNoRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + String v1 = new String("val1"); + String k2 = new String("second"); + String v2 = new String("first"); + Map mi = Map.of(k1,v1,k2,v2); + String out = mapper.writeValueAsString(mi); + assertJRefCount(out, 0); + trace("testMapKeyNoRef jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + @Test + public void testMapKeyRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + String v1 = new String("val1"); + String k2 = new String("second"); + // second value refs first key, but serialization treats + // it like separate string, since map keys cannot be jrefs + String v2 = k1; + Map mi = Map.of(k1,v1,k2,v2); + String out = mapper.writeValueAsString(mi); + // Because the reference is a key, it should not be serialized to jref + // so count is expected to be 0 + assertJRefCount(out, 0); + trace("testMapKeyRef jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + @Test + public void testMapValueNoRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + String v1 = new String("val1"); + String k2 = new String("second"); + String v2 = new String("val1"); + Map mi = Map.of(k1,v1,k2,v2); + String out = mapper.writeValueAsString(mi); + assertJRefCount(out, 1); + trace("testMapValueNoRef jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + @Test + public void testMapValueRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + String v1 = new String("val1"); + String k2 = new String("second"); + // second value refs first + String v2 = v1; + Map mi = Map.of(k1,v1,k2,v2); + String out = mapper.writeValueAsString(mi); + assertJRefCount(out, 1); + trace("testMapValueRef jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + @Test + public void testMapValueMultRef() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + String v1 = new String("val1"); + String k2 = new String("second"); + // second value refs first + String v2 = v1; + String k3 = "third"; + // third value refs first + String v3 = v1; + String k4 = "fourth"; + String v4 = v1; + Map mi = Map.of(k1,v1,k2,v2,k3,v3,k4,v4); + String out = mapper.writeValueAsString(mi); + assertJRefCount(out, 3); + trace("testMapValueMultRef jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + @Test + public void testMapKeyNoRefNoValueType() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + Object v1 = new String("val1"); + String k2 = new String("second"); + Object v2 = new String("first"); + Map mi = Map.of(k1,v1,k2,v2); + String out = mapper.writeValueAsString(mi); + assertJRefCount(out, 0); + trace("testMapKeyNoRefNoValueType jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + @Test + public void testMapKeyRefNoValueType() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + Object v1 = new String("val1"); + String k2 = new String("second"); + // second value refs first key, but serialization treats + // it like separate string, since map keys cannot be jrefs + Object v2 = k1; + Map mi = Map.of(k1,v1,k2,v2); + String out = mapper.writeValueAsString(mi); + // Because the reference is a key, it should not be serialized to jref + // so count is expected to be 0 + assertJRefCount(out, 0); + trace("testMapKeyRefNoValueType jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + @Test + public void testMapValueNoRefNoValueType() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + Object v1 = new String("val1"); + String k2 = new String("second"); + Object v2 = new String("val1"); + Map mi = Map.of(k1,v1,k2,v2); + String out = mapper.writeValueAsString(mi); + assertJRefCount(out, 1); + trace("testMapValueNoRefNoValueType jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + + @Test + public void testMapValueRefNoValueType() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + Object v1 = new String("val1"); + String k2 = new String("second"); + // second value refs first + Object v2 = v1; + Map mi = Map.of(k1,v1,k2,v2); + String out = mapper.writeValueAsString(mi); + assertJRefCount(out, 1); + trace("testMapValueRefNoValueType jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + + @Test + public void testMapValueMultRefNoValueType() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String k1 = new String("first"); + Object v1 = new String("val1"); + String k2 = new String("second"); + // second value refs first + Object v2 = v1; + String k3 = "third"; + // third value refs first + Object v3 = v1; + String k4 = "fourth"; + Object v4 = v1; + Map mi = Map.of(k1,v1,k2,v2,k3,v3,k4,v4); + String out = mapper.writeValueAsString(mi); + assertJRefCount(out, 3); + trace("testMapValueMultRefNoValueType jrefserialized=",out); + Map mo = mapper.readValue(out, Map.class); + assertEquals(mi, mo); + } + +} diff --git a/src/test/java/tools/jackson/databind/jref/JRefRecordBeanTest.java b/src/test/java/tools/jackson/databind/jref/JRefRecordBeanTest.java new file mode 100644 index 0000000000..d4ab97d5c9 --- /dev/null +++ b/src/test/java/tools/jackson/databind/jref/JRefRecordBeanTest.java @@ -0,0 +1,100 @@ +package tools.jackson.databind.jref; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import tools.jackson.databind.ObjectMapper; + +public class JRefRecordBeanTest extends JRefAbstractTest { + + static record IntBean(Integer j) { + } + + @Test + public void testIntBeanArray() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + IntBean i1 = new IntBean(10); + IntBean i2 = new IntBean(20); + IntBean[] beans = new IntBean[] { i1, i2, i1, i2}; + String out = mapper.writeValueAsString(beans); + trace("testIntBeanArray", out); + IntBean[] result = mapper.readValue(out, IntBean[].class); + assertEquals(result[0], result[2]); + assertEquals(result[1], result[3]); + } + + @Test + public void testIntBeanList() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + IntBean i1 = new IntBean(10); + IntBean i2 = new IntBean(20); + List beans = List.of(i1, i2, i1, i2); + String out = mapper.writeValueAsString(beans); + trace("testIntBeanList", out); + @SuppressWarnings("unchecked") + List result = (List) mapper.readValue(out, List.class); + assertEquals(result.get(0), result.get(2)); + assertEquals(result.get(1), result.get(3)); + } + + record StringKeyBeanMap(Map items) { + } + + @Test + public void testStringKeyMap() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + IntBean i1 = new IntBean(10); + IntBean i2 = new IntBean(20); + StringKeyBeanMap beanMap = new StringKeyBeanMap(Map.of("one",i1,"two",i2,"three",i1,"four",i2)); + String out = mapper.writeValueAsString(beanMap); + trace("testIntBeanStringKeyMap", out); + StringKeyBeanMap result = mapper.readValue(out, StringKeyBeanMap.class); + assertEquals(result.items().get("one"), result.items().get("three")); + assertEquals(result.items().get("two"), result.items().get("four")); + } + + record Node(@JsonProperty Node parent, @JsonProperty String name) { + } + + record NodeList(@JsonProperty List nodes) { + + } + @Test + public void testNodeTree() throws Exception { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + Node root = new Node(null,"root"); + String[] nodeNames = new String[] {"child1","child2","child3"}; + List nodeList = new ArrayList<>(); + for(int i =0; i< nodeNames.length; i++) { + nodeList.add(new Node(root,nodeNames[i])); + } + // Add references to previously added nodes in reverse order + nodeList.add(nodeList.get(2)); + nodeList.add(nodeList.get(1)); + nodeList.add(nodeList.get(0)); + String out = mapper.writeValueAsString(new NodeList(nodeList)); + trace("testNodeTree", out); + NodeList result = mapper.readValue(out, NodeList.class); + List nodes = result.nodes(); + // assert nodes list same as input + assertEquals(nodeList.size(),nodes.size()); + for(int firstIndex = 0; firstIndex < nodeList.size() /2; firstIndex++) { + int secondIndex = (nodeList.size() - 1) - firstIndex; + Node n1 = result.nodes().get(firstIndex); + assertEquals(root.name(),n1.parent().name()); + Node n2 = result.nodes().get(secondIndex); + assertEquals(root.name(),n2.parent().name()); + assertEquals(n1,n2); + + } + } + + +} diff --git a/src/test/java/tools/jackson/databind/jref/JRefSimpleNestedTypeTest.java b/src/test/java/tools/jackson/databind/jref/JRefSimpleNestedTypeTest.java new file mode 100644 index 0000000000..b7766d879f --- /dev/null +++ b/src/test/java/tools/jackson/databind/jref/JRefSimpleNestedTypeTest.java @@ -0,0 +1,76 @@ +package tools.jackson.databind.jref; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import tools.jackson.databind.ObjectMapper; + +public class JRefSimpleNestedTypeTest extends JRefAbstractTest { + + record TreeNode(TreeNode parent, String name, String data) { + } + private static final String ADDRESS = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.\r\n" + + "\r\n" + + "Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.\r\n" + + "\r\n" + + "But, in a larger sense, we can not dedicate -- we can not consecrate -- we can not hallow -- this ground. The brave men, living and dead, who struggled here, have consecrated it, far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us -- that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion -- that we here highly resolve that these dead shall not have died in vain -- that this nation, under God, shall have a new birth of freedom -- and that government of the people, by the people, for the people, shall not perish from the earth."; + + TreeNode[] buildTwoLevelThreeChildrenArray() { + TreeNode topNode = new TreeNode(null, "top", ADDRESS); + // Create three children + TreeNode firstChild = new TreeNode(topNode, "child1", "data1"); + TreeNode secondChild = new TreeNode(topNode, "child2", "data2"); + TreeNode thirdChild = new TreeNode(topNode, "child3" , "data3"); + // Put top and all nodes in array + return new TreeNode[] { topNode, firstChild, secondChild, thirdChild }; + } + @Test + void testSerializeSingleLevelTreeThreeChildrenNoJRef() { + + ObjectMapper mapper = buildObjectMapperWithoutJRefSupport(); + String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(buildTwoLevelThreeChildrenArray()); + // No jrefs + assertJRefCount(json, 0); + } + + @Test + void testSerializeSingleLevelTreeThreeChildrenJRef() { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(buildTwoLevelThreeChildrenArray()); + assertJRefCount(json, 3); + } + + static String JREF_JSON = "[ {\r\n" + + " \"parent\" : null,\r\n" + + " \"name\" : \"top\",\r\n" + + " \"data\" : \"Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.\\r\\n\\r\\nNow we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.\\r\\n\\r\\nBut, in a larger sense, we can not dedicate -- we can not consecrate -- we can not hallow -- this ground. The brave men, living and dead, who struggled here, have consecrated it, far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us -- that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion -- that we here highly resolve that these dead shall not have died in vain -- that this nation, under God, shall have a new birth of freedom -- and that government of the people, by the people, for the people, shall not perish from the earth.\"\r\n" + + "}, {\r\n" + + " \"parent\" : {\r\n" + + " \"$ref\" : \"#/0\"\r\n" + + " },\r\n" + + " \"name\" : \"child1\",\r\n" + + " \"data\" : \"data1\"\r\n" + + "}, {\r\n" + + " \"parent\" : {\r\n" + + " \"$ref\" : \"#/0\"\r\n" + + " },\r\n" + + " \"name\" : \"child2\",\r\n" + + " \"data\" : \"data2\"\r\n" + + "}, {\r\n" + + " \"parent\" : {\r\n" + + " \"$ref\" : \"#/0\"\r\n" + + " },\r\n" + + " \"name\" : \"child3\",\r\n" + + " \"data\" : \"data3\"\r\n" + + "} ]"; + @Test + void testDeerializeSingleLevelTreeThreeChildrenJRef() { + ObjectMapper mapper = buildObjectMapperWithJRefSupport(); + TreeNode[] nodes = mapper.readValue(JREF_JSON, TreeNode[].class); + assertEquals(nodes[0],nodes[1].parent); + assertEquals(nodes[0],nodes[2].parent); + assertEquals(nodes[0],nodes[3].parent); + } + +}