RangeSerializer.java
package tools.jackson.datatype.guava.ser;
import com.google.common.collect.BoundType;
import com.google.common.collect.Range;
import com.fasterxml.jackson.annotation.JsonFormat;
import tools.jackson.core.*;
import tools.jackson.core.type.WritableTypeId;
import tools.jackson.databind.*;
import tools.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
import tools.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
import tools.jackson.databind.jsontype.TypeSerializer;
import tools.jackson.databind.ser.std.StdSerializer;
import tools.jackson.datatype.guava.deser.util.RangeHelper;
/**
* 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>
*/
public class RangeSerializer extends StdSerializer<Range<?>>
{
protected final JavaType _rangeType;
protected final ValueSerializer<Object> _endpointSerializer;
protected final RangeHelper.RangeProperties _fieldNames;
protected final JsonFormat.Shape _shape;
/*
/**********************************************************************
/* Life-cycle
/**********************************************************************
*/
public RangeSerializer(JavaType type) {
this(type, null, RangeHelper.standardNames(), JsonFormat.Shape.ANY);
}
@SuppressWarnings("unchecked")
protected RangeSerializer(JavaType type, ValueSerializer<?> endpointSer,
RangeHelper.RangeProperties fieldNames,
JsonFormat.Shape shape)
{
super(type);
_rangeType = type;
_endpointSerializer = (ValueSerializer<Object>) endpointSer;
_fieldNames = fieldNames;
_shape = shape;
}
@Override
public boolean isEmpty(SerializationContext ctxt, Range<?> value) {
return value.isEmpty();
}
@Override
public ValueSerializer<?> createContextual(SerializationContext ctxt,
BeanProperty property)
{
final JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
final JsonFormat.Shape shape = format.getShape();
final RangeHelper.RangeProperties fieldNames = RangeHelper.getPropertyNames(ctxt.getConfig(),
ctxt.getConfig().getPropertyNamingStrategy());
ValueSerializer<?> 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)) {
endpointSer = ctxt.findContentValueSerializer(endpointType, property);
}
} else {
endpointSer = _endpointSerializer.createContextual(ctxt, property);
}
if ((endpointSer != _endpointSerializer)
|| (fieldNames != _fieldNames)
|| (shape != _shape)) {
return new RangeSerializer(_rangeType, endpointSer, fieldNames, shape);
}
return this;
}
/*
/**********************************************************************
/* Serialization methods
/**********************************************************************
*/
@Override
public void serialize(Range<?> value, JsonGenerator gen, SerializationContext ctxt)
throws JacksonException
{
if (_shape == JsonFormat.Shape.STRING) {
gen.writeString(_getStringFormat(value));
} else {
gen.writeStartObject(value);
_writeContents(value, gen, ctxt);
gen.writeEndObject();
}
}
@Override
public void serializeWithType(Range<?> value, JsonGenerator gen, SerializationContext ctxt,
TypeSerializer typeSer)
throws JacksonException
{
gen.assignCurrentValue(value);
if (_shape == JsonFormat.Shape.STRING) {
String rangeString = _getStringFormat(value);
WritableTypeId typeId = typeSer.writeTypeSuffix(gen, ctxt,
typeSer.typeId(rangeString, JsonToken.VALUE_STRING)
);
typeSer.writeTypeSuffix(gen, ctxt, typeId);
} else {
WritableTypeId typeIdDef = typeSer.writeTypePrefix(gen, ctxt,
typeSer.typeId(value, JsonToken.START_OBJECT));
_writeContents(value, gen, ctxt);
typeSer.writeTypeSuffix(gen, ctxt, typeIdDef);
}
}
private void _writeContents(Range<?> value, JsonGenerator g, SerializationContext ctxt)
throws JacksonException
{
if (value.hasLowerBound()) {
if (_endpointSerializer != null) {
g.writeName(_fieldNames.lowerEndpoint);
_endpointSerializer.serialize(value.lowerEndpoint(), g, ctxt);
} else {
ctxt.defaultSerializeProperty(_fieldNames.lowerEndpoint, value.lowerEndpoint(), g);
}
// 20-Mar-2016, tatu: Should not use default handling since it leads to
// [datatypes-collections#12] with default typing
g.writeStringProperty(_fieldNames.lowerBoundType, value.lowerBoundType().name());
}
if (value.hasUpperBound()) {
if (_endpointSerializer != null) {
g.writeName(_fieldNames.upperEndpoint);
_endpointSerializer.serialize(value.upperEndpoint(), g, ctxt);
} else {
ctxt.defaultSerializeProperty(_fieldNames.upperEndpoint, value.upperEndpoint(), g);
}
// same as above; should always be just String so
g.writeStringProperty(_fieldNames.upperBoundType, value.upperBoundType().name());
}
}
private 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();
}
@Override
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
{
if (visitor != null) {
JsonObjectFormatVisitor objectVisitor = visitor.expectObjectFormat(typeHint);
if (objectVisitor != null) {
if (_endpointSerializer != null) {
JavaType endpointType = _rangeType.containedType(0);
JavaType btType = visitor.getContext().constructType(BoundType.class);
// should probably keep track of `property`...
ValueSerializer<?> btSer = visitor.getContext()
.findContentValueSerializer(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);
}
}
}
}
}