diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 87f2bfce72..490d96d66a 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -112,3 +112,7 @@ Fouad Almalki (@Eng-Fouad) * Contributed fix for #5416: Annotations on an interface that's part of a JDK proxy are not honored with Jackson 3 [3.0.3] + +Hélios Gilles (@RoiSoleil) + * Contributed #5413: Add/support forward reference resolution for array values + [3.1.0] diff --git a/release-notes/VERSION b/release-notes/VERSION index 433e683e49..9a8b33f70e 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -12,6 +12,8 @@ Versions: 3.x (for earlier see VERSION-2.x) #5361: Fix Maven SBOM publishing (worked in 3.0.0-rc4 but not in rc5 or later) (date-time)#359: `InstantDeserializer` deserializes the nanosecond portion of fractional negative timestamps incorrectly +#5413: Add/support forward reference resolution for array values + (contributed by Hélios G) 3.0.3 (not yet released) diff --git a/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java index 6d97ac72b6..e5087aa2a1 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/CollectionDeserializer.java @@ -527,7 +527,7 @@ public static class CollectionReferringAccumulator { /** * A list of {@link CollectionReferring} to maintain ordering. */ - private List _accumulator = new ArrayList(); + private List _accumulator = new ArrayList<>(); public CollectionReferringAccumulator(Class elementType, Collection result) { _elementType = elementType; @@ -581,7 +581,7 @@ public void resolveForwardReference(DeserializationContext ctxt, Object id, Obje */ private final static class CollectionReferring extends Referring { private final CollectionReferringAccumulator _parent; - public final List next = new ArrayList(); + public final List next = new ArrayList<>(); CollectionReferring(CollectionReferringAccumulator parent, UnresolvedForwardReference reference, Class contentType) diff --git a/src/main/java/tools/jackson/databind/deser/jdk/ObjectArrayDeserializer.java b/src/main/java/tools/jackson/databind/deser/jdk/ObjectArrayDeserializer.java index 982d4ebb09..e8a40b831f 100644 --- a/src/main/java/tools/jackson/databind/deser/jdk/ObjectArrayDeserializer.java +++ b/src/main/java/tools/jackson/databind/deser/jdk/ObjectArrayDeserializer.java @@ -1,17 +1,27 @@ package tools.jackson.databind.deser.jdk; import java.lang.reflect.Array; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonFormat; - -import tools.jackson.core.*; -import tools.jackson.databind.*; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.core.JsonToken; +import tools.jackson.databind.BeanProperty; +import tools.jackson.databind.DatabindException; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ValueDeserializer; import tools.jackson.databind.annotation.JacksonStdImpl; import tools.jackson.databind.cfg.CoercionAction; import tools.jackson.databind.cfg.CoercionInputShape; import tools.jackson.databind.deser.NullValueProvider; +import tools.jackson.databind.deser.ReadableObjectId.Referring; +import tools.jackson.databind.deser.UnresolvedForwardReference; import tools.jackson.databind.deser.std.ContainerDeserializerBase; import tools.jackson.databind.jsontype.TypeDeserializer; import tools.jackson.databind.type.ArrayType; @@ -188,54 +198,13 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) if (!p.isExpectedStartArrayToken()) { return handleNonArray(p, ctxt); } - + if (_elementDeserializer.getObjectIdReader(ctxt) != null) { + return _deserializeWithObjectId(p, ctxt); + } final ObjectBuffer buffer = ctxt.leaseObjectBuffer(); Object[] chunk = buffer.resetAndStart(); int ix = 0; - JsonToken t; - - try { - while ((t = p.nextToken()) != JsonToken.END_ARRAY) { - // Note: must handle null explicitly here; value deserializers won't - Object value; - - if (t == JsonToken.VALUE_NULL) { - if (_skipNullValues) { - continue; - } - value = null; - } else { - value = _deserializeNoNullChecks(p, ctxt); - } - - if (value == null) { - value = _nullProvider.getNullValue(ctxt); - - if (value == null && _skipNullValues) { - continue; - } - } - - if (ix >= chunk.length) { - chunk = buffer.appendCompletedChunk(chunk); - ix = 0; - } - chunk[ix++] = value; - } - } catch (Exception e) { - throw DatabindException.wrapWithPath(ctxt, e, - new JacksonException.Reference(chunk, buffer.bufferedSize() + ix)); - } - - Object[] result; - - if (_untyped) { - result = buffer.completeAndClearBuffer(chunk, ix); - } else { - result = buffer.completeAndClearBuffer(chunk, ix, _elementClass); - } - ctxt.returnObjectBuffer(buffer); - return result; + return _deserialize(p, ctxt, buffer, ix, chunk); } @Override @@ -264,16 +233,23 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, System.arraycopy(arr, 0, result, offset, arr.length); return result; } + if (_elementDeserializer.getObjectIdReader(ctxt) != null) { + return _deserializeWithObjectId(p, ctxt); + } final ObjectBuffer buffer = ctxt.leaseObjectBuffer(); int ix = intoValue.length; Object[] chunk = buffer.resetAndStart(intoValue, ix); - JsonToken t; - - try { - while ((t = p.nextToken()) != JsonToken.END_ARRAY) { - Object value; + return _deserialize(p, ctxt, buffer, ix, chunk); + } + protected Object[] _deserialize(JsonParser p, DeserializationContext ctxt, + final ObjectBuffer buffer, int ix, Object[] chunk) + { + JsonToken t; + while ((t = p.nextToken()) != JsonToken.END_ARRAY) { + Object value; + try { if (t == JsonToken.VALUE_NULL) { if (_skipNullValues) { continue; @@ -282,28 +258,25 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, } else { value = _deserializeNoNullChecks(p, ctxt); } - if (value == null) { value = _nullProvider.getNullValue(ctxt); - if (value == null && _skipNullValues) { continue; } } + } catch (Exception e) { + throw DatabindException.wrapWithPath(ctxt, e, + new JacksonException.Reference(chunk, buffer.bufferedSize() + ix)); + } - if (ix >= chunk.length) { - chunk = buffer.appendCompletedChunk(chunk); - ix = 0; - } - chunk[ix++] = value; + if (ix >= chunk.length) { + chunk = buffer.appendCompletedChunk(chunk); + ix = 0; } - } catch (Exception e) { - throw DatabindException.wrapWithPath(ctxt, e, - new JacksonException.Reference(chunk, buffer.bufferedSize() + ix)); + chunk[ix++] = value; } - Object[] result; - + final Object[] result; if (_untyped) { result = buffer.completeAndClearBuffer(chunk, ix); } else { @@ -312,6 +285,51 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt, ctxt.returnObjectBuffer(buffer); return result; } + + protected Object[] _deserializeWithObjectId(JsonParser p, DeserializationContext ctxt) + { + final ObjectArrayReferringAccumulator acc = new ObjectArrayReferringAccumulator(_untyped, _elementClass); + + JsonToken t; + + int ix = 0; + while ((t = p.nextToken()) != JsonToken.END_ARRAY) { + try { + Object value; + + if (t == JsonToken.VALUE_NULL) { + if (_skipNullValues) { + continue; + } + value = null; + } else { + value = _deserializeNoNullChecks(p, ctxt); + } + + if (value == null) { + value = _nullProvider.getNullValue(ctxt); + + if (value == null && _skipNullValues) { + continue; + } + } + acc.add(value); + } catch (UnresolvedForwardReference reference) { + if (acc == null) { + throw reference; + } + ArrayReferring referring = new ArrayReferring(reference, _elementClass, acc); + reference.getRoid().appendReferring(referring); + } catch (Exception e) { + throw DatabindException.wrapWithPath(ctxt, e, + // 22-Nov-2025, tatu: Not ideal but has to do + new JacksonException.Reference(acc.buildArray(), ix)); + } + ++ix; + } + + return acc.buildArray(); + } /* /********************************************************************** @@ -420,4 +438,60 @@ protected Object _deserializeNoNullChecks(JsonParser p, DeserializationContext c } return _elementDeserializer.deserializeWithType(p, ctxt, _elementTypeDeserializer); } + + // @since 3.1 + private static class ObjectArrayReferringAccumulator { + private final boolean _untyped; + private final Class _elementType; + private final List _accumulator = new ArrayList<>(); + + private Object[] _array; + + ObjectArrayReferringAccumulator(boolean untyped, Class elementType) { + _untyped = untyped; + _elementType = elementType; + } + + void add(Object value) { + _accumulator.add(value); + } + + Object[] buildArray() { + if (_untyped) { + _array = new Object[_accumulator.size()]; + } else { + _array = (Object[]) Array.newInstance(_elementType, _accumulator.size()); + } + for (int i = 0; i < _accumulator.size(); i++) { + if (!(_accumulator.get(i) instanceof ArrayReferring)) { + _array[i] = _accumulator.get(i); + } + } + return _array; + } + } + + private static class ArrayReferring extends Referring { + private final ObjectArrayReferringAccumulator _parent; + + ArrayReferring(UnresolvedForwardReference ref, + Class type, + ObjectArrayReferringAccumulator acc) { + super(ref, type); + _parent = acc; + _parent._accumulator.add(this); + } + + @Override + public void handleResolvedForwardReference(DeserializationContext ctxt, + Object id, Object value) throws JacksonException { + for (int i = 0; i < _parent._accumulator.size(); i++) { + if (_parent._accumulator.get(i) == this) { + _parent._array[i] = value; + return; + } + } + throw new IllegalArgumentException("Trying to resolve unknown reference: " + id); + } + } } diff --git a/src/test/java/tools/jackson/databind/objectid/ObjectIdInObjectArray5413Test.java b/src/test/java/tools/jackson/databind/objectid/ObjectIdInObjectArray5413Test.java new file mode 100644 index 0000000000..d93637af56 --- /dev/null +++ b/src/test/java/tools/jackson/databind/objectid/ObjectIdInObjectArray5413Test.java @@ -0,0 +1,92 @@ +package tools.jackson.databind.objectid; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.annotation.JsonIdentityInfo; +import com.fasterxml.jackson.annotation.JsonIdentityReference; +import com.fasterxml.jackson.annotation.ObjectIdGenerators; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.testutil.DatabindTestUtil; + +/** + * This unit test verifies that the "Native" java type mapper can properly deal with + * "forward reference resolution" for values in Object arrays (not just + * {@link java.util.Collection}s). + */ +public class ObjectIdInObjectArray5413Test extends DatabindTestUtil +{ + static final class Draw { + private Shape[] ashapes; + private Point[] points; + + public Shape[] getAShapes() { + return ashapes; + } + + public void setAShapes(Shape[] shapes) { + this.ashapes = shapes; + } + + public Point[] getPoints() { + return points; + } + + public void setPoints(Point[] points) { + this.points = points; + } + } + + static final class Shape { + @JsonIdentityReference(alwaysAsId = true) + private Point[] points; + + public Point[] getPoints() { + return points; + } + + public void setPoints(Point[] points) { + this.points = points; + } + } + + @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") + public static final record Point(int id, int x, int y) { + } + + private final ObjectMapper MAPPER = newJsonMapper(); + + // [databind#5413] + @Test + public void testForwardReferenceResolution() + { + Draw draw = new Draw(); + Point point_0_0 = new Point(1, 0, 0); + Point point_0_2 = new Point(2, 0, 2); + Point point_2_2 = new Point(3, 2, 2); + Point point_2_0 = new Point(4, 2, 0); + Point point_1_3 = new Point(5, 1, 3); + Shape square = new Shape(); + square.setPoints(new Point[] { point_0_0, point_0_2, point_2_2, point_2_0 }); + Shape triangle = new Shape(); + triangle.setPoints(new Point[] { point_0_2, point_1_3, point_2_2 }); + draw.setAShapes(new Shape[] { square, triangle }); + draw.setPoints(new Point[] { point_0_0, point_0_2, point_2_2, point_2_0, point_1_3 }); + final String JSON = MAPPER.writeValueAsString(draw); + draw = MAPPER.readValue(JSON, Draw.class); + assertNotNull(draw); + assertEquals(5, draw.points.length); + assertEquals(2, draw.ashapes.length); + assertEquals(4, draw.ashapes[0].points.length); + assertEquals(3, draw.ashapes[1].points.length); + assertSame(draw.points[0], draw.ashapes[0].points[0]); + assertSame(draw.points[1], draw.ashapes[0].points[1]); + assertSame(draw.points[2], draw.ashapes[0].points[2]); + assertSame(draw.points[3], draw.ashapes[0].points[3]); + assertSame(draw.points[1], draw.ashapes[1].points[0]); + assertSame(draw.points[4], draw.ashapes[1].points[1]); + assertSame(draw.points[2], draw.ashapes[1].points[2]); + } + +}