AnyGetterWriter.java

package tools.jackson.databind.ser;

import java.util.Map;

import com.fasterxml.jackson.annotation.JsonApplyView;

import tools.jackson.core.*;
import tools.jackson.databind.*;
import tools.jackson.databind.introspect.AnnotatedMember;
import tools.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
import tools.jackson.databind.node.ObjectNode;
import tools.jackson.databind.ser.jdk.JDKKeySerializers;
import tools.jackson.databind.ser.jdk.MapProperty;
import tools.jackson.databind.ser.jdk.MapSerializer;
import tools.jackson.databind.util.IgnorePropertiesUtil;

/**
 * Class similar to {@link BeanPropertyWriter}, but that will be used
 * for serializing {@link com.fasterxml.jackson.annotation.JsonAnyGetter} annotated
 * (Map) properties
 */
public class AnyGetterWriter extends BeanPropertyWriter
{
    protected final BeanProperty _property;

    /**
     * Method (or Field) that represents the "any getter"
     */
    protected final AnnotatedMember _anyGetter;

    protected ValueSerializer<Object> _anySerializer;

    protected MapSerializer _mapSerializer;

    /**
     * For {@code ObjectNode}/{@code JsonNode}-valued any-getters only: property-level
     * {@code @JsonIgnoreProperties} / {@code @JsonIncludeProperties} rules to apply to
     * the emitted entries. Map-valued any-getters get the same treatment through their
     * {@code MapSerializer} during contextualization; the node path has no such
     * serializer, so the check is captured here instead. {@code null} when no rules apply.
     *
     * @since 3.3
     */
    protected final IgnorePropertiesUtil.Checker _inclusionChecker;

    /**
     * @since 2.19
     *
     * @deprecated Since 3.4 use
     *   {@link #AnyGetterWriter(BeanPropertyWriter, BeanProperty, AnnotatedMember, ValueSerializer, IgnorePropertiesUtil.Checker)}
     *   instead: this variant applies no {@code @JsonIgnoreProperties} /
     *   {@code @JsonIncludeProperties} rules to {@code ObjectNode}-valued any-getters
     */
    @Deprecated // since 3.4
    public AnyGetterWriter(BeanPropertyWriter parent, BeanProperty property,
            AnnotatedMember accessor, ValueSerializer<?> serializer)
    {
        this(parent, property, accessor, serializer, null);
    }

    /**
     * @param inclusionChecker Property-level inclusion rules for {@code ObjectNode}-valued
     *    any-getters (see {@link #_inclusionChecker}); {@code null} if none
     *
     * @since 3.4
     */
    @SuppressWarnings("unchecked")
    public AnyGetterWriter(BeanPropertyWriter parent, BeanProperty property,
            AnnotatedMember accessor, ValueSerializer<?> serializer,
            IgnorePropertiesUtil.Checker inclusionChecker)
    {
        super(parent);
        _anyGetter = accessor;
        _property = property;
        _anySerializer = (ValueSerializer<Object>) serializer;
        if (serializer instanceof MapSerializer mapSer) {
            _mapSerializer = mapSer;
        }
        _inclusionChecker = inclusionChecker;
    }

    @Override
    public void fixAccess(SerializationConfig config) {
        _anyGetter.fixAccess(
                config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS));
    }

    // Note: NOT part of ResolvableSerializer...
    @SuppressWarnings("unchecked")
    public void resolve(SerializationContext ctxt)
    {
        // [databind#3604]: _anySerializer may be null for ObjectNode/JsonNode any-getters
        if (_anySerializer == null) {
            return;
        }
        // 05-Sep-2013, tatu: I _think_ this can be considered a primary property...
        ValueSerializer<?> ser = ctxt.handlePrimaryContextualization(_anySerializer, _property);
        _anySerializer = (ValueSerializer<Object>) ser;
        if (ser instanceof MapSerializer mapSer) {
            _mapSerializer = mapSer;
        }
    }

    private boolean _isIgnored(String name) {
        return (_inclusionChecker != null) && _inclusionChecker.shouldIgnore(name);
    }

    public void getAndSerialize(Object bean, JsonGenerator gen, SerializationContext ctxt)
        throws Exception
    {
        Object value = _anyGetter.getValue(bean);
        if (value == null) {
            return;
        }
        // [databind#3604]: Support ObjectNode/JsonNode for @JsonAnyGetter
        if (value instanceof JsonNode) {
            _serializeObjectNodeEntries(_verifyObjectNode(value, ctxt), gen, ctxt);
            return;
        }
        if (!(value instanceof Map<?,?>)) {
            ctxt.reportBadDefinition(_property.getType(), "Value returned by 'any-getter' %s() not java.util.Map but %s".formatted(
                    _anyGetter.getName(), value.getClass().getName()));
        }
        // [databind#5745]: honor `@JsonApplyView`, same as `BeanPropertyWriter.serializeAsProperty()`
        if (_applyView == null) {
            _serializeMap(value, gen, ctxt);
        } else {
            ctxt.withActiveView((_applyView != JsonApplyView.NONE.class) ? _applyView : null,
                    () -> _serializeMap(value, gen, ctxt));
        }
    }

    private void _serializeMap(Object value, JsonGenerator gen, SerializationContext ctxt)
    {
        // 23-Feb-2015, tatu: Nasty, but has to do (for now)
        if (_mapSerializer != null) {
            _mapSerializer.serializeWithoutTypeInfo((Map<?,?>) value, gen, ctxt);
        } else {
            _anySerializer.serialize(value, gen, ctxt);
        }
    }

    @Override
    public void serializeAsProperty(Object bean, JsonGenerator gen, SerializationContext ctxt) throws Exception {
        getAndSerialize(bean, gen, ctxt);
    }

    /**
     * [databind#6136]: The any-getter accessor name is not a property name in the
     * output -- the entries it emits are -- so the filter decides inclusion per
     * entry, instead of once for the accessor.
     *<p>
     * Note that as a result the accessor name (e.g. {@code "anyProperties"}) is no
     * longer consulted as a filter key: inclusion is decided by the emitted entry
     * names only. In particular {@code filterOutAllExcept(accessorName)} no longer
     * includes all of its entries, and a custom {@link PropertyFilter} that excluded
     * the accessor to drop the whole map no longer does so.
     *
     * @since 3.3
     */
    @Override
    public void serializeFilteredAsProperty(Object bean, JsonGenerator gen,
            SerializationContext ctxt, PropertyFilter filter)
        throws Exception
    {
        getAndFilter(bean, gen, ctxt, filter);
    }

    public void getAndFilter(Object bean, JsonGenerator gen, SerializationContext ctxt,
            PropertyFilter filter)
        throws Exception
    {
        Object value = _anyGetter.getValue(bean);
        if (value == null) {
            return;
        }
        // [databind#3604]: Support ObjectNode/JsonNode for @JsonAnyGetter
        if (value instanceof JsonNode) {
            _serializeFilteredObjectNodeEntries(_verifyObjectNode(value, ctxt), gen, ctxt,
                    bean, filter);
            return;
        }
        if (!(value instanceof Map<?,?>)) {
            ctxt.reportBadDefinition(_property.getType(),
                    "Value returned by 'any-getter' (%s()) not java.util.Map but %s".formatted(
                            _anyGetter.getName(), value.getClass().getName()));
        }
        // [databind#5745]: honor `@JsonApplyView`, same as unfiltered path
        if (_applyView == null) {
            _serializeFilteredMap(value, bean, gen, ctxt, filter);
        } else {
            ctxt.withActiveView((_applyView != JsonApplyView.NONE.class) ? _applyView : null,
                    () -> _serializeFilteredMap(value, bean, gen, ctxt, filter));
        }
    }

    private void _serializeFilteredMap(Object value, Object bean, JsonGenerator gen,
            SerializationContext ctxt, PropertyFilter filter)
    {
        if (_mapSerializer != null) {
            _mapSerializer.serializeFilteredAnyProperties(ctxt, gen, bean, (Map<?,?>) value,
                    filter);
        } else {
            // ... not sure how custom handler would do it
            _anySerializer.serialize(value, gen, ctxt);
        }
    }

    /**
     * Helper method to verify that a {@link JsonNode} value is an {@link ObjectNode},
     * throwing a clear error if not (e.g. ArrayNode).
     *
     * @since 3.2
     */
    protected ObjectNode _verifyObjectNode(Object value, SerializationContext ctxt)
        throws DatabindException
    {
        if (value instanceof ObjectNode objectNode) {
            return objectNode;
        }
        return ctxt.reportBadDefinition(_property.getType(), String.format(
                "Value returned by 'any-getter' %s not `ObjectNode` but `%s`; only `ObjectNode`s can be used as `@JsonAnyGetter` values",
                _anyGetter.getName(), value.getClass().getName()));
    }

    /**
     * Helper method for serializing entries of an {@link ObjectNode}
     * as individual properties (for {@code @JsonAnyGetter} support).
     *
     * @since 3.2
     */
    protected void _serializeObjectNodeEntries(ObjectNode objectNode,
            JsonGenerator gen, SerializationContext ctxt)
        throws Exception
    {
        for (Map.Entry<String, JsonNode> entry : objectNode.properties()) {
            if (_isIgnored(entry.getKey())) {
                continue;
            }
            gen.writeName(entry.getKey());
            entry.getValue().serialize(gen, ctxt);
        }
    }

    /**
     * Variant of {@link #_serializeObjectNodeEntries} used when a JSON Filter is in
     * effect: entries become properties of the enclosing POJO, so each one is passed
     * to the filter the same way {@code MapSerializer} passes entries of a
     * {@link Map}-valued any-getter.
     *
     * @since 3.3
     */
    protected void _serializeFilteredObjectNodeEntries(ObjectNode objectNode,
            JsonGenerator gen, SerializationContext ctxt,
            Object bean, PropertyFilter filter)
        throws Exception
    {
        final MapProperty prop = new MapProperty(null, _property);
        // plain names, same as `_serializeObjectNodeEntries()` (no custom key serializer)
        final ValueSerializer<Object> keySer = JDKKeySerializers.getStdKeySerializer(
                ctxt.getConfig(), String.class, false);
        for (Map.Entry<String, JsonNode> entry : objectNode.properties()) {
            if (_isIgnored(entry.getKey())) {
                continue;
            }
            final JsonNode v = entry.getValue();
            prop.reset(entry.getKey(), v, keySer, ctxt.findValueSerializer(v.getClass()));
            filter.serializeAsProperty(bean, gen, ctxt, prop);
        }
    }

    @Override
    public void depositSchemaProperty(JsonObjectFormatVisitor v, SerializationContext ctxt)
    {
        // no-op
    }
}