Skip to content

Commit 2134584

Browse files
authored
Handle Enum introspection of values and aliases via AnnotatedClass instead of Class<?> (#3832)
1 parent ef63502 commit 2134584

File tree

10 files changed

+349
-45
lines changed

10 files changed

+349
-45
lines changed

src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,6 +1103,26 @@ public String[] findEnumValues(Class<?> enumType, Enum<?>[] enumValues, String[]
11031103
return names;
11041104
}
11051105

1106+
/**
1107+
* Finds the explicitly defined name of the given set of {@code Enum} values, if any.
1108+
* The method overwrites entries in the incoming {@code names} array with the explicit
1109+
* names found, if any, leaving other entries unmodified.
1110+
*
1111+
* @param config the mapper configuration to use
1112+
* @param enumValues the set of {@code Enum} values to find the explicit names for
1113+
* @param names the matching declared names of enumeration values (with indexes matching
1114+
* {@code enumValues} entries)
1115+
* @param annotatedClass the annotated class for which to find the explicit names
1116+
*
1117+
* @return an array of names to use (possibly {@code names} passed as argument)
1118+
*
1119+
* @since 2.16
1120+
*/
1121+
public String[] findEnumValues(MapperConfig<?> config, Enum<?>[] enumValues, String[] names,
1122+
AnnotatedClass annotatedClass){
1123+
return names;
1124+
}
1125+
11061126
/**
11071127
* Method that is related to {@link #findEnumValues} but is called to check if
11081128
* there are alternative names (aliased) that can be accepted for entries, in
@@ -1121,6 +1141,25 @@ public void findEnumAliases(Class<?> enumType, Enum<?>[] enumValues, String[][]
11211141
;
11221142
}
11231143

1144+
1145+
/**
1146+
* Method that is called to check if there are alternative names (aliases) that can be accepted for entries
1147+
* in addition to primary names that were introspected earlier, related to {@link #findEnumValues}.
1148+
* These aliases should be returned in {@code String[][] aliases} passed in as argument.
1149+
* The {@code aliases.length} is expected to match the number of {@code Enum} values.
1150+
*
1151+
* @param config The configuration of the mapper
1152+
* @param enumValues The values of the enumeration
1153+
* @param aliases (in/out) Pre-allocated array where aliases found, if any, may be added (in indexes
1154+
* matching those of {@code enumValues})
1155+
* @param annotatedClass The annotated class of the enumeration type
1156+
*
1157+
* @since 2.16
1158+
*/
1159+
public void findEnumAliases(MapperConfig<?> config, Enum<?>[] enumValues, String[][] aliases,
1160+
AnnotatedClass annotatedClass) {
1161+
return;
1162+
}
11241163
/**
11251164
* Finds the Enum value that should be considered the default value, if possible.
11261165
*

src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2428,9 +2428,7 @@ protected EnumResolver constructEnumResolver(Class<?> enumClass,
24282428
}
24292429
return EnumResolver.constructUsingMethod(config, enumClass, jvAcc);
24302430
}
2431-
// 14-Mar-2016, tatu: We used to check `DeserializationFeature.READ_ENUMS_USING_TO_STRING`
2432-
// here, but that won't do: it must be dynamically changeable...
2433-
return EnumResolver.constructFor(config, enumClass);
2431+
return EnumResolver.constructFor(config, beanDesc.getClassInfo());
24342432
}
24352433

24362434
/**

src/main/java/com/fasterxml/jackson/databind/introspect/AnnotatedFieldCollector.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,10 @@ private void _addFieldMixIns(Class<?> mixInCls, Class<?> targetClass,
124124

125125
private boolean _isIncludableField(Field f)
126126
{
127+
// [databind#2787]: Allow `Enum` mixins
128+
if (f.isEnumConstant()) {
129+
return true;
130+
}
127131
// Most likely synthetic fields, if any, are to be skipped similar to methods
128132
if (f.isSynthetic()) {
129133
return false;

src/main/java/com/fasterxml/jackson/databind/introspect/AnnotationIntrospectorPair.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,13 +625,28 @@ public String[] findEnumValues(Class<?> enumType, Enum<?>[] enumValues, String[
625625
return names;
626626
}
627627

628+
@Override
629+
public String[] findEnumValues(MapperConfig<?> config, Enum<?>[] enumValues, String[] names,
630+
AnnotatedClass annotatedClass) {
631+
names = _secondary.findEnumValues(config, enumValues, names, annotatedClass);
632+
names = _primary.findEnumValues(config, enumValues, names, annotatedClass);
633+
return names;
634+
}
635+
628636
@Override
629637
public void findEnumAliases(Class<?> enumType, Enum<?>[] enumValues, String[][] aliases) {
630638
// reverse order to give _primary higher precedence
631639
_secondary.findEnumAliases(enumType, enumValues, aliases);
632640
_primary.findEnumAliases(enumType, enumValues, aliases);
633641
}
634642

643+
@Override
644+
public void findEnumAliases(MapperConfig<?> config, Enum<?>[] enumConstants, String[][] aliases,
645+
AnnotatedClass annotatedClass) {
646+
_secondary.findEnumAliases(config, enumConstants, aliases, annotatedClass);
647+
_primary.findEnumAliases(config, enumConstants, aliases, annotatedClass);
648+
}
649+
635650
@Override
636651
public Enum<?> findDefaultEnumValue(Class<Enum<?>> enumCls) {
637652
Enum<?> en = _primary.findDefaultEnumValue(enumCls);

src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,31 @@ public String[] findEnumValues(Class<?> enumType, Enum<?>[] enumValues, String[]
240240
return names;
241241
}
242242

243+
@Override // since 2.16
244+
public String[] findEnumValues(MapperConfig<?> config, Enum<?>[] enumValues, String[] names,
245+
AnnotatedClass annotatedClass) {
246+
Map<String, String> enumToPropertyMap = new LinkedHashMap<String, String>();
247+
for (AnnotatedField field : annotatedClass.fields()) {
248+
JsonProperty property = field.getAnnotation(JsonProperty.class);
249+
if (property != null) {
250+
String propValue = property.value();
251+
if (propValue != null && !propValue.isEmpty()) {
252+
enumToPropertyMap.put(field.getName(), propValue);
253+
}
254+
}
255+
}
256+
257+
// and then stitch them together if and as necessary
258+
for (int i = 0, end = enumValues.length; i < end; ++i) {
259+
String defName = enumValues[i].name();
260+
String explValue = enumToPropertyMap.get(defName);
261+
if (explValue != null) {
262+
names[i] = explValue;
263+
}
264+
}
265+
return names;
266+
}
267+
243268
@Override // since 2.11
244269
public void findEnumAliases(Class<?> enumType, Enum<?>[] enumValues, String[][] aliasList)
245270
{
@@ -264,6 +289,23 @@ public void findEnumAliases(Class<?> enumType, Enum<?>[] enumValues, String[][]
264289
}
265290
}
266291

292+
@Override
293+
public void findEnumAliases(MapperConfig<?> config, Enum<?>[] enumValues, String[][] aliasList, AnnotatedClass annotatedClass)
294+
{
295+
HashMap<String, String[]> enumToAliasMap = new HashMap<>();
296+
for (AnnotatedField field : annotatedClass.fields()) {
297+
JsonAlias alias = field.getAnnotation(JsonAlias.class);
298+
if (alias != null) {
299+
enumToAliasMap.putIfAbsent(field.getName(), alias.value());
300+
}
301+
}
302+
303+
for (int i = 0, end = enumValues.length; i < end; ++i) {
304+
Enum<?> enumValue = enumValues[i];
305+
aliasList[i] = enumToAliasMap.getOrDefault(enumValue.name(), new String[]{});
306+
}
307+
}
308+
267309
/**
268310
* Finds the Enum value that should be considered the default value, if possible.
269311
* <p>

src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,10 @@ protected JsonSerializer<?> buildEnumSerializer(SerializationConfig config,
12091209
if (format.getShape() == JsonFormat.Shape.OBJECT) {
12101210
// one special case: suppress serialization of "getDeclaringClass()"...
12111211
((BasicBeanDescription) beanDesc).removeProperty("declaringClass");
1212+
// [databind#2787]: remove self-referencing enum fields introduced by annotation flattening of mixins
1213+
if (type.isEnumType()){
1214+
_removeEnumSelfReferences(((BasicBeanDescription) beanDesc));
1215+
}
12121216
// returning null will mean that eventually BeanSerializer gets constructed
12131217
return null;
12141218
}
@@ -1224,6 +1228,30 @@ protected JsonSerializer<?> buildEnumSerializer(SerializationConfig config,
12241228
return ser;
12251229
}
12261230

1231+
/**
1232+
* Helper method used for serialization {@link Enum} as {@link JsonFormat.Shape#OBJECT}. Removes any
1233+
* self-referencing properties from its bean description before it is transformed into a JSON Object
1234+
* as configured by {@link JsonFormat.Shape#OBJECT}.
1235+
* <p>
1236+
* Internally, this method iterates through {@link BeanDescription#findProperties()} and removes self.
1237+
*
1238+
* @param beanDesc the bean description to remove Enum properties from.
1239+
*
1240+
* @since 2.16
1241+
*/
1242+
private void _removeEnumSelfReferences(BasicBeanDescription beanDesc) {
1243+
Class<?> aClass = ClassUtil.findEnumType(beanDesc.getBeanClass());
1244+
Iterator<BeanPropertyDefinition> it = beanDesc.findProperties().iterator();
1245+
while (it.hasNext()) {
1246+
BeanPropertyDefinition property = it.next();
1247+
JavaType propType = property.getPrimaryType();
1248+
// is the property a self-reference?
1249+
if (propType.isEnumType() && propType.isTypeOrSubTypeOf(aClass)) {
1250+
it.remove();
1251+
}
1252+
}
1253+
}
1254+
12271255
/*
12281256
/**********************************************************
12291257
/* Other helper methods

src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.fasterxml.jackson.databind.EnumNamingStrategy;
99
import com.fasterxml.jackson.databind.cfg.MapperConfig;
1010
import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
11+
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
1112

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

8587
/**
8688
* @since 2.15
89+
* @deprecated Since 2.16 use {@link #_constructFor(DeserializationConfig, AnnotatedClass)} instead
8790
*/
8891
protected static EnumResolver _constructFor(DeserializationConfig config, Class<?> enumCls0)
8992
{
@@ -115,6 +118,60 @@ protected static EnumResolver _constructFor(DeserializationConfig config, Class<
115118
false);
116119
}
117120

121+
/**
122+
* Factory method for constructing an {@link EnumResolver} based on the given {@link DeserializationConfig} and
123+
* {@link AnnotatedClass} of the enum to be resolved.
124+
*
125+
* @param config the deserialization configuration to use
126+
* @param annotatedClass the annotated class of the enum to be resolved
127+
* @return the constructed {@link EnumResolver}
128+
*
129+
* @since 2.16
130+
*/
131+
public static EnumResolver constructFor(DeserializationConfig config, AnnotatedClass annotatedClass) {
132+
return _constructFor(config, annotatedClass);
133+
}
134+
135+
/**
136+
* Internal method for {@link #_constructFor(DeserializationConfig, AnnotatedClass)}.
137+
*
138+
* @since 2.16
139+
*/
140+
public static EnumResolver _constructFor(DeserializationConfig config, AnnotatedClass annotatedClass)
141+
{
142+
// prepare data
143+
final AnnotationIntrospector ai = config.getAnnotationIntrospector();
144+
final boolean isIgnoreCase = config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
145+
final Class<?> enumCls0 = annotatedClass.getRawType();
146+
final Class<Enum<?>> enumCls = _enumClass(enumCls0);
147+
final Enum<?>[] enumConstants = _enumConstants(enumCls0);
148+
149+
// introspect
150+
String[] names = ai.findEnumValues(config, enumConstants, new String[enumConstants.length], annotatedClass);
151+
final String[][] allAliases = new String[names.length][];
152+
ai.findEnumAliases(config, enumConstants, allAliases, annotatedClass);
153+
154+
// finally, build
155+
HashMap<String, Enum<?>> map = new HashMap<String, Enum<?>>();
156+
for (int i = 0, len = enumConstants.length; i < len; ++i) {
157+
final Enum<?> enumValue = enumConstants[i];
158+
String name = names[i];
159+
if (name == null) {
160+
name = enumValue.name();
161+
}
162+
map.put(name, enumValue);
163+
String[] aliases = allAliases[i];
164+
if (aliases != null) {
165+
for (String alias : aliases) {
166+
// Avoid overriding any primary names
167+
map.putIfAbsent(alias, enumValue);
168+
}
169+
}
170+
}
171+
return new EnumResolver(enumCls, enumConstants, map,
172+
_enumDefault(ai, enumCls), isIgnoreCase, false);
173+
}
174+
118175
/**
119176
* Factory method for constructing resolver that maps from Enum.toString() into
120177
* Enum value

0 commit comments

Comments
 (0)