Skip to content

Commit

Permalink
Handle Enum introspection of values and aliases via `AnnotatedClass…
Browse files Browse the repository at this point in the history
…` instead of `Class<?>` (#3832)
  • Loading branch information
JooHyukKim authored Jun 16, 2023
1 parent ef63502 commit 2134584
Show file tree
Hide file tree
Showing 10 changed files with 349 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -1103,6 +1103,26 @@ public String[] findEnumValues(Class<?> enumType, Enum<?>[] enumValues, String[]
return names;
}

/**
* Finds the explicitly defined name of the given set of {@code Enum} values, if any.
* The method overwrites entries in the incoming {@code names} array with the explicit
* names found, if any, leaving other entries unmodified.
*
* @param config the mapper configuration to use
* @param enumValues the set of {@code Enum} values to find the explicit names for
* @param names the matching declared names of enumeration values (with indexes matching
* {@code enumValues} entries)
* @param annotatedClass the annotated class for which to find the explicit names
*
* @return an array of names to use (possibly {@code names} passed as argument)
*
* @since 2.16
*/
public String[] findEnumValues(MapperConfig<?> config, Enum<?>[] enumValues, String[] names,
AnnotatedClass annotatedClass){
return names;
}

/**
* Method that is related to {@link #findEnumValues} but is called to check if
* there are alternative names (aliased) that can be accepted for entries, in
Expand All @@ -1121,6 +1141,25 @@ public void findEnumAliases(Class<?> enumType, Enum<?>[] enumValues, String[][]
;
}


/**
* Method that is called to check if there are alternative names (aliases) that can be accepted for entries
* in addition to primary names that were introspected earlier, related to {@link #findEnumValues}.
* These aliases should be returned in {@code String[][] aliases} passed in as argument.
* The {@code aliases.length} is expected to match the number of {@code Enum} values.
*
* @param config The configuration of the mapper
* @param enumValues The values of the enumeration
* @param aliases (in/out) Pre-allocated array where aliases found, if any, may be added (in indexes
* matching those of {@code enumValues})
* @param annotatedClass The annotated class of the enumeration type
*
* @since 2.16
*/
public void findEnumAliases(MapperConfig<?> config, Enum<?>[] enumValues, String[][] aliases,
AnnotatedClass annotatedClass) {
return;
}
/**
* Finds the Enum value that should be considered the default value, if possible.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2428,9 +2428,7 @@ protected EnumResolver constructEnumResolver(Class<?> enumClass,
}
return EnumResolver.constructUsingMethod(config, enumClass, jvAcc);
}
// 14-Mar-2016, tatu: We used to check `DeserializationFeature.READ_ENUMS_USING_TO_STRING`
// here, but that won't do: it must be dynamically changeable...
return EnumResolver.constructFor(config, enumClass);
return EnumResolver.constructFor(config, beanDesc.getClassInfo());
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ private void _addFieldMixIns(Class<?> mixInCls, Class<?> targetClass,

private boolean _isIncludableField(Field f)
{
// [databind#2787]: Allow `Enum` mixins
if (f.isEnumConstant()) {
return true;
}
// Most likely synthetic fields, if any, are to be skipped similar to methods
if (f.isSynthetic()) {
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -625,13 +625,28 @@ public String[] findEnumValues(Class<?> enumType, Enum<?>[] enumValues, String[
return names;
}

@Override
public String[] findEnumValues(MapperConfig<?> config, Enum<?>[] enumValues, String[] names,
AnnotatedClass annotatedClass) {
names = _secondary.findEnumValues(config, enumValues, names, annotatedClass);
names = _primary.findEnumValues(config, enumValues, names, annotatedClass);
return names;
}

@Override
public void findEnumAliases(Class<?> enumType, Enum<?>[] enumValues, String[][] aliases) {
// reverse order to give _primary higher precedence
_secondary.findEnumAliases(enumType, enumValues, aliases);
_primary.findEnumAliases(enumType, enumValues, aliases);
}

@Override
public void findEnumAliases(MapperConfig<?> config, Enum<?>[] enumConstants, String[][] aliases,
AnnotatedClass annotatedClass) {
_secondary.findEnumAliases(config, enumConstants, aliases, annotatedClass);
_primary.findEnumAliases(config, enumConstants, aliases, annotatedClass);
}

@Override
public Enum<?> findDefaultEnumValue(Class<Enum<?>> enumCls) {
Enum<?> en = _primary.findDefaultEnumValue(enumCls);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,31 @@ public String[] findEnumValues(Class<?> enumType, Enum<?>[] enumValues, String[]
return names;
}

@Override // since 2.16
public String[] findEnumValues(MapperConfig<?> config, Enum<?>[] enumValues, String[] names,
AnnotatedClass annotatedClass) {
Map<String, String> enumToPropertyMap = new LinkedHashMap<String, String>();
for (AnnotatedField field : annotatedClass.fields()) {
JsonProperty property = field.getAnnotation(JsonProperty.class);
if (property != null) {
String propValue = property.value();
if (propValue != null && !propValue.isEmpty()) {
enumToPropertyMap.put(field.getName(), propValue);
}
}
}

// and then stitch them together if and as necessary
for (int i = 0, end = enumValues.length; i < end; ++i) {
String defName = enumValues[i].name();
String explValue = enumToPropertyMap.get(defName);
if (explValue != null) {
names[i] = explValue;
}
}
return names;
}

@Override // since 2.11
public void findEnumAliases(Class<?> enumType, Enum<?>[] enumValues, String[][] aliasList)
{
Expand All @@ -264,6 +289,23 @@ public void findEnumAliases(Class<?> enumType, Enum<?>[] enumValues, String[][]
}
}

@Override
public void findEnumAliases(MapperConfig<?> config, Enum<?>[] enumValues, String[][] aliasList, AnnotatedClass annotatedClass)
{
HashMap<String, String[]> enumToAliasMap = new HashMap<>();
for (AnnotatedField field : annotatedClass.fields()) {
JsonAlias alias = field.getAnnotation(JsonAlias.class);
if (alias != null) {
enumToAliasMap.putIfAbsent(field.getName(), alias.value());
}
}

for (int i = 0, end = enumValues.length; i < end; ++i) {
Enum<?> enumValue = enumValues[i];
aliasList[i] = enumToAliasMap.getOrDefault(enumValue.name(), new String[]{});
}
}

/**
* Finds the Enum value that should be considered the default value, if possible.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,10 @@ protected JsonSerializer<?> buildEnumSerializer(SerializationConfig config,
if (format.getShape() == JsonFormat.Shape.OBJECT) {
// one special case: suppress serialization of "getDeclaringClass()"...
((BasicBeanDescription) beanDesc).removeProperty("declaringClass");
// [databind#2787]: remove self-referencing enum fields introduced by annotation flattening of mixins
if (type.isEnumType()){
_removeEnumSelfReferences(((BasicBeanDescription) beanDesc));
}
// returning null will mean that eventually BeanSerializer gets constructed
return null;
}
Expand All @@ -1224,6 +1228,30 @@ protected JsonSerializer<?> buildEnumSerializer(SerializationConfig config,
return ser;
}

/**
* Helper method used for serialization {@link Enum} as {@link JsonFormat.Shape#OBJECT}. Removes any
* self-referencing properties from its bean description before it is transformed into a JSON Object
* as configured by {@link JsonFormat.Shape#OBJECT}.
* <p>
* Internally, this method iterates through {@link BeanDescription#findProperties()} and removes self.
*
* @param beanDesc the bean description to remove Enum properties from.
*
* @since 2.16
*/
private void _removeEnumSelfReferences(BasicBeanDescription beanDesc) {
Class<?> aClass = ClassUtil.findEnumType(beanDesc.getBeanClass());
Iterator<BeanPropertyDefinition> it = beanDesc.findProperties().iterator();
while (it.hasNext()) {
BeanPropertyDefinition property = it.next();
JavaType propType = property.getPrimaryType();
// is the property a self-reference?
if (propType.isEnumType() && propType.isTypeOrSubTypeOf(aClass)) {
it.remove();
}
}
}

/*
/**********************************************************
/* Other helper methods
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.fasterxml.jackson.databind.EnumNamingStrategy;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;

/**
* Helper class used to resolve String values (either JSON Object field
Expand Down Expand Up @@ -76,6 +77,7 @@ protected EnumResolver(Class<Enum<?>> enumClass, Enum<?>[] enums,
* Enum value.
*
* @since 2.12
* @deprecated Since 2.16 use {@link #constructFor(DeserializationConfig, AnnotatedClass)} instead
*/
public static EnumResolver constructFor(DeserializationConfig config,
Class<?> enumCls) {
Expand All @@ -84,6 +86,7 @@ public static EnumResolver constructFor(DeserializationConfig config,

/**
* @since 2.15
* @deprecated Since 2.16 use {@link #_constructFor(DeserializationConfig, AnnotatedClass)} instead
*/
protected static EnumResolver _constructFor(DeserializationConfig config, Class<?> enumCls0)
{
Expand Down Expand Up @@ -115,6 +118,60 @@ protected static EnumResolver _constructFor(DeserializationConfig config, Class<
false);
}

/**
* 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}
*
* @since 2.16
*/
public static EnumResolver constructFor(DeserializationConfig config, AnnotatedClass annotatedClass) {
return _constructFor(config, annotatedClass);
}

/**
* Internal method for {@link #_constructFor(DeserializationConfig, AnnotatedClass)}.
*
* @since 2.16
*/
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(enumCls0);

// introspect
String[] names = ai.findEnumValues(config, enumConstants, new String[enumConstants.length], annotatedClass);
final String[][] allAliases = new String[names.length][];
ai.findEnumAliases(config, enumConstants, allAliases, annotatedClass);

// finally, build
HashMap<String, Enum<?>> map = new HashMap<String, Enum<?>>();
for (int i = 0, len = enumConstants.length; i < len; ++i) {
final Enum<?> enumValue = enumConstants[i];
String name = names[i];
if (name == null) {
name = enumValue.name();
}
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,
_enumDefault(ai, enumCls), isIgnoreCase, false);
}

/**
* Factory method for constructing resolver that maps from Enum.toString() into
* Enum value
Expand Down
Loading

0 comments on commit 2134584

Please sign in to comment.