Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature : Support naming strategy for enums #3792

Merged
merged 14 commits into from
Mar 15, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,21 @@ public JsonIncludeProperties.Value findPropertyInclusionByName(MapperConfig<?> c
*/
public Object findNamingStrategy(AnnotatedClass ac) { return null; }

/**
* 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 or instance of {@link EnumNamingStrategy}, if one
* is specified for given class; null if not.
*
* @since 2.15
*/
public Object findEnumNamingStrategy(AnnotatedClass ac) { return null; }
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved

/**
* Method used to check whether specified class defines a human-readable
* description to use for documentation.
Expand Down
129 changes: 129 additions & 0 deletions src/main/java/com/fasterxml/jackson/databind/EnumNamingStrategies.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.fasterxml.jackson.databind;

/**
* A container class for implementations of the {@link EnumNamingStrategy} interface.
*
* @since 2.15
*/
public class EnumNamingStrategies {
cowtowncoder marked this conversation as resolved.
Show resolved Hide resolved

private EnumNamingStrategies() {}

/**
* <p>
* 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.
*
* <ol>
* <li>converts any character preceded by an underscore into upper case character,
* regardless of its original case (upper or lower).</li>
* <li>converts any character NOT preceded by an underscore into a lower case character,
* regardless of its original case (upper or lower).</li>
* <li>converts contiguous sequence of underscores into a single underscore.</li>
* </ol>
*
* WARNING: Naming conversion conflicts caused by underscore usage should be handled by client.
* e.g. Both <code>PEANUT_BUTTER</code>, <code>PEANUT__BUTTER</code> are converted into "peanutButter".
* And "peanutButter" will be deserialized into enum with smaller <code>Enum.ordinal()</code> value.
*
* <p>
* These rules result in the following example conversions from upper snakecase names
* to camelcase names.
* <ul>
* <li>"USER_NAME" is converted into "userName"</li>
* <li>"USER______NAME" is converted into "userName"</li>
* <li>"USERNAME" is converted into "username"</li>
* <li>"User__Name" is converted into "userName"</li>
* <li>"_user_name" is converted into "UserName"</li>
* <li>"_user_name_s" is converted into "UserNameS"</li>
* <li>"__Username" is converted into "Username"</li>
* <li>"__username" is converted into "Username"</li>
* <li>"username" is converted into "username"</li>
* <li>"Username" is converted into "username"</li>
* </ul>
*
* @since 2.15
*/
public static class CamelCaseStrategy implements EnumNamingStrategy {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation of convertEnumToExternalName here is from Guava's com.google.base.CaseFormat utility class.


/**
* An intance of {@link CamelCaseStrategy} for reuse.
*
* @since 2.15
*/
public static final CamelCaseStrategy INSTANCE = new CamelCaseStrategy();

/**
* @since 2.15
*/
@Override
public String convertEnumToExternalName(String enumName) {
if (enumName == null) {
return null;
}

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 {
out.append(normalizeWord(enumName.substring(iterationCnt, lastSeparatorIdx)));
}
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;
}
}
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +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 {

/**
* Translates the given <code>enumName</code> 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 <code>enumName</code>
* according to the implementation of this {@link EnumNamingStrategy}.
*
* @since 2.15
*/
public String convertEnumToExternalName(String enumName);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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;

/**
* 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 {

/**
* @return Type of {@link EnumNamingStrategy} to use, if any. Default value
* of <code>EnumNamingStrategy.class</code> means "no strategy specified"
* (and may also be used for overriding to remove otherwise applicable
* naming strategy)
*
* @since 2.15
*/
public Class<? extends EnumNamingStrategy> value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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())
);
}
}
Expand Down Expand Up @@ -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()) {
Expand All @@ -1935,15 +1937,15 @@ 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 "
+enumClass.getName()+")");
}
}
// Also, need to consider @JsonValue, if one found
return StdKeyDeserializers.constructEnumKeyDeserializer(enumRes);
return StdKeyDeserializers.constructEnumKeyDeserializer(enumRes, byEnumNamingResolver);
}

/*
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,36 @@ public class EnumDeserializer
*/
protected final boolean _isFromIntValue;

/**
* Look up map with <b>key</b> as <code>Enum.name()</code> converted by
* {@link EnumNamingStrategy#convertEnumToExternalName(String)}
* and <b>value</b> as Enums.
*
* @since 2.15
*/
protected final CompactStringObjectMap _lookupByEnumNaming;

/**
* @since 2.9
*/
public EnumDeserializer(EnumResolver byNameResolver, Boolean caseInsensitive)
{
this(byNameResolver, caseInsensitive, null);
}

/**
* @since 2.15
*/
public EnumDeserializer(EnumResolver byNameResolver, boolean caseInsensitive,
EnumResolver byEnumNamingResolver)
{
super(byNameResolver.getEnumClass());
_lookupByName = byNameResolver.constructLookup();
_enumsByIndex = byNameResolver.getRawEnums();
_enumDefaultValue = byNameResolver.getDefaultValue();
_caseInsensitive = caseInsensitive;
_isFromIntValue = byNameResolver.isFromIntValue();
_lookupByEnumNaming = byEnumNamingResolver == null ? null : byEnumNamingResolver.constructLookup();
}

/**
Expand All @@ -92,6 +111,7 @@ protected EnumDeserializer(EnumDeserializer base, Boolean caseInsensitive,
_isFromIntValue = base._isFromIntValue;
_useDefaultValueForUnknownEnum = useDefaultValueForUnknownEnum;
_useNullForUnknownEnum = useNullForUnknownEnum;
_lookupByEnumNaming = base._lookupByEnumNaming;
}

/**
Expand Down Expand Up @@ -251,8 +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;
CompactStringObjectMap lookup = _resolveCurrentLookup(ctxt);
Object result = lookup.find(text);
if (result == null) {
String trimmed = text.trim();
Expand All @@ -263,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
Expand Down
Loading