From 5460b9bca71d8706730760d81045b93e63af262d Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Mon, 20 Feb 2023 23:34:10 +0900 Subject: [PATCH 01/14] initial commit --- .../databind/EnumNamingStrategies.java | 63 +++++++++++ .../jackson/databind/EnumNamingStrategy.java | 7 ++ .../databind/annotation/EnumNaming.java | 15 +++ .../databind/deser/std/EnumDeserializer.java | 66 +++++++++++ .../introspect/EnumNamingStrategiesTest.java | 103 ++++++++++++++++++ .../databind/introspect/EnumNamingTest.java | 64 +++++++++++ 6 files changed, 318 insertions(+) create mode 100644 src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java create mode 100644 src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategy.java create mode 100644 src/main/java/com/fasterxml/jackson/databind/annotation/EnumNaming.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java diff --git a/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java b/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java new file mode 100644 index 0000000000..9038d560a5 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java @@ -0,0 +1,63 @@ +package com.fasterxml.jackson.databind; + +public class EnumNamingStrategies { + + public static final EnumNamingStrategy CAMEL_CASE = CamelCaseStrategy.INSTANCE; + + /** + * no-op naming. Does nothing + */ + public static class NoOpEnumNamingStrategy implements EnumNamingStrategy { + + @Override + public String translate(String value) { + return value; + } + + } + + /** + *

+ * Used when external value is in conventional CamelCase. Examples are "numberValue", "namingStrategy", "theDefiniteProof". + * First underscore prefix will always be removed. + */ + public static class CamelCaseStrategy implements EnumNamingStrategy { + + /** + * @since 2.15 + */ + public final static CamelCaseStrategy INSTANCE + = new CamelCaseStrategy(); + + @Override + public String translate(String input) { + if (input == null) { + return input; + } + + int length = input.length(); + StringBuilder result = new StringBuilder(length * 2); + int resultLength = 0; + boolean wasPrevTranslated = false; + for (int i = 0; i < length; i++) { + char c = input.charAt(i); + if (i > 0 || c != '_') { + if (Character.isUpperCase(c)) { + if (!wasPrevTranslated && resultLength > 0 && result.charAt(resultLength - 1) != '_') { + result.append('_'); + resultLength++; + } + c = Character.toLowerCase(c); + wasPrevTranslated = true; + } else { + wasPrevTranslated = false; + } + result.append(c); + resultLength++; + } + } + String output = resultLength > 0 ? result.toString() : input; + return output.toUpperCase(); + } + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategy.java b/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategy.java new file mode 100644 index 0000000000..ad4b05b6c0 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategy.java @@ -0,0 +1,7 @@ +package com.fasterxml.jackson.databind; + +public interface EnumNamingStrategy { + + public String translate(String value); + +} diff --git a/src/main/java/com/fasterxml/jackson/databind/annotation/EnumNaming.java b/src/main/java/com/fasterxml/jackson/databind/annotation/EnumNaming.java new file mode 100644 index 0000000000..8576fabf0f --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/annotation/EnumNaming.java @@ -0,0 +1,15 @@ +package com.fasterxml.jackson.databind.annotation; + +import com.fasterxml.jackson.databind.EnumNamingStrategy; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@com.fasterxml.jackson.annotation.JacksonAnnotation +public @interface EnumNaming { + public Class value() default EnumNamingStrategy.class; +} diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java index 841be4416b..a8ad1bcc92 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java @@ -1,6 +1,7 @@ package com.fasterxml.jackson.databind.deser.std; import java.io.IOException; +import java.util.Arrays; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonFormat; @@ -8,6 +9,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.EnumNaming; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; @@ -65,6 +67,32 @@ public class EnumDeserializer */ protected final boolean _isFromIntValue; + protected Boolean _useEnumNaming; + + protected volatile EnumNamingStrategy _namingStrategy; + + /** + * Marker flag for cases where we expect actual integral value for Enum, + * based on {@code @JsonValue} (and equivalent) annotated accessor. + * + * @since 2.15 + */ + protected boolean _isEnumNamingSet; + + /** + * @since 2.15 + */ + protected EnumNamingStrategy _namingStrategy = new EnumNamingStrategies.NoOpEnumNamingStrategy(); + + + private void _initEnumNamingStrategy() { + EnumNaming enumNamingAnnotation = _valueClass.getAnnotation(EnumNaming.class); + if (enumNamingAnnotation != null) { + _isEnumNamingSet = true; + ClassUtil.createInstance(enumNamingAnnotation.value(), true); + } + } + /** * @since 2.9 */ @@ -76,6 +104,7 @@ public EnumDeserializer(EnumResolver byNameResolver, Boolean caseInsensitive) _enumDefaultValue = byNameResolver.getDefaultValue(); _caseInsensitive = caseInsensitive; _isFromIntValue = byNameResolver.isFromIntValue(); + _initEnumNamingStrategy(); } /** @@ -92,6 +121,7 @@ protected EnumDeserializer(EnumDeserializer base, Boolean caseInsensitive, _isFromIntValue = base._isFromIntValue; _useDefaultValueForUnknownEnum = useDefaultValueForUnknownEnum; _useNullForUnknownEnum = useNullForUnknownEnum; + _initEnumNamingStrategy(); } /** @@ -254,6 +284,12 @@ protected Object _fromString(JsonParser p, DeserializationContext ctxt, CompactStringObjectMap lookup = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING) ? _getToStringLookup(ctxt) : _lookupByName; Object result = lookup.find(text); + + if (result == null && _getUseEnumNaming()) { + String translatedText = _getNamingStrategy().translate(text); + result = lookup.find(translatedText); + } + if (result == null) { String trimmed = text.trim(); if ((trimmed == text) || (result = lookup.find(trimmed)) == null) { @@ -317,6 +353,12 @@ private final Object _deserializeAltString(JsonParser p, DeserializationContext CompactStringObjectMap lookup, String nameOrig) throws IOException { String name = nameOrig.trim(); + + if (_isEnumNamingSet) { + String translatedValue = _namingStrategy.translate(name); + return lookup.find(translatedValue); + } + if (name.isEmpty()) { // empty or blank // 07-Jun-2021, tatu: [databind#3171] Need to consider Default value first // (alas there's bit of duplication here) @@ -413,6 +455,30 @@ protected CompactStringObjectMap _getToStringLookup(DeserializationContext ctxt) return lookup; } + protected Boolean _getUseEnumNaming() { + if (_useEnumNaming == null) { + _useEnumNaming = _valueClass.getAnnotation(EnumNaming.class) != null; + } + return _useEnumNaming; + } + + protected EnumNamingStrategy _getNamingStrategy() { + EnumNamingStrategy namingStrategy = _namingStrategy; + if (namingStrategy == null) { + synchronized (this) { + namingStrategy = _namingStrategy; + if (namingStrategy == null) { + EnumNaming enumNamingAnnotation = _valueClass.getAnnotation(EnumNaming.class); + namingStrategy = enumNamingAnnotation == null + ? new EnumNamingStrategies.NoOpEnumNamingStrategy() + : ClassUtil.createInstance(enumNamingAnnotation.value(), true); + _namingStrategy = namingStrategy; + } + } + } + return namingStrategy; + } + // @since 2.15 protected boolean useNullForUnknownEnum(DeserializationContext ctxt) { return Boolean.TRUE.equals(_useNullForUnknownEnum) diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java new file mode 100644 index 0000000000..a022f8b23b --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java @@ -0,0 +1,103 @@ +package com.fasterxml.jackson.databind.introspect; + +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.EnumNamingStrategies; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyNamingStrategy; + +import java.util.Arrays; +import java.util.List; + +/** + * Unit tests to verify functioning of standard {@link PropertyNamingStrategy} + * implementations Jackson includes out of the box. + */ +public class EnumNamingStrategiesTest extends BaseMapTest { + + /* + /********************************************************** + /* Set Up + /********************************************************** + */ + + private final ObjectMapper VANILLA_MAPPER = newJsonMapper(); + + /* + /********************************************************** + /* Snake Case test + /********************************************************** + */ + + final static List UPPER_SNAKE_CASE_NAME_TRANSLATIONS = Arrays.asList(new Object[][]{ + {null, null}, + {"", ""}, + {"a", "A"}, + {"abc", "ABC"}, + {"1", "1"}, + {"123", "123"}, + {"1a", "1A"}, + {"a1", "A1"}, + {"$", "$"}, + {"$a", "$A"}, + {"a$", "A$"}, + {"$_a", "$_A"}, + {"a_$", "A_$"}, + {"a$a", "A$A"}, + {"$A", "$_A"}, + {"$_A", "$_A"}, + {"_", "_"}, + {"__", "_"}, + {"___", "__"}, + {"A", "A"}, + {"A1", "A1"}, + {"1A", "1_A"}, + {"_a", "A"}, + {"_A", "A"}, + {"a_a", "A_A"}, + {"a_A", "A_A"}, + {"A_A", "A_A"}, + {"A_a", "A_A"}, + {"WWW", "WWW"}, + {"someURI", "SOME_URI"}, + {"someURIs", "SOME_URIS"}, + {"Results", "RESULTS"}, + {"_Results", "RESULTS"}, + {"_results", "RESULTS"}, + {"__results", "_RESULTS"}, + {"__Results", "_RESULTS"}, + {"___results", "__RESULTS"}, + {"___Results", "__RESULTS"}, + {"userName", "USER_NAME"}, + {"user_name", "USER_NAME"}, + {"user__name", "USER__NAME"}, + {"UserName", "USER_NAME"}, + {"User_Name", "USER_NAME"}, + {"User__Name", "USER__NAME"}, + {"_user_name", "USER_NAME"}, + {"_UserName", "USER_NAME"}, + {"_User_Name", "USER_NAME"}, + {"USER_NAME", "USER_NAME"}, + {"_Bars", "BARS"}, + {"usId", "US_ID"}, + {"uId", "U_ID"}, + {"xCoordinate", "X_COORDINATE"}, + }); + + /** + * Unit test to verify translations of + * {@link EnumNamingStrategies#CAMEL_CASE} + * outside the context of an ObjectMapper. + */ + public void testSnakeCaseStrategyStandAlone() { + for (Object[] pair : UPPER_SNAKE_CASE_NAME_TRANSLATIONS) { + final String input = (String) pair[0]; + final String expected = (String) pair[1]; + + String actual = EnumNamingStrategies.CamelCaseStrategy.INSTANCE + .translate(input); + + assertEquals(expected, actual); + } + } + +} diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java new file mode 100644 index 0000000000..0b1e367531 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java @@ -0,0 +1,64 @@ +package com.fasterxml.jackson.databind.introspect; + +import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; +import com.fasterxml.jackson.databind.BaseMapTest; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.EnumNamingStrategies; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.annotation.EnumNaming; + +public class EnumNamingTest extends BaseMapTest { + + /* + /********************************************************** + /* Set Up + /********************************************************** + */ + + final ObjectMapper MAPPER = new ObjectMapper(); + + /* + /********************************************************** + /* Test + /********************************************************** + */ + + @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) + static enum EnumFlavorA { + PEANUT_BUTTER, + SALTED_CARAMEL, + @JsonEnumDefaultValue + VANILLA; + } + + public void testEnumNamingWithLowerCamelCaseStrategy() throws Exception { + EnumFlavorA result = MAPPER.readValue(q("saltedCaramel"), EnumFlavorA.class); + assertEquals(EnumFlavorA.SALTED_CARAMEL, result); + } + + + public void testEnumNamingToDefaultUnknownValue() throws Exception { + EnumFlavorA result = MAPPER.reader() + .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .readValue(q("0.12321"), EnumFlavorA.class); + + assertEquals(EnumFlavorA.VANILLA, result); + } + + public void testEnumNamingToDefaultNumber() throws Exception { + EnumFlavorA result = MAPPER.reader() + .without(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS) + .readValue(q("1"), EnumFlavorA.class); + + assertEquals(EnumFlavorA.SALTED_CARAMEL, result); + } + + public void testEnumNamingToDefaultEmptyString() throws Exception { + EnumFlavorA result = MAPPER.reader() + .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .readValue(q(""), EnumFlavorA.class); + + assertEquals(EnumFlavorA.VANILLA, result); + } + +} From 1d336f3fbe1cb30c9c3650e0a25a38a21a217cab Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Fri, 24 Feb 2023 21:19:19 +0900 Subject: [PATCH 02/14] Apply changes after first review --- .../databind/AnnotationIntrospector.java | 15 ++ .../databind/EnumNamingStrategies.java | 144 ++++++++++---- .../jackson/databind/EnumNamingStrategy.java | 18 +- .../databind/annotation/EnumNaming.java | 17 +- .../databind/deser/std/EnumDeserializer.java | 1 - .../introspect/EnumPropertiesCollector.java | 104 +++++++++++ .../JacksonAnnotationIntrospector.java | 6 + .../databind/ser/std/EnumSerializer.java | 66 +++++++ .../jackson/databind/util/EnumResolver.java | 42 ++++- .../jackson/databind/util/EnumValues.java | 20 ++ .../introspect/EnumNamingStrategiesTest.java | 176 ++++++++++-------- .../databind/introspect/EnumNamingTest.java | 139 +++++++++++++- 12 files changed, 622 insertions(+), 126 deletions(-) create mode 100644 src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java diff --git a/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java b/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java index 0dd972ff1c..6392a778cf 100644 --- a/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java +++ b/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java @@ -382,6 +382,21 @@ public JsonIncludeProperties.Value findPropertyInclusionByName(MapperConfig c */ public Object findNamingStrategy(AnnotatedClass ac) { return null; } + /** + * Method for finding {@link EnumNamingStrategy} implenetation class for given + * class, if any specified by annotations; and if so, either return + * a {@link EnumNamingStrategy} instance, or Class to use for + * creating instance + * + * @param ac Annotated class to introspect + * + * @return Subclass of {@link EnumNamingStrategy}, if one + * is specified for given class; null if not. + * + * @since 2.15 + */ + public Object findEnumNamingStrategy(AnnotatedClass ac) { return null; } + /** * Method used to check whether specified class defines a human-readable * description to use for documentation. diff --git a/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java b/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java index 9038d560a5..03be4860c9 100644 --- a/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java +++ b/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java @@ -1,63 +1,129 @@ package com.fasterxml.jackson.databind; +/** + * A container class for implementations of the {@link EnumNamingStrategy} interface. + * + * @since 2.15 + */ public class EnumNamingStrategies { - public static final EnumNamingStrategy CAMEL_CASE = CamelCaseStrategy.INSTANCE; - - /** - * no-op naming. Does nothing - */ - public static class NoOpEnumNamingStrategy implements EnumNamingStrategy { - - @Override - public String translate(String value) { - return value; - } - - } + private EnumNamingStrategies() {} /** *

- * Used when external value is in conventional CamelCase. Examples are "numberValue", "namingStrategy", "theDefiniteProof". - * First underscore prefix will always be removed. + * An implementation of {@link EnumNamingStrategy} that converts enum names in the typical upper + * snake case format to camel case format. This implementation follows three rules + * described below. + * + *

    + *
  1. converts any character preceded by an underscore into upper case character, + * regardless of its original case (upper or lower).
  2. + *
  3. converts any character NOT preceded by an underscore into a lower case character, + * regardless of its original case (upper or lower).
  4. + *
  5. converts contiguous sequence of underscores into a single underscore.
  6. + *
+ * + * WARNING: Naming conversion conflicts caused by underscore usage should be handled by client. + * e.g. Both PEANUT_BUTTER, PEANUT__BUTTER are converted into "peanutButter". + * And "peanutButter" will be deserialized into enum with smaller Enum.ordinal() value. + * + *

+ * These rules result in the following example conversions from upper snakecase names + * to camelcase names. + *

+ * + * @since 2.15 */ public static class CamelCaseStrategy implements EnumNamingStrategy { /** + * An intance of {@link CamelCaseStrategy} for reuse. + * * @since 2.15 */ - public final static CamelCaseStrategy INSTANCE - = new CamelCaseStrategy(); + public static final CamelCaseStrategy INSTANCE = new CamelCaseStrategy(); + /** + * @since 2.15 + */ @Override - public String translate(String input) { - if (input == null) { - return input; + public String convertEnumToExternalName(String enumName) { + if (enumName == null) { + return null; } - int length = input.length(); - StringBuilder result = new StringBuilder(length * 2); - int resultLength = 0; - boolean wasPrevTranslated = false; - for (int i = 0; i < length; i++) { - char c = input.charAt(i); - if (i > 0 || c != '_') { - if (Character.isUpperCase(c)) { - if (!wasPrevTranslated && resultLength > 0 && result.charAt(resultLength - 1) != '_') { - result.append('_'); - resultLength++; - } - c = Character.toLowerCase(c); - wasPrevTranslated = true; + final String UNDERSCORE = "_"; + StringBuilder out = null; + int iterationCnt = 0; + int lastSeparatorIdx = -1; + + do { + lastSeparatorIdx = indexIn(enumName, lastSeparatorIdx + 1); + if (lastSeparatorIdx != -1) { + if (iterationCnt == 0) { + out = new StringBuilder(enumName.length() + 4 * UNDERSCORE.length()); + out.append(toLowerCase(enumName.substring(iterationCnt, lastSeparatorIdx))); } else { - wasPrevTranslated = false; + out.append(normalizeWord(enumName.substring(iterationCnt, lastSeparatorIdx))); } - result.append(c); - resultLength++; + iterationCnt = lastSeparatorIdx + UNDERSCORE.length(); + } + } while (lastSeparatorIdx != -1); + + if (iterationCnt == 0) { + return toLowerCase(enumName); + } + out.append(normalizeWord(enumName.substring(iterationCnt))); + return out.toString(); + } + + private static int indexIn(CharSequence sequence, int start) { + int length = sequence.length(); + for (int i = start; i < length; i++) { + if ('_' == sequence.charAt(i)) { + return i; } } - String output = resultLength > 0 ? result.toString() : input; - return output.toUpperCase(); + return -1; + } + + private static String normalizeWord(String word) { + int length = word.length(); + if (length == 0) { + return word; + } + return new StringBuilder(length) + .append(charToUpperCaseIfLower(word.charAt(0))) + .append(toLowerCase(word.substring(1))) + .toString(); + } + + private static String toLowerCase(String string) { + int length = string.length(); + StringBuilder builder = new StringBuilder(length); + for (int i = 0; i < length; i++) { + builder.append(charToLowerCaseIfUpper(string.charAt(i))); + } + return builder.toString(); + } + + private static char charToUpperCaseIfLower(char c) { + return Character.isLowerCase(c) ? Character.toUpperCase(c) : c; + } + + private static char charToLowerCaseIfUpper(char c) { + return Character.isUpperCase(c) ? Character.toLowerCase(c) : c; } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategy.java b/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategy.java index ad4b05b6c0..9504d4f080 100644 --- a/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategy.java +++ b/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategy.java @@ -1,7 +1,23 @@ package com.fasterxml.jackson.databind; +/** + * Defines how the string representation of an enum is converted into an external property name for mapping + * during deserialization. + * + * @since 2.15 + */ public interface EnumNamingStrategy { - public String translate(String value); + /** + * Translates the given enumName into an external property name according to + * the implementation of this {@link EnumNamingStrategy}. + * + * @param enumName the name of the enum value to translate + * @return the external property name that corresponds to the given enumName + * according to the implementation of this {@link EnumNamingStrategy}. + * + * @since 2.15 + */ + public String convertEnumToExternalName(String enumName); } diff --git a/src/main/java/com/fasterxml/jackson/databind/annotation/EnumNaming.java b/src/main/java/com/fasterxml/jackson/databind/annotation/EnumNaming.java index 8576fabf0f..d3a49d2770 100644 --- a/src/main/java/com/fasterxml/jackson/databind/annotation/EnumNaming.java +++ b/src/main/java/com/fasterxml/jackson/databind/annotation/EnumNaming.java @@ -7,9 +7,24 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotation that can be used to indicate a {@link EnumNamingStrategy} + * to use for annotated class. + * + * @since 2.15 + */ @Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @com.fasterxml.jackson.annotation.JacksonAnnotation public @interface EnumNaming { - public Class value() default EnumNamingStrategy.class; + + /** + * @return Type of {@link EnumNamingStrategy} to use, if any. Default value + * of EnumNamingStrategy.class means "no strategy specified" + * (and may also be used for overriding to remove otherwise applicable + * naming strategy) + * + * @since 2.15 + */ + public Class value(); } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java index a8ad1bcc92..044880452b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java @@ -9,7 +9,6 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.annotation.EnumNaming; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java new file mode 100644 index 0000000000..48f6e2fe34 --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java @@ -0,0 +1,104 @@ +package com.fasterxml.jackson.databind.introspect; + +import com.fasterxml.jackson.databind.AnnotationIntrospector; +import com.fasterxml.jackson.databind.EnumNamingStrategy; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.annotation.EnumNaming; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.util.ClassUtil; + +/** + * Helper class used for aggregating information about all possible + * properties of a Enum. + * + * @since 2.15 + */ +public class EnumPropertiesCollector { + private EnumPropertiesCollector() {} + + /* + /********************************************************** + /* Public API: main-level collection + /********************************************************** + */ + + /** + * Helper method to resolve the an {@link EnumNamingStrategy} for the current {@link MapperConfig} + * using {@link AnnotationIntrospector}. + * + * @return An instance of a subtype of {@link EnumNamingStrategy} specified through + * {@link EnumNaming#value()} or null if current Enum class is not annotated with {@link EnumNaming} or + * {@link EnumNaming#value()} is set to {@link EnumNamingStrategy} interface itself. + * @since 2.15 + */ + public static EnumNamingStrategy findEnumNamingStrategy(MapperConfig config, Class handledType) { + AnnotatedClass classDef = _findAnnotatedClass(config, handledType); + Object namingDef = config.getAnnotationIntrospector().findEnumNamingStrategy(classDef); + return _findEnumNamingStrategy(namingDef, config.canOverrideAccessModifiers()); + + } + + /* + /********************************************************** + * Actual Implementation + ********************************************************** + */ + + /** + * @since 2.15 + */ + private static EnumNamingStrategy _findEnumNamingStrategy(Object namingDef, boolean canOverrideAccessModifiers) { + if (namingDef == null) { + return null; + } + + if (namingDef instanceof EnumNamingStrategy) { + return (EnumNamingStrategy) namingDef; + } + // Alas, there's no way to force return type of "either class + // X or Y" -- need to throw an exception after the fact + if (!(namingDef instanceof Class)) { + reportProblem("AnnotationIntrospector returned EnumNamingStrategy definition of type %s" + + "; expected type `Class` instead", ClassUtil.classNameOf(namingDef)); + } + + Class namingClass = (Class) namingDef; + // 09-Nov-2015, tatu: Need to consider pseudo-value of STD, which means "use default" + if (namingClass == EnumNamingStrategy.class) { + return null; + } + + if (!EnumNamingStrategy.class.isAssignableFrom(namingClass)) { + reportProblem("AnnotationIntrospector returned Class %s; expected `Class`", + ClassUtil.classNameOf(namingClass)); + } + + return (EnumNamingStrategy) ClassUtil.createInstance(namingClass, canOverrideAccessModifiers); + } + + /* + ********************************************************* + * Internal methods; helpers + ********************************************************** + */ + + /** + * @since 2.15 + */ + protected static AnnotatedClass _findAnnotatedClass(MapperConfig ctxt, Class handledType) { + JavaType javaType = ctxt.constructType(handledType); + return AnnotatedClassResolver.resolve(ctxt, javaType, ctxt); + } + + /** + * @since 2.15 + */ + protected static void reportProblem(String msg, Object... args) { + if (args.length > 0) { + msg = String.format(msg, args); + } + throw new IllegalArgumentException("Problem with " + msg); + } + + +} diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java index d13836794a..eefbc9ad36 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/JacksonAnnotationIntrospector.java @@ -349,6 +349,12 @@ public Object findNamingStrategy(AnnotatedClass ac) return (ann == null) ? null : ann.value(); } + @Override + public Object findEnumNamingStrategy(AnnotatedClass ac) { + EnumNaming ann = _findAnnotation(ac, EnumNaming.class); + return (ann == null) ? null : ann.value(); + } + @Override public String findClassDescription(AnnotatedClass ac) { JsonClassDescription ann = _findAnnotation(ac, JsonClassDescription.class); diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java index b8b8ebcf7a..a48117d8d3 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; +import com.fasterxml.jackson.databind.introspect.EnumPropertiesCollector; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -49,6 +50,22 @@ public class EnumSerializer */ protected final Boolean _serializeAsIndex; + /** + * Marker flag for deciding whether to search for {@link com.fasterxml.jackson.databind.annotation.EnumNaming} + * annotation. Value starts as null to indicate verification has not been performed yet. + * + * @since 2.15 + */ + private volatile Boolean _hasEnumNaming = null; + + /** + * Map with key as converted property class defined implementation of {@link EnumNamingStrategy} + * and with value as Enum names collected using Enum.name(). + * + * @since 2.15 + */ + protected volatile EnumValues _valuesByEnumNaming; + /* /********************************************************** /* Construction, initialization @@ -103,6 +120,50 @@ public JsonSerializer createContextual(SerializerProvider serializers, return this; } + /** + * Checks wheather current Enum class is annotated with + * {@link com.fasterxml.jackson.databind.annotation.EnumNaming} + * + * @since 2.15 + */ + private boolean _hasEnumNaming(SerializationConfig ctxt) { + Boolean exists = _hasEnumNaming; + if (exists == null) { + synchronized (this) { + exists = _hasEnumNaming; + if (exists == null) { + exists = EnumPropertiesCollector.findEnumNamingStrategy(ctxt, _handledType) != null; + _hasEnumNaming = exists; + } + } + } + return exists; + } + + + /** + * Returns {@link EnumValues} to use for enum name lookup of naming strategy. + * + * @since 2.15 + */ + protected EnumValues _getEnumNamingValues(SerializationConfig config) { + EnumValues lookup = _valuesByEnumNaming; + if (lookup == null) { + synchronized (this) { + lookup = _valuesByEnumNaming; + if (lookup == null) { + EnumNamingStrategy namingStrategy = EnumPropertiesCollector + .findEnumNamingStrategy(config, _handledType); + if (namingStrategy != null) { + lookup = EnumValues.constructUsingEnumNaming(config, _handledType, namingStrategy); + _valuesByEnumNaming = lookup; + } + } + } + } + return lookup; + } + /* /********************************************************** /* Extended API for Jackson databind core @@ -121,6 +182,11 @@ public JsonSerializer createContextual(SerializerProvider serializers, public final void serialize(Enum en, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (_hasEnumNaming(serializers.getConfig())) { + EnumValues enumValues = _getEnumNamingValues(serializers.getConfig()); + gen.writeString(enumValues.serializedValueFor(en)); + return; + } if (_serializeAsIndex(serializers)) { gen.writeNumber(en.ordinal()); return; diff --git a/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java b/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java index 73bfef58e0..4a4f3e35d9 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.EnumNamingStrategy; import com.fasterxml.jackson.databind.introspect.AnnotatedMember; /** @@ -113,8 +114,7 @@ protected static EnumResolver _constructFor(Class enumCls0, /** * Factory method for constructing resolver that maps from Enum.toString() into - * Enum value - * + * Enum value * * @since 2.12 */ public static EnumResolver constructUsingToString(DeserializationConfig config, @@ -148,7 +148,43 @@ private static EnumResolver _constructUsingIndex(Class> enumCls0, Annota _enumDefault(ai, enumCls), isIgnoreCase, false); } - /** + /** + * Factory method for constructing resolver that maps the name of enums converted to external property + * names into Enum value using an implementation of {@link EnumNamingStrategy}. + * + * @since 2.15 + */ + public static EnumResolver constructUsingEnumNamingStrategy(DeserializationConfig config, + Class enumCls, EnumNamingStrategy enumNamingStrategy) { + return _constructUsingEnumNamingStrategy(enumCls, config.getAnnotationIntrospector(), + config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS), enumNamingStrategy); + } + + /** + * Internal method for + * {@link EnumResolver#_constructUsingEnumNamingStrategy(Class, AnnotationIntrospector, boolean, EnumNamingStrategy)} + * + * @since 2.15 + */ + private static EnumResolver _constructUsingEnumNamingStrategy( + Class enumCls0, AnnotationIntrospector ai, boolean isIgnoreCase, EnumNamingStrategy enumNamingStrategy) { + + final Class> enumCls = _enumClass(enumCls0); + final Enum[] enumConstants = _enumConstants(enumCls0); + HashMap> map = new HashMap<>(); + + // from last to first, so that in case of duplicate values, first wins + for (int i = enumConstants.length; --i >= 0; ) { + Enum anEnum = enumConstants[i]; + String translatedExternalValue = enumNamingStrategy.convertEnumToExternalName(anEnum.name()); + map.put(translatedExternalValue, anEnum); + } + + return new EnumResolver(enumCls, enumConstants, map, + _enumDefault(ai, enumCls), isIgnoreCase, false); + } + + /** * @since 2.12 */ protected static EnumResolver _constructUsingToString(Class enumCls0, diff --git a/src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java b/src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java index 1cf7a276c2..5986c3f5f0 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java @@ -79,6 +79,26 @@ public static EnumValues constructFromToString(MapperConfig config, Class config, Class> enumClass, EnumNamingStrategy namingStrategy) { + Class> cls = ClassUtil.findEnumType(enumClass); + Enum[] values = cls.getEnumConstants(); + if (values == null) { + throw new IllegalArgumentException("Cannot determine enum constants for Class " + enumClass.getName()); + } + ArrayList external = new ArrayList<>(values.length); + for (Enum en : values) { + external.add(namingStrategy.convertEnumToExternalName(en.name())); + } + return construct(config, enumClass, external); + } + /** * @since 2.11 */ diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java index a022f8b23b..dd491e68e7 100644 --- a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java @@ -2,102 +2,130 @@ import com.fasterxml.jackson.databind.BaseMapTest; import com.fasterxml.jackson.databind.EnumNamingStrategies; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.PropertyNamingStrategy; import java.util.Arrays; import java.util.List; /** - * Unit tests to verify functioning of standard {@link PropertyNamingStrategy} + * Test case to verify functioning of standard + * {@link com.fasterxml.jackson.databind.EnumNamingStrategy} * implementations Jackson includes out of the box. + * + * @since 2.15 */ public class EnumNamingStrategiesTest extends BaseMapTest { - /* - /********************************************************** - /* Set Up - /********************************************************** + /** + * Test casess for {@link com.fasterxml.jackson.databind.EnumNamingStrategies.CamelCaseStrategy}. + * + *

+ * Each Object[] element is composed of {input, expectedOutput}. + * + * @since 2.15 */ + final static List CAMEL_CASE_NAME_TRANSLATIONS = Arrays.asList(new String[][]{ + // Empty values + {null, null}, + {"", ""}, - private final ObjectMapper VANILLA_MAPPER = newJsonMapper(); + // input values with no underscores + {"a", "a"}, + {"abc", "abc"}, + {"A", "a"}, + {"A1", "a1"}, + {"1A", "1a"}, + {"ABC", "abc"}, + {"User", "user"}, + {"Results", "results"}, + {"WWW", "www"}, + {"USER", "user"}, + {"userName", "username"}, + {"someURI", "someuri"}, + {"someURIs", "someuris"}, + {"theWWW", "thewww"}, + {"uId", "uid"}, + {"usId", "usid"}, + {"UserName", "username"}, + {"user", "user"}, + {"xCoordinate", "xcoordinate"}, - /* - /********************************************************** - /* Snake Case test - /********************************************************** - */ + // input values with single underscores + {"a_", "a"}, + {"_A", "A"}, + {"_a", "A"}, + {"a_A", "aA"}, + {"a_a", "aA"}, + {"A_A", "aA"}, + {"A_a", "aA"}, + {"BARS_", "bars"}, + {"BARS", "bars"}, + {"THE_WWW", "theWww"}, + {"U_ID", "uId"}, + {"US_ID", "usId"}, + {"X_COORDINATE", "xCoordinate"}, - final static List UPPER_SNAKE_CASE_NAME_TRANSLATIONS = Arrays.asList(new Object[][]{ - {null, null}, - {"", ""}, - {"a", "A"}, - {"abc", "ABC"}, - {"1", "1"}, - {"123", "123"}, - {"1a", "1A"}, - {"a1", "A1"}, + // heavy "username" example + {"USERNAME_", "username"}, + {"_User_Name", "UserName"}, + {"_UserName", "Username"}, + {"_Username", "Username"}, + {"_user_name", "UserName"}, + {"_USERNAME", "Username"}, + {"__USERNAME", "Username"}, + {"__Username", "Username"}, + {"__username", "Username"}, + {"USER______NAME", "userName"}, + {"USER_NAME", "userName"}, + {"USER__NAME", "userName"}, + {"USER_NAME_", "userName"}, + {"User__Name", "userName"}, + {"USER_NAME_S", "userNameS"}, + {"_user_name_s", "UserNameS"}, + {"USER_NAME_S", "userNameS"}, + {"user__name", "userName"}, + {"user_name", "userName"}, + {"USERNAME", "username"}, + {"username", "username"}, + {"User_Name", "userName"}, + {"User_Name_", "userName"}, + {"User_Name_", "userName"}, + {"User_Name__", "userName"}, + {"user_name_", "userName"}, + {"user_name__", "userName"}, + + // additional variations + {"a$a", "a$a"}, + {"A$A", "a$a"}, + {"a_$", "a$"}, + {"a$", "a$"}, + {"a1", "a1"}, {"$", "$"}, - {"$a", "$A"}, - {"a$", "A$"}, - {"$_a", "$_A"}, - {"a_$", "A_$"}, - {"a$a", "A$A"}, - {"$A", "$_A"}, - {"$_A", "$_A"}, - {"_", "_"}, - {"__", "_"}, - {"___", "__"}, - {"A", "A"}, - {"A1", "A1"}, - {"1A", "1_A"}, - {"_a", "A"}, - {"_A", "A"}, - {"a_a", "A_A"}, - {"a_A", "A_A"}, - {"A_A", "A_A"}, - {"A_a", "A_A"}, - {"WWW", "WWW"}, - {"someURI", "SOME_URI"}, - {"someURIs", "SOME_URIS"}, - {"Results", "RESULTS"}, - {"_Results", "RESULTS"}, - {"_results", "RESULTS"}, - {"__results", "_RESULTS"}, - {"__Results", "_RESULTS"}, - {"___results", "__RESULTS"}, - {"___Results", "__RESULTS"}, - {"userName", "USER_NAME"}, - {"user_name", "USER_NAME"}, - {"user__name", "USER__NAME"}, - {"UserName", "USER_NAME"}, - {"User_Name", "USER_NAME"}, - {"User__Name", "USER__NAME"}, - {"_user_name", "USER_NAME"}, - {"_UserName", "USER_NAME"}, - {"_User_Name", "USER_NAME"}, - {"USER_NAME", "USER_NAME"}, - {"_Bars", "BARS"}, - {"usId", "US_ID"}, - {"uId", "U_ID"}, - {"xCoordinate", "X_COORDINATE"}, + {"A$", "a$"}, + {"1", "1"}, + {"$_A", "$A"}, + {"$_a", "$A"}, + {"1_A", "1A"}, + {"1a", "1a"}, + {"A_$", "a$"}, + {"_123_41", "12341"}, }); /** - * Unit test to verify translations of - * {@link EnumNamingStrategies#CAMEL_CASE} - * outside the context of an ObjectMapper. + * Unit test to verify the implementation of + * {@link com.fasterxml.jackson.databind.EnumNamingStrategies.CamelCaseStrategy#convertEnumToExternalName(String)} + * without the context of an ObjectMapper. + * + * @since 2.15 */ - public void testSnakeCaseStrategyStandAlone() { - for (Object[] pair : UPPER_SNAKE_CASE_NAME_TRANSLATIONS) { - final String input = (String) pair[0]; - final String expected = (String) pair[1]; + public void testCamelCaseStrategyStandAlone() { + for (String[] pair : CAMEL_CASE_NAME_TRANSLATIONS) { + final String input = pair[0]; + final String expected = pair[1]; String actual = EnumNamingStrategies.CamelCaseStrategy.INSTANCE - .translate(input); + .convertEnumToExternalName(input); assertEquals(expected, actual); } } - } diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java index 0b1e367531..06c744d60d 100644 --- a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java @@ -1,10 +1,10 @@ package com.fasterxml.jackson.databind.introspect; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; -import com.fasterxml.jackson.databind.BaseMapTest; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.EnumNamingStrategies; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.EnumNaming; public class EnumNamingTest extends BaseMapTest { @@ -34,13 +34,14 @@ static enum EnumFlavorA { public void testEnumNamingWithLowerCamelCaseStrategy() throws Exception { EnumFlavorA result = MAPPER.readValue(q("saltedCaramel"), EnumFlavorA.class); assertEquals(EnumFlavorA.SALTED_CARAMEL, result); - } + String resultString = MAPPER.writeValueAsString(result); + } - public void testEnumNamingToDefaultUnknownValue() throws Exception { + public void testEnumNamingTranslateUnknownValueToDefault() throws Exception { EnumFlavorA result = MAPPER.reader() .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) - .readValue(q("0.12321"), EnumFlavorA.class); + .readValue(q("__salted_caramel"), EnumFlavorA.class); assertEquals(EnumFlavorA.VANILLA, result); } @@ -61,4 +62,128 @@ public void testEnumNamingToDefaultEmptyString() throws Exception { assertEquals(EnumFlavorA.VANILLA, result); } + @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) + static enum EnumFlavorB { + PEANUT_BUTTER, + } + + public void testOriginalEnamValueShouldNotBeFoundWithEnumNamingStrategy() throws Exception { + EnumFlavorB result = MAPPER.reader() + .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL) + .readValue(q("PEANUT_BUTTER"), EnumFlavorB.class); + + assertNull(result); + } + + + @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) + static enum EnumFlavorC { + CHOCOLATE_CHIPS, + HOT_CHEETOS; + + @Override + public String toString() { + return "HOT_CHOCOLATE_CHEETOS_AND_CHIPS"; + } + } + + public void testEnumNamingShouldOverrideToStringFeatue() throws Exception { + String resultStr = MAPPER.writer() + .with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .writeValueAsString(EnumFlavorC.CHOCOLATE_CHIPS); + + assertEquals(q("chocolateChips"), resultStr); + } + + @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) + static enum EnumSauceA { + KETCH_UP, + MAYO_NEZZ; + } + + public void testEnumNamingStrategySymmetryReadThenWrite() throws Exception { + EnumSauceA result = MAPPER.readValue(q("ketchUp"), EnumSauceA.class); + assertEquals(EnumSauceA.KETCH_UP, result); + + String resultString = MAPPER.writeValueAsString(result); + assertEquals(q("ketchUp"), resultString); + } + + public void testEnumNamingStrategySymmetryWriteThenRead() throws Exception { + String resultString = MAPPER.writeValueAsString(EnumSauceA.MAYO_NEZZ); + + EnumSauceA result = MAPPER.readValue(resultString, EnumSauceA.class); + + assertEquals(EnumSauceA.MAYO_NEZZ, result); + } + + + static class EnumFlavorWrapperBean { + public EnumSauceA sauce; + + @JsonCreator + public EnumFlavorWrapperBean(@JsonProperty("sce") EnumSauceA sce) { + this.sauce = sce; + } + } + + public void testReadWrapperValueWithEnumNamingStrategy() throws Exception { + String json = "{\"sauce\": \"ketchUp\"}"; + + EnumFlavorWrapperBean wrapper = MAPPER.readValue(json, EnumFlavorWrapperBean.class); + + assertEquals(EnumSauceA.KETCH_UP, wrapper.sauce); + } + + public void testWriteThenReadWrapperValueWithEnumNamingStrategy() throws Exception { + EnumFlavorWrapperBean sauceWrapper = new EnumFlavorWrapperBean(EnumSauceA.MAYO_NEZZ); + String json = MAPPER.writeValueAsString(sauceWrapper); + + EnumFlavorWrapperBean wrapper = MAPPER.readValue(json, EnumFlavorWrapperBean.class); + + assertEquals(EnumSauceA.MAYO_NEZZ, wrapper.sauce); + } + + + @EnumNaming(EnumNamingStrategy.class) + static enum EnumSauceB { + BARBEQ_UE, + SRIRACHA_MAYO; + } + + public void testEnumNamingStrategyNotApplied() throws Exception { + String resultString = MAPPER.writeValueAsString(EnumSauceB.SRIRACHA_MAYO); + assertEquals(q("SRIRACHA_MAYO"), resultString); + } + + public void testEnumNamingStrategyInterfaceIsNotApplied() throws Exception { + EnumSauceB sauce = MAPPER.readValue(q("SRIRACHA_MAYO"), EnumSauceB.class); + assertEquals(EnumSauceB.SRIRACHA_MAYO, sauce); + } + + @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) + static enum EnumFlavorD { + _PEANUT_BUTTER, + PEANUT__BUTTER, + PEANUT_BUTTER + } + + public void testEnumNamingStrategyStartingUnderscoreBecomesUpperCase() throws Exception { + String flavor = MAPPER.writeValueAsString(EnumFlavorD._PEANUT_BUTTER); + assertEquals(q("PeanutButter"), flavor); + } + + public void testEnumNamingStrategyNonPrefixContiguousUnderscoresBecomeOne() throws Exception { + String flavor1 = MAPPER.writeValueAsString(EnumFlavorD.PEANUT__BUTTER); + assertEquals(q("peanutButter"), flavor1); + + String flavor2 = MAPPER.writeValueAsString(EnumFlavorD.PEANUT_BUTTER); + assertEquals(q("peanutButter"), flavor2); + } + + public void testEnumNamingStrategyConflictWithUnderScores() throws Exception { + EnumFlavorD flavor = MAPPER.readValue(q("peanutButter"), EnumFlavorD.class); + assertEquals(EnumFlavorD.PEANUT__BUTTER, flavor); + } + } From c67804f88d340351f6b57c4c0e4e6478b753c724 Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Sun, 5 Mar 2023 15:03:55 +0900 Subject: [PATCH 03/14] Fix all conflicts from rebase. --- .../databind/EnumNamingStrategies.java | 6 +- .../databind/deser/std/EnumDeserializer.java | 111 ++++++------ .../introspect/EnumPropertiesCollector.java | 4 +- .../databind/ser/std/EnumSerializer.java | 11 +- .../jackson/databind/util/EnumResolver.java | 3 +- .../introspect/EnumNamingStrategiesTest.java | 162 +++++++++--------- .../databind/introspect/EnumNamingTest.java | 20 +-- 7 files changed, 159 insertions(+), 158 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java b/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java index 03be4860c9..0fe5989354 100644 --- a/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java +++ b/src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java @@ -104,9 +104,9 @@ private static String normalizeWord(String word) { return word; } return new StringBuilder(length) - .append(charToUpperCaseIfLower(word.charAt(0))) - .append(toLowerCase(word.substring(1))) - .toString(); + .append(charToUpperCaseIfLower(word.charAt(0))) + .append(toLowerCase(word.substring(1))) + .toString(); } private static String toLowerCase(String string) { diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java index 044880452b..e2118f2bbd 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java @@ -9,6 +9,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.EnumNaming; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; @@ -16,6 +17,7 @@ import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.deser.ValueInstantiator; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; +import com.fasterxml.jackson.databind.introspect.EnumPropertiesCollector; import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.CompactStringObjectMap; @@ -66,31 +68,21 @@ public class EnumDeserializer */ protected final boolean _isFromIntValue; - protected Boolean _useEnumNaming; - - protected volatile EnumNamingStrategy _namingStrategy; - /** - * Marker flag for cases where we expect actual integral value for Enum, - * based on {@code @JsonValue} (and equivalent) annotated accessor. + * Marker flag for deciding whether to search for {@link com.fasterxml.jackson.databind.annotation.EnumNaming} + * annotation. Value starts as null to indicate verification has not been performed yet. * * @since 2.15 */ - protected boolean _isEnumNamingSet; + private volatile Boolean _hasEnumNaming = null; /** + * Map with key as converted property class defined implementation of {@link EnumNamingStrategy} + * and with value as Enum names collected using Enum.name(). + * * @since 2.15 */ - protected EnumNamingStrategy _namingStrategy = new EnumNamingStrategies.NoOpEnumNamingStrategy(); - - - private void _initEnumNamingStrategy() { - EnumNaming enumNamingAnnotation = _valueClass.getAnnotation(EnumNaming.class); - if (enumNamingAnnotation != null) { - _isEnumNamingSet = true; - ClassUtil.createInstance(enumNamingAnnotation.value(), true); - } - } + protected volatile CompactStringObjectMap _lookupByEnumNaming; /** * @since 2.9 @@ -103,7 +95,6 @@ public EnumDeserializer(EnumResolver byNameResolver, Boolean caseInsensitive) _enumDefaultValue = byNameResolver.getDefaultValue(); _caseInsensitive = caseInsensitive; _isFromIntValue = byNameResolver.isFromIntValue(); - _initEnumNamingStrategy(); } /** @@ -120,7 +111,6 @@ protected EnumDeserializer(EnumDeserializer base, Boolean caseInsensitive, _isFromIntValue = base._isFromIntValue; _useDefaultValueForUnknownEnum = useDefaultValueForUnknownEnum; _useNullForUnknownEnum = useNullForUnknownEnum; - _initEnumNamingStrategy(); } /** @@ -282,13 +272,9 @@ protected Object _fromString(JsonParser p, DeserializationContext ctxt, { CompactStringObjectMap lookup = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING) ? _getToStringLookup(ctxt) : _lookupByName; - Object result = lookup.find(text); - - if (result == null && _getUseEnumNaming()) { - String translatedText = _getNamingStrategy().translate(text); - result = lookup.find(translatedText); - } + lookup = _hasEnumNaming(ctxt) ? _getEnumNamingLookup(ctxt) : lookup; + Object result = lookup.find(text); if (result == null) { String trimmed = text.trim(); if ((trimmed == text) || (result = lookup.find(trimmed)) == null) { @@ -352,12 +338,6 @@ private final Object _deserializeAltString(JsonParser p, DeserializationContext CompactStringObjectMap lookup, String nameOrig) throws IOException { String name = nameOrig.trim(); - - if (_isEnumNamingSet) { - String translatedValue = _namingStrategy.translate(name); - return lookup.find(translatedValue); - } - if (name.isEmpty()) { // empty or blank // 07-Jun-2021, tatu: [databind#3171] Need to consider Default value first // (alas there's bit of duplication here) @@ -454,30 +434,6 @@ protected CompactStringObjectMap _getToStringLookup(DeserializationContext ctxt) return lookup; } - protected Boolean _getUseEnumNaming() { - if (_useEnumNaming == null) { - _useEnumNaming = _valueClass.getAnnotation(EnumNaming.class) != null; - } - return _useEnumNaming; - } - - protected EnumNamingStrategy _getNamingStrategy() { - EnumNamingStrategy namingStrategy = _namingStrategy; - if (namingStrategy == null) { - synchronized (this) { - namingStrategy = _namingStrategy; - if (namingStrategy == null) { - EnumNaming enumNamingAnnotation = _valueClass.getAnnotation(EnumNaming.class); - namingStrategy = enumNamingAnnotation == null - ? new EnumNamingStrategies.NoOpEnumNamingStrategy() - : ClassUtil.createInstance(enumNamingAnnotation.value(), true); - _namingStrategy = namingStrategy; - } - } - } - return namingStrategy; - } - // @since 2.15 protected boolean useNullForUnknownEnum(DeserializationContext ctxt) { return Boolean.TRUE.equals(_useNullForUnknownEnum) @@ -490,4 +446,49 @@ protected boolean useDefaultValueForUnknownEnum(DeserializationContext ctxt) { && (Boolean.TRUE.equals(_useDefaultValueForUnknownEnum) || ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)); } + + /** + * Checks wheather current Enum class is annotated with + * {@link com.fasterxml.jackson.databind.annotation.EnumNaming} + * + * @since 2.15 + */ + private boolean _hasEnumNaming(DeserializationContext ctxt) { + Boolean exists = _hasEnumNaming; + if (exists == null) { + synchronized (this) { + exists = _hasEnumNaming; + if (exists == null) { + exists = _getEnumNamingLookup(ctxt) != null; + _hasEnumNaming = exists; + } + } + } + return exists; + } + + /** + * Returns the CompactStringObjectMap used for enum name lookup of naming strategy. + * + * @since 2.15 + */ + protected CompactStringObjectMap _getEnumNamingLookup(DeserializationContext ctxt) { + CompactStringObjectMap lookup = _lookupByEnumNaming; + if (lookup == null) { + synchronized (this) { + lookup = _lookupByEnumNaming; + if (lookup == null) { + EnumNamingStrategy namingStrategy = EnumPropertiesCollector + .findEnumNamingStrategy(ctxt.getConfig(), _valueClass); + if (namingStrategy != null) { + lookup = EnumResolver + .constructUsingEnumNamingStrategy(ctxt.getConfig(), _enumClass(), namingStrategy) + .constructLookup(); + _lookupByEnumNaming = lookup; + } + } + } + } + return lookup; + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java index 48f6e2fe34..0bfb691032 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java @@ -59,7 +59,7 @@ private static EnumNamingStrategy _findEnumNamingStrategy(Object namingDef, bool // X or Y" -- need to throw an exception after the fact if (!(namingDef instanceof Class)) { reportProblem("AnnotationIntrospector returned EnumNamingStrategy definition of type %s" - + "; expected type `Class` instead", ClassUtil.classNameOf(namingDef)); + + "; expected type `Class` instead", ClassUtil.classNameOf(namingDef)); } Class namingClass = (Class) namingDef; @@ -70,7 +70,7 @@ private static EnumNamingStrategy _findEnumNamingStrategy(Object namingDef, bool if (!EnumNamingStrategy.class.isAssignableFrom(namingClass)) { reportProblem("AnnotationIntrospector returned Class %s; expected `Class`", - ClassUtil.classNameOf(namingClass)); + ClassUtil.classNameOf(namingClass)); } return (EnumNamingStrategy) ClassUtil.createInstance(namingClass, canOverrideAccessModifiers); diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java index a48117d8d3..52daa9bd7e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java @@ -56,7 +56,7 @@ public class EnumSerializer * * @since 2.15 */ - private volatile Boolean _hasEnumNaming = null; + private volatile Boolean _hasEnumNaming; /** * Map with key as converted property class defined implementation of {@link EnumNamingStrategy} @@ -126,13 +126,13 @@ public JsonSerializer createContextual(SerializerProvider serializers, * * @since 2.15 */ - private boolean _hasEnumNaming(SerializationConfig ctxt) { + private boolean _hasEnumNaming(SerializationConfig config) { Boolean exists = _hasEnumNaming; if (exists == null) { synchronized (this) { exists = _hasEnumNaming; if (exists == null) { - exists = EnumPropertiesCollector.findEnumNamingStrategy(ctxt, _handledType) != null; + exists = _getEnumNamingValues(config) != null; _hasEnumNaming = exists; } } @@ -140,7 +140,6 @@ private boolean _hasEnumNaming(SerializationConfig ctxt) { return exists; } - /** * Returns {@link EnumValues} to use for enum name lookup of naming strategy. * @@ -152,8 +151,8 @@ protected EnumValues _getEnumNamingValues(SerializationConfig config) { synchronized (this) { lookup = _valuesByEnumNaming; if (lookup == null) { - EnumNamingStrategy namingStrategy = EnumPropertiesCollector - .findEnumNamingStrategy(config, _handledType); + EnumNamingStrategy namingStrategy = + EnumPropertiesCollector.findEnumNamingStrategy(config, _handledType); if (namingStrategy != null) { lookup = EnumValues.constructUsingEnumNaming(config, _handledType, namingStrategy); _valuesByEnumNaming = lookup; diff --git a/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java b/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java index 4a4f3e35d9..4643c1750f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/EnumResolver.java @@ -114,7 +114,8 @@ protected static EnumResolver _constructFor(Class enumCls0, /** * Factory method for constructing resolver that maps from Enum.toString() into - * Enum value * + * Enum value + * * @since 2.12 */ public static EnumResolver constructUsingToString(DeserializationConfig config, diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java index dd491e68e7..8f1b6060a6 100644 --- a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategiesTest.java @@ -24,90 +24,90 @@ public class EnumNamingStrategiesTest extends BaseMapTest { * @since 2.15 */ final static List CAMEL_CASE_NAME_TRANSLATIONS = Arrays.asList(new String[][]{ - // Empty values - {null, null}, - {"", ""}, + // Empty values + {null, null}, + {"", ""}, - // input values with no underscores - {"a", "a"}, - {"abc", "abc"}, - {"A", "a"}, - {"A1", "a1"}, - {"1A", "1a"}, - {"ABC", "abc"}, - {"User", "user"}, - {"Results", "results"}, - {"WWW", "www"}, - {"USER", "user"}, - {"userName", "username"}, - {"someURI", "someuri"}, - {"someURIs", "someuris"}, - {"theWWW", "thewww"}, - {"uId", "uid"}, - {"usId", "usid"}, - {"UserName", "username"}, - {"user", "user"}, - {"xCoordinate", "xcoordinate"}, + // input values with no underscores + {"a", "a"}, + {"abc", "abc"}, + {"A", "a"}, + {"A1", "a1"}, + {"1A", "1a"}, + {"ABC", "abc"}, + {"User", "user"}, + {"Results", "results"}, + {"WWW", "www"}, + {"USER", "user"}, + {"userName", "username"}, + {"someURI", "someuri"}, + {"someURIs", "someuris"}, + {"theWWW", "thewww"}, + {"uId", "uid"}, + {"usId", "usid"}, + {"UserName", "username"}, + {"user", "user"}, + {"xCoordinate", "xcoordinate"}, - // input values with single underscores - {"a_", "a"}, - {"_A", "A"}, - {"_a", "A"}, - {"a_A", "aA"}, - {"a_a", "aA"}, - {"A_A", "aA"}, - {"A_a", "aA"}, - {"BARS_", "bars"}, - {"BARS", "bars"}, - {"THE_WWW", "theWww"}, - {"U_ID", "uId"}, - {"US_ID", "usId"}, - {"X_COORDINATE", "xCoordinate"}, + // input values with single underscores + {"a_", "a"}, + {"_A", "A"}, + {"_a", "A"}, + {"a_A", "aA"}, + {"a_a", "aA"}, + {"A_A", "aA"}, + {"A_a", "aA"}, + {"BARS_", "bars"}, + {"BARS", "bars"}, + {"THE_WWW", "theWww"}, + {"U_ID", "uId"}, + {"US_ID", "usId"}, + {"X_COORDINATE", "xCoordinate"}, - // heavy "username" example - {"USERNAME_", "username"}, - {"_User_Name", "UserName"}, - {"_UserName", "Username"}, - {"_Username", "Username"}, - {"_user_name", "UserName"}, - {"_USERNAME", "Username"}, - {"__USERNAME", "Username"}, - {"__Username", "Username"}, - {"__username", "Username"}, - {"USER______NAME", "userName"}, - {"USER_NAME", "userName"}, - {"USER__NAME", "userName"}, - {"USER_NAME_", "userName"}, - {"User__Name", "userName"}, - {"USER_NAME_S", "userNameS"}, - {"_user_name_s", "UserNameS"}, - {"USER_NAME_S", "userNameS"}, - {"user__name", "userName"}, - {"user_name", "userName"}, - {"USERNAME", "username"}, - {"username", "username"}, - {"User_Name", "userName"}, - {"User_Name_", "userName"}, - {"User_Name_", "userName"}, - {"User_Name__", "userName"}, - {"user_name_", "userName"}, - {"user_name__", "userName"}, + // heavy "username" example + {"USERNAME_", "username"}, + {"_User_Name", "UserName"}, + {"_UserName", "Username"}, + {"_Username", "Username"}, + {"_user_name", "UserName"}, + {"_USERNAME", "Username"}, + {"__USERNAME", "Username"}, + {"__Username", "Username"}, + {"__username", "Username"}, + {"USER______NAME", "userName"}, + {"USER_NAME", "userName"}, + {"USER__NAME", "userName"}, + {"USER_NAME_", "userName"}, + {"User__Name", "userName"}, + {"USER_NAME_S", "userNameS"}, + {"_user_name_s", "UserNameS"}, + {"USER_NAME_S", "userNameS"}, + {"user__name", "userName"}, + {"user_name", "userName"}, + {"USERNAME", "username"}, + {"username", "username"}, + {"User_Name", "userName"}, + {"User_Name_", "userName"}, + {"User_Name_", "userName"}, + {"User_Name__", "userName"}, + {"user_name_", "userName"}, + {"user_name__", "userName"}, - // additional variations - {"a$a", "a$a"}, - {"A$A", "a$a"}, - {"a_$", "a$"}, - {"a$", "a$"}, - {"a1", "a1"}, - {"$", "$"}, - {"A$", "a$"}, - {"1", "1"}, - {"$_A", "$A"}, - {"$_a", "$A"}, - {"1_A", "1A"}, - {"1a", "1a"}, - {"A_$", "a$"}, - {"_123_41", "12341"}, + // additional variations + {"a$a", "a$a"}, + {"A$A", "a$a"}, + {"a_$", "a$"}, + {"a$", "a$"}, + {"a1", "a1"}, + {"$", "$"}, + {"A$", "a$"}, + {"1", "1"}, + {"$_A", "$A"}, + {"$_a", "$A"}, + {"1_A", "1A"}, + {"1a", "1a"}, + {"A_$", "a$"}, + {"_123_41", "12341"}, }); /** @@ -123,7 +123,7 @@ public void testCamelCaseStrategyStandAlone() { final String expected = pair[1]; String actual = EnumNamingStrategies.CamelCaseStrategy.INSTANCE - .convertEnumToExternalName(input); + .convertEnumToExternalName(input); assertEquals(expected, actual); } diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java index 06c744d60d..9302bb8416 100644 --- a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java @@ -40,24 +40,24 @@ public void testEnumNamingWithLowerCamelCaseStrategy() throws Exception { public void testEnumNamingTranslateUnknownValueToDefault() throws Exception { EnumFlavorA result = MAPPER.reader() - .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) - .readValue(q("__salted_caramel"), EnumFlavorA.class); + .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .readValue(q("__salted_caramel"), EnumFlavorA.class); assertEquals(EnumFlavorA.VANILLA, result); } public void testEnumNamingToDefaultNumber() throws Exception { EnumFlavorA result = MAPPER.reader() - .without(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS) - .readValue(q("1"), EnumFlavorA.class); + .without(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS) + .readValue(q("1"), EnumFlavorA.class); assertEquals(EnumFlavorA.SALTED_CARAMEL, result); } public void testEnumNamingToDefaultEmptyString() throws Exception { EnumFlavorA result = MAPPER.reader() - .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) - .readValue(q(""), EnumFlavorA.class); + .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .readValue(q(""), EnumFlavorA.class); assertEquals(EnumFlavorA.VANILLA, result); } @@ -69,8 +69,8 @@ static enum EnumFlavorB { public void testOriginalEnamValueShouldNotBeFoundWithEnumNamingStrategy() throws Exception { EnumFlavorB result = MAPPER.reader() - .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL) - .readValue(q("PEANUT_BUTTER"), EnumFlavorB.class); + .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL) + .readValue(q("PEANUT_BUTTER"), EnumFlavorB.class); assertNull(result); } @@ -89,8 +89,8 @@ public String toString() { public void testEnumNamingShouldOverrideToStringFeatue() throws Exception { String resultStr = MAPPER.writer() - .with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) - .writeValueAsString(EnumFlavorC.CHOCOLATE_CHIPS); + .with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .writeValueAsString(EnumFlavorC.CHOCOLATE_CHIPS); assertEquals(q("chocolateChips"), resultStr); } From 2e9a4b0c01a88ed5083d02ef736d0d5850b3f779 Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Sat, 11 Mar 2023 22:38:25 +0900 Subject: [PATCH 04/14] Apply review 2023.03.11 --- .../databind/AnnotationIntrospector.java | 4 +- .../deser/BasicDeserializerFactory.java | 23 ++- .../databind/deser/std/EnumDeserializer.java | 80 ++------ .../deser/std/StdKeyDeserializer.java | 23 +++ .../deser/std/StdKeyDeserializers.java | 16 ++ .../introspect/EnumPropertiesCollector.java | 42 +--- .../databind/ser/BasicSerializerFactory.java | 6 +- .../databind/ser/std/EnumSerializer.java | 89 +++----- .../databind/ser/std/StdKeySerializers.java | 61 ++++++ .../jackson/databind/util/EnumValues.java | 2 +- .../enums/EnumNamingDeserializationTest.java | 192 ++++++++++++++++++ .../databind/introspect/EnumNamingTest.java | 189 ----------------- .../ser/jdk/EnumNamingSerializationTest.java | 105 ++++++++++ 13 files changed, 482 insertions(+), 350 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumNamingDeserializationTest.java delete mode 100644 src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/ser/jdk/EnumNamingSerializationTest.java diff --git a/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java b/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java index 6392a778cf..5f37e28cba 100644 --- a/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java +++ b/src/main/java/com/fasterxml/jackson/databind/AnnotationIntrospector.java @@ -383,14 +383,14 @@ public JsonIncludeProperties.Value findPropertyInclusionByName(MapperConfig c public Object findNamingStrategy(AnnotatedClass ac) { return null; } /** - * Method for finding {@link EnumNamingStrategy} implenetation class for given + * Method for finding {@link EnumNamingStrategy} for given * class, if any specified by annotations; and if so, either return * a {@link EnumNamingStrategy} instance, or Class to use for * creating instance * * @param ac Annotated class to introspect * - * @return Subclass of {@link EnumNamingStrategy}, if one + * @return Subclass or instance of {@link EnumNamingStrategy}, if one * is specified for given class; null if not. * * @since 2.15 diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java index a1fbcdc76b..6d80c192f8 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -1706,7 +1706,8 @@ public JsonDeserializer createEnumDeserializer(DeserializationContext ctxt, if (deser == null) { deser = new EnumDeserializer(constructEnumResolver(enumClass, config, beanDesc.findJsonValueAccessor()), - config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS) + config.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS), + constructEnumNamingStrategyResolver(config, enumClass, beanDesc.getClassInfo()) ); } } @@ -1914,6 +1915,7 @@ private KeyDeserializer _createEnumKeyDeserializer(DeserializationContext ctxt, } } EnumResolver enumRes = constructEnumResolver(enumClass, config, beanDesc.findJsonValueAccessor()); + EnumResolver byEnumNamingResolver = constructEnumNamingStrategyResolver(config, enumClass, beanDesc.getClassInfo()); // May have @JsonCreator for static factory method for (AnnotatedMethod factory : beanDesc.getFactoryMethods()) { @@ -1935,7 +1937,7 @@ private KeyDeserializer _createEnumKeyDeserializer(DeserializationContext ctxt, ClassUtil.checkAndFixAccess(factory.getMember(), ctxt.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)); } - return StdKeyDeserializers.constructEnumKeyDeserializer(enumRes, factory); + return StdKeyDeserializers.constructEnumKeyDeserializer(enumRes, factory, byEnumNamingResolver); } } throw new IllegalArgumentException("Unsuitable method ("+factory+") decorated with @JsonCreator (for Enum type " @@ -1943,7 +1945,7 @@ private KeyDeserializer _createEnumKeyDeserializer(DeserializationContext ctxt, } } // Also, need to consider @JsonValue, if one found - return StdKeyDeserializers.constructEnumKeyDeserializer(enumRes); + return StdKeyDeserializers.constructEnumKeyDeserializer(enumRes, byEnumNamingResolver); } /* @@ -2424,6 +2426,21 @@ protected EnumResolver constructEnumResolver(Class enumClass, return EnumResolver.constructFor(config, enumClass); } + /** + * Factory method used to resolve an instance of {@link CompactStringObjectMap} + * with {@link EnumNamingStrategy} applied for the target class. + * + * @since 2.15 + */ + protected EnumResolver constructEnumNamingStrategyResolver(DeserializationConfig config, Class enumClass, + AnnotatedClass annotatedClass) { + Object namingDef = config.getAnnotationIntrospector().findEnumNamingStrategy(annotatedClass); + EnumNamingStrategy enumNamingStrategy = EnumPropertiesCollector.createEnumNamingStrategyInstance( + namingDef, config.canOverrideAccessModifiers()); + return enumNamingStrategy == null ? null + : EnumResolver.constructUsingEnumNamingStrategy(config, enumClass, enumNamingStrategy); + } + /** * @since 2.9 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java index e2118f2bbd..66c47c80aa 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java @@ -1,7 +1,6 @@ package com.fasterxml.jackson.databind.deser.std; import java.io.IOException; -import java.util.Arrays; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonFormat; @@ -9,7 +8,6 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.annotation.EnumNaming; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; @@ -17,7 +15,6 @@ import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.deser.ValueInstantiator; import com.fasterxml.jackson.databind.introspect.AnnotatedMethod; -import com.fasterxml.jackson.databind.introspect.EnumPropertiesCollector; import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.ClassUtil; import com.fasterxml.jackson.databind.util.CompactStringObjectMap; @@ -69,25 +66,33 @@ public class EnumDeserializer protected final boolean _isFromIntValue; /** - * Marker flag for deciding whether to search for {@link com.fasterxml.jackson.databind.annotation.EnumNaming} - * annotation. Value starts as null to indicate verification has not been performed yet. + * Look up map with key as Enum.name() converted by + * {@link EnumNamingStrategy#convertEnumToExternalName(String)} + * and value as Enums. * * @since 2.15 */ - private volatile Boolean _hasEnumNaming = null; + protected final CompactStringObjectMap _lookupByEnumNaming; /** - * Map with key as converted property class defined implementation of {@link EnumNamingStrategy} - * and with value as Enum names collected using Enum.name(). - * - * @since 2.15 + * @since 2.9 */ - protected volatile CompactStringObjectMap _lookupByEnumNaming; + public EnumDeserializer(EnumResolver byNameResolver, Boolean caseInsensitive) + { + super(byNameResolver.getEnumClass()); + _lookupByName = byNameResolver.constructLookup(); + _enumsByIndex = byNameResolver.getRawEnums(); + _enumDefaultValue = byNameResolver.getDefaultValue(); + _caseInsensitive = caseInsensitive; + _isFromIntValue = byNameResolver.isFromIntValue(); + _lookupByEnumNaming = null; + } /** - * @since 2.9 + * @since 2.15 */ - public EnumDeserializer(EnumResolver byNameResolver, Boolean caseInsensitive) + public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive, + EnumResolver byEnumNamingResolver) { super(byNameResolver.getEnumClass()); _lookupByName = byNameResolver.constructLookup(); @@ -95,6 +100,7 @@ public EnumDeserializer(EnumResolver byNameResolver, Boolean caseInsensitive) _enumDefaultValue = byNameResolver.getDefaultValue(); _caseInsensitive = caseInsensitive; _isFromIntValue = byNameResolver.isFromIntValue(); + _lookupByEnumNaming = byEnumNamingResolver == null ? null : byEnumNamingResolver.constructLookup(); } /** @@ -111,6 +117,7 @@ protected EnumDeserializer(EnumDeserializer base, Boolean caseInsensitive, _isFromIntValue = base._isFromIntValue; _useDefaultValueForUnknownEnum = useDefaultValueForUnknownEnum; _useNullForUnknownEnum = useNullForUnknownEnum; + _lookupByEnumNaming = base._lookupByEnumNaming; } /** @@ -272,7 +279,7 @@ protected Object _fromString(JsonParser p, DeserializationContext ctxt, { CompactStringObjectMap lookup = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING) ? _getToStringLookup(ctxt) : _lookupByName; - lookup = _hasEnumNaming(ctxt) ? _getEnumNamingLookup(ctxt) : lookup; + lookup = _lookupByEnumNaming == null ? lookup : _lookupByEnumNaming; Object result = lookup.find(text); if (result == null) { @@ -446,49 +453,4 @@ protected boolean useDefaultValueForUnknownEnum(DeserializationContext ctxt) { && (Boolean.TRUE.equals(_useDefaultValueForUnknownEnum) || ctxt.isEnabled(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)); } - - /** - * Checks wheather current Enum class is annotated with - * {@link com.fasterxml.jackson.databind.annotation.EnumNaming} - * - * @since 2.15 - */ - private boolean _hasEnumNaming(DeserializationContext ctxt) { - Boolean exists = _hasEnumNaming; - if (exists == null) { - synchronized (this) { - exists = _hasEnumNaming; - if (exists == null) { - exists = _getEnumNamingLookup(ctxt) != null; - _hasEnumNaming = exists; - } - } - } - return exists; - } - - /** - * Returns the CompactStringObjectMap used for enum name lookup of naming strategy. - * - * @since 2.15 - */ - protected CompactStringObjectMap _getEnumNamingLookup(DeserializationContext ctxt) { - CompactStringObjectMap lookup = _lookupByEnumNaming; - if (lookup == null) { - synchronized (this) { - lookup = _lookupByEnumNaming; - if (lookup == null) { - EnumNamingStrategy namingStrategy = EnumPropertiesCollector - .findEnumNamingStrategy(ctxt.getConfig(), _valueClass); - if (namingStrategy != null) { - lookup = EnumResolver - .constructUsingEnumNamingStrategy(ctxt.getConfig(), _enumClass(), namingStrategy) - .constructLookup(); - _lookupByEnumNaming = lookup; - } - } - } - } - return lookup; - } } diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializer.java index 19fd71fdb4..df0596d3aa 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializer.java @@ -379,6 +379,15 @@ final static class EnumKD extends StdKeyDeserializer */ protected volatile EnumResolver _byIndexResolver; + /** + * Look up map with key as Enum.name() converted by + * {@link EnumNamingStrategy#convertEnumToExternalName(String)} + * and value as Enums. + * + * @since 2.15 + */ + protected final EnumResolver _byEnumNamingResolver; + protected final Enum _enumDefaultValue; protected EnumKD(EnumResolver er, AnnotatedMethod factory) { @@ -386,6 +395,18 @@ protected EnumKD(EnumResolver er, AnnotatedMethod factory) { _byNameResolver = er; _factory = factory; _enumDefaultValue = er.getDefaultValue(); + _byEnumNamingResolver = null; + } + + /** + * @since 2.15 + */ + protected EnumKD(EnumResolver er, AnnotatedMethod factory, EnumResolver byEnumNamingResolver) { + super(-1, er.getEnumClass()); + _byNameResolver = er; + _factory = factory; + _enumDefaultValue = er.getDefaultValue(); + _byEnumNamingResolver = byEnumNamingResolver; } @Override @@ -400,6 +421,8 @@ public Object _parse(String key, DeserializationContext ctxt) throws IOException } EnumResolver res = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING) ? _getToStringResolver(ctxt) : _byNameResolver; + res = _byEnumNamingResolver == null ? res : _byEnumNamingResolver; + Enum e = res.findEnum(key); // If enum is found, no need to try deser using index if (e == null && ctxt.isEnabled(EnumFeature.READ_ENUM_KEYS_USING_INDEX)) { diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializers.java index 3c19661d64..6824feb714 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializers.java @@ -44,6 +44,22 @@ public static KeyDeserializer constructEnumKeyDeserializer(EnumResolver enumReso return new StdKeyDeserializer.EnumKD(enumResolver, factory); } + /** + * @since 2.15 + */ + public static KeyDeserializer constructEnumKeyDeserializer(EnumResolver enumResolver, + EnumResolver enumNamingResolver) { + return new StdKeyDeserializer.EnumKD(enumResolver, null, enumNamingResolver); + } + + /** + * @since 2.15 + */ + public static KeyDeserializer constructEnumKeyDeserializer(EnumResolver enumResolver, + AnnotatedMethod factory, EnumResolver enumNamingResolver) { + return new StdKeyDeserializer.EnumKD(enumResolver, factory, enumNamingResolver); + } + public static KeyDeserializer constructDelegatingKeyDeserializer(DeserializationConfig config, JavaType type, JsonDeserializer deser) { diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java index 0bfb691032..f097ec669d 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java @@ -1,10 +1,6 @@ package com.fasterxml.jackson.databind.introspect; -import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.EnumNamingStrategy; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.annotation.EnumNaming; -import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.util.ClassUtil; /** @@ -14,6 +10,7 @@ * @since 2.15 */ public class EnumPropertiesCollector { + private EnumPropertiesCollector() {} /* @@ -22,32 +19,10 @@ private EnumPropertiesCollector() {} /********************************************************** */ - /** - * Helper method to resolve the an {@link EnumNamingStrategy} for the current {@link MapperConfig} - * using {@link AnnotationIntrospector}. - * - * @return An instance of a subtype of {@link EnumNamingStrategy} specified through - * {@link EnumNaming#value()} or null if current Enum class is not annotated with {@link EnumNaming} or - * {@link EnumNaming#value()} is set to {@link EnumNamingStrategy} interface itself. - * @since 2.15 - */ - public static EnumNamingStrategy findEnumNamingStrategy(MapperConfig config, Class handledType) { - AnnotatedClass classDef = _findAnnotatedClass(config, handledType); - Object namingDef = config.getAnnotationIntrospector().findEnumNamingStrategy(classDef); - return _findEnumNamingStrategy(namingDef, config.canOverrideAccessModifiers()); - - } - - /* - /********************************************************** - * Actual Implementation - ********************************************************** - */ - /** * @since 2.15 */ - private static EnumNamingStrategy _findEnumNamingStrategy(Object namingDef, boolean canOverrideAccessModifiers) { + public static EnumNamingStrategy createEnumNamingStrategyInstance(Object namingDef, boolean canOverrideAccessModifiers) { if (namingDef == null) { return null; } @@ -59,7 +34,7 @@ private static EnumNamingStrategy _findEnumNamingStrategy(Object namingDef, bool // X or Y" -- need to throw an exception after the fact if (!(namingDef instanceof Class)) { reportProblem("AnnotationIntrospector returned EnumNamingStrategy definition of type %s" - + "; expected type `Class` instead", ClassUtil.classNameOf(namingDef)); + + "; expected type `Class` instead", ClassUtil.classNameOf(namingDef)); } Class namingClass = (Class) namingDef; @@ -70,7 +45,7 @@ private static EnumNamingStrategy _findEnumNamingStrategy(Object namingDef, bool if (!EnumNamingStrategy.class.isAssignableFrom(namingClass)) { reportProblem("AnnotationIntrospector returned Class %s; expected `Class`", - ClassUtil.classNameOf(namingClass)); + ClassUtil.classNameOf(namingClass)); } return (EnumNamingStrategy) ClassUtil.createInstance(namingClass, canOverrideAccessModifiers); @@ -82,14 +57,6 @@ private static EnumNamingStrategy _findEnumNamingStrategy(Object namingDef, bool ********************************************************** */ - /** - * @since 2.15 - */ - protected static AnnotatedClass _findAnnotatedClass(MapperConfig ctxt, Class handledType) { - JavaType javaType = ctxt.constructType(handledType); - return AnnotatedClassResolver.resolve(ctxt, javaType, ctxt); - } - /** * @since 2.15 */ @@ -100,5 +67,4 @@ protected static void reportProblem(String msg, Object... args) { throw new IllegalArgumentException("Problem with " + msg); } - } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java index 594f3150b7..ede8d57360 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/BasicSerializerFactory.java @@ -246,7 +246,8 @@ public JsonSerializer createKeySerializer(SerializerProvider ctxt, // null -> no TypeSerializer for key-serializer use case ser = new JsonValueSerializer(acc, null, delegate); } else { - ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass()); + ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass(), + beanDesc.getClassInfo()); } } } @@ -283,7 +284,8 @@ public JsonSerializer createKeySerializer(SerializationConfig config, if (ser == null) { ser = StdKeySerializers.getStdKeySerializer(config, keyType.getRawClass(), false); if (ser == null) { - ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass()); + ser = StdKeySerializers.getFallbackKeySerializer(config, keyType.getRawClass(), + beanDesc.getClassInfo()); } } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java index 52daa9bd7e..5c37669f61 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java @@ -13,6 +13,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; import com.fasterxml.jackson.databind.introspect.EnumPropertiesCollector; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; @@ -50,21 +51,13 @@ public class EnumSerializer */ protected final Boolean _serializeAsIndex; - /** - * Marker flag for deciding whether to search for {@link com.fasterxml.jackson.databind.annotation.EnumNaming} - * annotation. Value starts as null to indicate verification has not been performed yet. - * - * @since 2.15 - */ - private volatile Boolean _hasEnumNaming; - /** * Map with key as converted property class defined implementation of {@link EnumNamingStrategy} * and with value as Enum names collected using Enum.name(). * * @since 2.15 */ - protected volatile EnumValues _valuesByEnumNaming; + protected final EnumValues _valuesByEnumNaming; /* /********************************************************** @@ -77,6 +70,18 @@ public EnumSerializer(EnumValues v, Boolean serializeAsIndex) super(v.getEnumClass(), false); _values = v; _serializeAsIndex = serializeAsIndex; + _valuesByEnumNaming = null; + } + + /** + * @since 2.15 + */ + public EnumSerializer(EnumValues v, Boolean serializeAsIndex, EnumValues valuesByEnumNaming) + { + super(v.getEnumClass(), false); + _values = v; + _serializeAsIndex = serializeAsIndex; + _valuesByEnumNaming = valuesByEnumNaming; } /** @@ -94,8 +99,9 @@ public static EnumSerializer construct(Class enumClass, SerializationConfig c * handle toString() case dynamically (for example) */ EnumValues v = EnumValues.constructFromName(config, (Class>) enumClass); + EnumValues valuesByEnumNaming = constructEnumNamingStrategyValues(config, (Class>) enumClass, beanDesc.getClassInfo()); Boolean serializeAsIndex = _isShapeWrittenUsingIndex(enumClass, format, true, null); - return new EnumSerializer(v, serializeAsIndex); + return new EnumSerializer(v, serializeAsIndex, valuesByEnumNaming); } /** @@ -120,49 +126,6 @@ public JsonSerializer createContextual(SerializerProvider serializers, return this; } - /** - * Checks wheather current Enum class is annotated with - * {@link com.fasterxml.jackson.databind.annotation.EnumNaming} - * - * @since 2.15 - */ - private boolean _hasEnumNaming(SerializationConfig config) { - Boolean exists = _hasEnumNaming; - if (exists == null) { - synchronized (this) { - exists = _hasEnumNaming; - if (exists == null) { - exists = _getEnumNamingValues(config) != null; - _hasEnumNaming = exists; - } - } - } - return exists; - } - - /** - * Returns {@link EnumValues} to use for enum name lookup of naming strategy. - * - * @since 2.15 - */ - protected EnumValues _getEnumNamingValues(SerializationConfig config) { - EnumValues lookup = _valuesByEnumNaming; - if (lookup == null) { - synchronized (this) { - lookup = _valuesByEnumNaming; - if (lookup == null) { - EnumNamingStrategy namingStrategy = - EnumPropertiesCollector.findEnumNamingStrategy(config, _handledType); - if (namingStrategy != null) { - lookup = EnumValues.constructUsingEnumNaming(config, _handledType, namingStrategy); - _valuesByEnumNaming = lookup; - } - } - } - } - return lookup; - } - /* /********************************************************** /* Extended API for Jackson databind core @@ -181,9 +144,8 @@ protected EnumValues _getEnumNamingValues(SerializationConfig config) { public final void serialize(Enum en, JsonGenerator gen, SerializerProvider serializers) throws IOException { - if (_hasEnumNaming(serializers.getConfig())) { - EnumValues enumValues = _getEnumNamingValues(serializers.getConfig()); - gen.writeString(enumValues.serializedValueFor(en)); + if (_valuesByEnumNaming != null) { + gen.writeString(_valuesByEnumNaming.serializedValueFor(en)); return; } if (_serializeAsIndex(serializers)) { @@ -299,4 +261,19 @@ protected static Boolean _isShapeWrittenUsingIndex(Class enumClass, "Unsupported serialization shape (%s) for Enum %s, not supported as %s annotation", shape, enumClass.getName(), (fromClass? "class" : "property"))); } + + /** + * Factory method used to resolve an instance of {@link EnumValues} + * with {@link EnumNamingStrategy} applied for the target class. + * + * @since 2.15 + */ + protected static EnumValues constructEnumNamingStrategyValues(SerializationConfig config, Class> enumClass, + AnnotatedClass annotatedClass) { + Object namingDef = config.getAnnotationIntrospector().findEnumNamingStrategy(annotatedClass); + EnumNamingStrategy enumNamingStrategy = EnumPropertiesCollector.createEnumNamingStrategyInstance( + namingDef, config.canOverrideAccessModifiers()); + return enumNamingStrategy == null ? null : EnumValues.constructUsingEnumNaming( + config, enumClass, enumNamingStrategy); + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java index aad823f1ae..91f4d2f65c 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.introspect.AnnotatedClass; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap; import com.fasterxml.jackson.databind.util.ClassUtil; @@ -109,6 +110,38 @@ public static JsonSerializer getFallbackKeySerializer(SerializationConfi return new Default(Default.TYPE_TO_STRING, rawKeyType); } + /** + * Method called if no specified key serializer was located; will return a + * "default" key serializer. + * + * @since 2.15 + */ + @SuppressWarnings("unchecked") + public static JsonSerializer getFallbackKeySerializer(SerializationConfig config, + Class rawKeyType, AnnotatedClass annotatedClass) + { + if (rawKeyType != null) { + // 29-Sep-2015, tatu: Odd case here, of `Enum`, which we may get for `EnumMap`; not sure + // if that is a bug or feature. Regardless, it seems to require dynamic handling + // (compared to getting actual fully typed Enum). + // Note that this might even work from the earlier point, but let's play it safe for now + // 11-Aug-2016, tatu: Turns out we get this if `EnumMap` is the root value because + // then there is no static type + if (rawKeyType == Enum.class) { + return new Dynamic(); + } + // 29-Sep-2019, tatu: [databind#2457] can not use 'rawKeyType.isEnum()`, won't work + // for subtypes. + if (ClassUtil.isEnumType(rawKeyType)) { + return EnumKeySerializer.construct(rawKeyType, + EnumValues.constructFromName(config, (Class>) rawKeyType), + EnumSerializer.constructEnumNamingStrategyValues(config, (Class>) rawKeyType, annotatedClass)); + } + } + // 19-Oct-2016, tatu: Used to just return DEFAULT_KEY_SERIALIZER but why not: + return new Default(Default.TYPE_TO_STRING, rawKeyType); + } + /** * @deprecated since 2.7 */ @@ -276,9 +309,27 @@ public static class EnumKeySerializer extends StdSerializer { protected final EnumValues _values; + /** + * Map with key as converted property class defined implementation of {@link EnumNamingStrategy} + * and with value as Enum names collected using Enum.name(). + * + * @since 2.15 + */ + protected final EnumValues _valuesByEnumNaming; + protected EnumKeySerializer(Class enumType, EnumValues values) { super(enumType, false); _values = values; + _valuesByEnumNaming = null; + } + + /** + * @since 2.15 + */ + protected EnumKeySerializer(Class enumType, EnumValues values, EnumValues valuesByEnumNaming) { + super(enumType, false); + _values = values; + _valuesByEnumNaming = valuesByEnumNaming; } public static EnumKeySerializer construct(Class enumType, @@ -287,6 +338,12 @@ public static EnumKeySerializer construct(Class enumType, return new EnumKeySerializer(enumType, enumValues); } + public static EnumKeySerializer construct(Class enumType, + EnumValues enumValues, EnumValues valuesByEnumNaming) + { + return new EnumKeySerializer(enumType, enumValues, valuesByEnumNaming); + } + @Override public void serialize(Object value, JsonGenerator g, SerializerProvider serializers) throws IOException @@ -296,6 +353,10 @@ public void serialize(Object value, JsonGenerator g, SerializerProvider serializ return; } Enum en = (Enum) value; + if (_valuesByEnumNaming != null) { + g.writeFieldName(_valuesByEnumNaming.serializedValueFor(en)); + return; + } // 14-Sep-2019, tatu: [databind#2129] Use this specific feature if (serializers.isEnabled(SerializationFeature.WRITE_ENUM_KEYS_USING_INDEX)) { g.writeFieldName(String.valueOf(en.ordinal())); diff --git a/src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java b/src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java index 5986c3f5f0..cbfadd5132 100644 --- a/src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java +++ b/src/main/java/com/fasterxml/jackson/databind/util/EnumValues.java @@ -82,7 +82,7 @@ public static EnumValues constructFromToString(MapperConfig config, Class map; + } + + /* + /********************************************************** + /* Test + /********************************************************** + */ + + public void testEnumNamingWithLowerCamelCaseStrategy() throws Exception { + EnumFlavorA result = MAPPER.readValue(q("saltedCaramel"), EnumFlavorA.class); + assertEquals(EnumFlavorA.SALTED_CARAMEL, result); + + String resultString = MAPPER.writeValueAsString(result); + assertEquals(q("saltedCaramel"), resultString); + } + + public void testEnumNamingTranslateUnknownValueToDefault() throws Exception { + EnumFlavorA result = MAPPER.readerFor(EnumFlavorA.class) + .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .readValue(q("__salted_caramel")); + + assertEquals(EnumFlavorA.VANILLA, result); + } + + public void testEnumNamingToDefaultNumber() throws Exception { + EnumFlavorA result = MAPPER.readerFor(EnumFlavorA.class) + .without(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS) + .readValue(q("1")); + + assertEquals(EnumFlavorA.SALTED_CARAMEL, result); + } + + public void testEnumNamingToDefaultEmptyString() throws Exception { + EnumFlavorA result = MAPPER.readerFor(EnumFlavorA.class) + .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) + .readValue(q("")); + + assertEquals(EnumFlavorA.VANILLA, result); + } + + public void testOriginalEnamValueShouldNotBeFoundWithEnumNamingStrategy() throws Exception { + EnumFlavorB result = MAPPER.readerFor(EnumFlavorB.class) + .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL) + .readValue(q("PEANUT_BUTTER")); + + assertNull(result); + } + + public void testEnumNamingStrategySymmetryReadThenWrite() throws Exception { + EnumSauceC result = MAPPER.readValue(q("ketchUp"), EnumSauceC.class); + assertEquals(EnumSauceC.KETCH_UP, result); + + String resultString = MAPPER.writeValueAsString(result); + assertEquals(q("ketchUp"), resultString); + } + + public void testEnumNamingStrategySymmetryWriteThenRead() throws Exception { + String resultString = MAPPER.writeValueAsString(EnumSauceC.MAYO_NEZZ); + + EnumSauceC result = MAPPER.readValue(resultString, EnumSauceC.class); + + assertEquals(EnumSauceC.MAYO_NEZZ, result); + } + + + public void testReadWrapperValueWithEnumNamingStrategy() throws Exception { + String json = "{\"sauce\": \"ketchUp\"}"; + + EnumSauceWrapperBean wrapper = MAPPER.readValue(json, EnumSauceWrapperBean.class); + + assertEquals(EnumSauceC.KETCH_UP, wrapper.sauce); + } + + public void testReadWrapperValueWithCaseInsensitiveEnumNamingStrategy() throws Exception { + ObjectReader reader = MAPPER.enable(ACCEPT_CASE_INSENSITIVE_ENUMS) + .readerFor(EnumSauceWrapperBean.class); + + EnumSauceWrapperBean lowerCase = reader.readValue(a2q("{'sauce': 'ketchup'}")); + assertEquals(EnumSauceC.KETCH_UP, lowerCase.sauce); + + EnumSauceWrapperBean upperCase = reader.readValue(a2q("{'sauce': 'KETCHUP'}")); + assertEquals(EnumSauceC.KETCH_UP, upperCase.sauce); + + EnumSauceWrapperBean mixedCase = reader.readValue(a2q("{'sauce': 'kEtChUp'}")); + assertEquals(EnumSauceC.KETCH_UP, mixedCase.sauce); + } + + public void testWriteThenReadWrapperValueWithEnumNamingStrategy() throws Exception { + EnumSauceWrapperBean sauceWrapper = new EnumSauceWrapperBean(EnumSauceC.MAYO_NEZZ); + String json = MAPPER.writeValueAsString(sauceWrapper); + + EnumSauceWrapperBean wrapper = MAPPER.readValue(json, EnumSauceWrapperBean.class); + + assertEquals(EnumSauceC.MAYO_NEZZ, wrapper.sauce); + } + + public void testEnumNamingStrategyInterfaceIsNotApplied() throws Exception { + EnumSauceD sauce = MAPPER.readValue(q("SRIRACHA_MAYO"), EnumSauceD.class); + assertEquals(EnumSauceD.SRIRACHA_MAYO, sauce); + } + + public void testEnumNamingStrategyConflictWithUnderScores() throws Exception { + EnumFlavorE flavor = MAPPER.readValue(q("peanutButter"), EnumFlavorE.class); + assertEquals(EnumFlavorE.PEANUT__BUTTER, flavor); + } + + public void testCaseSensensitiveEnumMapKey() throws Exception { + String jsonStr = a2q("{'map':{'ketchUp':'val'}}"); + + ClassWithEnumMapSauceKey result = MAPPER.readValue(jsonStr, ClassWithEnumMapSauceKey.class); + + assertEquals(1, result.map.size()); + assertEquals("val", result.map.get(EnumSauceC.KETCH_UP)); + } + + public void testAllowCaseInsensensitiveEnumMapKey() throws Exception { + ObjectReader reader = MAPPER + .enable(ACCEPT_CASE_INSENSITIVE_ENUMS) + .readerFor(ClassWithEnumMapSauceKey.class); + + ClassWithEnumMapSauceKey result = reader.readValue(a2q("{'map':{'KeTcHuP':'val'}}")); + + assertEquals(1, result.map.size()); + assertEquals("val", result.map.get(EnumSauceC.KETCH_UP)); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java deleted file mode 100644 index 9302bb8416..0000000000 --- a/src/test/java/com/fasterxml/jackson/databind/introspect/EnumNamingTest.java +++ /dev/null @@ -1,189 +0,0 @@ -package com.fasterxml.jackson.databind.introspect; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonEnumDefaultValue; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.*; -import com.fasterxml.jackson.databind.annotation.EnumNaming; - -public class EnumNamingTest extends BaseMapTest { - - /* - /********************************************************** - /* Set Up - /********************************************************** - */ - - final ObjectMapper MAPPER = new ObjectMapper(); - - /* - /********************************************************** - /* Test - /********************************************************** - */ - - @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) - static enum EnumFlavorA { - PEANUT_BUTTER, - SALTED_CARAMEL, - @JsonEnumDefaultValue - VANILLA; - } - - public void testEnumNamingWithLowerCamelCaseStrategy() throws Exception { - EnumFlavorA result = MAPPER.readValue(q("saltedCaramel"), EnumFlavorA.class); - assertEquals(EnumFlavorA.SALTED_CARAMEL, result); - - String resultString = MAPPER.writeValueAsString(result); - } - - public void testEnumNamingTranslateUnknownValueToDefault() throws Exception { - EnumFlavorA result = MAPPER.reader() - .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) - .readValue(q("__salted_caramel"), EnumFlavorA.class); - - assertEquals(EnumFlavorA.VANILLA, result); - } - - public void testEnumNamingToDefaultNumber() throws Exception { - EnumFlavorA result = MAPPER.reader() - .without(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS) - .readValue(q("1"), EnumFlavorA.class); - - assertEquals(EnumFlavorA.SALTED_CARAMEL, result); - } - - public void testEnumNamingToDefaultEmptyString() throws Exception { - EnumFlavorA result = MAPPER.reader() - .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE) - .readValue(q(""), EnumFlavorA.class); - - assertEquals(EnumFlavorA.VANILLA, result); - } - - @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) - static enum EnumFlavorB { - PEANUT_BUTTER, - } - - public void testOriginalEnamValueShouldNotBeFoundWithEnumNamingStrategy() throws Exception { - EnumFlavorB result = MAPPER.reader() - .with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL) - .readValue(q("PEANUT_BUTTER"), EnumFlavorB.class); - - assertNull(result); - } - - - @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) - static enum EnumFlavorC { - CHOCOLATE_CHIPS, - HOT_CHEETOS; - - @Override - public String toString() { - return "HOT_CHOCOLATE_CHEETOS_AND_CHIPS"; - } - } - - public void testEnumNamingShouldOverrideToStringFeatue() throws Exception { - String resultStr = MAPPER.writer() - .with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) - .writeValueAsString(EnumFlavorC.CHOCOLATE_CHIPS); - - assertEquals(q("chocolateChips"), resultStr); - } - - @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) - static enum EnumSauceA { - KETCH_UP, - MAYO_NEZZ; - } - - public void testEnumNamingStrategySymmetryReadThenWrite() throws Exception { - EnumSauceA result = MAPPER.readValue(q("ketchUp"), EnumSauceA.class); - assertEquals(EnumSauceA.KETCH_UP, result); - - String resultString = MAPPER.writeValueAsString(result); - assertEquals(q("ketchUp"), resultString); - } - - public void testEnumNamingStrategySymmetryWriteThenRead() throws Exception { - String resultString = MAPPER.writeValueAsString(EnumSauceA.MAYO_NEZZ); - - EnumSauceA result = MAPPER.readValue(resultString, EnumSauceA.class); - - assertEquals(EnumSauceA.MAYO_NEZZ, result); - } - - - static class EnumFlavorWrapperBean { - public EnumSauceA sauce; - - @JsonCreator - public EnumFlavorWrapperBean(@JsonProperty("sce") EnumSauceA sce) { - this.sauce = sce; - } - } - - public void testReadWrapperValueWithEnumNamingStrategy() throws Exception { - String json = "{\"sauce\": \"ketchUp\"}"; - - EnumFlavorWrapperBean wrapper = MAPPER.readValue(json, EnumFlavorWrapperBean.class); - - assertEquals(EnumSauceA.KETCH_UP, wrapper.sauce); - } - - public void testWriteThenReadWrapperValueWithEnumNamingStrategy() throws Exception { - EnumFlavorWrapperBean sauceWrapper = new EnumFlavorWrapperBean(EnumSauceA.MAYO_NEZZ); - String json = MAPPER.writeValueAsString(sauceWrapper); - - EnumFlavorWrapperBean wrapper = MAPPER.readValue(json, EnumFlavorWrapperBean.class); - - assertEquals(EnumSauceA.MAYO_NEZZ, wrapper.sauce); - } - - - @EnumNaming(EnumNamingStrategy.class) - static enum EnumSauceB { - BARBEQ_UE, - SRIRACHA_MAYO; - } - - public void testEnumNamingStrategyNotApplied() throws Exception { - String resultString = MAPPER.writeValueAsString(EnumSauceB.SRIRACHA_MAYO); - assertEquals(q("SRIRACHA_MAYO"), resultString); - } - - public void testEnumNamingStrategyInterfaceIsNotApplied() throws Exception { - EnumSauceB sauce = MAPPER.readValue(q("SRIRACHA_MAYO"), EnumSauceB.class); - assertEquals(EnumSauceB.SRIRACHA_MAYO, sauce); - } - - @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) - static enum EnumFlavorD { - _PEANUT_BUTTER, - PEANUT__BUTTER, - PEANUT_BUTTER - } - - public void testEnumNamingStrategyStartingUnderscoreBecomesUpperCase() throws Exception { - String flavor = MAPPER.writeValueAsString(EnumFlavorD._PEANUT_BUTTER); - assertEquals(q("PeanutButter"), flavor); - } - - public void testEnumNamingStrategyNonPrefixContiguousUnderscoresBecomeOne() throws Exception { - String flavor1 = MAPPER.writeValueAsString(EnumFlavorD.PEANUT__BUTTER); - assertEquals(q("peanutButter"), flavor1); - - String flavor2 = MAPPER.writeValueAsString(EnumFlavorD.PEANUT_BUTTER); - assertEquals(q("peanutButter"), flavor2); - } - - public void testEnumNamingStrategyConflictWithUnderScores() throws Exception { - EnumFlavorD flavor = MAPPER.readValue(q("peanutButter"), EnumFlavorD.class); - assertEquals(EnumFlavorD.PEANUT__BUTTER, flavor); - } - -} diff --git a/src/test/java/com/fasterxml/jackson/databind/ser/jdk/EnumNamingSerializationTest.java b/src/test/java/com/fasterxml/jackson/databind/ser/jdk/EnumNamingSerializationTest.java new file mode 100644 index 0000000000..160c45818f --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/ser/jdk/EnumNamingSerializationTest.java @@ -0,0 +1,105 @@ +package com.fasterxml.jackson.databind.ser.jdk; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.annotation.EnumNaming; + +import java.util.EnumMap; +import java.util.EnumSet; + +public class EnumNamingSerializationTest extends BaseMapTest { + + /* + /********************************************************** + /* Set Up + /********************************************************** + */ + + final ObjectMapper MAPPER = new ObjectMapper(); + + @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) + static enum EnumFlavorA { + CHOCOLATE_CHIPS, + HOT_CHEETOS; + + @Override + public String toString() { + return "HOT_CHOCOLATE_CHEETOS_AND_CHIPS"; + } + } + + @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) + static enum EnumSauceB { + KETCH_UP, + MAYO_NEZZ; + } + + @EnumNaming(EnumNamingStrategy.class) + static enum EnumSauceC { + BARBEQ_UE, + SRIRACHA_MAYO; + } + + @EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class) + static enum EnumFlavorD { + _PEANUT_BUTTER, + PEANUT__BUTTER, + PEANUT_BUTTER + } + + static class EnumFlavorWrapperBean { + public EnumSauceB sauce; + + @JsonCreator + public EnumFlavorWrapperBean(@JsonProperty("sce") EnumSauceB sce) { + this.sauce = sce; + } + } + + /* + /********************************************************** + /* Test + /********************************************************** + */ + + public void testEnumNamingShouldOverrideToStringFeatue() throws Exception { + String resultStr = MAPPER.writer() + .with(SerializationFeature.WRITE_ENUMS_USING_TO_STRING) + .writeValueAsString(EnumFlavorA.CHOCOLATE_CHIPS); + + assertEquals(q("chocolateChips"), resultStr); + } + + public void testEnumNamingStrategyNotApplied() throws Exception { + String resultString = MAPPER.writeValueAsString(EnumSauceC.SRIRACHA_MAYO); + assertEquals(q("SRIRACHA_MAYO"), resultString); + } + + public void testEnumNamingStrategyStartingUnderscoreBecomesUpperCase() throws Exception { + String flavor = MAPPER.writeValueAsString(EnumFlavorD._PEANUT_BUTTER); + assertEquals(q("PeanutButter"), flavor); + } + + public void testEnumNamingStrategyNonPrefixContiguousUnderscoresBecomeOne() throws Exception { + String flavor1 = MAPPER.writeValueAsString(EnumFlavorD.PEANUT__BUTTER); + assertEquals(q("peanutButter"), flavor1); + + String flavor2 = MAPPER.writeValueAsString(EnumFlavorD.PEANUT_BUTTER); + assertEquals(q("peanutButter"), flavor2); + } + + public void testEnumSet() throws Exception { + final EnumSet value = EnumSet.of(EnumSauceB.KETCH_UP); + assertEquals("[\"ketchUp\"]", MAPPER.writeValueAsString(value)); + } + + public void testDesrEnumWithEnumMap() throws Exception { + EnumMap enums = new EnumMap(EnumSauceB.class); + enums.put(EnumSauceB.MAYO_NEZZ, "value"); + + String str = MAPPER.writeValueAsString(enums); + + assertEquals(a2q("{'mayoNezz':'value'}"), str); + } +} From 2a6b999ce7366286bd8efdbeb631436336279b0e Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Sat, 11 Mar 2023 22:53:06 +0900 Subject: [PATCH 05/14] Add more deser tests --- .../enums/EnumNamingDeserializationTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumNamingDeserializationTest.java b/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumNamingDeserializationTest.java index c1bfef9dca..6df58f261f 100644 --- a/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumNamingDeserializationTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/deser/enums/EnumNamingDeserializationTest.java @@ -66,6 +66,11 @@ static class ClassWithEnumMapSauceKey { Map map; } + static class ClassWithEnumMapSauceValue { + @JsonProperty + Map map; + } + /* /********************************************************** /* Test @@ -189,4 +194,30 @@ public void testAllowCaseInsensensitiveEnumMapKey() throws Exception { assertEquals(1, result.map.size()); assertEquals("val", result.map.get(EnumSauceC.KETCH_UP)); } + + public void testAllowCaseSensensitiveEnumMapValue() throws Exception { + ObjectReader reader = MAPPER + .enable(ACCEPT_CASE_INSENSITIVE_ENUMS) + .readerFor(ClassWithEnumMapSauceValue.class); + + ClassWithEnumMapSauceValue result = reader.readValue( + a2q("{'map':{'lowerSauce':'ketchUp', 'upperSauce':'mayoNezz'}}")); + + assertEquals(2, result.map.size()); + assertEquals(EnumSauceC.KETCH_UP, result.map.get("lowerSauce")); + assertEquals(EnumSauceC.MAYO_NEZZ, result.map.get("upperSauce")); + } + + public void testAllowCaseInsensensitiveEnumMapValue() throws Exception { + ObjectReader reader = MAPPER + .enable(ACCEPT_CASE_INSENSITIVE_ENUMS) + .readerFor(ClassWithEnumMapSauceValue.class); + + ClassWithEnumMapSauceValue result = reader.readValue( + a2q("{'map':{'lowerSauce':'ketchup', 'upperSauce':'MAYONEZZ'}}")); + + assertEquals(2, result.map.size()); + assertEquals(EnumSauceC.KETCH_UP, result.map.get("lowerSauce")); + assertEquals(EnumSauceC.MAYO_NEZZ, result.map.get("upperSauce")); + } } From 248af1d4130a3d7807e8751321d32295e4f235f1 Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Sun, 12 Mar 2023 08:27:52 +0900 Subject: [PATCH 06/14] Make JavaDoc more descriptive --- .../databind/introspect/EnumPropertiesCollector.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java index f097ec669d..3564667994 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java @@ -20,34 +20,34 @@ private EnumPropertiesCollector() {} */ /** + * Factory method for creating an instance of {@link EnumNamingStrategy} from a provided {@code namingDef}. + * + * @param namingDef subclass of {@link EnumNamingStrategy} to initialize an instance of. + * @param canOverrideAccessModifiers whether to override access modifiers when instantiating the naming strategy. + * * @since 2.15 */ public static EnumNamingStrategy createEnumNamingStrategyInstance(Object namingDef, boolean canOverrideAccessModifiers) { if (namingDef == null) { return null; } - if (namingDef instanceof EnumNamingStrategy) { return (EnumNamingStrategy) namingDef; } - // Alas, there's no way to force return type of "either class - // X or Y" -- need to throw an exception after the fact if (!(namingDef instanceof Class)) { reportProblem("AnnotationIntrospector returned EnumNamingStrategy definition of type %s" + "; expected type `Class` instead", ClassUtil.classNameOf(namingDef)); } Class namingClass = (Class) namingDef; - // 09-Nov-2015, tatu: Need to consider pseudo-value of STD, which means "use default" + if (namingClass == EnumNamingStrategy.class) { return null; } - if (!EnumNamingStrategy.class.isAssignableFrom(namingClass)) { reportProblem("AnnotationIntrospector returned Class %s; expected `Class`", ClassUtil.classNameOf(namingClass)); } - return (EnumNamingStrategy) ClassUtil.createInstance(namingClass, canOverrideAccessModifiers); } From 4ecda3c69dcd3256911076f8e98fb6b7688eb856 Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Sun, 12 Mar 2023 09:13:04 +0900 Subject: [PATCH 07/14] Improve JavaDoc of EnumPropertiessCollector --- .../databind/introspect/EnumPropertiesCollector.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java index 3564667994..258ff3f350 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java @@ -25,6 +25,13 @@ private EnumPropertiesCollector() {} * @param namingDef subclass of {@link EnumNamingStrategy} to initialize an instance of. * @param canOverrideAccessModifiers whether to override access modifiers when instantiating the naming strategy. * + * @throws IllegalArgumentException if {@code namingDef} is not an instance of {@link java.lang.Class} or + * not a subclass of {@link EnumNamingStrategy}. + * + * @return an instance of {@link EnumNamingStrategy} if {@code namingDef} is a subclass of {@link EnumNamingStrategy}, + * {@code null} if {@code namingDef} is {@code null}, + * and an instance of {@link EnumNamingStrategy} if {@code namingDef} already is one. + * * @since 2.15 */ public static EnumNamingStrategy createEnumNamingStrategyInstance(Object namingDef, boolean canOverrideAccessModifiers) { @@ -58,6 +65,8 @@ public static EnumNamingStrategy createEnumNamingStrategyInstance(Object namingD */ /** + * @throws IllegalArgumentException with provided message. + * * @since 2.15 */ protected static void reportProblem(String msg, Object... args) { From 6799d2d27b1dd670c92d06d494ef8e7edb156a25 Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Sun, 12 Mar 2023 10:25:12 +0900 Subject: [PATCH 08/14] Clean EnumPropertiesCollector --- .../introspect/EnumPropertiesCollector.java | 41 ++++++------------- 1 file changed, 12 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java index 258ff3f350..1177cdbd05 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java @@ -22,16 +22,16 @@ private EnumPropertiesCollector() {} /** * Factory method for creating an instance of {@link EnumNamingStrategy} from a provided {@code namingDef}. * - * @param namingDef subclass of {@link EnumNamingStrategy} to initialize an instance of. + * @param namingDef subclass of {@link EnumNamingStrategy} to initialize an instance of. * @param canOverrideAccessModifiers whether to override access modifiers when instantiating the naming strategy. * - * @throws IllegalArgumentException if {@code namingDef} is not an instance of {@link java.lang.Class} or - * not a subclass of {@link EnumNamingStrategy}. - * * @return an instance of {@link EnumNamingStrategy} if {@code namingDef} is a subclass of {@link EnumNamingStrategy}, - * {@code null} if {@code namingDef} is {@code null}, - * and an instance of {@link EnumNamingStrategy} if {@code namingDef} already is one. + * {@code null} if {@code namingDef} is {@code null}, + * and an instance of {@link EnumNamingStrategy} if {@code namingDef} already is one. * + * @throws IllegalArgumentException if {@code namingDef} is not an instance of {@link java.lang.Class} or + * not a subclass of {@link EnumNamingStrategy}. + * * @since 2.15 */ public static EnumNamingStrategy createEnumNamingStrategyInstance(Object namingDef, boolean canOverrideAccessModifiers) { @@ -42,8 +42,9 @@ public static EnumNamingStrategy createEnumNamingStrategyInstance(Object namingD return (EnumNamingStrategy) namingDef; } if (!(namingDef instanceof Class)) { - reportProblem("AnnotationIntrospector returned EnumNamingStrategy definition of type %s" - + "; expected type `Class` instead", ClassUtil.classNameOf(namingDef)); + throw new IllegalArgumentException(String.format( + "AnnotationIntrospector returned EnumNamingStrategy definition of type %s; " + + "expected type `Class` instead", ClassUtil.classNameOf(namingDef))); } Class namingClass = (Class) namingDef; @@ -52,28 +53,10 @@ public static EnumNamingStrategy createEnumNamingStrategyInstance(Object namingD return null; } if (!EnumNamingStrategy.class.isAssignableFrom(namingClass)) { - reportProblem("AnnotationIntrospector returned Class %s; expected `Class`", - ClassUtil.classNameOf(namingClass)); + throw new IllegalArgumentException(String.format( + "Problem with AnnotationIntrospector returned Class %s; " + + "expected `Class`", ClassUtil.classNameOf(namingClass))); } return (EnumNamingStrategy) ClassUtil.createInstance(namingClass, canOverrideAccessModifiers); } - - /* - ********************************************************* - * Internal methods; helpers - ********************************************************** - */ - - /** - * @throws IllegalArgumentException with provided message. - * - * @since 2.15 - */ - protected static void reportProblem(String msg, Object... args) { - if (args.length > 0) { - msg = String.format(msg, args); - } - throw new IllegalArgumentException("Problem with " + msg); - } - } From 9473b1b1b96c35ad7f5839db5c3890f0b9bc9187 Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Sun, 12 Mar 2023 13:29:35 +0900 Subject: [PATCH 09/14] reuse EnumDeserializer constructor --- .../jackson/databind/deser/std/EnumDeserializer.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java index 66c47c80aa..a146285052 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java @@ -79,13 +79,7 @@ public class EnumDeserializer */ public EnumDeserializer(EnumResolver byNameResolver, Boolean caseInsensitive) { - super(byNameResolver.getEnumClass()); - _lookupByName = byNameResolver.constructLookup(); - _enumsByIndex = byNameResolver.getRawEnums(); - _enumDefaultValue = byNameResolver.getDefaultValue(); - _caseInsensitive = caseInsensitive; - _isFromIntValue = byNameResolver.isFromIntValue(); - _lookupByEnumNaming = null; + this(byNameResolver, caseInsensitive, null); } /** From c2f3699c8e9947003234fc82852d144d0f51088e Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Sun, 12 Mar 2023 13:30:01 +0900 Subject: [PATCH 10/14] Override`findEnumNamingStrategy()` in AnnotationIntrospectorPair --- .../introspect/AnnotationIntrospectorPair.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotationIntrospectorPair.java b/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotationIntrospectorPair.java index bbd105f183..f19d908357 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotationIntrospectorPair.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/AnnotationIntrospectorPair.java @@ -161,6 +161,16 @@ public Object findNamingStrategy(AnnotatedClass ac) return str; } + @Override + public Object findEnumNamingStrategy(AnnotatedClass ac) + { + Object str = _primary.findEnumNamingStrategy(ac); + if (str == null) { + str = _secondary.findEnumNamingStrategy(ac); + } + return str; + } + @Override public String findClassDescription(AnnotatedClass ac) { String str = _primary.findClassDescription(ac); From e8558294382746edc59f143271a383322e9abd85 Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Mon, 13 Mar 2023 08:52:11 +0900 Subject: [PATCH 11/14] Improve decision-making on deserialization lookup and resolver --- .../databind/deser/std/EnumDeserializer.java | 17 +++++++++++++---- .../databind/deser/std/StdKeyDeserializer.java | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java index a146285052..005a8ffb8a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java @@ -271,10 +271,7 @@ protected Object _fromString(JsonParser p, DeserializationContext ctxt, String text) throws IOException { - CompactStringObjectMap lookup = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING) - ? _getToStringLookup(ctxt) : _lookupByName; - lookup = _lookupByEnumNaming == null ? lookup : _lookupByEnumNaming; - + CompactStringObjectMap lookup = _resolveCurrentLookup(ctxt); Object result = lookup.find(text); if (result == null) { String trimmed = text.trim(); @@ -285,6 +282,18 @@ protected Object _fromString(JsonParser p, DeserializationContext ctxt, return result; } + /** + * @since 2.15 + */ + private CompactStringObjectMap _resolveCurrentLookup(DeserializationContext ctxt) { + if (_lookupByEnumNaming != null) { + return _lookupByEnumNaming; + } + return ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING) + ? _getToStringLookup(ctxt) + : _lookupByName; + } + protected Object _fromInteger(JsonParser p, DeserializationContext ctxt, int index) throws IOException diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializer.java index df0596d3aa..6fc337ed04 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/StdKeyDeserializer.java @@ -419,10 +419,8 @@ public Object _parse(String key, DeserializationContext ctxt) throws IOException ClassUtil.unwrapAndThrowAsIAE(e); } } - EnumResolver res = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING) - ? _getToStringResolver(ctxt) : _byNameResolver; - res = _byEnumNamingResolver == null ? res : _byEnumNamingResolver; + EnumResolver res = _resolveCurrentResolver(ctxt); Enum e = res.findEnum(key); // If enum is found, no need to try deser using index if (e == null && ctxt.isEnabled(EnumFeature.READ_ENUM_KEYS_USING_INDEX)) { @@ -442,6 +440,18 @@ public Object _parse(String key, DeserializationContext ctxt) throws IOException return e; } + /** + * @since 2.15 + */ + protected EnumResolver _resolveCurrentResolver(DeserializationContext ctxt) { + if (_byEnumNamingResolver != null) { + return _byEnumNamingResolver; + } + return ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING) + ? _getToStringResolver(ctxt) + : _byNameResolver; + } + private EnumResolver _getToStringResolver(DeserializationContext ctxt) { EnumResolver res = _byToStringResolver; From 9526d3adbd5786d6c91afb0fcd4a0f3bba8f245e Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Tue, 14 Mar 2023 12:20:57 +0900 Subject: [PATCH 12/14] Deprecate StdKeySerializers.getFallbackKeySerializer() method --- .../fasterxml/jackson/databind/ser/std/StdKeySerializers.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java index 91f4d2f65c..6e24ac60cf 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java @@ -83,9 +83,12 @@ public static JsonSerializer getStdKeySerializer(SerializationConfig con * Method called if no specified key serializer was located; will return a * "default" key serializer. * + * @deprecated Since 2.15 -- use {@link StdKeySerializers#getFallbackKeySerializer(SerializationConfig, Class, AnnotatedClass)} + * instead. * @since 2.7 */ @SuppressWarnings("unchecked") + @Deprecated public static JsonSerializer getFallbackKeySerializer(SerializationConfig config, Class rawKeyType) { From f005b7f8a336cb83af1db4734b78535917d30bd8 Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Tue, 14 Mar 2023 12:23:10 +0900 Subject: [PATCH 13/14] Change EnumPropertiesCollector name to EnumNamingStrategyFactory --- .../jackson/databind/deser/BasicDeserializerFactory.java | 2 +- ...ropertiesCollector.java => EnumNamingStrategyFactory.java} | 4 ++-- .../fasterxml/jackson/databind/ser/std/EnumSerializer.java | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/main/java/com/fasterxml/jackson/databind/introspect/{EnumPropertiesCollector.java => EnumNamingStrategyFactory.java} (96%) diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java index 6d80c192f8..3afbd3c5d2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/BasicDeserializerFactory.java @@ -2435,7 +2435,7 @@ protected EnumResolver constructEnumResolver(Class enumClass, protected EnumResolver constructEnumNamingStrategyResolver(DeserializationConfig config, Class enumClass, AnnotatedClass annotatedClass) { Object namingDef = config.getAnnotationIntrospector().findEnumNamingStrategy(annotatedClass); - EnumNamingStrategy enumNamingStrategy = EnumPropertiesCollector.createEnumNamingStrategyInstance( + EnumNamingStrategy enumNamingStrategy = EnumNamingStrategyFactory.createEnumNamingStrategyInstance( namingDef, config.canOverrideAccessModifiers()); return enumNamingStrategy == null ? null : EnumResolver.constructUsingEnumNamingStrategy(config, enumClass, enumNamingStrategy); diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategyFactory.java similarity index 96% rename from src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java rename to src/main/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategyFactory.java index 1177cdbd05..bb5cecde2f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/introspect/EnumPropertiesCollector.java +++ b/src/main/java/com/fasterxml/jackson/databind/introspect/EnumNamingStrategyFactory.java @@ -9,9 +9,9 @@ * * @since 2.15 */ -public class EnumPropertiesCollector { +public class EnumNamingStrategyFactory { - private EnumPropertiesCollector() {} + private EnumNamingStrategyFactory() {} /* /********************************************************** diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java index 5c37669f61..dda6eae18a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/EnumSerializer.java @@ -14,7 +14,7 @@ import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.introspect.AnnotatedClass; -import com.fasterxml.jackson.databind.introspect.EnumPropertiesCollector; +import com.fasterxml.jackson.databind.introspect.EnumNamingStrategyFactory; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonStringFormatVisitor; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -271,7 +271,7 @@ protected static Boolean _isShapeWrittenUsingIndex(Class enumClass, protected static EnumValues constructEnumNamingStrategyValues(SerializationConfig config, Class> enumClass, AnnotatedClass annotatedClass) { Object namingDef = config.getAnnotationIntrospector().findEnumNamingStrategy(annotatedClass); - EnumNamingStrategy enumNamingStrategy = EnumPropertiesCollector.createEnumNamingStrategyInstance( + EnumNamingStrategy enumNamingStrategy = EnumNamingStrategyFactory.createEnumNamingStrategyInstance( namingDef, config.canOverrideAccessModifiers()); return enumNamingStrategy == null ? null : EnumValues.constructUsingEnumNaming( config, enumClass, enumNamingStrategy); From 4b783c7af6ab4c6f1aad41a18faeef3e117c55c3 Mon Sep 17 00:00:00 2001 From: joohyukkim Date: Tue, 14 Mar 2023 12:53:12 +0900 Subject: [PATCH 14/14] Improve JavaDoc --- .../fasterxml/jackson/databind/ser/std/StdKeySerializers.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java index 6e24ac60cf..71c7531027 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/StdKeySerializers.java @@ -115,7 +115,7 @@ public static JsonSerializer getFallbackKeySerializer(SerializationConfi /** * Method called if no specified key serializer was located; will return a - * "default" key serializer. + * "default" key serializer initialized by {@link EnumKeySerializer#construct(Class, EnumValues, EnumValues)} * * @since 2.15 */