PrimitiveCollectionSerializers.java

package tools.jackson.datatype.fastutil.ser;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;

import com.fasterxml.jackson.annotation.JsonFormat;

import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.JsonToken;
import tools.jackson.core.type.WritableTypeId;

import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.ValueSerializer;
import tools.jackson.databind.jsontype.TypeSerializer;
import tools.jackson.databind.ser.jdk.CollectionSerializer;
import tools.jackson.databind.ser.std.StdSerializer;
import tools.jackson.databind.type.TypeFactory;
import tools.jackson.databind.util.ClassUtil;
import tools.jackson.datatype.fastutil.util.ContentSerializers;
import tools.jackson.datatype.fastutil.util.OptionalTypes;
import tools.jackson.datatype.primitive_collections_base.ser.PrimitiveIterableSerializer;

import it.unimi.dsi.fastutil.booleans.BooleanCollection;
import it.unimi.dsi.fastutil.booleans.BooleanIterator;
import it.unimi.dsi.fastutil.bytes.ByteCollection;
import it.unimi.dsi.fastutil.bytes.ByteIterator;
import it.unimi.dsi.fastutil.chars.CharCollection;
import it.unimi.dsi.fastutil.chars.CharIterator;
import it.unimi.dsi.fastutil.doubles.DoubleCollection;
import it.unimi.dsi.fastutil.doubles.DoubleIterator;
import it.unimi.dsi.fastutil.floats.FloatCollection;
import it.unimi.dsi.fastutil.floats.FloatIterator;
import it.unimi.dsi.fastutil.ints.IntCollection;
import it.unimi.dsi.fastutil.ints.IntIterator;
import it.unimi.dsi.fastutil.longs.LongCollection;
import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.shorts.ShortCollection;
import it.unimi.dsi.fastutil.shorts.ShortIterator;

/**
 * Serializers for fastutil collections of primitive values, which avoid boxing
 * of elements. Collections are written as JSON Arrays, except for {@code char}
 * collections that are written as JSON Strings (unless
 * {@link SerializationFeature#WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS} is enabled).
 */
public final class PrimitiveCollectionSerializers
{
    private PrimitiveCollectionSerializers() { }

    // Collection types missing from `fastutil-core` (a subset of `fastutil`, without
    // `boolean` or `byte` collections) are skipped
    private static final List<Entry> ENTRIES = new ArrayList<>();

    static {
        _register(() -> new Entry(BooleanCollection.class, BooleanSerializer.INSTANCE));
        _register(() -> new Entry(ByteCollection.class, ByteSerializer.INSTANCE));
        _register(() -> new Entry(ShortCollection.class, ShortSerializer.INSTANCE));
        _register(() -> new Entry(CharCollection.class, CharSerializer.INSTANCE));
        _register(() -> new Entry(IntCollection.class, IntSerializer.INSTANCE));
        _register(() -> new Entry(LongCollection.class, LongSerializer.INSTANCE));
        _register(() -> new Entry(FloatCollection.class, FloatSerializer.INSTANCE));
        _register(() -> new Entry(DoubleCollection.class, DoubleSerializer.INSTANCE));
    }

    private static void _register(Supplier<Entry> entry) {
        OptionalTypes.registerIfPresent(() -> ENTRIES.add(entry.get()));
    }

    private record Entry(Class<?> collectionType, ValueSerializer<?> serializer) { }

    /**
     * @return Serializer for given type if it is a fastutil collection of primitive
     *    values; {@code null} otherwise
     */
    public static ValueSerializer<?> findSerializer(Class<?> rawType)
    {
        for (Entry entry : ENTRIES) {
            if (entry.collectionType().isAssignableFrom(rawType)) {
                return entry.serializer();
            }
        }
        return null;
    }

    /**
     * Custom content serializers ({@code @JsonSerialize(contentUsing = ...)} or
     * {@code contentConverter}) take boxed values: if the property specifies one,
     * the standard Jackson {@code Collection} serializer is used instead.
     *
     * @return Contextualized standard serializer, if needed; {@code null} otherwise
     */
    static ValueSerializer<?> _standardSerializerIfNeeded(SerializationContext ctxt,
            BeanProperty property, Class<?> primitiveType)
    {
        if (!ContentSerializers.hasCustomContentSerialization(ctxt, property)) {
            return null;
        }
        return new CollectionSerializer(ctxt.constructType(ClassUtil.wrapperType(primitiveType)),
                false, null, null)
            .createContextual(ctxt, property);
    }

    public abstract static class Base<C extends Collection<?>>
        extends PrimitiveIterableSerializer<C>
    {
        protected Base(Class<C> type, Class<?> rawElementType,
                BeanProperty property, Boolean unwrapSingle) {
            super(type, TypeFactory.unsafeSimpleType(rawElementType), property, unwrapSingle);
        }

        /**
         * @return Serializer that uses given settings (from per-property
         *    {@code @JsonFormat} overrides)
         */
        protected abstract Base<C> withResolved(BeanProperty property, Boolean unwrapSingle);

        @Override
        public ValueSerializer<?> createContextual(SerializationContext ctxt, BeanProperty property)
        {
            ValueSerializer<?> std = _standardSerializerIfNeeded(ctxt, property,
                    _elementType.getRawClass());
            if (std != null) {
                return std;
            }
            Boolean unwrapSingle = findFormatFeature(ctxt, property, handledType(),
                    JsonFormat.Feature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED);
            if (Objects.equals(unwrapSingle, _unwrapSingle)) {
                return this;
            }
            return withResolved(property, unwrapSingle);
        }

        @Override
        public boolean isEmpty(SerializationContext ctxt, C value) {
            return value.isEmpty();
        }

        @Override
        public boolean hasSingleElement(C value) {
            return (value != null) && (value.size() == 1);
        }
    }

    public static final class BooleanSerializer extends Base<BooleanCollection>
    {
        public static final BooleanSerializer INSTANCE = new BooleanSerializer(null, null);

        public BooleanSerializer(BeanProperty property, Boolean unwrapSingle) {
            super(BooleanCollection.class, boolean.class, property, unwrapSingle);
        }

        @Override
        protected BooleanSerializer withResolved(BeanProperty property, Boolean unwrapSingle) {
            return new BooleanSerializer(property, unwrapSingle);
        }

        @Override
        protected void serializeContents(BooleanCollection value, JsonGenerator g)
            throws JacksonException
        {
            for (BooleanIterator it = value.iterator(); it.hasNext(); ) {
                g.writeBoolean(it.nextBoolean());
            }
        }
    }

    public static final class ByteSerializer extends Base<ByteCollection>
    {
        public static final ByteSerializer INSTANCE = new ByteSerializer(null, null);

        public ByteSerializer(BeanProperty property, Boolean unwrapSingle) {
            super(ByteCollection.class, byte.class, property, unwrapSingle);
        }

        @Override
        protected ByteSerializer withResolved(BeanProperty property, Boolean unwrapSingle) {
            return new ByteSerializer(property, unwrapSingle);
        }

        @Override
        protected void serializeContents(ByteCollection value, JsonGenerator g)
            throws JacksonException
        {
            for (ByteIterator it = value.iterator(); it.hasNext(); ) {
                g.writeNumber(it.nextByte());
            }
        }
    }

    public static final class ShortSerializer extends Base<ShortCollection>
    {
        public static final ShortSerializer INSTANCE = new ShortSerializer(null, null);

        public ShortSerializer(BeanProperty property, Boolean unwrapSingle) {
            super(ShortCollection.class, short.class, property, unwrapSingle);
        }

        @Override
        protected ShortSerializer withResolved(BeanProperty property, Boolean unwrapSingle) {
            return new ShortSerializer(property, unwrapSingle);
        }

        @Override
        protected void serializeContents(ShortCollection value, JsonGenerator g)
            throws JacksonException
        {
            for (ShortIterator it = value.iterator(); it.hasNext(); ) {
                g.writeNumber(it.nextShort());
            }
        }
    }

    public static final class IntSerializer extends Base<IntCollection>
    {
        public static final IntSerializer INSTANCE = new IntSerializer(null, null);

        public IntSerializer(BeanProperty property, Boolean unwrapSingle) {
            super(IntCollection.class, int.class, property, unwrapSingle);
        }

        @Override
        protected IntSerializer withResolved(BeanProperty property, Boolean unwrapSingle) {
            return new IntSerializer(property, unwrapSingle);
        }

        @Override
        protected void serializeContents(IntCollection value, JsonGenerator g)
            throws JacksonException
        {
            for (IntIterator it = value.iterator(); it.hasNext(); ) {
                g.writeNumber(it.nextInt());
            }
        }
    }

    public static final class LongSerializer extends Base<LongCollection>
    {
        public static final LongSerializer INSTANCE = new LongSerializer(null, null);

        public LongSerializer(BeanProperty property, Boolean unwrapSingle) {
            super(LongCollection.class, long.class, property, unwrapSingle);
        }

        @Override
        protected LongSerializer withResolved(BeanProperty property, Boolean unwrapSingle) {
            return new LongSerializer(property, unwrapSingle);
        }

        @Override
        protected void serializeContents(LongCollection value, JsonGenerator g)
            throws JacksonException
        {
            for (LongIterator it = value.iterator(); it.hasNext(); ) {
                g.writeNumber(it.nextLong());
            }
        }
    }

    public static final class FloatSerializer extends Base<FloatCollection>
    {
        public static final FloatSerializer INSTANCE = new FloatSerializer(null, null);

        public FloatSerializer(BeanProperty property, Boolean unwrapSingle) {
            super(FloatCollection.class, float.class, property, unwrapSingle);
        }

        @Override
        protected FloatSerializer withResolved(BeanProperty property, Boolean unwrapSingle) {
            return new FloatSerializer(property, unwrapSingle);
        }

        @Override
        protected void serializeContents(FloatCollection value, JsonGenerator g)
            throws JacksonException
        {
            for (FloatIterator it = value.iterator(); it.hasNext(); ) {
                g.writeNumber(it.nextFloat());
            }
        }
    }

    public static final class DoubleSerializer extends Base<DoubleCollection>
    {
        public static final DoubleSerializer INSTANCE = new DoubleSerializer(null, null);

        public DoubleSerializer(BeanProperty property, Boolean unwrapSingle) {
            super(DoubleCollection.class, double.class, property, unwrapSingle);
        }

        @Override
        protected DoubleSerializer withResolved(BeanProperty property, Boolean unwrapSingle) {
            return new DoubleSerializer(property, unwrapSingle);
        }

        @Override
        protected void serializeContents(DoubleCollection value, JsonGenerator g)
            throws JacksonException
        {
            for (DoubleIterator it = value.iterator(); it.hasNext(); ) {
                g.writeNumber(it.nextDouble());
            }
        }
    }

    /**
     * Writes {@code char} collections as JSON Strings, or as JSON Arrays of
     * single-character Strings if
     * {@link SerializationFeature#WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS} is enabled.
     */
    public static final class CharSerializer extends StdSerializer<CharCollection>
    {
        public static final CharSerializer INSTANCE = new CharSerializer();

        private CharSerializer() {
            super(CharCollection.class);
        }

        @Override
        public ValueSerializer<?> createContextual(SerializationContext ctxt, BeanProperty property)
        {
            ValueSerializer<?> std = _standardSerializerIfNeeded(ctxt, property, char.class);
            return (std == null) ? this : std;
        }

        @Override
        public boolean isEmpty(SerializationContext ctxt, CharCollection value) {
            return value.isEmpty();
        }

        @Override
        public void serialize(CharCollection value, JsonGenerator g, SerializationContext ctxt)
            throws JacksonException
        {
            if (ctxt.isEnabled(SerializationFeature.WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS)) {
                g.writeStartArray(value);
                _writeContentsAsArray(value, g);
                g.writeEndArray();
            } else {
                char[] chars = value.toCharArray();
                g.writeString(chars, 0, chars.length);
            }
        }

        @Override
        public void serializeWithType(CharCollection value, JsonGenerator g,
                SerializationContext ctxt, TypeSerializer typeSer)
            throws JacksonException
        {
            g.assignCurrentValue(value);
            WritableTypeId typeIdDef;
            if (ctxt.isEnabled(SerializationFeature.WRITE_CHAR_ARRAYS_AS_JSON_ARRAYS)) {
                typeIdDef = typeSer.writeTypePrefix(g, ctxt, typeSer.typeId(value, JsonToken.START_ARRAY));
                _writeContentsAsArray(value, g);
            } else {
                typeIdDef = typeSer.writeTypePrefix(g, ctxt, typeSer.typeId(value, JsonToken.VALUE_STRING));
                char[] chars = value.toCharArray();
                g.writeString(chars, 0, chars.length);
            }
            typeSer.writeTypeSuffix(g, ctxt, typeIdDef);
        }

        private void _writeContentsAsArray(CharCollection value, JsonGenerator g)
            throws JacksonException
        {
            char[] buf = new char[1];
            for (CharIterator it = value.iterator(); it.hasNext(); ) {
                buf[0] = it.nextChar();
                g.writeString(buf, 0, 1);
            }
        }
    }
}