Skip to content

Commit 1d336f3

Browse files
committed
Apply changes after first review
1 parent 5460b9b commit 1d336f3

File tree

12 files changed

+622
-126
lines changed

12 files changed

+622
-126
lines changed

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

+15
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,21 @@ public JsonIncludeProperties.Value findPropertyInclusionByName(MapperConfig<?> c
382382
*/
383383
public Object findNamingStrategy(AnnotatedClass ac) { return null; }
384384

385+
/**
386+
* Method for finding {@link EnumNamingStrategy} implenetation class for given
387+
* class, if any specified by annotations; and if so, either return
388+
* a {@link EnumNamingStrategy} instance, or Class to use for
389+
* creating instance
390+
*
391+
* @param ac Annotated class to introspect
392+
*
393+
* @return Subclass of {@link EnumNamingStrategy}, if one
394+
* is specified for given class; null if not.
395+
*
396+
* @since 2.15
397+
*/
398+
public Object findEnumNamingStrategy(AnnotatedClass ac) { return null; }
399+
385400
/**
386401
* Method used to check whether specified class defines a human-readable
387402
* description to use for documentation.
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,129 @@
11
package com.fasterxml.jackson.databind;
22

3+
/**
4+
* A container class for implementations of the {@link EnumNamingStrategy} interface.
5+
*
6+
* @since 2.15
7+
*/
38
public class EnumNamingStrategies {
49

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-
}
10+
private EnumNamingStrategies() {}
1811

1912
/**
2013
* <p>
21-
* Used when external value is in conventional CamelCase. Examples are "numberValue", "namingStrategy", "theDefiniteProof".
22-
* First underscore prefix will always be removed.
14+
* An implementation of {@link EnumNamingStrategy} that converts enum names in the typical upper
15+
* snake case format to camel case format. This implementation follows three rules
16+
* described below.
17+
*
18+
* <ol>
19+
* <li>converts any character preceded by an underscore into upper case character,
20+
* regardless of its original case (upper or lower).</li>
21+
* <li>converts any character NOT preceded by an underscore into a lower case character,
22+
* regardless of its original case (upper or lower).</li>
23+
* <li>converts contiguous sequence of underscores into a single underscore.</li>
24+
* </ol>
25+
*
26+
* WARNING: Naming conversion conflicts caused by underscore usage should be handled by client.
27+
* e.g. Both <code>PEANUT_BUTTER</code>, <code>PEANUT__BUTTER</code> are converted into "peanutButter".
28+
* And "peanutButter" will be deserialized into enum with smaller <code>Enum.ordinal()</code> value.
29+
*
30+
* <p>
31+
* These rules result in the following example conversions from upper snakecase names
32+
* to camelcase names.
33+
* <ul>
34+
* <li>"USER_NAME" is converted into "userName"</li>
35+
* <li>"USER______NAME" is converted into "userName"</li>
36+
* <li>"USERNAME" is converted into "username"</li>
37+
* <li>"User__Name" is converted into "userName"</li>
38+
* <li>"_user_name" is converted into "UserName"</li>
39+
* <li>"_user_name_s" is converted into "UserNameS"</li>
40+
* <li>"__Username" is converted into "Username"</li>
41+
* <li>"__username" is converted into "Username"</li>
42+
* <li>"username" is converted into "username"</li>
43+
* <li>"Username" is converted into "username"</li>
44+
* </ul>
45+
*
46+
* @since 2.15
2347
*/
2448
public static class CamelCaseStrategy implements EnumNamingStrategy {
2549

2650
/**
51+
* An intance of {@link CamelCaseStrategy} for reuse.
52+
*
2753
* @since 2.15
2854
*/
29-
public final static CamelCaseStrategy INSTANCE
30-
= new CamelCaseStrategy();
55+
public static final CamelCaseStrategy INSTANCE = new CamelCaseStrategy();
3156

57+
/**
58+
* @since 2.15
59+
*/
3260
@Override
33-
public String translate(String input) {
34-
if (input == null) {
35-
return input;
61+
public String convertEnumToExternalName(String enumName) {
62+
if (enumName == null) {
63+
return null;
3664
}
3765

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;
66+
final String UNDERSCORE = "_";
67+
StringBuilder out = null;
68+
int iterationCnt = 0;
69+
int lastSeparatorIdx = -1;
70+
71+
do {
72+
lastSeparatorIdx = indexIn(enumName, lastSeparatorIdx + 1);
73+
if (lastSeparatorIdx != -1) {
74+
if (iterationCnt == 0) {
75+
out = new StringBuilder(enumName.length() + 4 * UNDERSCORE.length());
76+
out.append(toLowerCase(enumName.substring(iterationCnt, lastSeparatorIdx)));
5277
} else {
53-
wasPrevTranslated = false;
78+
out.append(normalizeWord(enumName.substring(iterationCnt, lastSeparatorIdx)));
5479
}
55-
result.append(c);
56-
resultLength++;
80+
iterationCnt = lastSeparatorIdx + UNDERSCORE.length();
81+
}
82+
} while (lastSeparatorIdx != -1);
83+
84+
if (iterationCnt == 0) {
85+
return toLowerCase(enumName);
86+
}
87+
out.append(normalizeWord(enumName.substring(iterationCnt)));
88+
return out.toString();
89+
}
90+
91+
private static int indexIn(CharSequence sequence, int start) {
92+
int length = sequence.length();
93+
for (int i = start; i < length; i++) {
94+
if ('_' == sequence.charAt(i)) {
95+
return i;
5796
}
5897
}
59-
String output = resultLength > 0 ? result.toString() : input;
60-
return output.toUpperCase();
98+
return -1;
99+
}
100+
101+
private static String normalizeWord(String word) {
102+
int length = word.length();
103+
if (length == 0) {
104+
return word;
105+
}
106+
return new StringBuilder(length)
107+
.append(charToUpperCaseIfLower(word.charAt(0)))
108+
.append(toLowerCase(word.substring(1)))
109+
.toString();
110+
}
111+
112+
private static String toLowerCase(String string) {
113+
int length = string.length();
114+
StringBuilder builder = new StringBuilder(length);
115+
for (int i = 0; i < length; i++) {
116+
builder.append(charToLowerCaseIfUpper(string.charAt(i)));
117+
}
118+
return builder.toString();
119+
}
120+
121+
private static char charToUpperCaseIfLower(char c) {
122+
return Character.isLowerCase(c) ? Character.toUpperCase(c) : c;
123+
}
124+
125+
private static char charToLowerCaseIfUpper(char c) {
126+
return Character.isUpperCase(c) ? Character.toLowerCase(c) : c;
61127
}
62128
}
63129
}
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
11
package com.fasterxml.jackson.databind;
22

3+
/**
4+
* Defines how the string representation of an enum is converted into an external property name for mapping
5+
* during deserialization.
6+
*
7+
* @since 2.15
8+
*/
39
public interface EnumNamingStrategy {
410

5-
public String translate(String value);
11+
/**
12+
* Translates the given <code>enumName</code> into an external property name according to
13+
* the implementation of this {@link EnumNamingStrategy}.
14+
*
15+
* @param enumName the name of the enum value to translate
16+
* @return the external property name that corresponds to the given <code>enumName</code>
17+
* according to the implementation of this {@link EnumNamingStrategy}.
18+
*
19+
* @since 2.15
20+
*/
21+
public String convertEnumToExternalName(String enumName);
622

723
}

src/main/java/com/fasterxml/jackson/databind/annotation/EnumNaming.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,24 @@
77
import java.lang.annotation.RetentionPolicy;
88
import java.lang.annotation.Target;
99

10+
/**
11+
* Annotation that can be used to indicate a {@link EnumNamingStrategy}
12+
* to use for annotated class.
13+
*
14+
* @since 2.15
15+
*/
1016
@Target({ElementType.ANNOTATION_TYPE, ElementType.TYPE})
1117
@Retention(RetentionPolicy.RUNTIME)
1218
@com.fasterxml.jackson.annotation.JacksonAnnotation
1319
public @interface EnumNaming {
14-
public Class<? extends EnumNamingStrategy> value() default EnumNamingStrategy.class;
20+
21+
/**
22+
* @return Type of {@link EnumNamingStrategy} to use, if any. Default value
23+
* of <code>EnumNamingStrategy.class</code> means "no strategy specified"
24+
* (and may also be used for overriding to remove otherwise applicable
25+
* naming strategy)
26+
*
27+
* @since 2.15
28+
*/
29+
public Class<? extends EnumNamingStrategy> value();
1530
}

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

-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import com.fasterxml.jackson.core.*;
1010

1111
import com.fasterxml.jackson.databind.*;
12-
import com.fasterxml.jackson.databind.annotation.EnumNaming;
1312
import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
1413
import com.fasterxml.jackson.databind.cfg.CoercionAction;
1514
import com.fasterxml.jackson.databind.cfg.CoercionInputShape;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.fasterxml.jackson.databind.introspect;
2+
3+
import com.fasterxml.jackson.databind.AnnotationIntrospector;
4+
import com.fasterxml.jackson.databind.EnumNamingStrategy;
5+
import com.fasterxml.jackson.databind.JavaType;
6+
import com.fasterxml.jackson.databind.annotation.EnumNaming;
7+
import com.fasterxml.jackson.databind.cfg.MapperConfig;
8+
import com.fasterxml.jackson.databind.util.ClassUtil;
9+
10+
/**
11+
* Helper class used for aggregating information about all possible
12+
* properties of a Enum.
13+
*
14+
* @since 2.15
15+
*/
16+
public class EnumPropertiesCollector {
17+
private EnumPropertiesCollector() {}
18+
19+
/*
20+
/**********************************************************
21+
/* Public API: main-level collection
22+
/**********************************************************
23+
*/
24+
25+
/**
26+
* Helper method to resolve the an {@link EnumNamingStrategy} for the current {@link MapperConfig}
27+
* using {@link AnnotationIntrospector}.
28+
*
29+
* @return An instance of a subtype of {@link EnumNamingStrategy} specified through
30+
* {@link EnumNaming#value()} or null if current Enum class is not annotated with {@link EnumNaming} or
31+
* {@link EnumNaming#value()} is set to {@link EnumNamingStrategy} interface itself.
32+
* @since 2.15
33+
*/
34+
public static EnumNamingStrategy findEnumNamingStrategy(MapperConfig<?> config, Class<?> handledType) {
35+
AnnotatedClass classDef = _findAnnotatedClass(config, handledType);
36+
Object namingDef = config.getAnnotationIntrospector().findEnumNamingStrategy(classDef);
37+
return _findEnumNamingStrategy(namingDef, config.canOverrideAccessModifiers());
38+
39+
}
40+
41+
/*
42+
/**********************************************************
43+
* Actual Implementation
44+
**********************************************************
45+
*/
46+
47+
/**
48+
* @since 2.15
49+
*/
50+
private static EnumNamingStrategy _findEnumNamingStrategy(Object namingDef, boolean canOverrideAccessModifiers) {
51+
if (namingDef == null) {
52+
return null;
53+
}
54+
55+
if (namingDef instanceof EnumNamingStrategy) {
56+
return (EnumNamingStrategy) namingDef;
57+
}
58+
// Alas, there's no way to force return type of "either class
59+
// X or Y" -- need to throw an exception after the fact
60+
if (!(namingDef instanceof Class)) {
61+
reportProblem("AnnotationIntrospector returned EnumNamingStrategy definition of type %s"
62+
+ "; expected type `Class<EnumNamingStrategy>` instead", ClassUtil.classNameOf(namingDef));
63+
}
64+
65+
Class<?> namingClass = (Class<?>) namingDef;
66+
// 09-Nov-2015, tatu: Need to consider pseudo-value of STD, which means "use default"
67+
if (namingClass == EnumNamingStrategy.class) {
68+
return null;
69+
}
70+
71+
if (!EnumNamingStrategy.class.isAssignableFrom(namingClass)) {
72+
reportProblem("AnnotationIntrospector returned Class %s; expected `Class<EnumNamingStrategy>`",
73+
ClassUtil.classNameOf(namingClass));
74+
}
75+
76+
return (EnumNamingStrategy) ClassUtil.createInstance(namingClass, canOverrideAccessModifiers);
77+
}
78+
79+
/*
80+
*********************************************************
81+
* Internal methods; helpers
82+
**********************************************************
83+
*/
84+
85+
/**
86+
* @since 2.15
87+
*/
88+
protected static AnnotatedClass _findAnnotatedClass(MapperConfig<?> ctxt, Class<?> handledType) {
89+
JavaType javaType = ctxt.constructType(handledType);
90+
return AnnotatedClassResolver.resolve(ctxt, javaType, ctxt);
91+
}
92+
93+
/**
94+
* @since 2.15
95+
*/
96+
protected static void reportProblem(String msg, Object... args) {
97+
if (args.length > 0) {
98+
msg = String.format(msg, args);
99+
}
100+
throw new IllegalArgumentException("Problem with " + msg);
101+
}
102+
103+
104+
}

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

+6
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,12 @@ public Object findNamingStrategy(AnnotatedClass ac)
349349
return (ann == null) ? null : ann.value();
350350
}
351351

352+
@Override
353+
public Object findEnumNamingStrategy(AnnotatedClass ac) {
354+
EnumNaming ann = _findAnnotation(ac, EnumNaming.class);
355+
return (ann == null) ? null : ann.value();
356+
}
357+
352358
@Override
353359
public String findClassDescription(AnnotatedClass ac) {
354360
JsonClassDescription ann = _findAnnotation(ac, JsonClassDescription.class);

0 commit comments

Comments
 (0)