ThrowableDeserializer.java

package tools.jackson.databind.deser.jdk;

import java.util.Arrays;
import java.util.Set;

import tools.jackson.core.*;
import tools.jackson.core.sym.PropertyNameMatcher;
import tools.jackson.databind.*;
import tools.jackson.databind.annotation.JacksonStdImpl;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.deser.SettableBeanProperty;
import tools.jackson.databind.deser.bean.BeanDeserializer;
import tools.jackson.databind.deser.bean.BeanPropertyMap;
import tools.jackson.databind.deser.bean.PropertyBasedCreator;
import tools.jackson.databind.deser.impl.UnwrappedPropertyHandler;
import tools.jackson.databind.introspect.AnnotatedMethod;
import tools.jackson.databind.introspect.BeanPropertyDefinition;
import tools.jackson.databind.util.ClassUtil;
import tools.jackson.databind.util.IgnorePropertiesUtil;
import tools.jackson.databind.util.NameTransformer;
import tools.jackson.databind.util.ViewMatcher;

/**
 * Deserializer that builds on basic {@link BeanDeserializer} but
 * override some aspects like instance construction.
 */
@JacksonStdImpl
public class ThrowableDeserializer
    extends BeanDeserializer // not the greatest idea but...
{
    public final static String PROP_NAME_MESSAGE = "message";
    public final static String PROP_NAME_SUPPRESSED = "suppressed";

    public final static String PROP_NAME_LOCALIZED_MESSAGE = "localizedMessage";

    // Properties that should not be set if value is null (would cause NPE or other issues)
    public final static String PROP_NAME_CAUSE = "cause";
    public final static String PROP_NAME_STACK_TRACE = "stackTrace";

    /**
     * Internal (Java) names of the standard {@link Throwable} properties, as declared by
     * {@link Throwable} itself. Exposed because {@code BeanDeserializerFactory} needs the
     * same set when deciding which properties are exempt from {@code @JsonView} filtering
     * ([databind#6174], [databind#6190]); keeping one copy here, next to the constants it
     * is built from, prevents the two from drifting apart.
     *<p>
     * NOTE: these are the canonical names -- a {@link PropertyNamingStrategy} may rename
     * the properties, in which case the external names are resolved separately (see
     * [databind#6188]).
     *
     * @since 3.3
     */
    public final static Set<String> STD_PROP_NAMES = Set.of(PROP_NAME_MESSAGE,
            PROP_NAME_LOCALIZED_MESSAGE, PROP_NAME_SUPPRESSED,
            PROP_NAME_CAUSE, PROP_NAME_STACK_TRACE);

    /**
     * External ("JSON") names of the standard {@link Throwable} properties: needed
     * because a {@link PropertyNamingStrategy} may rename them. Unlike simple
     * case-changing renames -- which {@code equalsIgnoreCase()} can absorb --
     * snake- and kebab-cased ones cannot be matched against the canonical names
     * at all (see [databind#3497], [databind#6188]), so names are resolved once,
     * at construction.
     *
     * @since 3.3
     */
    protected static class StdPropNames
    {
        public final String message, localizedMessage, suppressed, cause, stackTrace;

        /**
         * Explicit view definitions for "message" and "suppressed", if any; {@code null}
         * if unrestricted (included under every active view, per [databind#6174]).
         *<p>
         * Needed because -- unlike "cause" and "stackTrace" -- these two have no setter
         * in the common case, so they are never bound as regular properties and are
         * instead consumed by the "unknown name" branches of the read loop, where no
         * {@link SettableBeanProperty} (and hence no view matcher) is available.
         * "localizedMessage" needs none: it is discarded regardless of views.
         *
         * @since 3.3
         */
        public final ViewMatcher messageViews, suppressedViews;

        /**
         * Whether either of the two properties carries an explicit {@code @JsonView}:
         * if so the read loop must resolve the active view even when no regular
         * property has views of its own.
         *
         * @since 3.3
         */
        public boolean hasExplicitViews() {
            return (messageViews != null) || (suppressedViews != null);
        }

        protected StdPropNames(String msg, String localizedMsg, String suppr,
                String cse, String stackTr,
                ViewMatcher msgViews, ViewMatcher supprViews) {
            message = msg;
            localizedMessage = localizedMsg;
            suppressed = suppr;
            cause = cse;
            stackTrace = stackTr;
            messageViews = msgViews;
            suppressedViews = supprViews;
        }

        /**
         * Names to use when no {@link PropertyNamingStrategy} is configured.
         */
        protected final static StdPropNames DEFAULT = new StdPropNames(PROP_NAME_MESSAGE,
                PROP_NAME_LOCALIZED_MESSAGE, PROP_NAME_SUPPRESSED,
                PROP_NAME_CAUSE, PROP_NAME_STACK_TRACE, null, null);
    }

    /**
     * Resolved external names of the standard {@link Throwable} properties.
     *
     * @since 3.3
     */
    protected final StdPropNames _stdPropNames;

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

    protected ThrowableDeserializer(BeanDeserializer baseDeserializer,
            StdPropNames stdPropNames) {
        super(baseDeserializer);
        // need to disable this, since we do post-processing
        _vanillaProcessing = false;
        _stdPropNames = stdPropNames;
    }

    /**
     * @deprecated Since 3.3 use variant that takes {@link BeanDescription.Supplier}:
     *    without it standard {@link Throwable} property names cannot be resolved
     *    against a configured {@link PropertyNamingStrategy}.
     */
    @Deprecated
    public static ThrowableDeserializer construct(DeserializationContext ctxt,
            BeanDeserializer baseDeserializer)
    {
        return construct(ctxt, baseDeserializer, null);
    }

    /**
     * @since 3.3
     */
    public static ThrowableDeserializer construct(DeserializationContext ctxt,
            BeanDeserializer baseDeserializer, BeanDescription.Supplier beanDescRef)
    {
        return new ThrowableDeserializer(baseDeserializer,
                _resolveStdPropNames(ctxt, beanDescRef));
    }

    /**
     * Helper for finding the views a class-level {@code @JsonView} places all properties
     * of given type in, if any; {@code null} if there is no such annotation.
     *<p>
     * Exists because the distinction matters for the standard {@link Throwable}
     * properties and is easy to get wrong. Only a missing annotation exempts them from
     * view filtering ([databind#6174]); any {@code @JsonView} that IS present must be
     * honored ([databind#6190]), including the degenerate {@code @JsonView({})}, which
     * places its properties in no view at all -- just as it does for regular properties.
     * Shared with {@code BeanDeserializerFactory}, which assigns the views this class
     * then reads back.
     *
     * @since 3.3
     */
    public static Class<?>[] explicitClassViews(MapperConfig<?> config, BeanDescription beanDesc) {
        // NOTE: deliberately NOT via `BeanDescription.findDefaultViews()`: that substitutes
        // an empty array for "no annotation" when `DEFAULT_VIEW_INCLUSION` is disabled,
        // which is indistinguishable from the empty array a degenerate `@JsonView({})`
        // yields. Asking the introspector keeps the two apart: `null` for no annotation.
        return config.getAnnotationIntrospector().findViews(config, beanDesc.getClassInfo());
    }

    /**
     * Helper method for resolving the external names of the standard
     * {@link Throwable} properties ([databind#6188]; completes what was left
     * undone by [databind#3497]).
     *<p>
     * Names are taken from the property definitions regular introspection already
     * produced, rather than re-derived here: that way they match whatever the rest
     * of databind bound the properties to, accounting for a mapper-level
     * {@link PropertyNamingStrategy}, a class-level {@code @JsonNaming} (including
     * its "use default" pseudo-value, which overrides the mapper-level one) and an
     * explicit {@code @JsonProperty} rename alike.
     */
    private static StdPropNames _resolveStdPropNames(DeserializationContext ctxt,
            BeanDescription.Supplier beanDescRef)
    {
        // No introspection available (deprecated `construct()`): canonical names apply
        if (beanDescRef == null) {
            return StdPropNames.DEFAULT;
        }
        final BeanDescription beanDesc = beanDescRef.get();
        // [databind#6190]: a class-level `@JsonView` covers every property, including these
        final Class<?>[] classViews = explicitClassViews(ctxt.getConfig(), beanDesc);
        return new StdPropNames(
                _externalName(beanDesc, "getMessage", PROP_NAME_MESSAGE),
                _externalName(beanDesc, "getLocalizedMessage", PROP_NAME_LOCALIZED_MESSAGE),
                _externalName(beanDesc, "getSuppressed", PROP_NAME_SUPPRESSED),
                _externalName(beanDesc, "getCause", PROP_NAME_CAUSE),
                _externalName(beanDesc, "getStackTrace", PROP_NAME_STACK_TRACE),
                _viewMatcher(beanDesc, "getMessage", classViews),
                _viewMatcher(beanDesc, "getSuppressed", classViews));
    }

    /**
     * Helper for finding explicit view definitions of one standard {@link Throwable}
     * property: its own {@code @JsonView} if present, else the class-level one (if any),
     * else {@code null} for "no restriction".
     */
    private static ViewMatcher _viewMatcher(BeanDescription beanDesc, String getterName,
            Class<?>[] classViews)
    {
        Class<?>[] views = null;
        AnnotatedMethod m = beanDesc.findMethod(getterName, null);
        if (m != null) {
            for (BeanPropertyDefinition propDef : beanDesc.findProperties()) {
                if (m.equals(propDef.getGetter())) {
                    views = propDef.findViews();
                    break;
                }
            }
        }
        if (views == null) {
            views = classViews;
        }
        return (views == null) ? null : ViewMatcher.construct(views);
    }

    /**
     * Finds the external name that the property using given standard {@link Throwable}
     * accessor was bound to; the accessor is located by signature (not by matching
     * property names), so a rename cannot hide it.
     */
    private static String _externalName(BeanDescription beanDesc, String getterName,
            String defaultName)
    {
        AnnotatedMethod m = beanDesc.findMethod(getterName, null);
        if (m != null) {
            for (BeanPropertyDefinition propDef : beanDesc.findProperties()) {
                if (m.equals(propDef.getGetter())) {
                    return propDef.getName();
                }
            }
        }
        // Not found (should not happen for `Throwable`): fall back to canonical
        return defaultName;
    }

    /**
     * Alternative constructor used when creating "unwrapping" deserializers
     */
    protected ThrowableDeserializer(BeanDeserializer src,
            UnwrappedPropertyHandler unwrapHandler, PropertyBasedCreator pbCreator,
                    BeanPropertyMap renamedProperties,
            boolean ignoreAllUnknown, StdPropNames stdPropNames) {
        super(src, unwrapHandler, pbCreator, renamedProperties, ignoreAllUnknown);
        _stdPropNames = stdPropNames;
    }

    @Override
    public ValueDeserializer<Object> unwrappingDeserializer(DeserializationContext ctxt,
            NameTransformer transformer)
    {
        if (getClass() != ThrowableDeserializer.class) {
            return this;
        }
        // main thing really is to just enforce ignoring of unknown properties; since
        // there may be multiple unwrapped values and properties for all may be interleaved...
        UnwrappedPropertyHandler uwHandler = _unwrappedPropertyHandler;
        // delegate further unwraps, if any
        if (uwHandler != null) {
            uwHandler = uwHandler.renameAll(ctxt, transformer);
        }
        PropertyBasedCreator pbCreator = _propertyBasedCreator;
        if (pbCreator != null) {
            pbCreator = pbCreator.renameAll(ctxt, transformer);
        }
        // and handle direct unwrapping as well:
        return new ThrowableDeserializer(this, uwHandler, pbCreator,
                _beanProperties.renameAll(ctxt, transformer), true, _stdPropNames);
    }

    /*
    /**********************************************************************
    /* Overridden methods
    /**********************************************************************
     */

    @Override
    public Object deserializeFromObject(JsonParser p, DeserializationContext ctxt)
        throws JacksonException
    {
        // 30-Sep-2010, tatu: Need to allow use of @JsonCreator, so:
        if (_propertyBasedCreator != null) { // proper @JsonCreator
            return _deserializeUsingPropertyBased(p, ctxt);
        }
        if (_delegateDeserializer != null) {
            return _valueInstantiator.createUsingDelegate(ctxt,
                    _delegateDeserializer.deserialize(p, ctxt));
        }
        if (_beanType.isAbstract()) { // for good measure, check this too
            return ctxt.handleMissingInstantiator(handledType(), getValueInstantiator(), p,
                    "abstract type (need to add/enable type information?)");
        }
        boolean hasStringCreator = _valueInstantiator.canCreateFromString();
        boolean hasDefaultCtor = _valueInstantiator.canCreateUsingDefault();
        // and finally, verify we do have single-String arg constructor (if no @JsonCreator)
        if (!hasStringCreator && !hasDefaultCtor) {
            return ctxt.handleMissingInstantiator(handledType(), getValueInstantiator(), p,
                    "Throwable needs a default constructor, a single-String-arg constructor; or explicit @JsonCreator");
        }
        Throwable throwable = null;
        Object[] pending = null;
        Throwable[] suppressed = null;
        int pendingIx = 0;

        // [databind#6190]: `_needViewProcesing` only accounts for settable properties, but
        // "message"/"suppressed" may carry an explicit `@JsonView` without being bound as
        // one -- so consult those too, else their views would be ignored whenever no
        // regular property has any (as happens with `DEFAULT_VIEW_INCLUSION` enabled)
        final Class<?> activeView = (_needViewProcesing || _stdPropNames.hasExplicitViews())
                ? ctxt.getActiveView() : null;
        int ix = p.currentNameMatch(_propNameMatcher);
        for (; ; ix = p.nextNameMatch(_propNameMatcher)) {
            if (ix >= 0) {
                p.nextToken();
                SettableBeanProperty prop = _propsByIndex[ix];
                // Property not part of the active view must not be set from input.
                // Standard `Throwable` properties without explicit views have no view restrictions
                // configured, while those with explicit `@JsonView` honor them (see [databind#6190]).
                if ((activeView != null) && !prop.visibleInView(activeView)) {
                    _handleViewExcluded(p, ctxt, prop.getName(), activeView);
                    continue;
                }
                if (throwable != null) {
                    // 07-Dec-2023, tatu: [databind#4248] Interesting that "cause"
                    //    with `null` blows up. So, avoid.
                    // Same for "stackTrace" - setStackTrace(null) throws NPE
                    if (p.hasToken(JsonToken.VALUE_NULL)
                            && _shouldSkipNullValue(prop.getName())) {
                        continue;
                    }
                    prop.deserializeAndSet(p, ctxt, throwable);
                    continue;
                }
                // nope; need to defer
                if (pending == null) {
                    int len = _beanProperties.size();
                    pending = new Object[len + len];
                } else if (pendingIx == pending.length) {
                    // NOTE: only occurs with duplicate properties, possible
                    // with some formats (most notably XML; but possibly with
                    // JSON if duplicate detection not enabled). Most likely
                    // only occurs with malicious content so use linear buffer
                    // resize (no need to optimize performance)
                    pending = Arrays.copyOf(pending, pendingIx + 16);
                }
                pending[pendingIx++] = prop;
                pending[pendingIx++] = prop.deserialize(p, ctxt);
                continue;
            }
            if (ix != PropertyNameMatcher.MATCH_UNKNOWN_NAME) {
                if (ix == PropertyNameMatcher.MATCH_END_OBJECT) {
                    break;
                }
                return _handleUnexpectedWithin(p, ctxt, throwable);
            }
            // Maybe it's "message"?
            String propName = p.currentName();
            p.nextToken();
            // 04-Sep-2026: [databind#6188] Names compared against are the ones resolved
            //    at construction, so a `PropertyNamingStrategy` is accounted for; the
            //    case-insensitive compare remains for case-insensitive input matching
            if (_stdPropNames.message.equalsIgnoreCase(propName)) {
                // [databind#6190]: explicit `@JsonView` on "message" must be honored;
                // left unset, it is instantiated with `null` message after the loop
                if (!_visibleInView(_stdPropNames.messageViews, activeView)) {
                    _handleViewExcluded(p, ctxt, propName, activeView);
                    continue;
                }
                throwable = _instantiate(ctxt, hasStringCreator, p.getValueAsString());
                // any pending values?
                if (pending != null) {
                    for (int i = 0, len = pendingIx; i < len; i += 2) {
                        SettableBeanProperty prop = (SettableBeanProperty)pending[i];
                        Object value = pending[i+1];
                        // Skip null values for properties that don't accept them
                        if (value == null && _shouldSkipNullValue(prop.getName())) {
                            continue;
                        }
                        prop.set(ctxt, throwable, value);
                    }
                    pending = null;
                }
                continue;
            }

            if (_stdPropNames.suppressed.equalsIgnoreCase(propName)) {
                // [databind#6190]: explicit `@JsonView` on "suppressed" must be honored
                if (!_visibleInView(_stdPropNames.suppressedViews, activeView)) {
                    _handleViewExcluded(p, ctxt, propName, activeView);
                    continue;
                }
                // 07-Dec-2023, tatu: Not sure how/why, but JSON Null is otherwise
                //    not handled with such call so...
                if (p.hasToken(JsonToken.VALUE_NULL)) {
                    suppressed = null;
                } else {
                    // Inlined `DeserializationContext.readValue()` to minimize call depth
                    ValueDeserializer<Object> deser = ctxt.findRootValueDeserializer(
                            ctxt.constructType(Throwable[].class));
                    suppressed = (Throwable[]) deser.deserialize(p, ctxt);
                }
                continue;
            }
            if (_stdPropNames.localizedMessage.equalsIgnoreCase(propName)) {
                p.skipChildren();
                continue;
            }
            // Things marked as ignorable (or not in the "include" allow-list) should
            // not be passed to any setter. NOTE: checked only after the standard
            // `Throwable` properties above, so those are never dropped by the
            // ignore/include lists ([databind#6157]). They may still be excluded by an
            // explicit `@JsonView`, but that is decided by the branches above, not here.
            if (IgnorePropertiesUtil.shouldIgnore(propName, _ignorableProps, _includableProps)) {
                handleIgnoredProperty(p, ctxt, handledType(), propName);
                continue;
            }
            if (_anySetter != null) {
                // [databind#4316] Since 2.16.2 : at this point throwable should be non-null
                if (throwable == null) {
                    throwable = _instantiate(ctxt, hasStringCreator, null);
                }
                _anySetter.deserializeAndSet(p, ctxt, throwable, propName);
                continue;
            }

            // 23-Jan-2018, tatu: One concern would be `message`, but without any-setter or single-String-ctor
            //   (or explicit constructor). We could just ignore it but for now, let it fail
            // [databind#4071]: In case of "message", skip for default constructor
            if (_stdPropNames.message.equalsIgnoreCase(propName)) {
                p.skipChildren();
                continue;
            }

            // Unknown: let's call handler method
            handleUnknownProperty(p, ctxt, throwable, propName);
        }
        // Sanity check: did we find "message"?
        if (throwable == null) {
            throwable = _instantiate(ctxt, hasStringCreator, null);
        }

        // any pending values?
        if (pending != null) {
            for (int i = 0, len = pendingIx; i < len; i += 2) {
                SettableBeanProperty prop = (SettableBeanProperty)pending[i];
                Object value = pending[i+1];
                // Skip null values for properties that don't accept them
                if (value == null && _shouldSkipNullValue(prop.getName())) {
                    continue;
                }
                prop.set(ctxt, throwable, value);
            }
        }

        // any suppressed exceptions?
        if (suppressed != null) {
            for (Throwable s : suppressed) {
                // 13-Dec-2023, tatu: But skip any `null` entries we might have gotten
                if (s != null) {
                    throwable.addSuppressed(s);
                }
            }
        }

        return throwable;
    }

    /*
    /**********************************************************
    /* Internal helper methods
    /**********************************************************
     */

    /**
     * Helper method to initialize Throwable
     *
     * @since 2.16.2
     */
    private Throwable _instantiate(DeserializationContext ctxt, boolean hasStringCreator, String valueAsString)
    {
        /* 15-Oct-2010, tatu: Can't assume missing message is an error, since it may be
         *   suppressed during serialization.
         *
         *   Should probably allow use of default constructor, too...
         */
        //throw new XxxException("No 'message' property found: could not deserialize "+_beanType);
        if (hasStringCreator) {
            if (valueAsString != null) {
                return (Throwable) _valueInstantiator.createFromString(ctxt, valueAsString);
            } else {
                return (Throwable) _valueInstantiator.createFromString(ctxt, null);
            }
        } else {
            return (Throwable) _valueInstantiator.createUsingDefault(ctxt);
        }
    }

    /**
     * Helper for the "unknown name" branches, which have no {@link SettableBeanProperty}
     * to ask: a {@code null} matcher means the property carries no explicit
     * {@code @JsonView} and so is included under every active view ([databind#6174]).
     *
     * @since 3.3
     */
    /**
     * Helper for a property the active view excludes: reported as an unexpected property
     * if {@code FAIL_ON_UNEXPECTED_VIEW_PROPERTIES} is enabled ([databind#437]), and
     * simply skipped otherwise. Shared by all branches of the read loop so that a
     * property behaves the same whether or not it happens to be bound as a settable one.
     *
     * @since 3.3
     */
    private void _handleViewExcluded(JsonParser p, DeserializationContext ctxt,
            String propName, Class<?> activeView)
        throws JacksonException
    {
        if (ctxt.isEnabled(DeserializationFeature.FAIL_ON_UNEXPECTED_VIEW_PROPERTIES)) {
            ctxt.reportInputMismatch(handledType(),
                    String.format("Input mismatch while deserializing %s. Property '%s' is not part of current active view '%s'" +
                            " (disable 'DeserializationFeature.FAIL_ON_UNEXPECTED_VIEW_PROPERTIES' to allow)",
                            ClassUtil.nameOf(handledType()), propName, activeView.getName()));
        }
        p.skipChildren();
    }

    private boolean _visibleInView(ViewMatcher views, Class<?> activeView) {
        return (activeView == null) || (views == null) || views.isVisibleForView(activeView);
    }

    /**
     * Helper method to check if a property with null value should be skipped
     * during deserialization. Some Throwable setters throw NPE when called with null.
     *
     * @since 3.1
     */
    private boolean _shouldSkipNullValue(String propertyName) {
        return _stdPropNames.cause.equals(propertyName)
                || _stdPropNames.stackTrace.equals(propertyName);
    }

}