StdSerializer.java

package tools.jackson.databind.ser.std;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
import java.util.IdentityHashMap;
import java.util.Map;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonInclude;

import tools.jackson.core.*;
import tools.jackson.core.JsonParser.NumberType;
import tools.jackson.core.exc.JacksonIOException;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JacksonStdImpl;
import tools.jackson.databind.introspect.AnnotatedMember;
import tools.jackson.databind.jsonFormatVisitors.*;
import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.ser.FilterProvider;
import tools.jackson.databind.ser.PropertyFilter;
import tools.jackson.databind.util.ClassUtil;
import tools.jackson.databind.util.Converter;

/**
 * Base class used by all standard serializers, and can also
 * be used for custom serializers (in fact, this is the recommended
 * base class to use).
 */
public abstract class StdSerializer<T>
    extends ValueSerializer<T>
    implements JsonFormatVisitable
{
    /**
     * Key used for storing a lock object to prevent infinite recursion when
     * constructing converting serializers.
     */
    private final static Object KEY_CONTENT_CONVERTER_LOCK = new Object();

    /**
     * Nominal type supported, usually declared type of
     * property for which serializer is used.
     */
    protected final Class<?> _handledType;

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

    protected StdSerializer(Class<?> t) {
        _handledType = t;
    }

    protected StdSerializer(JavaType type) {
        _handledType = type.getRawClass();
    }

    /**
     * Alternate constructor that is (alas!) needed to work
     * around kinks of generic type handling
     */
    @Deprecated // since 3.0
    protected StdSerializer(Class<?> t, boolean dummy) {
        _handledType = t;
    }

    protected StdSerializer(StdSerializer<?> src) {
        _handledType = src._handledType;
    }

    /*
    /**********************************************************************
    /* Accessors
    /**********************************************************************
     */

    @Override
    public Class<?> handledType() { return _handledType; }

    /*
    /**********************************************************************
    /* Serialization
    /**********************************************************************
     */

    @Override
    public abstract void serialize(T value, JsonGenerator gen, SerializationContext provider)
        throws JacksonException;

    /*
    /**********************************************************************
    /* Type introspection API, partial/default implementation
    /**********************************************************************
     */

    /**
     * Default implementation specifies no format. This behavior is usually
     * overriden by custom serializers.
     */
    @Override
    public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
    {
        visitor.expectAnyFormat(typeHint);
    }

    /**
     * Helper method for handling Binary values: typically serialized as Base64-encoded
     * data (in textual formats) or native binary (binary formats).
     */
    protected void acceptJsonFormatVisitorForBinary(JsonFormatVisitorWrapper visitor, JavaType typeHint)
    {
        // 14-Mar-2016, tatu: while logically (and within JVM) binary, gets often encoded
        // as Base64 String, let's try to indicate it is array of Bytes... difficult,
        // thanks to JSON Schema's lackluster set of types available
        //
        // TODO: make work either as String/base64, or array of numbers,
        //   with a qualifier that can be used to determine it's byte[]

        JsonArrayFormatVisitor v2 = visitor.expectArrayFormat(typeHint);
        if (v2 != null) {
            v2.itemsFormat(JsonFormatTypes.INTEGER);
        }
    }

    /*
    /**********************************************************************
    /* Helper methods for JSON Schema generation
    /**********************************************************************
     */

    protected ObjectNode createSchemaNode(String type)
    {
        ObjectNode schema = JsonNodeFactory.instance.objectNode();
        schema.put("type", type);
        return schema;
    }

    protected ObjectNode createSchemaNode(String type, boolean isOptional)
    {
        ObjectNode schema = createSchemaNode(type);
        if (!isOptional) {
            schema.put("required", true);
        }
        return schema;
    }

    /**
     * Helper method that calls necessary visit method(s) to indicate that the
     * underlying JSON type is JSON String.
     */
    protected void visitStringFormat(JsonFormatVisitorWrapper visitor, JavaType typeHint) {
        /*JsonStringFormatVisitor v2 =*/ visitor.expectStringFormat(typeHint);
    }

    /**
     * Helper method that calls necessary visit method(s) to indicate that the
     * underlying JSON type is JSON String, but that there is a more refined
     * logical type
     */
    protected void visitStringFormat(JsonFormatVisitorWrapper visitor, JavaType typeHint,
            JsonValueFormat format)
    {
        JsonStringFormatVisitor v2 = visitor.expectStringFormat(typeHint);
        if (v2 != null) {
            v2.format(format);
        }
    }

    /**
     * Helper method that calls necessary visit method(s) to indicate that the
     * underlying JSON type is JSON Integer number.
     */
    protected void visitIntFormat(JsonFormatVisitorWrapper visitor, JavaType typeHint,
            NumberType numberType)
    {
        JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint);
        if (_neitherNull(v2, numberType)) {
            v2.numberType(numberType);
        }
    }

    /**
     * Helper method that calls necessary visit method(s) to indicate that the
     * underlying JSON type is JSON Integer number, but that there is also a further
     * format restriction involved.
     */
    protected void visitIntFormat(JsonFormatVisitorWrapper visitor, JavaType typeHint,
            NumberType numberType, JsonValueFormat format)
    {
        JsonIntegerFormatVisitor v2 = visitor.expectIntegerFormat(typeHint);
        if (v2 != null) {
            if (numberType != null) {
                v2.numberType(numberType);
            }
            if (format != null) {
                v2.format(format);
            }
        }
    }

    /**
     * Helper method that calls necessary visit method(s) to indicate that the
     * underlying JSON type is a floating-point JSON number.
     */
    protected void visitFloatFormat(JsonFormatVisitorWrapper visitor, JavaType typeHint,
            NumberType numberType)
    {
        JsonNumberFormatVisitor v2 = visitor.expectNumberFormat(typeHint);
        if (v2 != null) {
            v2.numberType(numberType);
        }
    }

    protected void visitArrayFormat(JsonFormatVisitorWrapper visitor, JavaType typeHint,
            ValueSerializer<?> itemSerializer, JavaType itemType)
    {
        JsonArrayFormatVisitor v2 = visitor.expectArrayFormat(typeHint);
        if (_neitherNull(v2, itemSerializer)) {
            v2.itemsFormat(itemSerializer, itemType);
        }
    }

    protected void visitArrayFormat(JsonFormatVisitorWrapper visitor, JavaType typeHint,
            JsonFormatTypes itemType)
    {
        JsonArrayFormatVisitor v2 = visitor.expectArrayFormat(typeHint);
        if (v2 != null) {
            v2.itemsFormat(itemType);
        }
    }

    /*
    /**********************************************************************
    /* Helper methods for exception handling
    /**********************************************************************
     */

    /**
     * Method that will modify caught exception (passed in as argument)
     * as necessary to include reference information, and to ensure it
     * is a subtype of {@link JacksonException}, or an unchecked exception.
     *<p>
     * Rules for wrapping and unwrapping are bit complicated; essentially:
     *<ul>
     * <li>Errors are to be passed as is (if uncovered via unwrapping)
     * <li>Wrapped {@code IOException}s are unpeeled
     *</ul>
     */
    public void wrapAndThrow(SerializationContext ctxt,
            Throwable t, Object bean, String fieldName)
        throws JacksonException
    {
        // 05-Mar-2009, tatu: But one nasty edge is when we get
        //   StackOverflow: usually due to infinite loop. But that
        //   usually gets hidden within an InvocationTargetException...
        while (t instanceof InvocationTargetException && t.getCause() != null) {
            t = t.getCause();
        }
        // 16-Jan-2021, tatu: Let's peel off useless wrapper as well
        while (t instanceof JacksonIOException && t.getCause() != null) {
            t = t.getCause();
        }
        // Errors to be passed as is, most others not
        ClassUtil.throwIfError(t);
        if (!(t instanceof JacksonException)) {
            boolean wrap = (ctxt == null) || ctxt.isEnabled(SerializationFeature.WRAP_EXCEPTIONS);
            if (!wrap) {
                ClassUtil.throwIfRTE(t);
            }
        }
        // Need to add reference information
        throw DatabindException.wrapWithPath(ctxt, t,
                new JacksonException.Reference(bean, fieldName));
    }

    public void wrapAndThrow(SerializationContext ctxt,
            Throwable t, Object bean, int index)
        throws JacksonException
    {
        while (t instanceof InvocationTargetException && t.getCause() != null) {
            t = t.getCause();
        }
        while (t instanceof JacksonIOException && t.getCause() != null) {
            t = t.getCause();
        }
        // Errors to be passed as is, most others not
        ClassUtil.throwIfError(t);
        if (!(t instanceof JacksonException)) {
            boolean wrap = (ctxt == null) || ctxt.isEnabled(SerializationFeature.WRAP_EXCEPTIONS);
            if (!wrap) {
                ClassUtil.throwIfRTE(t);
            }
        }
        // Need to add reference information
        throw DatabindException.wrapWithPath(ctxt, t,
                new JacksonException.Reference(bean, index));
    }

    /*
    /**********************************************************************
    /* Helper methods, accessing annotation-based configuration
    /**********************************************************************
     */

    /**
     * Helper method that can be used to see if specified property has annotation
     * indicating that a converter is to be used for contained values (contents
     * of structured types; array/List/Map values)
     *
     * @param existingSerializer (optional) configured content
     *    serializer if one already exists.
     */
    protected ValueSerializer<?> findContextualConvertingSerializer(SerializationContext provider,
            BeanProperty prop, ValueSerializer<?> existingSerializer)
    {
        // 08-Dec-2016, tatu: to fix [databind#357], need to prevent recursive calls for
        //     same property
        @SuppressWarnings("unchecked")
        Map<Object,Object> conversions = (Map<Object,Object>) provider.getAttribute(KEY_CONTENT_CONVERTER_LOCK);
        if (conversions != null) {
            Object lock = conversions.get(prop);
            if (lock != null) {
                return existingSerializer;
            }
        } else {
            conversions = new IdentityHashMap<>();
            provider.setAttribute(KEY_CONTENT_CONVERTER_LOCK, conversions);
        }
        final AnnotationIntrospector intr = provider.getAnnotationIntrospector();
        if (_neitherNull(intr, prop)) {
            conversions.put(prop, Boolean.TRUE);
            try {
                ValueSerializer<?> ser = _findConvertingContentSerializer(provider, intr,
                        prop, existingSerializer);
                if (ser != null) {
                    return provider.handleSecondaryContextualization(ser, prop);
                }
            } finally {
                conversions.remove(prop);
            }
        }
        return existingSerializer;
    }

    private ValueSerializer<?> _findConvertingContentSerializer(SerializationContext provider,
            AnnotationIntrospector intr, BeanProperty prop, ValueSerializer<?> existingSerializer)
    {
        AnnotatedMember m = prop.getMember();
        if (m != null) {
            Object convDef = intr.findSerializationContentConverter(provider.getConfig(), m);
            if (convDef != null) {
                Converter<Object,Object> conv = provider.converterInstance(prop.getMember(), convDef);
                JavaType delegateType = conv.getOutputType(provider.getTypeFactory());
                // [databind#731]: Should skip if nominally java.lang.Object
                if ((existingSerializer == null) && !delegateType.isJavaLangObject()) {
                    existingSerializer = provider.findValueSerializer(delegateType);
                }
                return new StdDelegatingSerializer(conv, delegateType, existingSerializer, prop);
            }
        }
        return existingSerializer;
    }

    /**
     * Helper method used to locate filter that is needed, based on filter id
     * this serializer was constructed with.
     */
    protected PropertyFilter findPropertyFilter(SerializationContext provider,
            Object filterId, Object valueToFilter)
    {
        FilterProvider filters = provider.getFilterProvider();
        // Not ok to miss the provider, if a filter is declared to be needed.
        if (filters == null) {
            return provider.reportBadDefinition(handledType(),
                    "Cannot resolve PropertyFilter with id '"+filterId+"'; no FilterProvider configured");
        }
        // But whether unknown ids are ok just depends on filter provider; if we get null that's fine
        return filters.findPropertyFilter(provider, filterId, valueToFilter);
    }

    /**
     * Helper method that may be used to find if this deserializer has specific
     * {@link JsonFormat} settings, either via property, or through type-specific
     * defaulting.
     *
     * @param typeForDefaults Type (erased) used for finding default format settings, if any
     */
    protected JsonFormat.Value findFormatOverrides(SerializationContext provider,
            BeanProperty prop, Class<?> typeForDefaults)
    {
        if (prop != null) {
            return prop.findPropertyFormat(provider.getConfig(), typeForDefaults);
        }
        // even without property or AnnotationIntrospector, may have type-specific defaults
        return provider.getDefaultPropertyFormat(typeForDefaults);
    }

    /**
     * Convenience method that uses {@link #findFormatOverrides} to find possible
     * defaults and/of overrides, and then calls <code>JsonFormat.Value.getFeature(...)</code>
     * to find whether that feature has been specifically marked as enabled or disabled.
     *
     * @param typeForDefaults Type (erased) used for finding default format settings, if any
     */
    protected Boolean findFormatFeature(SerializationContext provider,
            BeanProperty prop, Class<?> typeForDefaults, JsonFormat.Feature feat)
    {
        JsonFormat.Value format = findFormatOverrides(provider, prop, typeForDefaults);
        if (format != null) {
            return format.getFeature(feat);
        }
        return null;
    }

    protected JsonInclude.Value findIncludeOverrides(SerializationContext provider,
            BeanProperty prop, Class<?> typeForDefaults)
    {
        if (prop != null) {
            return prop.findPropertyInclusion(provider.getConfig(), typeForDefaults);
        }
        // even without property or AnnotationIntrospector, may have type-specific defaults
        return provider.getDefaultPropertyInclusion(typeForDefaults);
    }

    /**
     * Convenience method for finding out possibly configured content value serializer.
     */
    protected ValueSerializer<?> findAnnotatedContentSerializer(SerializationContext serializers,
            BeanProperty property)
    {
        if (property != null) {
            // First: if we have a property, may have property-annotation overrides
            AnnotatedMember m = property.getMember();
            final AnnotationIntrospector intr = serializers.getAnnotationIntrospector();
            if (m != null) {
                return serializers.serializerInstance(m,
                        intr.findContentSerializer(serializers.getConfig(), m));
            }
        }
        return null;
    }

    /*
    /**********************************************************************
    /* Helper methods, other
    /**********************************************************************
     */

    /**
     * Method that can be called to determine if given serializer is the default
     * serializer Jackson uses; as opposed to a custom serializer installed by
     * a module or calling application. Determination is done using
     * {@link JacksonStdImpl} annotation on serializer class.
     */
    protected boolean isDefaultSerializer(ValueSerializer<?> serializer) {
        return ClassUtil.isJacksonStdImpl(serializer);
    }

    protected final static boolean _neitherNull(Object a, Object b) {
        return (a != null) && (b != null);
    }

    protected final static boolean _nonEmpty(Collection<?> c) {
        return (c != null) && !c.isEmpty();
    }

    // @since 3.0
    protected JacksonException _wrapIOFailure(SerializationContext ctxt, IOException e) {
        return JacksonIOException.construct(e, ctxt.getGenerator());
    }
}