EnumResolver.java
package tools.jackson.databind.util;
import java.util.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import tools.jackson.databind.*;
import tools.jackson.databind.cfg.MapperConfig;
import tools.jackson.databind.introspect.AnnotatedClass;
import tools.jackson.databind.introspect.AnnotatedMember;
/**
* Helper class used to resolve String values (either JSON Object field
* names or regular String values) into Java Enum instances.
*/
public class EnumResolver implements java.io.Serializable
{
private static final long serialVersionUID = 1L;
protected final Class<Enum<?>> _enumClass;
protected final Enum<?>[] _enums;
protected final HashMap<String, Enum<?>> _enumsById;
/**
* @since 3.2
*/
protected final Map<Integer, Enum<?>> _enumsByNumericIndex;
protected final Enum<?> _defaultValue;
/**
* Marker for case-insensitive handling
*/
protected final boolean _isIgnoreCase;
/**
* Marker for case where value may come from {@code @JsonValue} annotated
* accessor and is expected/likely to come from actual integral number
* value (and not String).
*<p>
* Special case is needed since this specifically means that {@code Enum.index()}
* should NOT be used or default to.
*/
protected final boolean _isFromIntValue;
/**
* Marker for case where enum values to match are from {@code @JsonValue}-annotated
* method.
*/
protected final boolean _hasAsValueAnnotation;
/**
* Marker for case where numeric input (JSON number or quoted number) should be
* resolved using numeric-index lookup derived from {@code @JsonProperty} values,
* instead of ordinal index (Enum.values()).
* <p>
* Intended to be enabled when {@code @JsonFormat(shape = NUMBER/ARRAY)} selects
* numeric representation for Enum values.
*
* @since 3.2
*/
protected final boolean _useNumericIndexForNumbers;
/*
/**********************************************************************
/* Constructors
/**********************************************************************
*/
/**
* @since 3.2 (added 2 more arguments)
*/
protected EnumResolver(Class<Enum<?>> enumClass, Enum<?>[] enums,
HashMap<String, Enum<?>> enumsById, Enum<?> defaultValue,
boolean isIgnoreCase, boolean isFromIntValue,
boolean hasAsValueAnnotation,
Map<Integer, Enum<?>> enumsByNumericIndex, boolean useNumericIndexForNumbers)
{
_enumClass = enumClass;
_enums = enums;
_enumsById = enumsById;
_enumsByNumericIndex = enumsByNumericIndex;
_defaultValue = defaultValue;
_isIgnoreCase = isIgnoreCase;
_isFromIntValue = isFromIntValue;
_hasAsValueAnnotation = hasAsValueAnnotation;
_useNumericIndexForNumbers = useNumericIndexForNumbers;
}
/*
/**********************************************************************
/* Factory methods
/**********************************************************************
*/
/**
* Factory method for constructing an {@link EnumResolver} based on the given {@link DeserializationConfig} and
* {@link AnnotatedClass} of the enum to be resolved.
*
* @param config the deserialization configuration to use
* @param annotatedClass the annotated class of the enum to be resolved
* @return the constructed {@link EnumResolver}
*/
public static EnumResolver constructFor(DeserializationConfig config, AnnotatedClass annotatedClass)
{
// prepare data
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
final boolean isIgnoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
final Class<?> enumCls0 = annotatedClass.getRawType();
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
final Enum<?>[] enumConstants = _enumConstants(enumCls);
final Enum<?> defaultEnum = _enumDefault(config, annotatedClass, enumConstants);
// Determine whether numeric values should use numeric-index lookup, based on
// class-level @JsonFormat(shape=NUMBER/ARRAY...). Uses AnnotatedClass so Mix-ins apply.
JsonFormat.Value value = ai.findFormat(config, annotatedClass);
boolean useNumericIndexForNumbers = (value != null)
&& (value.getShape().isNumeric() || value.getShape() == JsonFormat.Shape.ARRAY);
// introspect
String[] names = ai.findEnumValues(config, annotatedClass,
enumConstants, new String[enumConstants.length]);
final String[][] allAliases = new String[names.length][];
ai.findEnumAliases(config, annotatedClass, enumConstants, allAliases);
// finally, build
HashMap<String, Enum<?>> map = new HashMap<>();
Map<Integer, Enum<?>> numericIndexMap = null;
for (int i = 0, len = enumConstants.length; i < len; ++i) {
final Enum<?> enumValue = enumConstants[i];
final String rawName = names[i];
String name = rawName;
if (name == null) {
name = enumValue.name();
}
map.put(name, enumValue);
if (rawName != null && NumberUtil.isValidJDKIntNumber(rawName)) {
try {
final int numericIndex = Integer.parseInt(rawName);
if (numericIndexMap == null) {
numericIndexMap = new HashMap<>();
}
numericIndexMap.put(numericIndex, enumValue);
} catch (NumberFormatException e) {
// out of int range, ignore
}
}
String[] aliases = allAliases[i];
if (aliases != null) {
for (String alias : aliases) {
// Avoid overriding any primary names
map.putIfAbsent(alias, enumValue);
}
}
}
return new EnumResolver(enumCls, enumConstants, map,
defaultEnum, isIgnoreCase, false, false,
numericIndexMap, useNumericIndexForNumbers);
}
/**
* Factory method for constructing resolver that maps from Enum.toString() into
* Enum value
*/
public static EnumResolver constructUsingToString(DeserializationConfig config, AnnotatedClass annotatedClass) {
// prepare data
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
final boolean isIgnoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
final Class<?> enumCls0 = annotatedClass.getRawType();
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
final Enum<?>[] enumConstants = _enumConstants(enumCls);
final Enum<?> defaultEnum = _enumDefault(config, annotatedClass, enumConstants);
// introspect
final String[] names = new String[enumConstants.length];
final String[][] allAliases = new String[enumConstants.length][];
if (ai != null) {
ai.findEnumValues(config, annotatedClass, enumConstants, names);
ai.findEnumAliases(config, annotatedClass, enumConstants, allAliases);
}
// finally, build
// from last to first, so that in case of duplicate values, first wins
HashMap<String, Enum<?>> map = new HashMap<>();
for (int i = enumConstants.length; --i >= 0; ) {
Enum<?> enumValue = enumConstants[i];
String name = names[i];
if (name == null) {
name = enumValue.toString();
}
map.put(name, enumValue);
String[] aliases = allAliases[i];
if (aliases != null) {
for (String alias : aliases) {
// Avoid overriding any primary names
map.putIfAbsent(alias, enumValue);
}
}
}
return new EnumResolver(enumCls, enumConstants, map,
defaultEnum, isIgnoreCase, false, false,
null, false);
}
/**
* Factory method for constructing resolver that maps from index of Enum.values() into
* Enum value.
*/
public static EnumResolver constructUsingIndex(DeserializationConfig config, AnnotatedClass annotatedClass)
{
// prepare data
final boolean isIgnoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
final Class<?> enumCls0 = annotatedClass.getRawType();
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
final Enum<?>[] enumConstants = _enumConstants(enumCls);
final Enum<?> defaultEnum = _enumDefault(config, annotatedClass, enumConstants);
// finally, build
// from last to first, so that in case of duplicate values, first wins
HashMap<String, Enum<?>> map = new HashMap<>();
for (int i = enumConstants.length; --i >= 0; ) {
Enum<?> enumValue = enumConstants[i];
map.put(String.valueOf(i), enumValue);
}
return new EnumResolver(enumCls, enumConstants, map,
defaultEnum, isIgnoreCase, false, false,
null, false);
}
/**
* Factory method for constructing an {@link EnumResolver} with {@link EnumNamingStrategy} applied.
*/
public static EnumResolver constructUsingEnumNamingStrategy(DeserializationConfig config,
AnnotatedClass annotatedClass, EnumNamingStrategy enumNamingStrategy)
{
// prepare data
final boolean isIgnoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
final Class<?> enumCls0 = annotatedClass.getRawType();
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
final Enum<?>[] enumConstants = _enumConstants(enumCls);
final Enum<?> defaultEnum = _enumDefault(config, annotatedClass, enumConstants);
// introspect
final String[] names = new String[enumConstants.length];
final String[][] allAliases = new String[enumConstants.length][];
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
if (ai != null) {
ai.findEnumValues(config, annotatedClass, enumConstants, names);
ai.findEnumAliases(config, annotatedClass, enumConstants, allAliases);
}
// finally build
// from last to first, so that in case of duplicate values, first wins
HashMap<String, Enum<?>> map = new HashMap<>();
for (int i = enumConstants.length; --i >= 0; ) {
Enum<?> anEnum = enumConstants[i];
String name = names[i];
if (name == null) {
name = enumNamingStrategy.convertEnumToExternalName(config,
annotatedClass,
anEnum.name());
}
map.put(name, anEnum);
String[] aliases = allAliases[i];
if (aliases != null) {
for (String alias : aliases) {
// avoid replacing any primary names
map.putIfAbsent(alias, anEnum);
}
}
}
return new EnumResolver(enumCls, enumConstants, map,
defaultEnum, isIgnoreCase, false, false,
null, false);
}
/**
* Method used when actual String serialization is indicated using @JsonValue
* on a method in Enum class.
*/
public static EnumResolver constructUsingMethod(DeserializationConfig config,
AnnotatedClass annotatedClass, AnnotatedMember accessor)
{
// prepare data
final boolean isIgnoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
final Class<?> enumCls0 = annotatedClass.getRawType();
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
final Enum<?>[] enumConstants = _enumConstants(enumCls);
final Enum<?> defaultEnum = _enumDefault(config, annotatedClass, enumConstants);
// build
HashMap<String, Enum<?>> map = new HashMap<>();
// from last to first, so that in case of duplicate values, first wins
for (int i = enumConstants.length; --i >= 0; ) {
Enum<?> en = enumConstants[i];
try {
Object o = accessor.getValue(en);
if (o != null) {
map.put(o.toString(), en);
}
} catch (Exception e) {
throw new IllegalArgumentException("Failed to access @JsonValue of Enum value "+en+": "+e.getMessage());
}
}
return new EnumResolver(enumCls, enumConstants, map,
defaultEnum, isIgnoreCase,
// 26-Sep-2021, tatu: [databind#1850] Need to consider "from int" case
_isIntType(accessor.getRawType()),
true,
null, false
);
}
@SuppressWarnings("unchecked")
protected static Class<Enum<?>> _enumClass(Class<?> cls) {
return (Class<Enum<?>>) cls;
}
protected static Enum<?>[] _enumConstants(Class<Enum<?>> enumCls) {
final Enum<?>[] ecs = enumCls.getEnumConstants();
if (ecs == null) {
throw new IllegalArgumentException("No enum constants for class "+enumCls.getName());
}
return ecs;
}
public CompactStringObjectMap constructLookup() {
return CompactStringObjectMap.construct(_enumsById);
}
/**
* Internal helper method used to resolve {@link com.fasterxml.jackson.annotation.JsonEnumDefaultValue}
*/
protected static Enum<?> _enumDefault(MapperConfig<?> config,
AnnotatedClass annotatedClass, Enum<?>[] enums) {
final AnnotationIntrospector intr = config.getAnnotationIntrospector();
return (intr != null) ? intr.findDefaultEnumValue(config, annotatedClass, enums) : null;
}
protected static boolean _isIntType(Class<?> erasedType) {
if (erasedType.isPrimitive()) {
erasedType = ClassUtil.wrapperType(erasedType);
}
return (erasedType == Long.class)
|| (erasedType == Integer.class)
|| (erasedType == Short.class)
|| (erasedType == Byte.class)
;
}
/*
/**********************************************************************
/* Public API
/**********************************************************************
*/
public Enum<?> findEnum(String key) {
Enum<?> en = _enumsById.get(key);
if (en == null) {
if (_isIgnoreCase) {
return _findEnumIgnoreCase(key);
}
}
return en;
}
private final Enum<?> _findEnumIgnoreCase(final String key) {
// potential hot-spot, do not use streams:
for (Map.Entry<String, Enum<?>> entry : _enumsById.entrySet()) {
if (entry.getKey().equalsIgnoreCase(key)) {
return entry.getValue();
}
}
return null;
}
public Enum<?> getEnum(int index) {
if (index < 0 || index >= _enums.length) {
return null;
}
return _enums[index];
}
public Enum<?> getDefaultValue() {
return _defaultValue;
}
public Enum<?>[] getRawEnums() {
return _enums;
}
public List<Enum<?>> getEnums() {
ArrayList<Enum<?>> enums = new ArrayList<Enum<?>>(_enums.length);
for (Enum<?> e : _enums) {
enums.add(e);
}
return enums;
}
public Collection<String> getEnumIds() {
return _enumsById.keySet();
}
public Class<Enum<?>> getEnumClass() { return _enumClass; }
public int lastValidIndex() { return _enums.length-1; }
/**
* @since 3.2
*/
public Map<Integer, Enum<?>> getNumericIndexLookup() {
return _enumsByNumericIndex;
}
/**
* Accessor for checking whether numeric input should use numeric-index lookup
* derived from {@code @JsonProperty} values.
*
* @since 3.2
*/
public boolean useNumericIndexForNumbers() {
return _useNumericIndexForNumbers;
}
/**
* Accessor for checking if we have a special case in which value to map
* is from {@code @JsonValue} annotated accessor with integral type: this
* matters for cases where incoming content value is of integral type
* and should be mapped to specific value and NOT to {@code Enum.index()}.
*/
public boolean isFromIntValue() {
return _isFromIntValue;
}
/**
* Accessor for checking whether {@code @JsonValue} annotated accessor is used
* to get enum values to use for deserialization.
*/
public boolean hasAsValueAnnotation() {
return _hasAsValueAnnotation;
}
}