BoxedDelegatingDeserializer.java

package tools.jackson.datatype.fastutil.deser;

import java.util.Collection;
import java.util.Map;

import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;

import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.DeserializationConfig;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.JavaType;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.deser.std.StdDeserializer;
import tools.jackson.databind.jsontype.TypeDeserializer;
import tools.jackson.databind.type.LogicalType;
import tools.jackson.databind.util.AccessPattern;
import tools.jackson.databind.util.ClassUtil;
import tools.jackson.datatype.fastutil.util.Merging;

/**
 * Deserializer that reads content as another container type (with boxed keys and
 * values), using the deserializer Jackson finds for that type, and then copies it
 * into the fastutil container to return. Used where handling of individual primitive
 * values is not enough: custom deserializers for primitive elements, keys or values
 * (like {@code @JsonDeserialize(contentUsing = ...)} for an {@code IntList}), which
 * produce boxed values. Content is read as an {@code ArrayList} or {@code LinkedHashMap}
 * by standard Jackson deserializers, which use them.
 *
 * @param <C> Type of container returned
 */
public final class BoxedDelegatingDeserializer<C> extends StdDeserializer<C>
{
    private final JavaType _delegateType;
    private final ContainerFamily.Creator<C> _creator;

    // Primitive types of map keys, and of map values or collection elements, if
    // primitive; null for reference types (which may be null)
    private final Class<?> _primitiveKeyType, _primitiveValueType;

    // null until contextualized
    private final ValueDeserializer<Object> _delegate;

    /**
     * @param delegateType Type to read content as: {@code ArrayList} or
     *    {@code LinkedHashMap} with boxed element (or key and value) types
     * @param primitiveKeys Whether map keys are primitive (cannot be null)
     * @param primitiveValues Whether map values or collection elements are primitive
     */
    public BoxedDelegatingDeserializer(JavaType type, JavaType delegateType,
            ContainerFamily.Creator<C> creator, boolean primitiveKeys, boolean primitiveValues) {
        this(type, delegateType, creator,
                primitiveKeys ? ClassUtil.primitiveType(delegateType.getKeyType().getRawClass()) : null,
                primitiveValues ? ClassUtil.primitiveType(delegateType.getContentType().getRawClass()) : null,
                null);
    }

    private BoxedDelegatingDeserializer(JavaType type, JavaType delegateType,
            ContainerFamily.Creator<C> creator, Class<?> primitiveKeyType, Class<?> primitiveValueType,
            ValueDeserializer<Object> delegate) {
        super(type);
        _delegateType = delegateType;
        _creator = creator;
        _primitiveKeyType = primitiveKeyType;
        _primitiveValueType = primitiveValueType;
        _delegate = delegate;
    }

    @Override
    public ValueDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
    {
        ValueDeserializer<Object> delegate = ctxt.findContextualValueDeserializer(_delegateType, property);
        if (delegate == _delegate) {
            return this;
        }
        return new BoxedDelegatingDeserializer<>(getValueType(), _delegateType, _creator,
                _primitiveKeyType, _primitiveValueType, delegate);
    }

    @Override
    public C deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
        Object values = _delegate.deserialize(p, ctxt);
        // delegate may coerce input to null (like empty String, if so configured)
        if (values == null) {
            return null;
        }
        C result = _creator.create(ctxt);
        _copy(ctxt, values, result);
        return _creator.finish(ctxt, result);
    }

    @Override
    public C deserialize(JsonParser p, DeserializationContext ctxt, C intoValue) throws JacksonException {
        Object values = _delegate.deserialize(p, ctxt);
        if (values != null) {
            // copy directly into existing value (no need to create a new one)
            try {
                _copy(ctxt, values, intoValue);
            } catch (UnsupportedOperationException e) {
                throw Merging.failure(ctxt, intoValue, e);
            }
        }
        return intoValue;
    }

    @Override
    public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
            TypeDeserializer typeDeserializer)
        throws JacksonException
    {
        // Delegate knows the expected shape (JSON Array or Object)
        return _delegate.deserializeWithType(p, ctxt, typeDeserializer);
    }

    @Override
    public Object getEmptyValue(DeserializationContext ctxt) {
        return _creator.createEmpty(ctxt);
    }

    @Override
    public AccessPattern getEmptyAccessPattern() {
        return AccessPattern.DYNAMIC;
    }

    @Override
    public Boolean supportsUpdate(DeserializationConfig config) {
        return Boolean.TRUE;
    }

    @Override
    public LogicalType logicalType() {
        return _delegateType.isMapLikeType() ? LogicalType.Map : LogicalType.Collection;
    }

    @SuppressWarnings("unchecked")
    private void _copy(DeserializationContext ctxt, Object source, C target) throws JacksonException
    {
        // Primitive sides cannot contain nulls, which (custom) deserializers may produce
        if (target instanceof Map<?, ?> map) {
            for (Map.Entry<?, ?> entry : ((Map<?, ?>) source).entrySet()) {
                Object key = entry.getKey();
                if (key == null && _primitiveKeyType != null) {
                    ctxt.reportInputMismatch(this, "Cannot add `null` key (as produced by deserializer) to %s",
                            ClassUtil.getTypeDescription(getValueType()));
                }
                ((Map<Object, Object>) map).put(key, _value(ctxt, entry.getValue()));
            }
        } else {
            for (Object value : (Collection<?>) source) {
                ((Collection<Object>) target).add(_value(ctxt, value));
            }
        }
    }

    // Nulls for primitive values handled the same way as without custom deserializers:
    // as specified by `FAIL_ON_NULL_FOR_PRIMITIVES` (default value if not failing)
    private Object _value(DeserializationContext ctxt, Object value) throws JacksonException {
        if (value == null && _primitiveValueType != null) {
            _verifyNullForPrimitive(ctxt);
            return ClassUtil.defaultValue(_primitiveValueType);
        }
        return value;
    }
}