Skip to content

Commit 5460b9b

Browse files
committed
initial commit
1 parent 5be343d commit 5460b9b

File tree

6 files changed

+318
-0
lines changed

6 files changed

+318
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.fasterxml.jackson.databind;
2+
3+
public class EnumNamingStrategies {
4+
5+
public static final EnumNamingStrategy CAMEL_CASE = CamelCaseStrategy.INSTANCE;
6+
7+
/**
8+
* no-op naming. Does nothing
9+
*/
10+
public static class NoOpEnumNamingStrategy implements EnumNamingStrategy {
11+
12+
@Override
13+
public String translate(String value) {
14+
return value;
15+
}
16+
17+
}
18+
19+
/**
20+
* <p>
21+
* Used when external value is in conventional CamelCase. Examples are "numberValue", "namingStrategy", "theDefiniteProof".
22+
* First underscore prefix will always be removed.
23+
*/
24+
public static class CamelCaseStrategy implements EnumNamingStrategy {
25+
26+
/**
27+
* @since 2.15
28+
*/
29+
public final static CamelCaseStrategy INSTANCE
30+
= new CamelCaseStrategy();
31+
32+
@Override
33+
public String translate(String input) {
34+
if (input == null) {
35+
return input;
36+
}
37+
38+
int length = input.length();
39+
StringBuilder result = new StringBuilder(length * 2);
40+
int resultLength = 0;
41+
boolean wasPrevTranslated = false;
42+
for (int i = 0; i < length; i++) {
43+
char c = input.charAt(i);
44+
if (i > 0 || c != '_') {
45+
if (Character.isUpperCase(c)) {
46+
if (!wasPrevTranslated && resultLength > 0 && result.charAt(resultLength - 1) != '_') {
47+
result.append('_');
48+
resultLength++;
49+
}
50+
c = Character.toLowerCase(c);
51+
wasPrevTranslated = true;
52+
} else {
53+
wasPrevTranslated = false;
54+
}
55+
result.append(c);
56+
resultLength++;
57+
}
58+
}
59+
String output = resultLength > 0 ? result.toString() : input;
60+
return output.toUpperCase();
61+
}
62+
}
63+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.fasterxml.jackson.databind;
2+
3+
public interface EnumNamingStrategy {
4+
5+
public String translate(String value);
6+
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.fasterxml.jackson.databind.annotation;
2+
3+
import com.fasterxml.jackson.databind.EnumNamingStrategy;
4+
5+
import java.lang.annotation.ElementType;
6+
import java.lang.annotation.Retention;
7+
import java.lang.annotation.RetentionPolicy;
8+
import java.lang.annotation.Target;
9+
10+
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
11+
@Retention(RetentionPolicy.RUNTIME)
12+
@com.fasterxml.jackson.annotation.JacksonAnnotation
13+
public @interface EnumNaming {
14+
public Class<? extends EnumNamingStrategy> value() default EnumNamingStrategy.class;
15+
}

src/main/java/com/fasterxml/jackson/databind/deser/std/EnumDeserializer.java

+66
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
package com.fasterxml.jackson.databind.deser.std;
22

33
import java.io.IOException;
4+
import java.util.Arrays;
45
import java.util.Objects;
56

67
import com.fasterxml.jackson.annotation.JsonFormat;
78

89
import com.fasterxml.jackson.core.*;
910

1011
import com.fasterxml.jackson.databind.*;
12+
import com.fasterxml.jackson.databind.annotation.EnumNaming;
1113
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
1214
import com.fasterxml.jackson.databind.cfg.CoercionAction;
1315
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
@@ -65,6 +67,32 @@ public class EnumDeserializer
6567
*/
6668
protected final boolean _isFromIntValue;
6769

70+
protected Boolean _useEnumNaming;
71+
72+
protected volatile EnumNamingStrategy _namingStrategy;
73+
74+
/**
75+
* Marker flag for cases where we expect actual integral value for Enum,
76+
* based on {@code @JsonValue} (and equivalent) annotated accessor.
77+
*
78+
* @since 2.15
79+
*/
80+
protected boolean _isEnumNamingSet;
81+
82+
/**
83+
* @since 2.15
84+
*/
85+
protected EnumNamingStrategy _namingStrategy = new EnumNamingStrategies.NoOpEnumNamingStrategy();
86+
87+
88+
private void _initEnumNamingStrategy() {
89+
EnumNaming enumNamingAnnotation = _valueClass.getAnnotation(EnumNaming.class);
90+
if (enumNamingAnnotation != null) {
91+
_isEnumNamingSet = true;
92+
ClassUtil.createInstance(enumNamingAnnotation.value(), true);
93+
}
94+
}
95+
6896
/**
6997
* @since 2.9
7098
*/
@@ -76,6 +104,7 @@ public EnumDeserializer(EnumResolver byNameResolver, Boolean caseInsensitive)
76104
_enumDefaultValue = byNameResolver.getDefaultValue();
77105
_caseInsensitive = caseInsensitive;
78106
_isFromIntValue = byNameResolver.isFromIntValue();
107+
_initEnumNamingStrategy();
79108
}
80109

81110
/**
@@ -92,6 +121,7 @@ protected EnumDeserializer(EnumDeserializer base, Boolean caseInsensitive,
92121
_isFromIntValue = base._isFromIntValue;
93122
_useDefaultValueForUnknownEnum = useDefaultValueForUnknownEnum;
94123
_useNullForUnknownEnum = useNullForUnknownEnum;
124+
_initEnumNamingStrategy();
95125
}
96126

97127
/**
@@ -254,6 +284,12 @@ protected Object _fromString(JsonParser p, DeserializationContext ctxt,
254284
CompactStringObjectMap lookup = ctxt.isEnabled(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
255285
? _getToStringLookup(ctxt) : _lookupByName;
256286
Object result = lookup.find(text);
287+
288+
if (result == null && _getUseEnumNaming()) {
289+
String translatedText = _getNamingStrategy().translate(text);
290+
result = lookup.find(translatedText);
291+
}
292+
257293
if (result == null) {
258294
String trimmed = text.trim();
259295
if ((trimmed == text) || (result = lookup.find(trimmed)) == null) {
@@ -317,6 +353,12 @@ private final Object _deserializeAltString(JsonParser p, DeserializationContext
317353
CompactStringObjectMap lookup, String nameOrig) throws IOException
318354
{
319355
String name = nameOrig.trim();
356+
357+
if (_isEnumNamingSet) {
358+
String translatedValue = _namingStrategy.translate(name);
359+
return lookup.find(translatedValue);
360+
}
361+
320362
if (name.isEmpty()) { // empty or blank
321363
// 07-Jun-2021, tatu: [databind#3171] Need to consider Default value first
322364
// (alas there's bit of duplication here)
@@ -413,6 +455,30 @@ protected CompactStringObjectMap _getToStringLookup(DeserializationContext ctxt)
413455
return lookup;
414456
}
415457

458+
protected Boolean _getUseEnumNaming() {
459+
if (_useEnumNaming == null) {
460+
_useEnumNaming = _valueClass.getAnnotation(EnumNaming.class) != null;
461+
}
462+
return _useEnumNaming;
463+
}
464+
465+
protected EnumNamingStrategy _getNamingStrategy() {
466+
EnumNamingStrategy namingStrategy = _namingStrategy;
467+
if (namingStrategy == null) {
468+
synchronized (this) {
469+
namingStrategy = _namingStrategy;
470+
if (namingStrategy == null) {
471+
EnumNaming enumNamingAnnotation = _valueClass.getAnnotation(EnumNaming.class);
472+
namingStrategy = enumNamingAnnotation == null
473+
? new EnumNamingStrategies.NoOpEnumNamingStrategy()
474+
: ClassUtil.createInstance(enumNamingAnnotation.value(), true);
475+
_namingStrategy = namingStrategy;
476+
}
477+
}
478+
}
479+
return namingStrategy;
480+
}
481+
416482
// @since 2.15
417483
protected boolean useNullForUnknownEnum(DeserializationContext ctxt) {
418484
return Boolean.TRUE.equals(_useNullForUnknownEnum)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package com.fasterxml.jackson.databind.introspect;
2+
3+
import com.fasterxml.jackson.databind.BaseMapTest;
4+
import com.fasterxml.jackson.databind.EnumNamingStrategies;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
7+
8+
import java.util.Arrays;
9+
import java.util.List;
10+
11+
/**
12+
* Unit tests to verify functioning of standard {@link PropertyNamingStrategy}
13+
* implementations Jackson includes out of the box.
14+
*/
15+
public class EnumNamingStrategiesTest extends BaseMapTest {
16+
17+
/*
18+
/**********************************************************
19+
/* Set Up
20+
/**********************************************************
21+
*/
22+
23+
private final ObjectMapper VANILLA_MAPPER = newJsonMapper();
24+
25+
/*
26+
/**********************************************************
27+
/* Snake Case test
28+
/**********************************************************
29+
*/
30+
31+
final static List<Object[]> UPPER_SNAKE_CASE_NAME_TRANSLATIONS = Arrays.asList(new Object[][]{
32+
{null, null},
33+
{"", ""},
34+
{"a", "A"},
35+
{"abc", "ABC"},
36+
{"1", "1"},
37+
{"123", "123"},
38+
{"1a", "1A"},
39+
{"a1", "A1"},
40+
{"$", "$"},
41+
{"$a", "$A"},
42+
{"a$", "A$"},
43+
{"$_a", "$_A"},
44+
{"a_$", "A_$"},
45+
{"a$a", "A$A"},
46+
{"$A", "$_A"},
47+
{"$_A", "$_A"},
48+
{"_", "_"},
49+
{"__", "_"},
50+
{"___", "__"},
51+
{"A", "A"},
52+
{"A1", "A1"},
53+
{"1A", "1_A"},
54+
{"_a", "A"},
55+
{"_A", "A"},
56+
{"a_a", "A_A"},
57+
{"a_A", "A_A"},
58+
{"A_A", "A_A"},
59+
{"A_a", "A_A"},
60+
{"WWW", "WWW"},
61+
{"someURI", "SOME_URI"},
62+
{"someURIs", "SOME_URIS"},
63+
{"Results", "RESULTS"},
64+
{"_Results", "RESULTS"},
65+
{"_results", "RESULTS"},
66+
{"__results", "_RESULTS"},
67+
{"__Results", "_RESULTS"},
68+
{"___results", "__RESULTS"},
69+
{"___Results", "__RESULTS"},
70+
{"userName", "USER_NAME"},
71+
{"user_name", "USER_NAME"},
72+
{"user__name", "USER__NAME"},
73+
{"UserName", "USER_NAME"},
74+
{"User_Name", "USER_NAME"},
75+
{"User__Name", "USER__NAME"},
76+
{"_user_name", "USER_NAME"},
77+
{"_UserName", "USER_NAME"},
78+
{"_User_Name", "USER_NAME"},
79+
{"USER_NAME", "USER_NAME"},
80+
{"_Bars", "BARS"},
81+
{"usId", "US_ID"},
82+
{"uId", "U_ID"},
83+
{"xCoordinate", "X_COORDINATE"},
84+
});
85+
86+
/**
87+
* Unit test to verify translations of
88+
* {@link EnumNamingStrategies#CAMEL_CASE}
89+
* outside the context of an ObjectMapper.
90+
*/
91+
public void testSnakeCaseStrategyStandAlone() {
92+
for (Object[] pair : UPPER_SNAKE_CASE_NAME_TRANSLATIONS) {
93+
final String input = (String) pair[0];
94+
final String expected = (String) pair[1];
95+
96+
String actual = EnumNamingStrategies.CamelCaseStrategy.INSTANCE
97+
.translate(input);
98+
99+
assertEquals(expected, actual);
100+
}
101+
}
102+
103+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.fasterxml.jackson.databind.introspect;
2+
3+
import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
4+
import com.fasterxml.jackson.databind.BaseMapTest;
5+
import com.fasterxml.jackson.databind.DeserializationFeature;
6+
import com.fasterxml.jackson.databind.EnumNamingStrategies;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
8+
import com.fasterxml.jackson.databind.annotation.EnumNaming;
9+
10+
public class EnumNamingTest extends BaseMapTest {
11+
12+
/*
13+
/**********************************************************
14+
/* Set Up
15+
/**********************************************************
16+
*/
17+
18+
final ObjectMapper MAPPER = new ObjectMapper();
19+
20+
/*
21+
/**********************************************************
22+
/* Test
23+
/**********************************************************
24+
*/
25+
26+
@EnumNaming(EnumNamingStrategies.CamelCaseStrategy.class)
27+
static enum EnumFlavorA {
28+
PEANUT_BUTTER,
29+
SALTED_CARAMEL,
30+
@JsonEnumDefaultValue
31+
VANILLA;
32+
}
33+
34+
public void testEnumNamingWithLowerCamelCaseStrategy() throws Exception {
35+
EnumFlavorA result = MAPPER.readValue(q("saltedCaramel"), EnumFlavorA.class);
36+
assertEquals(EnumFlavorA.SALTED_CARAMEL, result);
37+
}
38+
39+
40+
public void testEnumNamingToDefaultUnknownValue() throws Exception {
41+
EnumFlavorA result = MAPPER.reader()
42+
.with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
43+
.readValue(q("0.12321"), EnumFlavorA.class);
44+
45+
assertEquals(EnumFlavorA.VANILLA, result);
46+
}
47+
48+
public void testEnumNamingToDefaultNumber() throws Exception {
49+
EnumFlavorA result = MAPPER.reader()
50+
.without(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS)
51+
.readValue(q("1"), EnumFlavorA.class);
52+
53+
assertEquals(EnumFlavorA.SALTED_CARAMEL, result);
54+
}
55+
56+
public void testEnumNamingToDefaultEmptyString() throws Exception {
57+
EnumFlavorA result = MAPPER.reader()
58+
.with(DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_USING_DEFAULT_VALUE)
59+
.readValue(q(""), EnumFlavorA.class);
60+
61+
assertEquals(EnumFlavorA.VANILLA, result);
62+
}
63+
64+
}

0 commit comments

Comments
 (0)