Skip to content

Commit

Permalink
Finalizing aspects of #2800 and #2624 resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
cowtowncoder committed Oct 13, 2020
1 parent 63af35e commit 1a30aea
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,25 @@
public class DefaultAccessorNamingStrategy
extends AccessorNamingStrategy
{
/**
* Definition of a handler API to use for checking whether given base name
* (remainder of accessor method name after removing prefix) is acceptable
* based on various rules.
*
* @since 2.12
*/
public interface BaseNameValidator {
public boolean accept(char firstChar, String basename, int offset);
}

protected final MapperConfig<?> _config;
protected final AnnotatedClass _forClass;

/**
* Optional validator for checking that base name
*/
protected final BaseNameValidator _baseNameValidator;

protected final boolean _stdBeanNaming;

protected final String _getterPrefix;
Expand All @@ -34,7 +50,8 @@ public class DefaultAccessorNamingStrategy
protected final String _mutatorPrefix;

protected DefaultAccessorNamingStrategy(MapperConfig<?> config, AnnotatedClass forClass,
String mutatorPrefix, String getterPrefix, String isGetterPrefix)
String mutatorPrefix, String getterPrefix, String isGetterPrefix,
BaseNameValidator baseNameValidator)
{
_config = config;
_forClass = forClass;
Expand All @@ -43,6 +60,7 @@ protected DefaultAccessorNamingStrategy(MapperConfig<?> config, AnnotatedClass f
_mutatorPrefix = mutatorPrefix;
_getterPrefix = getterPrefix;
_isGetterPrefix = isGetterPrefix;
_baseNameValidator = baseNameValidator;
}

@Override
Expand Down Expand Up @@ -116,14 +134,22 @@ public String modifyFieldName(AnnotatedField field, String name) {
* @param basename Name of accessor/mutator method, not including prefix
* ("get"/"is"/"set")
*/
protected static String legacyManglePropertyName(final String basename, final int offset)
protected String legacyManglePropertyName(final String basename, final int offset)
{
final int end = basename.length();
if (end == offset) { // empty name, nope
return null;
}
// next check: is the first character upper case? If not, return as is
char c = basename.charAt(offset);
// 12-Oct-2020, tatu: Additional configurability; allow checking that
// base name is acceptable (currently just by checking first character)
if (_baseNameValidator != null) {
if (!_baseNameValidator.accept(c, basename, offset)) {
return null;
}
}

// next check: is the first character upper case? If not, return as is
char d = Character.toLowerCase(c);

if (c == d) {
Expand All @@ -145,14 +171,22 @@ protected static String legacyManglePropertyName(final String basename, final in
return sb.toString();
}

protected static String stdManglePropertyName(final String basename, final int offset)
protected String stdManglePropertyName(final String basename, final int offset)
{
final int end = basename.length();
if (end == offset) { // empty name, nope
return null;
}
// first: if it doesn't start with capital, return as-is
char c0 = basename.charAt(offset);
// 12-Oct-2020, tatu: Additional configurability; allow checking that
// base name is acceptable (currently just by checking first character)
if (_baseNameValidator != null) {
if (!_baseNameValidator.accept(c0, basename, offset)) {
return null;
}
}

char c1 = Character.toLowerCase(c0);
if (c0 == c1) {
return basename.substring(offset);
Expand Down Expand Up @@ -230,8 +264,9 @@ protected boolean _isGroovyMetaClassGetter(AnnotatedMethod am) {
* <li>Is-getter (for Boolean values): "is"
* </li>
* <ul>
*<p>
*
* and no additional restrictions on base names accepted (configurable for
* limits using {@link BaseNameValidator}), allowing names like
* "get_value()" and "getvalue()".
*/
public static class Provider
extends AccessorNamingStrategy.Provider
Expand All @@ -245,44 +280,38 @@ public static class Provider
protected final String _getterPrefix;
protected final String _isGetterPrefix;

protected final boolean _allowLowerCaseFirstChar;
protected final boolean _allowNonLetterFirstChar;
protected final BaseNameValidator _baseNameValidator;

public Provider() {
this("set", JsonPOJOBuilder.DEFAULT_WITH_PREFIX,
"get", "is",
true, true);
"get", "is", null);
}

protected Provider(Provider p,
String setterPrefix, String withPrefix,
String getterPrefix, String isGetterPrefix)
{
this(setterPrefix, withPrefix, getterPrefix, isGetterPrefix,
p._allowLowerCaseFirstChar, p._allowNonLetterFirstChar);
p._baseNameValidator);
}

protected Provider(Provider p,
boolean allowLowerCaseFirstChar, boolean allowNonLetterFirstChar)
protected Provider(Provider p, BaseNameValidator vld)
{
this(p._setterPrefix, p._withPrefix,
p._getterPrefix, p._isGetterPrefix,
allowLowerCaseFirstChar, allowNonLetterFirstChar);
p._getterPrefix, p._isGetterPrefix, vld);
}

protected Provider(String setterPrefix, String withPrefix,
String getterPrefix, String isGetterPrefix,
boolean allowLowerCaseFirstChar, boolean allowNonLetterFirstChar)
BaseNameValidator vld)
{
_setterPrefix = setterPrefix;
_withPrefix = withPrefix;
_getterPrefix = getterPrefix;
_isGetterPrefix = isGetterPrefix;
_allowLowerCaseFirstChar = allowLowerCaseFirstChar;
_allowNonLetterFirstChar = allowNonLetterFirstChar;
_baseNameValidator = vld;
}



/**
* Mutant factory for changing the prefix used for "setter"
* methods
Expand Down Expand Up @@ -371,19 +400,34 @@ public Provider withIsGetterPrefix(String prefix) {
* character (like {@code "_"} or number {@code 1}) are accepted as valid or not:
* consider difference between "setter-methods" {@code setValue()} and {@code set_value()}.
*
* @return Provider instance with specified is-getter-prefix
* @return Provider instance with specified validity rules
*/
public Provider withFirstCharAcceptance(boolean allowLowerCaseFirstChar,
boolean allowNonLetterFirstChar) {
return new Provider(this,
allowLowerCaseFirstChar, allowNonLetterFirstChar);
return withBaseNameValidator(
FirstCharBasedValidator.forFirstNameRule(allowLowerCaseFirstChar, allowNonLetterFirstChar));
}

/**
* Mutant factory for specifying validator that is used to further verify that
* base name derived from accessor name is acceptable: this can be used to add
* further restrictions such as limit that the first character of the base name
* is an upper-case letter.
*
* @param vld Validator to use, if any; {@code null} to indicate no additional rules
*
* @return Provider instance with specified base name validator to use, if any
*/
public Provider withBaseNameValidator(BaseNameValidator vld) {
return new Provider(this, vld);
}

@Override
public AccessorNamingStrategy forPOJO(MapperConfig<?> config, AnnotatedClass targetClass)
{
return new DefaultAccessorNamingStrategy(config, targetClass,
_setterPrefix, _getterPrefix, _isGetterPrefix);
_setterPrefix, _getterPrefix, _isGetterPrefix,
_baseNameValidator);
}

@Override
Expand All @@ -394,7 +438,8 @@ public AccessorNamingStrategy forBuilder(MapperConfig<?> config,
JsonPOJOBuilder.Value builderConfig = (ai == null) ? null : ai.findPOJOBuilderConfig(builderClass);
String mutatorPrefix = (builderConfig == null) ? _withPrefix : builderConfig.withPrefix;
return new DefaultAccessorNamingStrategy(config, builderClass,
mutatorPrefix, _getterPrefix, _isGetterPrefix);
mutatorPrefix, _getterPrefix, _isGetterPrefix,
_baseNameValidator);
}

@Override
Expand All @@ -404,6 +449,60 @@ public AccessorNamingStrategy forRecord(MapperConfig<?> config, AnnotatedClass r
}
}

/**
* Simple implementation of {@link BaseNameValidator} that checks the
* first character and nothing else.
*<p>
* Instances are to be constructed using method
* {@link FirstCharBasedValidator#forFirstNameRule}.
*/
public static class FirstCharBasedValidator
implements BaseNameValidator
{
private final boolean _allowLowerCaseFirstChar;
private final boolean _allowNonLetterFirstChar;

protected FirstCharBasedValidator(boolean allowLowerCaseFirstChar,
boolean allowNonLetterFirstChar) {
_allowLowerCaseFirstChar = allowLowerCaseFirstChar;
_allowNonLetterFirstChar = allowNonLetterFirstChar;
}

/**
* Factory method to use for getting an instance with specified first-character
* restrictions, if any; or {@code null} if no checking is needed.
*
* @param allowLowerCaseFirstChar Whether base names that start with lower-case
* letter (like {@code "a"} or {@code "b"}) are accepted as valid or not:
* consider difference between "setter-methods" {@code setValue()} and {@code setvalue()}.
* @param allowNonLetterFirstChar Whether base names that start with non-letter
* character (like {@code "_"} or number {@code 1}) are accepted as valid or not:
* consider difference between "setter-methods" {@code setValue()} and {@code set_value()}.
*
* @return Validator instance to use, if any; {@code null} to indicate no additional
* rules applied (case when both arguments are {@code false})
*/
public static BaseNameValidator forFirstNameRule(boolean allowLowerCaseFirstChar,
boolean allowNonLetterFirstChar) {
if (!allowLowerCaseFirstChar && !allowNonLetterFirstChar) {
return null;
}
return new FirstCharBasedValidator(allowLowerCaseFirstChar,
allowNonLetterFirstChar);
}

@Override
public boolean accept(char firstChar, String basename, int offset) {
// Ok, so... If UTF-16 letter, then check whether lc allowed
// (title-case and upper-case both assumed to be acceptable by default)
if (Character.isLetter(firstChar)) {
return _allowLowerCaseFirstChar || !Character.isLowerCase(firstChar);
}
// Otherwise, non-letter checking applied
return _allowNonLetterFirstChar;
}
}

/**
* Implementation used for supporting "non-prefix" naming convention of
* Java 14 {@code java.lang.Record} types, and in particular find default
Expand All @@ -427,7 +526,7 @@ public RecordNaming(MapperConfig<?> config, AnnotatedClass forClass) {
null,
// trickier: regular fields are ok (handled differently), but should
// we also allow getter discovery? For now let's do so
"get", "is");
"get", "is", null);
_fieldNames = new HashSet<>();
for (String name : JDK14Util.getRecordFieldNames(forClass.getRawType())) {
_fieldNames.add(name);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ static class MixedBean2800_X {
public int getY() { return 3; }
}

// Bean for checking optional
@JsonPropertyOrder(alphabetic = true)
static class FirstLetterVariesBean {
// Do we allow lower-case letter as first letter following prefix?
public boolean island() { return true; }

// Do we allow non-letter
public int get4Roses() { return 42; }

// But good old upper-case letter is solid always...
public int getValue() { return 31337; }
}

static class AccNaming2800Underscore extends AccessorNamingStrategy
{
@Override
Expand Down Expand Up @@ -177,4 +190,48 @@ public void testBaseAccessorCustomSetter() throws Exception
SetterBean2800_Y result = mapper.readValue(a2q("{'y':42}"), SetterBean2800_Y.class);
assertEquals(42, result.yyy);
}

/*
public boolean island() { return true; }
// Do we allow non-letter
public int get_lost() { return 42; }
// But good old upper-case letter is solid always...
public int getValue() { return 31337; }
*/

public void testFirstLetterConfigs() throws Exception
{
final FirstLetterVariesBean input = new FirstLetterVariesBean();
final String STD_EXP = a2q("{'4Roses':42,'land':true,'value':31337}");

// First: vanilla? About anything goes
ObjectMapper mapper = newJsonMapper();
assertEquals(STD_EXP, mapper.writeValueAsString(input));

// also if explicitly configured as default:
mapper = JsonMapper.builder()
.accessorNaming(new DefaultAccessorNamingStrategy.Provider()
.withFirstCharAcceptance(true, true))
.build();
assertEquals(STD_EXP, mapper.writeValueAsString(input));

// But we can vary it
mapper = JsonMapper.builder()
.accessorNaming(new DefaultAccessorNamingStrategy.Provider()
// lower-case = ok; non-letter = not ok
.withFirstCharAcceptance(true, false))
.build();
assertEquals(a2q("{'land':true,'value':31337}"),
mapper.writeValueAsString(input));

mapper = JsonMapper.builder()
.accessorNaming(new DefaultAccessorNamingStrategy.Provider()
// lower-case = not ok; non-letter = ok
.withFirstCharAcceptance(false, true))
.build();
assertEquals(a2q("{'4Roses':42,'value':31337}"),
mapper.writeValueAsString(input));
}
}

0 comments on commit 1a30aea

Please sign in to comment.