PropertyBasedCreator.java

package tools.jackson.databind.deser.bean;

import java.util.*;

import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;

import tools.jackson.databind.*;
import tools.jackson.databind.deser.SettableAnyProperty;
import tools.jackson.databind.deser.SettableBeanProperty;
import tools.jackson.databind.deser.ValueInstantiator;
import tools.jackson.databind.deser.impl.ManagedReferenceProperty;
import tools.jackson.databind.deser.impl.ObjectIdReader;
import tools.jackson.databind.util.NameTransformer;

/**
 * Object that is used to collect arguments for non-default creator
 * (non-default-constructor, or argument-taking factory method)
 * before creator can be called.
 * Since ordering of JSON properties is not guaranteed, this may
 * require buffering of values other than ones being passed to
 * creator.
 */
public final class PropertyBasedCreator
{
    /**
     * Number of properties: usually same as size of {@link #_propertyLookup},
     * but not necessarily, when we have unnamed injectable properties.
     */
    protected final int _propertyCount;

    /**
     * Helper object that knows how to actually construct the instance by
     * invoking creator method with buffered arguments.
     */
    protected final ValueInstantiator _valueInstantiator;

    /**
     * Map that contains property objects for either constructor or factory
     * method (whichever one is null: one property for each
     * parameter for that one), keyed by logical property name
     */
    protected final HashMap<String, SettableBeanProperty> _propertyLookup;

    /**
     * Array that contains properties that match creator properties
     */
    protected final SettableBeanProperty[] _propertiesInOrder;

    /**
     * Indexes of properties with associated Injectable values, if any:
     * {@code null} if none.
     *
     * @since 2.21
     */
    protected final BitSet _injectablePropIndexes;

    /**
     * Flag indicating whether any creator properties are ManagedReferenceProperty instances,
     * used to optimize deserialization by skipping back-reference injection when not needed.
     *
     * @since 3.1
     */
    protected final boolean _hasManagedReferenceProperties;

    /*
    /**********************************************************************
    /* Construction, initialization
    /**********************************************************************
     */

    protected PropertyBasedCreator(DeserializationContext ctxt,
            ValueInstantiator valueInstantiator,
            SettableBeanProperty[] creatorProps,
            boolean caseInsensitive,
            boolean addAliases)
    {
        _valueInstantiator = valueInstantiator;
        if (caseInsensitive) {
            _propertyLookup = CaseInsensitiveMap.construct(ctxt.getConfig().getLocale());
        } else {
            _propertyLookup = new HashMap<>();
        }

        // 26-Feb-2017, tatu: Let's start by aliases, so that there is no
        //    possibility of accidental override of primary names
        if (addAliases) {
            final DeserializationConfig config = ctxt.getConfig();
            for (SettableBeanProperty prop : creatorProps) {
                // 22-Jan-2018, tatu: ignorable entries should be ignored, even if got aliases
                if (!prop.isIgnorable()) {
                    List<PropertyName> aliases = prop.findAliases(config);
                    if (!aliases.isEmpty()) {
                        for (PropertyName pn : aliases) {
                            _propertyLookup.put(pn.getSimpleName(), prop);
                        }
                    }
                }
            }
        }
        final int len = creatorProps.length;
        _propertyCount = len;
        _propertiesInOrder = new SettableBeanProperty[len];
        BitSet injectablePropIndexes = null;
        boolean hasManagedRef = false;

        for (int i = 0; i < len; ++i) {
            SettableBeanProperty prop = creatorProps[i];
            _propertiesInOrder[i] = prop;
            // 22-Jan-2018, tatu: ignorable entries should be skipped
            if (!prop.isIgnorable()) {
                _propertyLookup.put(prop.getName(), prop);
            }
            if (prop.getInjectionDefinition() != null) {
                if (injectablePropIndexes == null) {
                    injectablePropIndexes = new BitSet(len);
                }
                injectablePropIndexes.set(i);
            }

            // [databind#1516]: detect whether any ManagedReferenceProperty exists
            // so we can avoid iterating over all properties when none are present
            hasManagedRef |= prop instanceof ManagedReferenceProperty;
        }

        _injectablePropIndexes = injectablePropIndexes;
        _hasManagedReferenceProperties = hasManagedRef;
    }

    protected PropertyBasedCreator(PropertyBasedCreator base,
            HashMap<String, SettableBeanProperty> propertyLookup,
            SettableBeanProperty[] allProperties)
    {
        _propertyCount = base._propertyCount;
        _valueInstantiator = base._valueInstantiator;
        _injectablePropIndexes = base._injectablePropIndexes;
        _propertyLookup = propertyLookup;
        _propertiesInOrder = allProperties;
        _hasManagedReferenceProperties = base._hasManagedReferenceProperties;
    }

    /**
     * Factory method used for building actual instances to be used with POJOS:
     * resolves deserializers, checks for "null values".
     */
    public static PropertyBasedCreator construct(DeserializationContext ctxt,
            ValueInstantiator valueInstantiator, SettableBeanProperty[] srcCreatorProps,
            BeanPropertyMap allProperties)
    {
        final int len = srcCreatorProps.length;
        SettableBeanProperty[] creatorProps = new SettableBeanProperty[len];
        for (int i = 0; i < len; ++i) {
            SettableBeanProperty prop = srcCreatorProps[i];
            if (!prop.hasValueDeserializer()) {
                // 15-Apr-2020, tatu: [databind#962] Avoid getting deserializer for Inject-only
                //     cases
                if (!prop.isInjectionOnly()) {
                    prop = prop.withValueDeserializer(ctxt.findContextualValueDeserializer(prop.getType(), prop));
                }
            }
            creatorProps[i] = prop;
        }
        return new PropertyBasedCreator(ctxt, valueInstantiator, creatorProps,
                allProperties.isCaseInsensitive(),
// 05-Sep-2019, tatu: As per [databind#2378] looks like not all aliases get merged into
//    `allProperties` so force lookup anyway.
//                allProperties.hasAliases()
                true);
    }

    /**
     * Factory method used for building actual instances to be used with types
     * OTHER than POJOs.
     * resolves deserializers and checks for "null values".
     */
    public static PropertyBasedCreator construct(DeserializationContext ctxt,
            ValueInstantiator valueInstantiator, SettableBeanProperty[] srcCreatorProps,
            boolean caseInsensitive)
    {
        final int len = srcCreatorProps.length;
        SettableBeanProperty[] creatorProps = new SettableBeanProperty[len];
        for (int i = 0; i < len; ++i) {
            SettableBeanProperty prop = srcCreatorProps[i];
            if (!prop.hasValueDeserializer()) {
                prop = prop.withValueDeserializer(ctxt.findContextualValueDeserializer(prop.getType(), prop));
            }
            creatorProps[i] = prop;
        }
        return new PropertyBasedCreator(ctxt, valueInstantiator, creatorProps,
                caseInsensitive, false);
    }

    /**
     * Mutant factory method for constructing a map where the names of all properties
     * are transformed using the given {@link NameTransformer}.
     *
     * @since 2.19
     */
    public PropertyBasedCreator renameAll(DeserializationContext ctxt,
            NameTransformer transformer)
    {
        if (transformer == null || (transformer == NameTransformer.NOP)) {
            return this;
        }

        final int len = _propertiesInOrder.length;
        HashMap<String, SettableBeanProperty> newLookup = new HashMap<>(_propertyLookup);
        List<SettableBeanProperty> newProps = new ArrayList<>(len);

        for (SettableBeanProperty prop : _propertiesInOrder) {
            if (prop == null) {
                newProps.add(null);
                continue;
            }

            SettableBeanProperty renamedProperty = prop.unwrapped(ctxt, transformer);
            String oldName = prop.getName();
            String newName = renamedProperty.getName();

            newProps.add(renamedProperty);

            if (!oldName.equals(newName) && newLookup.containsKey(oldName)) {
                newLookup.remove(oldName);
                newLookup.put(newName, renamedProperty);
            }
        }

        return new PropertyBasedCreator(this,
                newLookup,
                newProps.toArray(new SettableBeanProperty[0])
        );
    }

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

    public Collection<SettableBeanProperty> properties() {
        return _propertyLookup.values();
    }

    public SettableBeanProperty findCreatorProperty(String name) {
        return _propertyLookup.get(name);
    }

    public SettableBeanProperty findCreatorProperty(int propertyIndex) {
        for (SettableBeanProperty prop : _propertyLookup.values()) {
            if (prop.getPropertyIndex() == propertyIndex) {
                return prop;
            }
        }
        return null;
    }

    /**
     * @since 3.1
     */
    public boolean hasManagedReferenceProperties() {
        return _hasManagedReferenceProperties;
    }

    /*
    /**********************************************************************
    /* Building process
    /**********************************************************************
     */

    /**
     * Method called when starting to build a bean instance.
     */
    public PropertyValueBuffer startBuilding(JsonParser p, DeserializationContext ctxt,
            ObjectIdReader oir) {
        return new PropertyValueBuffer(p, ctxt, _propertyCount, oir, null,
                _injectablePropIndexes);
    }

    /**
     * Method called when starting to build a bean instance.
     *
     * @since 2.18 (added SettableAnyProperty parameter)
     */
    public PropertyValueBuffer startBuildingWithAnySetter(JsonParser p, DeserializationContext ctxt,
            ObjectIdReader oir, SettableAnyProperty anySetter
    ) {
        return new PropertyValueBuffer(p, ctxt, _propertyCount, oir, anySetter,
                _injectablePropIndexes);
    }

    public Object build(DeserializationContext ctxt, PropertyValueBuffer buffer)
        throws JacksonException
    {
        Object bean = _valueInstantiator.createFromObjectWith(ctxt,
                _propertiesInOrder, buffer);
        // returning null isn't quite legal, but let's let caller deal with that
        if (bean != null) {
            // Object Id to handle?
            bean = buffer.handleIdValue(ctxt, bean);

            // Anything buffered?
            for (PropertyValue pv = buffer.buffered(); pv != null; pv = pv.next) {
                pv.assign(ctxt, bean);
            }
        }
        return bean;
    }

    /*
    /**********************************************************************
    /* Helper classes
    /**********************************************************************
     */

    /**
     * Simple override of standard {@link java.util.HashMap} to support
     * case-insensitive access to creator properties
     */
    static class CaseInsensitiveMap extends HashMap<String, SettableBeanProperty>
    {
        // doesn't really need to be Serializable with 3.x but... whatever
        private static final long serialVersionUID = 3L;

        /**
         * Lower-casing can have Locale-specific minor variations.
         */
        protected final Locale _locale;

        public CaseInsensitiveMap(Locale l) {
            _locale = l;
        }

        public static CaseInsensitiveMap construct(Locale l) {
            return new CaseInsensitiveMap(l);
        }

        @Override
        public SettableBeanProperty get(Object key0) {
            return super.get(((String) key0).toLowerCase(_locale));
        }

        @Override
        public SettableBeanProperty put(String key, SettableBeanProperty value) {
            key = key.toLowerCase(_locale);
            return super.put(key, value);
        }
    }
}