BeanPropertyMap.java

package tools.jackson.databind.deser.bean;

import java.util.*;

import tools.jackson.core.TokenStreamFactory;
import tools.jackson.core.sym.PropertyNameMatcher;
import tools.jackson.core.util.Named;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.PropertyName;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.deser.SettableBeanProperty;
import tools.jackson.databind.util.IgnorePropertiesUtil;
import tools.jackson.databind.util.NameTransformer;

/**
 * Helper class used for storing mapping from property name to
 * {@link SettableBeanProperty} instances.
 *<p>
 * Note that this class is used instead of generic {@link java.util.HashMap}
 * for bit of performance gain (and some memory savings): although default
 * implementation is very good for generic use cases, it can be streamlined
 * a bit for specific use case we have. Even relatively small improvements
 * matter since this is directly on the critical path during deserialization,
 * as it is done for each and every POJO property deserialized.
 */
public class BeanPropertyMap
    implements Iterable<SettableBeanProperty>
{
    /*
    /**********************************************************************
    /* Configuration
    /**********************************************************************
     */

    /**
     * Array of properties in the exact order they were handed in. This is
     * used by as-array serialization, deserialization.
     * Contains both primary properties (first <code>_primaryCount</code>
     * entries) and possible aliased mappings
     */
    private SettableBeanProperty[] _propsInOrder;

    /**
     * Configuration of alias mappings, if any (`null` if none),
     * aligned with properties in <code>_propsInOrder</code>
     */
    private final PropertyName[][] _aliasDefs;

    private final Locale _locale;

    private final boolean _caseInsensitive;

    /*
    /**********************************************************************
    /* Lookup index information constructed
    /**********************************************************************
     */

    private transient PropertyNameMatcher _nameMatcher;

    /**
     * Lazily instantiated array of properties mapped from lookup index, in which
     * first entries are same as in <code>_propsInOrder</code> followed by alias
     * mappings.
     */
    private transient SettableBeanProperty[] _propsWithAliases;

    /*
    /**********************************************************************
    /* Construction
    /**********************************************************************
     */

    /**
     * @param caseInsensitive Whether property name matching should case-insensitive or not
     * @param props Sequence of primary properties to index
     * @param aliasDefs Alias mappings, if any (null if none)
     * @param assignIndexes Whether to assign indices to property entities or not
     */
    protected BeanPropertyMap(Collection<SettableBeanProperty> props,
            PropertyName[][] aliasDefs,
            Locale locale, boolean caseInsensitive,
            boolean assignIndexes)
    {
        _locale = locale;
        _caseInsensitive = caseInsensitive;
        _aliasDefs = aliasDefs;
        _propsInOrder = props.toArray(new SettableBeanProperty[0]);
        // Former `assignIndexes`
        // order is arbitrary, but stable:
        if (assignIndexes) {
            // note: only assign to primary entries, not to aliased (since they are dups)
            for (int i = 0, end = props.size(); i < end; ++i) {
                _propsInOrder[i].assignIndex(i);
            }
        }
    }

    protected BeanPropertyMap(BeanPropertyMap base, boolean caseInsensitive)
    {
        _locale = base._locale;
        _caseInsensitive = caseInsensitive;
        _aliasDefs = base._aliasDefs;

        // 16-May-2016, tatu: Alas, not enough to just change flag, need to re-init as well.
        _propsInOrder = Arrays.copyOf(base._propsInOrder, base._propsInOrder.length);
//        init(Arrays.asList(_propsInOrder));
    }

    public static BeanPropertyMap construct(MapperConfig<?> config,
            Collection<SettableBeanProperty> props,
            PropertyName[][] aliases,
            boolean caseInsensitive)
    {
        return new BeanPropertyMap(props, aliases,
                config.getLocale(), caseInsensitive, true);
    }

    /*
    /**********************************************************************
    /* "Mutant factory" methods
    /**********************************************************************
     */

    /**
     * Mutant factory method that constructs a new instance if desired case-insensitivity
     * state differs from the state of this instance; if states are the same, returns
     * <code>this</code>.
     */
    public BeanPropertyMap withCaseInsensitivity(boolean state) {
        if (_caseInsensitive == state) {
            return this;
        }
        return new BeanPropertyMap(this, state);
    }

    /**
     * Fluent copy method that creates a new instance that is a copy
     * of this instance except for one additional property that is
     * passed as the argument.
     * Note that method does not modify this instance but constructs
     * and returns a new one.
     */
    public BeanPropertyMap withProperty(SettableBeanProperty newProp)
    {
        // First: maybe just replace in place?
        final String key = newProp.getName();
        for (int i = 0, end = _propsInOrder.length; i < end; ++i) {
            if (_propsInOrder[i].getName().equals(key)) {
                _propsInOrder[i] = newProp;
                return this;
            }
        }

        // If not, append
        ArrayList<SettableBeanProperty> newProps = new ArrayList<SettableBeanProperty>(Arrays.asList(_propsInOrder));
        newProps.add(newProp);
        // !!! TODO: assign index for the last entry?
        return new BeanPropertyMap(newProps, _aliasDefs, _locale, _caseInsensitive, false);
    }

    /**
     * Mutant factory method for constructing a map where all entries use given
     * prefix
     */
    public BeanPropertyMap renameAll(DeserializationContext ctxt,
            NameTransformer transformer)
    {
        if (transformer == null || (transformer == NameTransformer.NOP)) {
            return this;
        }
        // Try to retain insertion ordering as well
        final int len = _propsInOrder.length;
        ArrayList<SettableBeanProperty> newProps = new ArrayList<SettableBeanProperty>(_propsInOrder.length);
        for (int i = 0; i < len; ++i) {
            SettableBeanProperty orig = _propsInOrder[i];
            SettableBeanProperty prop = orig.unwrapped(ctxt, transformer);
            newProps.add(prop);
        }
        // 26-Feb-2017, tatu: Probably SHOULD handle renaming wrt Aliases?
        // NOTE: do NOT try reassigning indexes of properties; number doesn't change

        // !!! 18-Nov-2017, tatu: Should try recreating PropertyNameMatcher here but...
        return new BeanPropertyMap(newProps, _aliasDefs, _locale, _caseInsensitive, false)
                .initMatcher(ctxt.tokenStreamFactory());
    }

    /**
     * Mutant factory method that will use this instance as the base, and
     * construct an instance that is otherwise same except for excluding
     * properties with specified names.
     */
    public BeanPropertyMap withoutProperties(Collection<String> toExclude)
    {
        return withoutProperties(toExclude, null);
    }

    /**
     * Mutant factory method that will use this instance as the base, and
     * construct an instance that is otherwise same except for excluding
     * properties with specified names, or including only the one marked
     * as included
     */
    public BeanPropertyMap withoutProperties(Collection<String> toExclude, Collection<String> toInclude)
    {
        if ((toExclude == null) || toExclude.isEmpty()) {
            if (toInclude == null) {
                return this;
            }
            toExclude = Collections.emptySet();
        }
        final int len = _propsInOrder.length;
        ArrayList<SettableBeanProperty> newProps = new ArrayList<SettableBeanProperty>(len);

        for (int i = 0; i < len; ++i) {
            SettableBeanProperty prop = _propsInOrder[i];
            // 23-Jul-2020, tatu: Earlier comment from 2.x suggested `prop` could be null,
            //    checked, skipped, if so... but no more null checks
            if (!toExclude.contains(prop.getName())) {
                if (!IgnorePropertiesUtil.shouldIgnore(prop.getName(), toExclude, toInclude)) {
                    newProps.add(prop);
                }
            }
        }
        // should we try to re-index? Apparently no need
        // 17-Nov-2017, tatu: do NOT try to change indexes since this could lead to discrepancies
        //    (unless we actually copy property instances)
        return new BeanPropertyMap(newProps, _aliasDefs, _locale, _caseInsensitive, false);
    }

    /**
     * Specialized method that can be used to replace an existing entry
     * (note: entry MUST exist; otherwise exception is thrown) with
     * specified replacement.
     */
    public void replace(SettableBeanProperty oldProp, SettableBeanProperty newProp)
    {
        for (int i = 0, end = _propsInOrder.length; i < end; ++i) {
            if (_propsInOrder[i] == oldProp) {
                _propsInOrder[i] = newProp;
                return;
            }
        }
        throw new NoSuchElementException("No entry '"+oldProp.getName()+"' found, can't replace");
    }

    /**
     * Specialized method for removing specified existing entry.
     * NOTE: entry MUST exist, otherwise an exception is thrown.
     */
    public void remove(SettableBeanProperty propToRm)
    {
        final String key = propToRm.getName();
        ArrayList<SettableBeanProperty> props = new ArrayList<SettableBeanProperty>(_propsInOrder.length);
        boolean found = false;
        for (SettableBeanProperty prop : _propsInOrder) {
            if (!found) {
                String match = prop.getName();
                if (found = match.equals(key)) {
                    continue;
                }
            }
            props.add(prop);
        }
        if (!found) {
            throw new NoSuchElementException("No entry '"+propToRm.getName()+"' found, can't remove");
        }
        _propsInOrder = props.toArray(new SettableBeanProperty[0]);
    }

    /*
    /**********************************************************************
    /* Factory method(s) for helpers
    /**********************************************************************
     */

    public BeanPropertyMap initMatcher(TokenStreamFactory tsf)
    {
        List<Named> names;
        if (_aliasDefs == null) { // simple case, no aliases
            _propsWithAliases = _propsInOrder;
            names = Arrays.asList(_propsInOrder);
        } else {
            // must make an actual copy (not just array-backed) as we'll append entries:
            List<SettableBeanProperty> allProps = new ArrayList<>(Arrays.asList(_propsInOrder));
            names = new ArrayList<>(allProps);

            // map aliases
            for (int i = 0, end = _aliasDefs.length; i < end; ++i) {
                PropertyName[] aliases = _aliasDefs[i];
                if (aliases != null) {
                    SettableBeanProperty primary = _propsInOrder[i];
                    for (PropertyName alias : aliases) {
                        names.add(alias);
                        allProps.add(primary);
                    }
                }
            }
            _propsWithAliases = allProps.toArray(new SettableBeanProperty[0]);
        }
        // `true` -> yes, they are intern()ed alright
        if (_caseInsensitive) {
            _nameMatcher = tsf.constructCINameMatcher(names, true, _locale);
        } else {
            _nameMatcher = tsf.constructNameMatcher(names, true);
        }
        return this;
    }

    public PropertyNameMatcher getNameMatcher() { return _nameMatcher; }
    public SettableBeanProperty[] getNameMatcherProperties() { return _propsWithAliases; }

    /*
    /**********************************************************************
    /* Public API, simple accessors
    /**********************************************************************
     */

    public int size() { return _propsInOrder.length; }

    public boolean isCaseInsensitive() {
        return _caseInsensitive;
    }

    public boolean hasAliases() {
        return _aliasDefs != null;
    }

    /**
     * Accessor for traversing over all contained properties.
     */
    @Override
    public Iterator<SettableBeanProperty> iterator() {
        return Arrays.asList(_propsInOrder).iterator();
    }

    /**
     * Method that will re-create initial insertion-ordering of
     * properties contained in this map. Note that if properties
     * have been removed, array may contain nulls; otherwise
     * it should be consecutive.
     */
    public SettableBeanProperty[] getPrimaryProperties() {
        return _propsInOrder;
    }

    /*
    /**********************************************************************
    /* Public API, property definition lookup
    /**********************************************************************
     */

    public SettableBeanProperty findDefinition(int index)
    {
        for (SettableBeanProperty prop : _propsInOrder) {
            if ((prop != null) && (index == prop.getPropertyIndex())) {
                return prop;
            }
        }
        return null;
    }

    /**
     * NOTE: does NOT do case-insensitive matching -- only to be used during construction
     * and never during deserialization process -- nor alias expansion.
     */
    public SettableBeanProperty findDefinition(String key)
    {
        if (key == null) {
            throw new IllegalArgumentException("Cannot pass null property name");
        }
        for (SettableBeanProperty prop : _propsInOrder) {
            if (key.equals(prop.getName())) {
                return prop;
            }
        }
        return null;
    }

    /*
    /**********************************************************************
    /* Std method overrides
    /**********************************************************************
     */

    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        sb.append("Properties=[");
        int count = 0;

        Iterator<SettableBeanProperty> it = iterator();
        while (it.hasNext()) {
            SettableBeanProperty prop = it.next();
            if (count++ > 0) {
                sb.append(", ");
            }
            sb.append(String.format("%s(%s)", prop.getName(), prop.getType()));
        }
        sb.append(']');
        if (_aliasDefs != null) {
            sb.append(String.format("(aliases: %s)", _aliasDefs.length));
        }
        return sb.toString();
    }
}