CollectionDeserializer.java

package tools.jackson.databind.deser.jdk;

import java.util.*;
import java.util.Objects;

import com.fasterxml.jackson.annotation.JsonFormat;

import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JacksonStdImpl;
import tools.jackson.databind.cfg.CoercionAction;
import tools.jackson.databind.cfg.CoercionInputShape;
import tools.jackson.databind.deser.*;
import tools.jackson.databind.deser.ReadableObjectId.Referring;
import tools.jackson.databind.deser.std.ContainerDeserializerBase;
import tools.jackson.databind.introspect.AnnotatedClass;
import tools.jackson.databind.jsontype.TypeDeserializer;
import tools.jackson.databind.type.LogicalType;
import tools.jackson.databind.util.ClassUtil;

/**
 * Basic serializer that can take JSON "Array" structure and
 * construct a {@link java.util.Collection} instance, with typed contents.
 *<p>
 * Note: for untyped content (one indicated by passing Object.class
 * as the type), {@link UntypedObjectDeserializer} is used instead.
 * It can also construct {@link java.util.List}s, but not with specific
 * POJO types, only other containers and primitives/wrappers.
 */
@JacksonStdImpl
public class CollectionDeserializer
    extends ContainerDeserializerBase<Collection<Object>>
{
    // // Configuration

    /**
     * Value deserializer.
     */
    protected final ValueDeserializer<Object> _valueDeserializer;

    /**
     * If element instances have polymorphic type information, this
     * is the type deserializer that can handle it
     */
    protected final TypeDeserializer _valueTypeDeserializer;

    // // Instance construction settings:

    protected final ValueInstantiator _valueInstantiator;

    /**
     * Deserializer that is used iff delegate-based creator is
     * to be used for deserializing from JSON Object.
     */
    protected final ValueDeserializer<Object> _delegateDeserializer;

    /**
     * Annotations defined on the actual Collection class; retained to avoid
     * re-introspection overhead during {@link #createContextual} calls.
     *
     * @since 3.1
     */
    protected transient final AnnotatedClass _classInfo;

    // NOTE: no PropertyBasedCreator, as JSON Arrays have no properties

    /*
    /**********************************************************
    /* Life-cycle
    /**********************************************************
     */

    /**
     * Constructor for context-free instances, where we do not yet know
     * which property is using this deserializer.
     *
     * @since 3.1
     */
    public CollectionDeserializer(JavaType collectionType,
            ValueDeserializer<Object> valueDeser,
            TypeDeserializer valueTypeDeser, ValueInstantiator valueInstantiator,
            AnnotatedClass classInfo)
    {
        this(collectionType, valueDeser, valueTypeDeser, valueInstantiator,
                null, null, null, classInfo);
    }

    @Deprecated // since 3.1
    public CollectionDeserializer(JavaType collectionType,
            ValueDeserializer<Object> valueDeser,
            TypeDeserializer valueTypeDeser, ValueInstantiator valueInstantiator)
    {
        this(collectionType, valueDeser, valueTypeDeser, valueInstantiator,
                null, null, null, null);
    }

    /**
     * Constructor used when creating contextualized instances.
     *
     * @since 3.1
     */
    protected CollectionDeserializer(JavaType collectionType,
            ValueDeserializer<Object> valueDeser, TypeDeserializer valueTypeDeser,
            ValueInstantiator valueInstantiator, ValueDeserializer<Object> delegateDeser,
            NullValueProvider nuller, Boolean unwrapSingle, AnnotatedClass classInfo)
    {
        super(collectionType, nuller, unwrapSingle);
        _valueDeserializer = valueDeser;
        _valueTypeDeserializer = valueTypeDeser;
        _valueInstantiator = valueInstantiator;
        _delegateDeserializer = delegateDeser;
        _classInfo = classInfo;
    }

    /**
     * Copy-constructor that can be used by sub-classes to allow
     * copy-on-write styling copying of settings of an existing instance.
     */
    protected CollectionDeserializer(CollectionDeserializer src)
    {
        super(src);
        _valueDeserializer = src._valueDeserializer;
        _valueTypeDeserializer = src._valueTypeDeserializer;
        _valueInstantiator = src._valueInstantiator;
        _delegateDeserializer = src._delegateDeserializer;
        _classInfo = src._classInfo;
    }

    /**
     * Factory method called by {@code BasicDeserializerFactory} to create
     * an instance
     *
     * @since 3.1
     */
    public static CollectionDeserializer create(JavaType collectionType,
            BeanDescription.Supplier beanDescRef,
            ValueDeserializer<Object> valueDeser,
            TypeDeserializer valueTypeDeser, ValueInstantiator valueInstantiator)
    {
        return new CollectionDeserializer(collectionType, valueDeser, valueTypeDeser,
                valueInstantiator, beanDescRef.getClassInfo());
    }

    /**
     * Fluent-factory method call to construct contextual instance.
     */
    @SuppressWarnings("unchecked")
    protected CollectionDeserializer withResolved(ValueDeserializer<?> dd,
            ValueDeserializer<?> vd, TypeDeserializer vtd,
            NullValueProvider nuller, Boolean unwrapSingle)
    {
        return new CollectionDeserializer(_containerType,
                (ValueDeserializer<Object>) vd, vtd,
                _valueInstantiator, (ValueDeserializer<Object>) dd,
                nuller, unwrapSingle, _classInfo);
    }

    // Important: do NOT cache if polymorphic values
    @Override
    public boolean isCachable() {
        // 26-Mar-2015, tatu: As per [databind#735], need to be careful
        return (_valueDeserializer == null)
                && (_valueTypeDeserializer == null)
                && (_delegateDeserializer == null)
                ;
    }

    @Override // since 2.12
    public LogicalType logicalType() {
        return LogicalType.Collection;
    }

    /*
    /**********************************************************
    /* Validation, post-processing (ResolvableDeserializer)
    /**********************************************************
     */

    /**
     * Method called to finalize setup of this deserializer,
     * when it is known for which property deserializer is needed for.
     */
    @Override
    public CollectionDeserializer createContextual(DeserializationContext ctxt,
            BeanProperty property)
    {
        // May need to resolve types for delegate-based creators:
        ValueDeserializer<Object> delegateDeser = null;
        if (_valueInstantiator != null) {
            if (_valueInstantiator.canCreateUsingDelegate()) {
                JavaType delegateType = _valueInstantiator.getDelegateType(ctxt.getConfig());
                if (delegateType == null) {
                    ctxt.reportBadDefinition(_containerType, String.format(
                            "Invalid delegate-creator definition for %s: value instantiator (%s) returned true for 'canCreateUsingDelegate()', but null for 'getDelegateType()'",
                            _containerType,
                            _valueInstantiator.getClass().getName()));
                }
                delegateDeser = findDeserializer(ctxt, delegateType, property);
            } else if (_valueInstantiator.canCreateUsingArrayDelegate()) {
                JavaType delegateType = _valueInstantiator.getArrayDelegateType(ctxt.getConfig());
                if (delegateType == null) {
                    ctxt.reportBadDefinition(_containerType, String.format(
                            "Invalid delegate-creator definition for %s: value instantiator (%s) returned true for 'canCreateUsingArrayDelegate()', but null for 'getArrayDelegateType()'",
                            _containerType,
                            _valueInstantiator.getClass().getName()));
                }
                delegateDeser = findDeserializer(ctxt, delegateType, property);
            }
        }
        // [databind#1043]: allow per-property allow-wrapping of single overrides:
        // 11-Dec-2015, tatu: Should we pass basic `Collection.class`, or more refined? Mostly
        //   comes down to "List vs Collection" I suppose... for now, pass Collection
        Boolean unwrapSingle = findFormatFeature(ctxt, property, Collection.class,
                _classInfo,
                JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
        // also, often value deserializer is resolved here:
        ValueDeserializer<?> valueDeser = _valueDeserializer;

        // May have a content converter
        valueDeser = findConvertingContentDeserializer(ctxt, property, valueDeser);
        final JavaType vt = _containerType.getContentType();
        if (valueDeser == null) {
            valueDeser = ctxt.findContextualValueDeserializer(vt, property);
        } else { // if directly assigned, probably not yet contextual, so:
            valueDeser = ctxt.handleSecondaryContextualization(valueDeser, property, vt);
        }
        // and finally, type deserializer needs context as well
        TypeDeserializer valueTypeDeser = _valueTypeDeserializer;
        if (valueTypeDeser != null) {
            valueTypeDeser = valueTypeDeser.forProperty(property);
        }
        NullValueProvider nuller = findContentNullProvider(ctxt, property, valueDeser);
        if ((!Objects.equals(unwrapSingle, _unwrapSingle))
                || (nuller != _nullProvider)
                || (delegateDeser != _delegateDeserializer)
                || (valueDeser != _valueDeserializer)
                || (valueTypeDeser != _valueTypeDeserializer)
        ) {
            return withResolved(delegateDeser, valueDeser, valueTypeDeser,
                    nuller, unwrapSingle);
        }
        return this;
    }

    /*
    /**********************************************************
    /* ContainerDeserializerBase API
    /**********************************************************
     */

    @Override
    public ValueDeserializer<Object> getContentDeserializer() {
        return _valueDeserializer;
    }

    @Override
    public ValueInstantiator getValueInstantiator() {
        return _valueInstantiator;
    }

    /*
    /**********************************************************
    /* ValueDeserializer impl
    /**********************************************************
     */

    @SuppressWarnings("unchecked")
    @Override
    public Collection<Object> deserialize(JsonParser p, DeserializationContext ctxt)
        throws JacksonException
    {
        if (_delegateDeserializer != null) {
            return (Collection<Object>) _valueInstantiator.createUsingDelegate(ctxt,
                    _delegateDeserializer.deserialize(p, ctxt));
        }
        // 16-May-2020, tatu: As per [dataformats-text#199] need to first check for
        //   possible Array-coercion and only after that String coercion
        if (p.isExpectedStartArrayToken()) {
            return _deserializeFromArray(p, ctxt, createDefaultInstance(ctxt));
        }
        // Empty String may be ok; bit tricky to check, however, since
        // there is also possibility of "auto-wrapping" of single-element arrays.
        // Hence we only accept empty String here.
        if (p.hasToken(JsonToken.VALUE_STRING)) {
            return _deserializeFromString(p, ctxt, p.getString());
        }
        return _handleNonArray(p, ctxt, createDefaultInstance(ctxt));
    }

    @SuppressWarnings("unchecked")
    protected Collection<Object> createDefaultInstance(DeserializationContext ctxt)
        throws JacksonException
    {
        return (Collection<Object>) _valueInstantiator.createUsingDefault(ctxt);
    }

    @Override
    public Collection<Object> deserialize(JsonParser p, DeserializationContext ctxt,
            Collection<Object> result)
        throws JacksonException
    {
        // Ok: must point to START_ARRAY (or equivalent)
        if (p.isExpectedStartArrayToken()) {
            return _deserializeFromArray(p, ctxt, result);
        }
        return _handleNonArray(p, ctxt, result);
    }

    @Override
    public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
            TypeDeserializer typeDeserializer)
        throws JacksonException
    {
        // In future could check current token... for now this should be enough:
        return typeDeserializer.deserializeTypedFromArray(p, ctxt);
    }

    /**
     * Logic extracted to deal with incoming String value.
     *
     * @since 2.12
     */
    @SuppressWarnings("unchecked")
    protected Collection<Object> _deserializeFromString(JsonParser p, DeserializationContext ctxt,
            String value)
        throws JacksonException
    {
        final Class<?> rawTargetType = handledType();

        // 05-Nov-2020, ckozak: As per [jackson-databind#2922] string values may be handled
        // using handleNonArray, however empty strings may result in a null or empty collection
        // depending on configuration.

        // Start by verifying if we got empty/blank string since accessing
        // CoercionAction may be costlier than String value we'll almost certainly
        // need anyway
        if (value.isEmpty()) {
            CoercionAction act = ctxt.findCoercionAction(logicalType(), rawTargetType,
                    CoercionInputShape.EmptyString);
            // handleNonArray may successfully deserialize the result (if
            // ACCEPT_SINGLE_VALUE_AS_ARRAY is enabled, for example) otherwise it
            // is capable of failing just as well as _deserializeFromEmptyString.
            if (act != null && act != CoercionAction.Fail) {
                return (Collection<Object>) _deserializeFromEmptyString(
                        p, ctxt, act, rawTargetType, "empty String (\"\")");
            }
            // note: `CoercionAction.Fail` falls through because we may need to allow
            // `ACCEPT_SINGLE_VALUE_AS_ARRAY` handling later on
        }
        // 26-Mar-2021, tatu: Some day is today; as per [dataformat-xml#460],
        //    we do need to support blank String too...
        else if (_isBlank(value)) {
            final CoercionAction act = ctxt.findCoercionFromBlankString(logicalType(), rawTargetType,
                    CoercionAction.Fail);
            if (act != CoercionAction.Fail) {
                return (Collection<Object>) _deserializeFromEmptyString(
                        p, ctxt, act, rawTargetType, "blank String (all whitespace)");
            }
            // note: `CoercionAction.Fail` falls through because we may need to allow
            // `ACCEPT_SINGLE_VALUE_AS_ARRAY` handling later on
        }
        return _handleNonArray(p, ctxt, createDefaultInstance(ctxt));
    }

    /**
     * @since 2.12
     */
    protected Collection<Object> _deserializeFromArray(JsonParser p, DeserializationContext ctxt,
            Collection<Object> result)
        throws JacksonException
    {
        // [databind#631]: Assign current value, to be accessible by custom serializers
        p.assignCurrentValue(result);

        // Let's offline handling of values with Object Ids (simplifies code here)
        if (_valueDeserializer.getObjectIdReader(ctxt) != null) {
            return _deserializeWithObjectId(p, ctxt, result);
        }
        JsonToken t;
        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);

                    // _skipNullValues is checked by _tryToAddNull.
                    if (value == null) {
                        _tryToAddNull(p, ctxt, result);
                        continue;
                    }
                }

                result.add(value);

                /* 17-Dec-2017, tatu: should not occur at this level...
            } catch (UnresolvedForwardReference reference) {
                throw DatabindException
                    .from(p, "Unresolved forward reference but no identity info", reference);
                */
            } catch (Exception e) {
                boolean wrap = (ctxt == null) || ctxt.isEnabled(DeserializationFeature.WRAP_EXCEPTIONS);
                if (!wrap) {
                    ClassUtil.throwIfRTE(e);
                }
                throw DatabindException.wrapWithPath(ctxt, e,
                        new JacksonException.Reference(result, result.size()));
            }
        }
        return result;
    }

    /**
     * Helper method called when current token is no START_ARRAY. Will either
     * throw an exception, or try to handle value as if member of implicit
     * array, depending on configuration.
     */
    @SuppressWarnings("unchecked")
    protected final Collection<Object> _handleNonArray(JsonParser p,
            DeserializationContext ctxt, Collection<Object> result)
        throws JacksonException
    {
        // Implicit arrays from single values?
        boolean canWrap = (_unwrapSingle == Boolean.TRUE) ||
                ((_unwrapSingle == null) &&
                        ctxt.isEnabled(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY));
        if (!canWrap) {
            return (Collection<Object>) ctxt.handleUnexpectedToken(_containerType, p);
        }
        JsonToken t = p.currentToken();

        // 03-Jan-2026: [databind#5537] Support Object Id for implicit Collections too
        if (_valueDeserializer.getObjectIdReader(ctxt) != null) {
            return _wrapSingleWithObjectId(p, ctxt, result);
        }

        Object value;

        try {
            if (t == JsonToken.VALUE_NULL) {
                // 03-Feb-2017, tatu: Hmmh. I wonder... let's try skipping here, too
                if (_skipNullValues) {
                    return result;
                }
                value = null;
            } else {
                value = _deserializeNoNullChecks(p, ctxt);
            }

            if (value == null) {
                value = _nullProvider.getNullValue(ctxt);

                // _skipNullValues is checked by _tryToAddNull.
                if (value == null) {
                    _tryToAddNull(p, ctxt, result);
                    return result;
                }
            }
        } catch (Exception e) {
            boolean wrap = ctxt.isEnabled(DeserializationFeature.WRAP_EXCEPTIONS);
            if (!wrap) {
                ClassUtil.throwIfRTE(e);
            }
            // note: pass Object.class, not Object[].class, as we need element type for error info
            throw DatabindException.wrapWithPath(ctxt, e,
                    new JacksonException.Reference(_containerType.getContentType().getRawClass(), result.size()));
        }
        result.add(value);
        return result;
    }

    protected Collection<Object> _deserializeWithObjectId(JsonParser p,
            DeserializationContext ctxt, Collection<Object> result)
        throws JacksonException
    {
        // Ok: must point to START_ARRAY (or equivalent)
        if (!p.isExpectedStartArrayToken()) {
            return _handleNonArray(p, ctxt, result);
        }
        // [databind#631]: Assign current value, to be accessible by custom serializers
        p.assignCurrentValue(result);

        CollectionReferringAccumulator referringAccumulator =
                new CollectionReferringAccumulator(_containerType.getContentType().getRawClass(), result);

        JsonToken t;
        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;
                    }
                }
                referringAccumulator.add(value);
            } catch (UnresolvedForwardReference reference) {
                Referring ref = referringAccumulator.handleUnresolvedReference(reference);
                reference.getRoid().appendReferring(ref);
            } catch (Exception e) {
                boolean wrap = (ctxt == null) || ctxt.isEnabled(DeserializationFeature.WRAP_EXCEPTIONS);
                if (!wrap) {
                    ClassUtil.throwIfRTE(e);
                }
                throw DatabindException.wrapWithPath(ctxt, e,
                        new JacksonException.Reference(result, result.size()));
            }
        }
        return result;
    }

    // @since 2.20.2
    // Copied from `_deserializeWithObjectId()` above
    protected Collection<Object> _wrapSingleWithObjectId(JsonParser p,
            DeserializationContext ctxt, Collection<Object> result)
        throws JacksonException
    {
        final CollectionReferringAccumulator referringAccumulator =
                new CollectionReferringAccumulator(getContentType().getRawClass(), result);

        try {
            Object value;
            if (p.hasToken(JsonToken.VALUE_NULL)) {
                if (_skipNullValues) {
                    return result;
                }
                value = null;
            } else {
                value = _deserializeNoNullChecks(p, ctxt);
            }

            if (value == null) {
                value = _nullProvider.getNullValue(ctxt);
                if (value == null) {
                    _tryToAddNull(p, ctxt, result);
                    return result;
                }
            }
            referringAccumulator.add(value);
        } catch (UnresolvedForwardReference reference) {
            Referring ref = referringAccumulator.handleUnresolvedReference(reference);
            reference.getRoid().appendReferring(ref);
        } catch (Exception e) {
            if (!ctxt.isEnabled(DeserializationFeature.WRAP_EXCEPTIONS)) {
                ClassUtil.throwIfRTE(e);
            }
            throw DatabindException.wrapWithPath(ctxt, e,
                    new JacksonException.Reference(result, result.size()));
        }
        return result;
    }

    /**
     * Deserialize the content of the collection.
     * If _valueTypeDeserializer is null, use _valueDeserializer.deserialize; if non-null,
     * use _valueDeserializer.deserializeWithType to deserialize value.
     * This method only performs deserialization and does not consider _skipNullValues, _nullProvider, etc.
     */
    protected Object _deserializeNoNullChecks(JsonParser p, DeserializationContext ctxt)
    {
        if (_valueTypeDeserializer == null) {
            return _valueDeserializer.deserialize(p, ctxt);
        }
        return _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
    }

    /**
     * {@code java.util.TreeSet} (and possibly other {@link Collection} types) does not
     * allow addition of {@code null} values, so isolate handling here.
     *
     */
    protected void _tryToAddNull(JsonParser p, DeserializationContext ctxt, Collection<?> set)
    {
        if (_skipNullValues) {
            return;
        }

        // Ideally we'd have better idea of where nulls are accepted, but first
        // let's just produce something better than NPE:
        try {
            set.add(null);
        } catch (NullPointerException e) {
            ctxt.handleUnexpectedToken(_valueType, JsonToken.VALUE_NULL, p,
                    "`java.util.Collection` of type %s does not accept `null` values",
                    ClassUtil.getTypeDescription(getValueType(ctxt)));
        }
    }
    /**
     * Helper class for dealing with Object Id references for values contained in
     * collections being deserialized.
     */
    public static class CollectionReferringAccumulator {
        private final Class<?> _elementType;
        private final Collection<Object> _result;

        /**
         * Values read at or after the first still unresolved reference, in encounter
         * order: plain values, and {@link CollectionReferring}s for references (resolved
         * or not). Values are moved to the result collection from the front (starting at
         * {@link #_pendingStart}) once no unresolved reference precedes them: this retains
         * ordering while moving each value just once -- instead of merging buffers of
         * resolved references into preceding ones, which takes O(N^2) time when
         * references resolve in reverse order (see [databind#6204]).
         */
        private final List<Object> _pending = new ArrayList<>();

        /**
         * Index of the first entry in {@link #_pending} not yet moved to the result.
         */
        private int _pendingStart;

        /**
         * Lookup from unresolved id to the -- usually single -- {@link CollectionReferring}s
         * with that id, in encounter order. Without this, resolution would need to scan
         * all pending references to find the matching one, which makes resolving N
         * references take O(N^2) time (see [databind#6204]).
         */
        private final Map<Object, Deque<CollectionReferring>> _unresolvedById = new HashMap<>();

        /**
         * Identity set of items already replaced in a {@link HashSet} result (when
         * rebound), to avoid scanning the result for each further reference to them.
         * Lazily created.
         */
        private Set<Object> _replacedSetItems;

        public CollectionReferringAccumulator(Class<?> elementType, Collection<Object> result) {
            _elementType = elementType;
            _result = result;
        }

        public void add(Object value)
        {
            if (_pendingStart == _pending.size()) {
                _result.add(value);
            } else {
                _pending.add(value);
            }
        }

        public Referring handleUnresolvedReference(UnresolvedForwardReference reference)
        {
            CollectionReferring id = new CollectionReferring(this, reference, _elementType);
            _pending.add(id);
            // Ids are usable as hash keys: `ObjectIdResolver`s already rely on that
            Deque<CollectionReferring> refs = _unresolvedById.get(reference.getUnresolvedId());
            if (refs == null) {
                refs = new ArrayDeque<>(2);
                _unresolvedById.put(reference.getUnresolvedId(), refs);
            }
            refs.add(id);
            return id;
        }

        public void resolveForwardReference(DeserializationContext ctxt, Object id, Object value) throws JacksonException
        {
            Deque<CollectionReferring> refs = _unresolvedById.get(id);
            if (refs == null) {
                throw new IllegalArgumentException("Trying to resolve a forward reference with id [" + id
                        + "] that wasn't previously seen as unresolved.");
            }
            CollectionReferring ref = refs.removeFirst();
            if (refs.isEmpty()) {
                _unresolvedById.remove(id);
            }
            ref.resolve(value);

            // Move values no longer preceded by an unresolved reference to the result
            final int end = _pending.size();
            int i = _pendingStart;
            for (; i < end; ++i) {
                Object pending = _pending.get(i);
                if (pending instanceof CollectionReferring) {
                    CollectionReferring pendingRef = (CollectionReferring) pending;
                    if (!pendingRef.isResolved()) {
                        break;
                    }
                    pendingRef.markMoved((_result instanceof List<?>) ? _result.size() : -1);
                    pending = pendingRef.resolvedValue();
                }
                _result.add(pending);
                _pending.set(i, null);
            }
            if (i == end) {
                _pending.clear();
                _pendingStart = 0;
            } else {
                _pendingStart = i;
            }
        }

        /**
         * Replace a resolved item in the result collection. Called when the bound
         * item is rebound (e.g., builder ��� built object) via
         * {@link Referring#handleItemRebind}.
         *
         * @param oldItem Item to replace (Builder)
         * @param newItem Item to replace {@code oldItem} with (Built value)
         *
         * @since 3.2
         */
        public void replaceResolvedItem(Object oldItem, Object newItem) {
            if (_result instanceof List<?>) {
                @SuppressWarnings("unchecked")
                List<Object> list = (List<Object>) _result;
                for (int i = 0, len = list.size(); i < len; i++) {
                    if (list.get(i) == oldItem) {
                        list.set(i, newItem);
                    }
                }
            } else if (_containsIdentity(_result, oldItem)) {
                // Non-list collections (e.g. LinkedHashSet, TreeSet): snapshot,
                // clear, and replay with substitution so insertion order /
                // comparator-based ordering is preserved and counts stay correct
                // for any Collection impl.
                List<Object> snapshot = new ArrayList<>(_result);
                _result.clear();
                for (Object item : snapshot) {
                    _result.add(item == oldItem ? newItem : item);
                }
            }
            // Same item may also still be pending, if an earlier forward reference has
            // not yet been resolved: as the value of a resolved reference, or as a plain
            // value (see [databind#6204] for the pending list)
            for (int i = _pendingStart, end = _pending.size(); i < end; ++i) {
                Object pending = _pending.get(i);
                if (pending instanceof CollectionReferring) {
                    CollectionReferring ref = (CollectionReferring) pending;
                    if (ref.isResolved() && (ref.resolvedValue() == oldItem)) {
                        ref.resolve(newItem);
                    }
                } else if (pending == oldItem) {
                    _pending.set(i, newItem);
                }
            }
        }

        /**
         * Replace the item resolved for given reference, after the bound item is rebound
         * (e.g., builder ��� built object). Unlike {@link #replaceResolvedItem(Object, Object)},
         * only replaces the slot of the reference itself (other references to the same
         * item get calls of their own) where possible: scanning the whole collection
         * instead would make rebinding N references take O(N^2) time.
         *
         * @since 3.2.3
         */
        void replaceResolvedItem(CollectionReferring ref, Object oldItem, Object newItem) {
            if (ref.resolvedValue() == oldItem) {
                ref.resolve(newItem);
                // Still pending: will be moved to result as is
                if (!ref.isMoved()) {
                    return;
                }
                final int index = ref.resultIndex();
                if (index >= 0) {
                    @SuppressWarnings("unchecked")
                    List<Object> list = (List<Object>) _result;
                    if ((index < list.size()) && (list.get(index) == oldItem)) {
                        list.set(index, newItem);
                        return;
                    }
                } else if (_result.getClass() == HashSet.class) {
                    // Unordered so position need not be retained
                    if (_replacedSetItems == null) {
                        _replacedSetItems = Collections.newSetFromMap(new IdentityHashMap<>());
                    }
                    // Already replaced via another reference to it? (must check by
                    // identity: an equal but different built value may be present)
                    if (!_replacedSetItems.add(oldItem)) {
                        return;
                    }
                    if (_result.remove(oldItem)) {
                        _result.add(newItem);
                        return;
                    }
                    // Not found: hash code of `oldItem` changed after being added
                    // (need to scan)
                }
            }
            // Ordered Sets and other Collections (or List modified by caller): need to
            // replace by scanning, to retain ordering
            replaceResolvedItem(oldItem, newItem);
        }

        // @since 3.2
        private static boolean _containsIdentity(Collection<?> coll, Object target) {
            for (Object item : coll) {
                if (item == target) {
                    return true;
                }
            }
            return false;
        }
    }

    /**
     * Placeholder for a value that is a forward reference, to maintain processing order
     * of values: holds the resolved value, once the reference has been resolved.
     */
    private final static class CollectionReferring extends Referring {
        private final CollectionReferringAccumulator _parent;

        private boolean _resolved;
        private Object _value;

        /**
         * Whether resolved value has been moved to the result collection.
         */
        private boolean _moved;

        /**
         * Index of the resolved value in the result collection, if moved there and
         * result is a {@link List}; -1 otherwise.
         */
        private int _resultIndex = -1;

        CollectionReferring(CollectionReferringAccumulator parent,
                UnresolvedForwardReference reference, Class<?> contentType)
        {
            super(reference, contentType);
            _parent = parent;
        }

        void resolve(Object value) {
            _value = value;
            _resolved = true;
        }

        boolean isResolved() { return _resolved; }

        Object resolvedValue() { return _value; }

        void markMoved(int resultIndex) {
            _moved = true;
            _resultIndex = resultIndex;
        }

        boolean isMoved() { return _moved; }

        int resultIndex() { return _resultIndex; }

        @Override
        public void handleResolvedForwardReference(DeserializationContext ctxt, Object id, Object value) throws JacksonException {
            _parent.resolveForwardReference(ctxt, id, value);
        }

        @Override
        public void handleItemRebind(Object oldItem, Object newItem) {
            _parent.replaceResolvedItem(this, oldItem, newItem);
        }
    }

}