package com.fasterxml.jackson.databind.deser.impl; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.*; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.PropertyName; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.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>, java.io.Serializable { private static final long serialVersionUID = 2L; /** * @since 2.5 */ protected final boolean _caseInsensitive; private int _hashMask; /** * Number of entries stored in the hash area. */ private int _size; private int _spillCount; /** * Hash area that contains key/property pairs in adjacent elements. */ private Object[] _hashArea; /** * Array of properties in the exact order they were handed in. This is * used by as-array serialization, deserialization. */ private SettableBeanProperty[] _propsInOrder; /** * Configuration of alias mappings, indexed by unmodified property name * to unmodified aliases, if any; entries only included for properties * that do have aliases. * This is is used for constructing actual reverse lookup mapping, if * needed, taking into account possible case-insensitivity, as well * as possibility of name prefixes. * * @since 2.9 */ private final Map<String,List<PropertyName>> _aliasDefs; /** * Mapping from secondary names (aliases) to primary names. * * @since 2.9 */ private final Map<String,String> _aliasMapping; /** * @since 2.9 */ public BeanPropertyMap(boolean caseInsensitive, Collection<SettableBeanProperty> props, Map<String,List<PropertyName>> aliasDefs) { _caseInsensitive = caseInsensitive; _propsInOrder = props.toArray(new SettableBeanProperty[props.size()]); _aliasDefs = aliasDefs; _aliasMapping = _buildAliasMapping(aliasDefs); init(props); } @Deprecated // since 2.8 public BeanPropertyMap(boolean caseInsensitive, Collection<SettableBeanProperty> props) { this(caseInsensitive, props, Collections.<String,List<PropertyName>>emptyMap()); } /** * @since 2.8 */ protected BeanPropertyMap(BeanPropertyMap base, boolean caseInsensitive) { _caseInsensitive = caseInsensitive; _aliasDefs = base._aliasDefs; _aliasMapping = base._aliasMapping; // 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)); } /** * 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>. * * @since 2.8 */ public BeanPropertyMap withCaseInsensitivity(boolean state) { if (_caseInsensitive == state) { return this; } return new BeanPropertyMap(this, state); } protected void init(Collection<SettableBeanProperty> props) { _size = props.size(); // First: calculate size of primary hash area final int hashSize = findSize(_size); _hashMask = hashSize-1; // and allocate enough to contain primary/secondary, expand for spillovers as need be int alloc = (hashSize + (hashSize>>1)) * 2; Object[] hashed = new Object[alloc]; int spillCount = 0; for (SettableBeanProperty prop : props) { // Due to removal, renaming, theoretically possible we'll have "holes" so: if (prop == null) { continue; } String key = getPropertyName(prop); int slot = _hashCode(key); int ix = (slot<<1); // primary slot not free? if (hashed[ix] != null) { // secondary? ix = (hashSize + (slot >> 1)) << 1; if (hashed[ix] != null) { // ok, spill over. ix = ((hashSize + (hashSize >> 1) ) << 1) + spillCount; spillCount += 2; if (ix >= hashed.length) { hashed = Arrays.copyOf(hashed, hashed.length + 4); } } } //System.err.println(" add '"+key+" at #"+(ix>>1)+"/"+size+" (hashed at "+slot+")"); hashed[ix] = key; hashed[ix+1] = prop; // and aliases } /* for (int i = 0; i < hashed.length; i += 2) { System.err.printf("#%02d: %s\n", i>>1, (hashed[i] == null) ? "-" : hashed[i]); } */ _hashArea = hashed; _spillCount = spillCount; } private final static int findSize(int size) { if (size <= 5) { return 8; } if (size <= 12) { return 16; } int needed = size + (size >> 2); // at most 80% full int result = 32; while (result < needed) { result += result; } return result; } /** * @since 2.6 */ public static BeanPropertyMap construct(Collection<SettableBeanProperty> props, boolean caseInsensitive, Map<String,List<PropertyName>> aliasMapping) { return new BeanPropertyMap(caseInsensitive, props, aliasMapping); } @Deprecated // since 2.9 public static BeanPropertyMap construct(Collection<SettableBeanProperty> props, boolean caseInsensitive) { return construct(props, caseInsensitive, Collections.<String,List<PropertyName>>emptyMap()); } /** * 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: may be able to just replace? String key = getPropertyName(newProp); for (int i = 1, end = _hashArea.length; i < end; i += 2) { SettableBeanProperty prop = (SettableBeanProperty) _hashArea[i]; if ((prop != null) && prop.getName().equals(key)) { _hashArea[i] = newProp; _propsInOrder[_findFromOrdered(prop)] = newProp; return this; } } // If not, append final int slot = _hashCode(key); final int hashSize = _hashMask+1; int ix = (slot<<1); // primary slot not free? if (_hashArea[ix] != null) { // secondary? ix = (hashSize + (slot >> 1)) << 1; if (_hashArea[ix] != null) { // ok, spill over. ix = ((hashSize + (hashSize >> 1) ) << 1) + _spillCount; _spillCount += 2; if (ix >= _hashArea.length) { _hashArea = Arrays.copyOf(_hashArea, _hashArea.length + 4); // Uncomment for debugging only /* for (int i = 0; i < _hashArea.length; i += 2) { if (_hashArea[i] != null) { System.err.println("Property #"+(i/2)+" '"+_hashArea[i]+"'..."); } } System.err.println("And new propr #"+slot+" '"+key+"'"); */ } } } _hashArea[ix] = key; _hashArea[ix+1] = newProp; int last = _propsInOrder.length; _propsInOrder = Arrays.copyOf(_propsInOrder, last+1); _propsInOrder[last] = newProp; // should we just create a new one? Or is resetting ok? return this; } public BeanPropertyMap assignIndexes() { // order is arbitrary, but stable: int index = 0; for (int i = 1, end = _hashArea.length; i < end; i += 2) { SettableBeanProperty prop = (SettableBeanProperty) _hashArea[i]; if (prop != null) { prop.assignIndex(index++); } } return this; } /** * Mutant factory method for constructing a map where all entries use given * prefix */ public BeanPropertyMap renameAll(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>(len); for (int i = 0; i < len; ++i) { SettableBeanProperty prop = _propsInOrder[i]; // What to do with holes? For now, retain if (prop == null) { newProps.add(prop); continue; } newProps.add(_rename(prop, transformer)); } // should we try to re-index? Ordering probably changed but caller probably doesn't want changes... // 26-Feb-2017, tatu: Probably SHOULD handle renaming wrt Aliases? return new BeanPropertyMap(_caseInsensitive, newProps, _aliasDefs); } /* /********************************************************** /* Public API, mutators /********************************************************** */ /** * 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. * * @since 2.8 */ public BeanPropertyMap withoutProperties(Collection<String> toExclude) { if (toExclude.isEmpty()) { return this; } final int len = _propsInOrder.length; ArrayList<SettableBeanProperty> newProps = new ArrayList<SettableBeanProperty>(len); for (int i = 0; i < len; ++i) { SettableBeanProperty prop = _propsInOrder[i]; // 01-May-2015, tatu: Not 100% sure if existing `null`s should be retained; // or, if entries to ignore should be retained as nulls. For now just // prune them out if (prop != null) { // may contain holes, too, check. if (!toExclude.contains(prop.getName())) { newProps.add(prop); } } } // should we try to re-index? Apparently no need return new BeanPropertyMap(_caseInsensitive, newProps, _aliasDefs); } /** * 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 newProp) { String key = getPropertyName(newProp); int ix = _findIndexInHash(key); if (ix < 0) { throw new NoSuchElementException("No entry '"+key+"' found, can't replace"); } SettableBeanProperty prop = (SettableBeanProperty) _hashArea[ix]; _hashArea[ix] = newProp; // also, replace in in-order _propsInOrder[_findFromOrdered(prop)] = newProp; } /** * Specialized method for removing specified existing entry. * NOTE: entry MUST exist, otherwise an exception is thrown. */ public void remove(SettableBeanProperty propToRm) { ArrayList<SettableBeanProperty> props = new ArrayList<SettableBeanProperty>(_size); String key = getPropertyName(propToRm); boolean found = false; for (int i = 1, end = _hashArea.length; i < end; i += 2) { SettableBeanProperty prop = (SettableBeanProperty) _hashArea[i]; if (prop == null) { continue; } if (!found) { // 09-Jan-2017, tatu: Important: must check name slot and NOT property name, // as only former is lower-case in case-insensitive case found = key.equals(_hashArea[i-1]); if (found) { // need to leave a hole here _propsInOrder[_findFromOrdered(prop)] = null; continue; } } props.add(prop); } if (!found) { throw new NoSuchElementException("No entry '"+propToRm.getName()+"' found, can't remove"); } init(props); } /* /********************************************************** /* Public API, simple accessors /********************************************************** */ public int size() { return _size; } /** * @since 2.9 */ public boolean isCaseInsensitive() { return _caseInsensitive; } /** * @since 2.9 */ public boolean hasAliases() { return !_aliasDefs.isEmpty(); } /** * Accessor for traversing over all contained properties. */ @Override public Iterator<SettableBeanProperty> iterator() { return _properties().iterator(); } private List<SettableBeanProperty> _properties() { ArrayList<SettableBeanProperty> p = new ArrayList<SettableBeanProperty>(_size); for (int i = 1, end = _hashArea.length; i < end; i += 2) { SettableBeanProperty prop = (SettableBeanProperty) _hashArea[i]; if (prop != null) { p.add(prop); } } return p; } /** * 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. * * @since 2.1 */ public SettableBeanProperty[] getPropertiesInInsertionOrder() { return _propsInOrder; } // Confining this case insensitivity to this function (and the find method) in case we want to // apply a particular locale to the lower case function. For now, using the default. protected final String getPropertyName(SettableBeanProperty prop) { return _caseInsensitive ? prop.getName().toLowerCase() : prop.getName(); } /* /********************************************************** /* Public API, property lookup /********************************************************** */ /** * @since 2.3 */ public SettableBeanProperty find(int index) { // note: will scan the whole area, including primary, secondary and // possible spill-area for (int i = 1, end = _hashArea.length; i < end; i += 2) { SettableBeanProperty prop = (SettableBeanProperty) _hashArea[i]; if ((prop != null) && (index == prop.getPropertyIndex())) { return prop; } } return null; } public SettableBeanProperty find(String key) { if (key == null) { throw new IllegalArgumentException("Can not pass null property name"); } if (_caseInsensitive) { key = key.toLowerCase(); } // inlined `_hashCode(key)` int slot = key.hashCode() & _hashMask; // int h = key.hashCode(); // int slot = (h + (h >> 13)) & _hashMask; int ix = (slot<<1); Object match = _hashArea[ix]; if ((match == key) || key.equals(match)) { return (SettableBeanProperty) _hashArea[ix+1]; } return _find2(key, slot, match); } private final SettableBeanProperty _find2(String key, int slot, Object match) { if (match == null) { // 26-Feb-2017, tatu: Need to consider aliases return _findWithAlias(_aliasMapping.get(key)); } // no? secondary? int hashSize = _hashMask+1; int ix = hashSize + (slot>>1) << 1; match = _hashArea[ix]; if (key.equals(match)) { return (SettableBeanProperty) _hashArea[ix+1]; } if (match != null) { // _findFromSpill(...) int i = (hashSize + (hashSize>>1)) << 1; for (int end = i + _spillCount; i < end; i += 2) { match = _hashArea[i]; if ((match == key) || key.equals(match)) { return (SettableBeanProperty) _hashArea[i+1]; } } } // 26-Feb-2017, tatu: Need to consider aliases return _findWithAlias(_aliasMapping.get(key)); } private SettableBeanProperty _findWithAlias(String keyFromAlias) { if (keyFromAlias == null) { return null; } // NOTE: need to inline much of handling do avoid cyclic calls via alias // first, inlined main `find(String)` int slot = _hashCode(keyFromAlias); int ix = (slot<<1); Object match = _hashArea[ix]; if (keyFromAlias.equals(match)) { return (SettableBeanProperty) _hashArea[ix+1]; } if (match == null) { return null; } return _find2ViaAlias(keyFromAlias, slot, match); } private SettableBeanProperty _find2ViaAlias(String key, int slot, Object match) { // no? secondary? int hashSize = _hashMask+1; int ix = hashSize + (slot>>1) << 1; match = _hashArea[ix]; if (key.equals(match)) { return (SettableBeanProperty) _hashArea[ix+1]; } if (match != null) { // _findFromSpill(...) int i = (hashSize + (hashSize>>1)) << 1; for (int end = i + _spillCount; i < end; i += 2) { match = _hashArea[i]; if ((match == key) || key.equals(match)) { return (SettableBeanProperty) _hashArea[i+1]; } } } return null; } /* /********************************************************** /* Public API, deserialization support /********************************************************** */ /** * Convenience method that tries to find property with given name, and * if it is found, call {@link SettableBeanProperty#deserializeAndSet} * on it, and return true; or, if not found, return false. * Note, too, that if deserialization is attempted, possible exceptions * are wrapped if and as necessary, so caller need not handle those. * * @since 2.5 */ public boolean findDeserializeAndSet(JsonParser p, DeserializationContext ctxt, Object bean, String key) throws IOException { final SettableBeanProperty prop = find(key); if (prop == null) { return false; } try { prop.deserializeAndSet(p, ctxt, bean); } catch (Exception e) { wrapAndThrow(e, bean, key, ctxt); } return true; } /* /********************************************************** /* 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(prop.getName()); sb.append('('); sb.append(prop.getType()); sb.append(')'); } sb.append(']'); if (!_aliasDefs.isEmpty()) { sb.append("(aliases: "); sb.append(_aliasDefs); sb.append(")"); } return sb.toString(); } /* /********************************************************** /* Helper methods /********************************************************** */ protected SettableBeanProperty _rename(SettableBeanProperty prop, NameTransformer xf) { if (prop == null) { return prop; } String newName = xf.transform(prop.getName()); prop = prop.withSimpleName(newName); JsonDeserializer<?> deser = prop.getValueDeserializer(); if (deser != null) { @SuppressWarnings("unchecked") JsonDeserializer<Object> newDeser = (JsonDeserializer<Object>) deser.unwrappingDeserializer(xf); if (newDeser != deser) { prop = prop.withValueDeserializer(newDeser); } } return prop; } protected void wrapAndThrow(Throwable t, Object bean, String fieldName, DeserializationContext ctxt) throws IOException { // inlined 'throwOrReturnThrowable' while (t instanceof InvocationTargetException && t.getCause() != null) { t = t.getCause(); } // Errors to be passed as is ClassUtil.throwIfError(t); // StackOverflowErrors are tricky ones; need to be careful... boolean wrap = (ctxt == null) || ctxt.isEnabled(DeserializationFeature.WRAP_EXCEPTIONS); // Ditto for IOExceptions; except we may want to wrap JSON exceptions if (t instanceof IOException) { if (!wrap || !(t instanceof JsonProcessingException)) { throw (IOException) t; } } else if (!wrap) { // allow disabling wrapping for unchecked exceptions ClassUtil.throwIfRTE(t); } throw JsonMappingException.wrapWithPath(t, bean, fieldName); } /** * Helper method used to find exact location of a property with name * given exactly, not subject to case changes, within hash area. * Expectation is that such property SHOULD exist, although no * exception is thrown. * * @since 2.7 */ private final int _findIndexInHash(String key) { final int slot = _hashCode(key); int ix = (slot<<1); // primary match? if (key.equals(_hashArea[ix])) { return ix+1; } // no? secondary? int hashSize = _hashMask+1; ix = hashSize + (slot>>1) << 1; if (key.equals(_hashArea[ix])) { return ix+1; } // perhaps spill then int i = (hashSize + (hashSize>>1)) << 1; for (int end = i + _spillCount; i < end; i += 2) { if (key.equals(_hashArea[i])) { return i+1; } } return -1; } private final int _findFromOrdered(SettableBeanProperty prop) { for (int i = 0, end = _propsInOrder.length; i < end; ++i) { if (_propsInOrder[i] == prop) { return i; } } throw new IllegalStateException("Illegal state: property '"+prop.getName()+"' missing from _propsInOrder"); } // Offlined version for convenience if we want to change hashing scheme private final int _hashCode(String key) { // This method produces better hash, fewer collisions... yet for some // reason produces slightly worse performance. Very strange. // 05-Aug-2015, tatu: ... still true? /* int h = key.hashCode(); return (h + (h >> 13)) & _hashMask; */ return key.hashCode() & _hashMask; } // @since 2.9 private Map<String,String> _buildAliasMapping(Map<String,List<PropertyName>> defs) { if ((defs == null) || defs.isEmpty()) { return Collections.emptyMap(); } Map<String,String> aliases = new HashMap<>(); for (Map.Entry<String,List<PropertyName>> entry : defs.entrySet()) { String key = entry.getKey(); if (_caseInsensitive) { key = key.toLowerCase(); } for (PropertyName pn : entry.getValue()) { String mapped = pn.getSimpleName(); if (_caseInsensitive) { mapped = mapped.toLowerCase(); } aliases.put(mapped, key); } } return aliases; } }