All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fasterxml.jackson.databind.introspect.DefaultAccessorNamingStrategy Maven / Gradle / Ivy

There is a newer version: 4.0.0
Show newest version
package com.fasterxml.jackson.databind.introspect;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.jdk14.JDK14Util;

/**
 * Default {@link AccessorNamingStrategy} used by Jackson: to be used either as-is,
 * or as base-class with overrides.
 *
 * @since 2.12
 */
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 boolean _isGettersNonBoolean;

    protected final String _getterPrefix;

    /**
     * @since 2.14
     */
    protected final String _isGetterPrefix;

    /**
     * Prefix used by auto-detected mutators ("setters"): usually "set",
     * but differs for builder objects ("with" by default).
     */
    protected final String _mutatorPrefix;

    protected DefaultAccessorNamingStrategy(MapperConfig config, AnnotatedClass forClass,
            String mutatorPrefix, String getterPrefix, String isGetterPrefix,
            BaseNameValidator baseNameValidator)
    {
        _config = config;
        _forClass = forClass;

        _stdBeanNaming = config.isEnabled(MapperFeature.USE_STD_BEAN_NAMING);
        _isGettersNonBoolean = config.isEnabled(MapperFeature.ALLOW_IS_GETTERS_FOR_NON_BOOLEAN);
        _mutatorPrefix = mutatorPrefix;
        _getterPrefix = getterPrefix;
        _isGetterPrefix = isGetterPrefix;
        _baseNameValidator = baseNameValidator;
    }
    
    @Override
    public String findNameForIsGetter(AnnotatedMethod am, String name)
    {
        if (_isGetterPrefix != null) {
            final Class rt = am.getRawType();
            if (_isGettersNonBoolean || rt == Boolean.class || rt == Boolean.TYPE) {
                if (name.startsWith(_isGetterPrefix)) {
                    return _stdBeanNaming
                            ? stdManglePropertyName(name, 2)
                            : legacyManglePropertyName(name, 2);
                }
            }
        }
        return null;
    }

    @Override
    public String findNameForRegularGetter(AnnotatedMethod am, String name)
    {
        if ((_getterPrefix != null) && name.startsWith(_getterPrefix)) {
            // 16-Feb-2009, tatu: To handle [JACKSON-53], need to block CGLib-provided
            // method "getCallbacks". Not sure of exact safe criteria to get decent
            // coverage without false matches; but for now let's assume there is
            // no reason to use any such getter from CGLib.
            if ("getCallbacks".equals(name)) {
                if (_isCglibGetCallbacks(am)) {
                    return null;
                }
            } else if ("getMetaClass".equals(name)) {
                // 30-Apr-2009, tatu: Need to suppress serialization of a cyclic reference
                if (_isGroovyMetaClassGetter(am)) {
                    return null;
                }
            }
            return _stdBeanNaming
                    ? stdManglePropertyName(name, _getterPrefix.length())
                    : legacyManglePropertyName(name, _getterPrefix.length());
        }
        return null;
    }

    @Override
    public String findNameForMutator(AnnotatedMethod am, String name)
    {
        if ((_mutatorPrefix != null) && name.startsWith(_mutatorPrefix)) {
            return _stdBeanNaming
                    ? stdManglePropertyName(name, _mutatorPrefix.length())
                    : legacyManglePropertyName(name, _mutatorPrefix.length());
        }
        return null;
    }

    // Default implementation simply returns name as-is
    @Override
    public String modifyFieldName(AnnotatedField field, String name) {
        return name;
    }

    /*
    /**********************************************************************
    /* Name-mangling methods copied in 2.12 from "BeanUtil"
    /**********************************************************************
     */

    /**
     * Method called to figure out name of the property, given 
     * corresponding suggested name based on a method or field name.
     *
     * @param basename Name of accessor/mutator method, not including prefix
     *  ("get"/"is"/"set")
     */
    protected String legacyManglePropertyName(final String basename, final int offset)
    {
        final int end = basename.length();
        if (end == offset) { // empty name, nope
            return null;
        }
        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) {
            return basename.substring(offset);
        }
        // otherwise, lower case initial chars. Common case first, just one char
        StringBuilder sb = new StringBuilder(end - offset);
        sb.append(d);
        int i = offset+1;
        for (; i < end; ++i) {
            c = basename.charAt(i);
            d = Character.toLowerCase(c);
            if (c == d) {
                sb.append(basename, i, end);
                break;
            }
            sb.append(d);
        }
        return sb.toString();
    }

    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);
        }
        // 17-Dec-2014, tatu: As per [databind#653], need to follow more
        //   closely Java Beans spec; specifically, if two first are upper-case,
        //   then no lower-casing should be done.
        if ((offset + 1) < end) {
            if (Character.isUpperCase(basename.charAt(offset+1))) {
                return basename.substring(offset);
            }
        }
        StringBuilder sb = new StringBuilder(end - offset);
        sb.append(c1);
        sb.append(basename, offset+1, end);
        return sb.toString();
    }

    /*
    /**********************************************************************
    /* Legacy methods moved in 2.12 from "BeanUtil" -- are these still needed?
    /**********************************************************************
     */

    // This method was added to address the need to weed out CGLib-injected
    // "getCallbacks" method. 
    // At this point caller has detected a potential getter method with
    // name "getCallbacks" and we need to determine if it is indeed injected
    // by Cglib. We do this by verifying that the  result type is "net.sf.cglib.proxy.Callback[]"
    protected boolean _isCglibGetCallbacks(AnnotatedMethod am)
    {
        Class rt = am.getRawType();
        // Ok, first: must return an array type
        if (rt.isArray()) {
            // And that type needs to be "net.sf.cglib.proxy.Callback".
            // Theoretically could just be a type that implements it, but
            // for now let's keep things simple, fix if need be.

            Class compType = rt.getComponentType();
            // Actually, let's just verify it's a "net.sf.cglib.*" class/interface
            final String className = compType.getName();
            if (className.contains(".cglib")) {
                return className.startsWith("net.sf.cglib")
                    // also, as per [JACKSON-177]
                    || className.startsWith("org.hibernate.repackage.cglib")
                    // and [core#674]
                    || className.startsWith("org.springframework.cglib");
            }
        }
        return false;
    }

    // Another helper method to deal with Groovy's problematic metadata accessors
    protected boolean _isGroovyMetaClassGetter(AnnotatedMethod am) {
        return am.getRawType().getName().startsWith("groovy.lang");
    }

    /*
    /**********************************************************************
    /* Standard Provider implementation
    /**********************************************************************
     */

    /**
     * Provider for {@link DefaultAccessorNamingStrategy}.
     *

* Default instance will use following default prefixes: *

    *
  • Setter for regular POJOs: "set" *
  • *
  • Builder-mutator: "with" *
  • *
  • Regular getter: "get" *
  • *
  • Is-getter (for Boolean values): "is" *
  • *
* 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 implements java.io.Serializable { private static final long serialVersionUID = 1L; protected final String _setterPrefix; protected final String _withPrefix; protected final String _getterPrefix; protected final String _isGetterPrefix; protected final BaseNameValidator _baseNameValidator; public Provider() { this("set", JsonPOJOBuilder.DEFAULT_WITH_PREFIX, "get", "is", null); } protected Provider(Provider p, String setterPrefix, String withPrefix, String getterPrefix, String isGetterPrefix) { this(setterPrefix, withPrefix, getterPrefix, isGetterPrefix, p._baseNameValidator); } protected Provider(Provider p, BaseNameValidator vld) { this(p._setterPrefix, p._withPrefix, p._getterPrefix, p._isGetterPrefix, vld); } protected Provider(String setterPrefix, String withPrefix, String getterPrefix, String isGetterPrefix, BaseNameValidator vld) { _setterPrefix = setterPrefix; _withPrefix = withPrefix; _getterPrefix = getterPrefix; _isGetterPrefix = isGetterPrefix; _baseNameValidator = vld; } /** * Mutant factory for changing the prefix used for "setter" * methods * * @param prefix Prefix to use; or empty String {@code ""} to not use * any prefix (meaning signature-compatible method name is used as * the property basename (and subject to name mangling)), * or {@code null} to prevent name-based detection. * * @return Provider instance with specified setter-prefix */ public Provider withSetterPrefix(String prefix) { return new Provider(this, prefix, _withPrefix, _getterPrefix, _isGetterPrefix); } /** * Mutant factory for changing the prefix used for Builders * (from default {@link JsonPOJOBuilder#DEFAULT_WITH_PREFIX}) * * @param prefix Prefix to use; or empty String {@code ""} to not use * any prefix (meaning signature-compatible method name is used as * the property basename (and subject to name mangling)), * or {@code null} to prevent name-based detection. * * @return Provider instance with specified with-prefix */ public Provider withBuilderPrefix(String prefix) { return new Provider(this, _setterPrefix, prefix, _getterPrefix, _isGetterPrefix); } /** * Mutant factory for changing the prefix used for "getter" * methods * * @param prefix Prefix to use; or empty String {@code ""} to not use * any prefix (meaning signature-compatible method name is used as * the property basename (and subject to name mangling)), * or {@code null} to prevent name-based detection. * * @return Provider instance with specified getter-prefix */ public Provider withGetterPrefix(String prefix) { return new Provider(this, _setterPrefix, _withPrefix, prefix, _isGetterPrefix); } /** * Mutant factory for changing the prefix used for "is-getter" * methods (getters that return boolean/Boolean value). * * @param prefix Prefix to use; or empty String {@code ""} to not use * any prefix (meaning signature-compatible method name is used as * the property basename (and subject to name mangling)). * or {@code null} to prevent name-based detection. * * @return Provider instance with specified is-getter-prefix */ public Provider withIsGetterPrefix(String prefix) { return new Provider(this, _setterPrefix, _withPrefix, _getterPrefix, prefix); } /** * Mutant factory for changing the rules regarding which characters * are allowed as the first character of property base name, after * checking and removing prefix. *

* For example, consider "getter" method candidate (no arguments, has return * type) named {@code getValue()} is considered, with "getter-prefix" * defined as {@code get}, then base name is {@code Value} and the * first character to consider is {@code V}. Upper-case letters are * always accepted so this is fine. * But with similar settings, method {@code get_value()} would only be * recognized as getter if {@code allowNonLetterFirstChar} is set to * {@code true}: otherwise it will not be considered a getter-method. * Similarly "is-getter" candidate method with name {@code island()} * would only be considered if {@code allowLowerCaseFirstChar} is set * to {@code true}. * * @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 Provider instance with specified validity rules */ public Provider withFirstCharAcceptance(boolean allowLowerCaseFirstChar, boolean 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, _baseNameValidator); } @Override public AccessorNamingStrategy forBuilder(MapperConfig config, AnnotatedClass builderClass, BeanDescription valueTypeDesc) { AnnotationIntrospector ai = config.isAnnotationProcessingEnabled() ? config.getAnnotationIntrospector() : null; JsonPOJOBuilder.Value builderConfig = (ai == null) ? null : ai.findPOJOBuilderConfig(builderClass); String mutatorPrefix = (builderConfig == null) ? _withPrefix : builderConfig.withPrefix; return new DefaultAccessorNamingStrategy(config, builderClass, mutatorPrefix, _getterPrefix, _isGetterPrefix, _baseNameValidator); } @Override public AccessorNamingStrategy forRecord(MapperConfig config, AnnotatedClass recordClass) { return new RecordNaming(config, recordClass); } } /** * Simple implementation of {@link BaseNameValidator} that checks the * first character and nothing else. *

* 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 * accessors for declared record fields. *

* Current / initial implementation will also recognize additional "normal" * getters ("get"-prefix) and is-getters ("is"-prefix and boolean return value) * by name. */ public static class RecordNaming extends DefaultAccessorNamingStrategy { /** * Names of actual Record fields from definition; auto-detected. */ protected final Set _fieldNames; public RecordNaming(MapperConfig config, AnnotatedClass forClass) { super(config, forClass, // no setters for (immutable) Records: null, // trickier: regular fields are ok (handled differently), but should // we also allow getter discovery? For now let's do so "get", "is", null); String[] recordFieldNames = JDK14Util.getRecordFieldNames(forClass.getRawType()); // 01-May-2022, tatu: Due to [databind#3417] may return null when no info available _fieldNames = recordFieldNames == null ? Collections.emptySet() : new HashSet<>(Arrays.asList(recordFieldNames)); } @Override public String findNameForRegularGetter(AnnotatedMethod am, String name) { // By default, field names are un-prefixed, but verify so that we will not // include "toString()" or additional custom methods (unless latter are // annotated for inclusion) if (_fieldNames.contains(name)) { return name; } // but also allow auto-detecting additional getters, if any? return super.findNameForRegularGetter(am, name); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy