RangeSerializer.java

package com.fasterxml.jackson.datatype.guava.ser;

import java.io.IOException;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.type.WritableTypeId;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.datatype.guava.deser.util.RangeHelper;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;

/**
 * Jackson serializer for Guava Range objects with enhanced serialization capabilities.
 * When the range property is annotated with {@code @JsonFormat(JsonFormat.Shape.STRING)},
 * it generates bracket notation for a more concise and human-readable representation.
 * Otherwise, it defaults to a more verbose standard serialization, explicitly writing out endpoints and bound types.
 *
 * <p>
 * Usage Example for bracket notation:
 * <pre>{@code
 *   @JsonFormat(JsonFormat.Shape.STRING)
 *   private Range<Integer> r;
 * }</pre>
 * When the range field is annotated with {@code @JsonFormat(JsonFormat.Shape.STRING)}, the serializer
 * will output the range that would look something like: [X..Y] or (X..Y).
 * <br><br>
 * By default, when the range property lacks the string shape annotation,
 * the serializer will output the JSON in following manner:
 * <pre>{@code
 *   {
 *      "lowerEndpoint": X,
 *      "lowerBoundType": "CLOSED",
 *      "upperEndpoint": Y,
 *      "upperBoundType": "CLOSED"
 *   }
 * }</pre>
 */
@SuppressWarnings("serial")
public class RangeSerializer extends StdSerializer<Range<?>>
    implements ContextualSerializer
{
    protected final JavaType _rangeType;

    protected final JsonSerializer<Object> _endpointSerializer;

    protected final RangeHelper.RangeProperties _fieldNames;

    /**
     * @since 2.17
     */
    protected final JsonFormat.Shape _shape;

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

    public RangeSerializer(JavaType type) { this(type, null); }

    @Deprecated // since 2.10
    public RangeSerializer(JavaType type, JsonSerializer<?> endpointSer)
    {
        this(type, endpointSer, RangeHelper.standardNames());
    }

    /**
     * @since 2.10
     *
     * @deprecated Since 2.17
     */
    @Deprecated
    public RangeSerializer(JavaType type, JsonSerializer<?> endpointSer,
            RangeHelper.RangeProperties fieldNames)
    {
        this(type, endpointSer, fieldNames, JsonFormat.Shape.ANY);
    }

    /**
     * @since 2.17
     */
    @SuppressWarnings("unchecked")
    public RangeSerializer(JavaType type, JsonSerializer<?> endpointSer,
                           RangeHelper.RangeProperties fieldNames, JsonFormat.Shape shape)
    {
        super(type);
        _rangeType = type;
        _endpointSerializer = (JsonSerializer<Object>) endpointSer;
        _fieldNames = fieldNames;
        _shape = shape;
    }

    @Override
    public boolean isEmpty(SerializerProvider prov, Range<?> value) {
        return value.isEmpty();
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov,
            BeanProperty property) throws JsonMappingException
    {
        final JsonFormat.Value format = findFormatOverrides(prov, property, handledType());
        final JsonFormat.Shape shape = format.getShape();

        final PropertyNamingStrategy propertyNamingStrategy = prov.getConfig().getPropertyNamingStrategy();
        final RangeHelper.RangeProperties nameMapping = RangeHelper.getPropertyNames(prov.getConfig(), propertyNamingStrategy);
        JsonSerializer<?> endpointSer = _endpointSerializer;

        if (endpointSer == null) {
            JavaType endpointType = _rangeType.containedTypeOrUnknown(0);
            // let's not consider "untyped" (java.lang.Object) to be meaningful here...
            if (endpointType != null && !endpointType.hasRawClass(Object.class)) {
                JsonSerializer<?> ser = prov.findValueSerializer(endpointType, property);
                return new RangeSerializer(_rangeType, ser, nameMapping, shape);
            }
            /* 21-Sep-2014, tatu: Need to make sure all serializers get proper contextual
             *   access, in case they rely on annotations on properties... (or, more generally,
             *   in getting a chance to be contextualized)
             */
        } else if (endpointSer instanceof ContextualSerializer) {
            endpointSer = ((ContextualSerializer) endpointSer).createContextual(prov, property);
        }
        if ((endpointSer != _endpointSerializer) || (nameMapping != null)) {
            return new RangeSerializer(_rangeType, endpointSer, nameMapping, shape);
        }
        return this;
    }

    /*
    /**********************************************************
    /* Serialization methods
    /**********************************************************
     */

    @Override
    public void serialize(Range<?> value, JsonGenerator gen, SerializerProvider provider)
        throws IOException, JsonGenerationException
    {
        if (_shape == JsonFormat.Shape.STRING) {
            gen.writeString(getStringFormat(value));
        } else {
            gen.writeStartObject(value);
            _writeContents(value, gen, provider);
            gen.writeEndObject();
        }
    }

    @Override
    public void serializeWithType(Range<?> value, JsonGenerator gen, SerializerProvider provider,
            TypeSerializer typeSer)
        throws IOException
    {
        gen.assignCurrentValue(value);
        if (_shape == JsonFormat.Shape.STRING) {
            String rangeString = getStringFormat(value);
            WritableTypeId typeId = typeSer.writeTypeSuffix(gen,
                    typeSer.typeId(rangeString, JsonToken.VALUE_STRING)
            );
            typeSer.writeTypeSuffix(gen, typeId);
        } else {
            WritableTypeId typeIdDef = typeSer.writeTypePrefix(gen,
                    typeSer.typeId(value, JsonToken.START_OBJECT));
            _writeContents(value, gen, provider);
            typeSer.writeTypeSuffix(gen, typeIdDef);
        }
    }

    protected String getStringFormat(Range<?> range){
        StringBuilder builder = new StringBuilder();

        if (range.hasLowerBound()) {
            builder.append(range.lowerBoundType() == BoundType.CLOSED ? '[' : '(').append(range.lowerEndpoint());
        } else {
            builder.append("(-���");
        }

        builder.append("..");

        if (range.hasUpperBound()) {
            builder.append(range.upperEndpoint()).append(range.upperBoundType() == BoundType.CLOSED ? ']' : ')');
        } else {
            builder.append("+���)");
        }

        return builder.toString();
    }

    protected void _writeContents(Range<?> value, JsonGenerator g, SerializerProvider provider)
        throws IOException
    {
        if (value.hasLowerBound()) {
            final String fieldName = _fieldNames.lowerEndpoint;
            if (_endpointSerializer != null) {
                g.writeFieldName(fieldName);
                _endpointSerializer.serialize(value.lowerEndpoint(), g, provider);
            } else {
                provider.defaultSerializeField(fieldName, value.lowerEndpoint(), g);
            }
            // 20-Mar-2016, tatu: Should not use default handling since it leads to
            //    [datatypes-collections#12] with default typing
            g.writeStringField(_fieldNames.lowerBoundType, value.lowerBoundType().name());
        }
        if (value.hasUpperBound()) {
            final String fieldName = _fieldNames.upperEndpoint;
            if (_endpointSerializer != null) {
                g.writeFieldName(fieldName);
                _endpointSerializer.serialize(value.upperEndpoint(), g, provider);
            } else {
                provider.defaultSerializeField(fieldName, value.upperEndpoint(), g);
            }
            // same as above; should always be just String so
            g.writeStringField(_fieldNames.upperBoundType, value.upperBoundType().name());
        }
    }

    @Override
    public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
        throws JsonMappingException
    {
        if (visitor != null) {
            JsonObjectFormatVisitor objectVisitor = visitor.expectObjectFormat(typeHint);
            if (objectVisitor != null) {
                if (_endpointSerializer != null) {
                    JavaType endpointType = _rangeType.containedType(0);
                    JavaType btType = visitor.getProvider().constructType(BoundType.class);
                    JsonSerializer<?> btSer = visitor.getProvider()
                            .findValueSerializer(btType, null);
                    objectVisitor.property(_fieldNames.lowerEndpoint, _endpointSerializer, endpointType);
                    objectVisitor.property(_fieldNames.lowerBoundType, btSer, btType);
                    objectVisitor.property(_fieldNames.upperEndpoint, _endpointSerializer, endpointType);
                    objectVisitor.property(_fieldNames.upperBoundType, btSer, btType);
                }
            }
        }
    }
}