SimpleBeanPropertyDefinitionTest.java

package tools.jackson.databind.util;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.annotation.JsonAlias;
import com.fasterxml.jackson.annotation.JsonInclude;

import tools.jackson.databind.*;
import tools.jackson.databind.introspect.*;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.testutil.DatabindTestUtil;
import tools.jackson.databind.type.TypeFactory;

import static org.junit.jupiter.api.Assertions.*;

public class SimpleBeanPropertyDefinitionTest extends DatabindTestUtil
{
    static class SimpleBean {
        public String name;

        public SimpleBean(String name) {
            this.name = name;
        }

        public String getName() { return name; }

        public void setName(String name) { this.name = name; }
    }

    static class AliasedBean {
        @JsonAlias({ "nm", "fullName" })
        public String name;
    }

    private final ObjectMapper MAPPER = newJsonMapper();

    /*
    /**********************************************************************
    /* Helper methods
    /**********************************************************************
     */

    private AnnotatedClass _resolveAnnotatedClass(Class<?> cls) {
        DeserializationConfig config = MAPPER.deserializationConfig();
        JavaType type = MAPPER.constructType(cls);
        return AnnotatedClassResolver.resolve(config, type, config);
    }

    private AnnotatedField _fieldOf(Class<?> cls) {
        return _resolveAnnotatedClass(cls).fields().iterator().next();
    }

    private AnnotatedMethod _getterOf(Class<?> cls, String name) {
        return _resolveAnnotatedClass(cls).findMethod(name, new Class<?>[0]);
    }

    private AnnotatedMethod _setterOf(Class<?> cls, String name, Class<?>... paramTypes) {
        return _resolveAnnotatedClass(cls).findMethod(name, paramTypes);
    }

    private AnnotatedParameter _constructorParamOf(Class<?> cls, int index) {
        return _resolveAnnotatedClass(cls).getConstructors().get(0).getParameter(index);
    }

    private DeserializationConfig _config() {
        return MAPPER.deserializationConfig();
    }

    /*
    /**********************************************************************
    /* Test methods: construction / factory methods
    /**********************************************************************
     */

    @Test
    public void testConstructWithMember() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field);
        assertEquals("name", prop.getName());
        assertSame(field, prop.getPrimaryMember());
    }

    @Test
    public void testConstructWithName() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        PropertyName pn = PropertyName.construct("foo");
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, pn);
        assertEquals("foo", prop.getName());
    }

    @Test
    public void testConstructWithIncludeNull() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("x"), null,
                (JsonInclude.Include) null);
        assertNotNull(prop.findInclusion());
    }

    @Test
    public void testConstructWithIncludeUseDefaults() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("x"), null,
                JsonInclude.Include.USE_DEFAULTS);
        assertNotNull(prop.findInclusion());
    }

    @Test
    public void testConstructWithIncludeNonNull() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("x"), null,
                JsonInclude.Include.NON_NULL);
        assertEquals(JsonInclude.Include.NON_NULL,
                prop.findInclusion().getValueInclusion());
    }

    @Test
    public void testConstructWithNullMetadata() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("x"), null,
                (JsonInclude.Value) null);
        // null metadata should default to STD_OPTIONAL
        assertEquals(PropertyMetadata.STD_OPTIONAL, prop.getMetadata());
    }

    @Test
    public void testConstructWithExplicitMetadata() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        PropertyMetadata md = PropertyMetadata.STD_REQUIRED;
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("x"), md,
                (JsonInclude.Value) null);
        assertEquals(PropertyMetadata.STD_REQUIRED, prop.getMetadata());
    }

    /*
    /**********************************************************************
    /* Test methods: fluent factories
    /**********************************************************************
     */

    @Test
    public void testWithSimpleNameSameName() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("foo"));
        BeanPropertyDefinition same = prop.withSimpleName("foo");
        assertSame(prop, same);
    }

    @Test
    public void testWithSimpleNameDifferentName() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("foo"));
        BeanPropertyDefinition diff = prop.withSimpleName("bar");
        assertNotSame(prop, diff);
        assertEquals("bar", diff.getName());
    }

    @Test
    public void testWithSimpleNameHasNamespace() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        // PropertyName with a namespace
        PropertyName nameWithNs = PropertyName.construct("foo", "http://ns");
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, nameWithNs);
        // Same simple name but has namespace, so should create new instance
        BeanPropertyDefinition diff = prop.withSimpleName("foo");
        assertNotSame(prop, diff);
        assertEquals("foo", diff.getName());
    }

    @Test
    public void testWithNameSameName() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        PropertyName pn = PropertyName.construct("foo");
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, pn);
        BeanPropertyDefinition same = prop.withName(pn);
        assertSame(prop, same);
    }

    @Test
    public void testWithNameDifferentName() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("foo"));
        PropertyName newName = PropertyName.construct("bar");
        BeanPropertyDefinition diff = prop.withName(newName);
        assertNotSame(prop, diff);
        assertEquals("bar", diff.getName());
    }

    @Test
    public void testWithMetadataSame() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        PropertyMetadata md = PropertyMetadata.STD_REQUIRED;
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("x"), md,
                (JsonInclude.Value) null);
        BeanPropertyDefinition same = prop.withMetadata(PropertyMetadata.STD_REQUIRED);
        assertSame(prop, same);
    }

    @Test
    public void testWithMetadataDifferent() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("x"), PropertyMetadata.STD_REQUIRED,
                (JsonInclude.Value) null);
        BeanPropertyDefinition diff = prop.withMetadata(PropertyMetadata.STD_OPTIONAL);
        assertNotSame(prop, diff);
    }

    @Test
    public void testWithInclusionSame() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        JsonInclude.Value incl = JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, null);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("x"), null, incl);
        BeanPropertyDefinition same = prop.withInclusion(incl);
        assertSame(prop, same);
    }

    @Test
    public void testWithInclusionDifferent() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        JsonInclude.Value incl1 = JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, null);
        JsonInclude.Value incl2 = JsonInclude.Value.construct(JsonInclude.Include.NON_EMPTY, null);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("x"), null, incl1);
        BeanPropertyDefinition diff = prop.withInclusion(incl2);
        assertNotSame(prop, diff);
    }

    /*
    /**********************************************************************
    /* Test methods: basic property info
    /**********************************************************************
     */

    @Test
    public void testGetName() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("myProp"));
        assertEquals("myProp", prop.getName());
        assertEquals("myProp", prop.getInternalName());
    }

    @Test
    public void testGetFullName() {
        PropertyName pn = PropertyName.construct("myProp");
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, pn);
        assertEquals(pn, prop.getFullName());
    }

    @Test
    public void testHasName() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        PropertyName pn = PropertyName.construct("myProp");
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, pn);
        assertTrue(prop.hasName(PropertyName.construct("myProp")));
        assertFalse(prop.hasName(PropertyName.construct("other")));
    }

    @Test
    public void testIsExplicitlyIncludedAndNamed() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field);
        assertFalse(prop.isExplicitlyIncluded());
        assertFalse(prop.isExplicitlyNamed());
    }

    @Test
    public void testGetWrapperNameWithMember() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field);
        // No @JsonProperty(wrapperName) on the field, should return null
        PropertyName wn = prop.getWrapperName();
        assertNull(wn);
    }

    @Test
    public void testGetPrimaryTypeWithMember() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field);
        JavaType type = prop.getPrimaryType();
        assertEquals(String.class, type.getRawClass());
    }

    @Test
    public void testGetPrimaryTypeWithNullMember() {
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), null, PropertyName.construct("virtual"), null,
                (JsonInclude.Value) null);
        JavaType type = prop.getPrimaryType();
        // Null member should return unknownType
        assertEquals(TypeFactory.unknownType(), type);
    }

    @Test
    public void testGetRawPrimaryTypeWithMember() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field);
        assertEquals(String.class, prop.getRawPrimaryType());
    }

    @Test
    public void testGetRawPrimaryTypeWithNullMember() {
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), null, PropertyName.construct("virtual"), null,
                (JsonInclude.Value) null);
        assertEquals(Object.class, prop.getRawPrimaryType());
    }

    @Test
    public void testFindAliasesNoMember() {
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), null, PropertyName.construct("virtual"), null,
                (JsonInclude.Value) null);
        List<PropertyName> aliases = prop.findAliases();
        assertEquals(Collections.emptyList(), aliases);
    }

    @Test
    public void testFindAliasesWithMember() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field);
        // No aliases defined on the field
        List<PropertyName> aliases = prop.findAliases();
        assertEquals(Collections.emptyList(), aliases);
    }

    /*
    /**********************************************************************
    /* Test methods: accessor detection with field member
    /**********************************************************************
     */

    @Test
    public void testAccessorsWithFieldMember() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field);

        // Field-based: hasField true, hasGetter/hasSetter/hasConstructorParameter false
        assertTrue(prop.hasField());
        assertFalse(prop.hasGetter());
        assertFalse(prop.hasSetter());
        assertFalse(prop.hasConstructorParameter());

        assertSame(field, prop.getField());
        assertNull(prop.getGetter());
        assertNull(prop.getSetter());
        assertNull(prop.getConstructorParameter());

        // getConstructorParameters should return empty iterator
        Iterator<AnnotatedParameter> it = prop.getConstructorParameters();
        assertFalse(it.hasNext());
    }

    /*
    /**********************************************************************
    /* Test methods: accessor detection with getter member
    /**********************************************************************
     */

    @Test
    public void testAccessorsWithGetterMember() {
        AnnotatedMethod getter = _getterOf(SimpleBean.class, "getName");
        assertNotNull(getter);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), getter);

        // Getter-based: hasGetter true, rest false
        assertTrue(prop.hasGetter());
        assertFalse(prop.hasSetter());
        assertFalse(prop.hasField());
        assertFalse(prop.hasConstructorParameter());

        assertSame(getter, prop.getGetter());
        assertNull(prop.getSetter());
        assertNull(prop.getField());
        assertNull(prop.getConstructorParameter());
    }

    /*
    /**********************************************************************
    /* Test methods: accessor detection with setter member
    /**********************************************************************
     */

    @Test
    public void testAccessorsWithSetterMember() {
        AnnotatedMethod setter = _setterOf(SimpleBean.class, "setName", String.class);
        assertNotNull(setter);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), setter);

        // Setter-based: hasSetter true, hasGetter false (1 param = not a getter)
        assertTrue(prop.hasSetter());
        assertFalse(prop.hasGetter());
        assertFalse(prop.hasField());
        assertFalse(prop.hasConstructorParameter());

        assertSame(setter, prop.getSetter());
        assertNull(prop.getGetter());
        assertNull(prop.getField());
        assertNull(prop.getConstructorParameter());
    }

    /*
    /**********************************************************************
    /* Test methods: accessor detection with constructor param member
    /**********************************************************************
     */

    @Test
    public void testAccessorsWithConstructorParameter() {
        AnnotatedParameter param = _constructorParamOf(SimpleBean.class, 0);
        assertNotNull(param);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), param);

        assertTrue(prop.hasConstructorParameter());
        assertFalse(prop.hasGetter());
        assertFalse(prop.hasSetter());
        assertFalse(prop.hasField());

        assertSame(param, prop.getConstructorParameter());
        assertNull(prop.getGetter());
        assertNull(prop.getSetter());
        assertNull(prop.getField());

        // getConstructorParameters should return single-element iterator
        Iterator<AnnotatedParameter> it = prop.getConstructorParameters();
        assertTrue(it.hasNext());
        assertSame(param, it.next());
        assertFalse(it.hasNext());
    }

    /*
    /**********************************************************************
    /* Test methods: accessor detection with null member (virtual prop)
    /**********************************************************************
     */

    @Test
    public void testAccessorsWithNullMember() {
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), null, PropertyName.construct("virtual"), null,
                (JsonInclude.Value) null);

        assertFalse(prop.hasGetter());
        assertFalse(prop.hasSetter());
        assertFalse(prop.hasField());
        assertFalse(prop.hasConstructorParameter());

        assertNull(prop.getGetter());
        assertNull(prop.getSetter());
        assertNull(prop.getField());
        assertNull(prop.getConstructorParameter());
        assertNull(prop.getPrimaryMember());
    }

    @Test
    public void testFindInclusion() {
        AnnotatedField field = _fieldOf(SimpleBean.class);
        JsonInclude.Value incl = JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, null);
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), field, PropertyName.construct("x"), null, incl);
        assertSame(incl, prop.findInclusion());
    }

    @Test
    public void testGetWrapperNameNullMember() {
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), null, PropertyName.construct("virtual"), null,
                (JsonInclude.Value) null);
        assertNull(prop.getWrapperName());
    }

    @Test
    public void testFindAliasesNullMember() {
        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                _config(), null, PropertyName.construct("virtual"), null,
                (JsonInclude.Value) null);
        assertTrue(prop.findAliases().isEmpty());
    }

    /*
    /**********************************************************************
    /* Test methods: findAliases returning non-null aliases
    /**********************************************************************
     */

    @Test
    public void testFindAliasesWithJsonAlias() {
        // Use AliasedBean which has @JsonAlias on its field
        DeserializationConfig config = MAPPER.deserializationConfig();
        JavaType type = MAPPER.constructType(AliasedBean.class);
        AnnotatedClass ac = AnnotatedClassResolver.resolve(config, type, config);
        AnnotatedField field = ac.fields().iterator().next();

        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                config, field);
        List<PropertyName> aliases = prop.findAliases();
        assertNotNull(aliases);
        assertEquals(2, aliases.size());
    }

    /*
    /**********************************************************************
    /* Test methods: null AnnotationIntrospector branches
    /**********************************************************************
     */

    @Test
    public void testGetWrapperNameNullIntrospector() {
        ObjectMapper noIntrMapper = JsonMapper.builder()
                .annotationIntrospector(null)
                .build();
        DeserializationConfig config = noIntrMapper.deserializationConfig();
        JavaType type = noIntrMapper.constructType(SimpleBean.class);
        AnnotatedClass ac = AnnotatedClassResolver.resolve(config, type, config);
        AnnotatedField field = ac.fields().iterator().next();

        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                config, field);
        assertNull(prop.getWrapperName());
    }

    @Test
    public void testFindAliasesNullIntrospector() {
        ObjectMapper noIntrMapper = JsonMapper.builder()
                .annotationIntrospector(null)
                .build();
        DeserializationConfig config = noIntrMapper.deserializationConfig();
        JavaType type = noIntrMapper.constructType(SimpleBean.class);
        AnnotatedClass ac = AnnotatedClassResolver.resolve(config, type, config);
        AnnotatedField field = ac.fields().iterator().next();

        SimpleBeanPropertyDefinition prop = SimpleBeanPropertyDefinition.construct(
                config, field);
        assertTrue(prop.findAliases().isEmpty());
    }
}