GuavaImmutableCollectionDeserializer.java

package tools.jackson.datatype.guava.deser;

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

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.NullValueProvider;
import tools.jackson.databind.jsontype.TypeDeserializer;
import tools.jackson.databind.util.AccessPattern;
import tools.jackson.databind.util.ClassUtil;

import com.google.common.collect.ImmutableCollection;

abstract class GuavaImmutableCollectionDeserializer<T extends ImmutableCollection<Object>>
        extends GuavaCollectionDeserializer<T>
{
    GuavaImmutableCollectionDeserializer(JavaType selfType,
            ValueDeserializer<?> deser, TypeDeserializer typeDeser,
            NullValueProvider nuller, Boolean unwrapSingle) {
        super(selfType, deser, typeDeser, nuller, unwrapSingle);
    }

    protected abstract ImmutableCollection.Builder<Object> createBuilder();

    // Can not modify Immutable collections now can we
    @Override
    public Boolean supportsUpdate(DeserializationConfig config) {
        return Boolean.FALSE;
    }

    @Override
    public AccessPattern getEmptyAccessPattern() {
        // But we should be able to just share immutable empty instance
        return AccessPattern.CONSTANT;
    }

    @Override
    public T getEmptyValue(DeserializationContext ctxt) {
        return _createEmpty(ctxt);
    }

    @Override
    protected T _deserializeContents(JsonParser p, DeserializationContext ctxt)
        throws JacksonException
    {
        Object first = null;
        boolean hasFirst = false;

        while (p.nextToken() != JsonToken.END_ARRAY) {
            Object value = _deserializeSingleValue(p, ctxt);
            if (value == null) {
                // Null values need builder for proper error handling
                ImmutableCollection.Builder<Object> builder = createBuilder();
                if (hasFirst) {
                    builder.add(first);
                }
                if (!_skipNullValues) {
                    _tryToAddNull(p, ctxt, builder);
                }
                return _finishWithBuilder(p, ctxt, builder);
            }
            if (!hasFirst) {
                first = value;
                hasFirst = true;
            } else {
                ImmutableCollection.Builder<Object> builder = createBuilder();
                builder.add(first);
                builder.add(value);
                return _finishWithBuilder(p, ctxt, builder);
            }
        }

        if (!hasFirst) {
            return _createEmpty(ctxt);
        }
        return _createWithSingleElement(ctxt, first);
    }

    private Object _deserializeSingleValue(JsonParser p, DeserializationContext ctxt)
        throws JacksonException
    {
        if (p.currentToken() == JsonToken.VALUE_NULL) {
            return _resolveNullToValue(ctxt);
        }
        return _valueTypeDeserializer == null
            ? _valueDeserializer.deserialize(p, ctxt)
            : _valueDeserializer.deserializeWithType(p, ctxt, _valueTypeDeserializer);
    }

    private T _finishWithBuilder(JsonParser p, DeserializationContext ctxt,
            ImmutableCollection.Builder<Object> builder) throws JacksonException
    {
        while (p.nextToken() != JsonToken.END_ARRAY) {
            Object value = _deserializeSingleValue(p, ctxt);
            if (value == null) {
                if (!_skipNullValues) {
                    _tryToAddNull(p, ctxt, builder);
                }
            } else {
                builder.add(value);
            }
        }
        // No class outside of the package will be able to subclass us,
        // and we provide the proper builder for the subclasses we implement.
        @SuppressWarnings("unchecked")
        T collection = (T) builder.build();
        return collection;
    }

    protected Object _resolveNullToValue(DeserializationContext ctxt)
        throws JacksonException
    {
        Object value = _nullProvider.getNullValue(ctxt);

        // Since Guava (immutable) collections are not very happy with {@code null} values,
        // we may eventually need to do additional mapping (see [databind#53]).
        // But for now just use null provider:

        /*
        if (value == null) {
            value = _valueDeserializer.getNullValue(ctxt);
         }
         */
        return value;
    }

    /**
     * Some/many Guava containers do not allow addition of {@code null} values,
     * so isolate handling here.
     */
    protected void _tryToAddNull(JsonParser p, DeserializationContext ctxt,
            ImmutableCollection.Builder<Object> builder)
    {
        // Ideally we'd have better idea of where nulls are accepted, but first
        // let's just produce something better than NPE:
        try {
            builder.add((Object) null);
        } catch (NullPointerException e) {
            ctxt.handleUnexpectedToken(_valueType, JsonToken.VALUE_NULL, p,
                    "Guava `Collection` of type %s does not accept `null` values",
                    ClassUtil.getTypeDescription(getValueType(ctxt)));
        }
    }
}