GuavaCollectionDeserializer.java

package tools.jackson.datatype.guava.deser;

import java.util.Collection;
import java.util.function.Function;

import com.fasterxml.jackson.annotation.JsonFormat;

import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.deser.NullValueProvider;
import tools.jackson.databind.deser.std.ContainerDeserializerBase;
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.guava.util.NullElementHelper;

/**
 * Base class for Guava-specific collection deserializers.
 */
public abstract class GuavaCollectionDeserializer<T>
    extends ContainerDeserializerBase<T>
{
    /**
     * Marker value returned by {@link #_deserializeElement} for elements
     * to skip.
     */
    protected static final Object SKIPPED_ELEMENT = new Object();

    /**
     * Deserializer used for values contained in collection being deserialized;
     * either assigned on constructor, or during resolve().
     */
    protected final ValueDeserializer<?> _valueDeserializer;

    /**
     * If value instances have polymorphic type information, this
     * is the type deserializer that can deserialize required type
     * information
     */
    protected final TypeDeserializer _valueTypeDeserializer;

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

    protected GuavaCollectionDeserializer(JavaType selfType,
            ValueDeserializer<?> deser, TypeDeserializer typeDeser,
            NullValueProvider nuller, Boolean unwrapSingle)
    {
        super(selfType, nuller, unwrapSingle);
        _valueTypeDeserializer = typeDeser;
        _valueDeserializer = deser;
    }

    /**
     * Overridable fluent factory method used for creating contextual
     * instances.
     */
    public abstract GuavaCollectionDeserializer<T> withResolved(
            ValueDeserializer<?> valueDeser, TypeDeserializer typeDeser, 
            NullValueProvider nuller, Boolean unwrapSingle);

    /**
     * Method called to finalize setup of this deserializer,
     * after deserializer itself has been registered. This
     * is needed to handle recursive and transitive dependencies.
     */
    @Override
    public ValueDeserializer<?> createContextual(DeserializationContext ctxt,
            BeanProperty property)
    {
        Boolean unwrapSingle = findFormatFeature(ctxt, property, Collection.class,
                JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY);

        ValueDeserializer<?> valueDeser = _valueDeserializer;
        valueDeser = findConvertingContentDeserializer(ctxt, property, valueDeser);
        TypeDeserializer valueTypeDeser = _valueTypeDeserializer;
        if (valueDeser == null) {
            valueDeser = ctxt.findContextualValueDeserializer(_containerType.getContentType(), property);
        }
        if (valueTypeDeser != null) {
            valueTypeDeser = valueTypeDeser.forProperty(property);
        }

        NullValueProvider nuller = findContentNullProvider(ctxt, property, valueDeser);

        if ( (unwrapSingle != _unwrapSingle)
                || (nuller != _nullProvider)
                || (valueDeser != _valueDeserializer)
                || (valueTypeDeser != _valueTypeDeserializer)) {
            return withResolved(valueDeser, valueTypeDeser, nuller, unwrapSingle);
        }
        return this;
    }

    /*
    /**********************************************************
    /* Base class method implementations
    /**********************************************************
     */

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

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

    /*
    /**********************************************************
    /* Deserialization interface
    /**********************************************************
     */

    /**
     * Base implementation that does not assume specific type
     * inclusion mechanism. Sub-classes are expected to override
     * this method if they are to handle type information.
     */
    @Override
    public Object deserializeWithType(JsonParser p, DeserializationContext ctxt,
            TypeDeserializer typeDeserializer)
        throws JacksonException
    {
        return typeDeserializer.deserializeTypedFromArray(p, ctxt);
    }
    
    @SuppressWarnings("unchecked")
    @Override
    public T deserialize(JsonParser p, DeserializationContext ctxt)
            throws JacksonException
    {
        // Should usually point to START_ARRAY
        if (p.isExpectedStartArrayToken()) {
            return _deserializeContents(p, ctxt);
        }
        // But may support implicit arrays from single values?
        final boolean canWrap = (_unwrapSingle == Boolean.TRUE) ||
                ((_unwrapSingle == null) && ctxt.isEnabled(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY));
        if (canWrap) {
            return _deserializeFromSingleValue(p, ctxt);
        }
        // Otherwise, we have a problem
        return (T) ctxt.handleUnexpectedToken(getValueType(ctxt), p);
    }

    /*
    /**********************************************************************
    /* Abstract methods for impl classes
    /**********************************************************************
     */

    // Force abstract-ness for subclasses
    @Override
    public abstract AccessPattern getEmptyAccessPattern();

    // Force abstract-ness for subclasses
    @Override
    public abstract Object getEmptyValue(DeserializationContext ctxt);

    protected abstract T _deserializeContents(JsonParser p, DeserializationContext ctxt)
        throws JacksonException;

    /**
     * Method used to support implicit coercion from a single non-array value
     * into single-element collection.
     */
    protected T _deserializeFromSingleValue(JsonParser p, DeserializationContext ctxt)
        throws JacksonException
    {
        final JsonToken t = p.currentToken();
        Object value = _deserializeElement(p, ctxt, t);
        if (value != SKIPPED_ELEMENT) {
            // Avoid lambda allocation for the common non-null case
            if (value != null) {
                return _createWithSingleElement(ctxt, value);
            }
            T result = _addElement(p, ctxt, p.currentToken(), null,
                    v -> _createWithSingleElement(ctxt, v));
            // `null` if element dropped by problem handler
            if (result != null) {
                return result;
            }
        }
        return _createEmpty(ctxt);
    }

    /**
     * Helper method for deserializing a single element, given its first token.
     * Only JSON {@code null} tokens are skipped (if so configured): {@code null}
     * returned by value deserializer (or null provider) is returned as-is.
     *
     * @return Element deserialized (possibly {@code null}), or {@link #SKIPPED_ELEMENT}
     *    if element is to be skipped
     */
    protected Object _deserializeElement(JsonParser p, DeserializationContext ctxt,
            JsonToken t)
        throws JacksonException
    {
        if (t == JsonToken.VALUE_NULL) {
            if (_skipNullValues) {
                return SKIPPED_ELEMENT;
            }
            return _nullProvider.getNullValue(ctxt);
        }
        if (_valueTypeDeserializer == null) {
            return _valueDeserializer.deserialize(p, ctxt);
        }
        return _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
    }

    /**
     * Helper method for adding an element using given adder; {@code null}s that
     * the container does not accept are reported as problems.
     *
     * @param t Token parser is currently positioned at (for structured values,
     *    the matching {@code END_OBJECT} / {@code END_ARRAY}, not the start token)
     *
     * @return Result of adder, or {@code null} if element was dropped by
     *    a problem handler
     */
    protected <R> R _addElement(JsonParser p, DeserializationContext ctxt, JsonToken t,
            Object value, Function<Object, R> adder)
        throws JacksonException
    {
        return NullElementHelper.addElement(p, ctxt, t, _containerType.getContentType(),
                "Guava `Collection` of type "+ClassUtil.getTypeDescription(_containerType),
                value, adder);
    }

    protected abstract T _createEmpty(DeserializationContext ctxt);

    protected abstract T _createWithSingleElement(DeserializationContext ctxt, Object value);

    /**
     * Helper method for checking whether element type is untyped ({@code Object}),
     * in which case sorted containers need to use {@link UntypedElementComparator}
     * instead of natural ordering.
     */
    protected boolean _hasUntypedElements() {
        return _containerType.getContentType().hasRawClass(Object.class);
    }

    /**
     * Helper method for reporting failure to build a container, typically a sorted
     * one failing (with {@link ClassCastException}) due to elements that are not
     * mutually comparable.
     */
    protected <R> R _reportBuildFailure(DeserializationContext ctxt, String msg)
        throws JacksonException
    {
        return ctxt.reportInputMismatch(this,
                "Failed to build %s from deserialized elements: %s",
                ClassUtil.getTypeDescription(_containerType), msg);
    }

    /**
     * Some/many Guava containers do not allow addition of {@code null} values,
     * so isolate handling here.
     *
     * @since 2.17
     */
    protected void _tryToAddNull(JsonParser p, DeserializationContext ctxt, Collection<?> set)
    {
        _addElement(p, ctxt, p.currentToken(), null, v -> {
            @SuppressWarnings("unchecked")
            Collection<Object> coll = (Collection<Object>) set;
            return coll.add(v);
        });
    }
}