From 928d356f8eeea95e17632ed76700c65ad4135216 Mon Sep 17 00:00:00 2001 From: fxshlein Date: Mon, 23 Sep 2024 08:29:53 +0200 Subject: [PATCH] fix(model): use SettableBeanProperty.Delegating for jackson delegation (6343) fix(model): Use SettableBeanProperty.Delegating for jackson delegation Changes the delegation in SettableBeanPropertyDelegate from a custom implementation to the standard way of implementing a delegating property in jackson. This way, if some jackson module overrides methods that are not delegated explicitely here, they will continue to work. Fixes: fabric8io/kubernetes-client#6342 --- fix: SettableBeanProperty.deserializeSetAndReturn should return instance instead of null Signed-off-by: Marc Nuri --- test:refactor: SettableBeanPropertyDelegateTest doesn't rely on mocks Signed-off-by: Marc Nuri --- fix: SettableBeanPropertyDelegate implements all methods from SettableBeanProperty Includes tests to ensure all methods are implemented in future Jackson versions too. Signed-off-by: Marc Nuri Co-authored-by: Marc Nuri Signed-off-by: Marc Nuri --- CHANGELOG.md | 2 +- .../jackson/SettableBeanPropertyDelegate.java | 113 ++-- .../SettableBeanPropertyDelegateTest.java | 497 ++++++++++++++---- 3 files changed, 479 insertions(+), 133 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index edd8e45908e..eeca9ad641a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ #### Bugs * Fix #6247: Support for proxy authentication from proxy URL user info - +* Fix #6342: UnmatchedFieldTypeModule prevents certain jackson features from working ### 6.13.3 (2024-08-13) diff --git a/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegate.java b/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegate.java index 021864e308a..8cf6b68a8e6 100644 --- a/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegate.java +++ b/kubernetes-model-generator/kubernetes-model-common/src/main/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegate.java @@ -16,36 +16,36 @@ package io.fabric8.kubernetes.model.jackson; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.databind.DeserializationConfig; import com.fasterxml.jackson.databind.DeserializationContext; -import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.deser.NullValueProvider; import com.fasterxml.jackson.databind.deser.SettableAnyProperty; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.introspect.ObjectIdInfo; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; import java.io.IOException; import java.lang.annotation.Annotation; import java.util.function.BooleanSupplier; /** - * This concrete sub-class encapsulates a {@link SettableBeanProperty} delegate that is always tried first. + * This concrete subclass encapsulates a {@link SettableBeanProperty} delegate that is always tried first. * *

* A fall-back mechanism is implemented in the deserializeAndSet methods to allow field values that don't match the * target type to be preserved in the anySetter method if exists. */ -public class SettableBeanPropertyDelegate extends SettableBeanProperty { +public class SettableBeanPropertyDelegate extends SettableBeanProperty.Delegating { - private final SettableBeanProperty delegate; private final SettableAnyProperty anySetter; private final transient BooleanSupplier useAnySetter; SettableBeanPropertyDelegate(SettableBeanProperty delegate, SettableAnyProperty anySetter, BooleanSupplier useAnySetter) { super(delegate); - this.delegate = delegate; this.anySetter = anySetter; this.useAnySetter = useAnySetter; } @@ -54,64 +54,113 @@ public class SettableBeanPropertyDelegate extends SettableBeanProperty { * {@inheritDoc} */ @Override - public SettableBeanProperty withValueDeserializer(JsonDeserializer deser) { - return new SettableBeanPropertyDelegate(delegate.withValueDeserializer(deser), anySetter, useAnySetter); + protected SettableBeanProperty withDelegate(SettableBeanProperty d) { + return new SettableBeanPropertyDelegate(d, anySetter, useAnySetter); } /** * {@inheritDoc} */ @Override - public SettableBeanProperty withName(PropertyName newName) { - return new SettableBeanPropertyDelegate(delegate.withName(newName), anySetter, useAnySetter); + public void markAsIgnorable() { + delegate.markAsIgnorable(); } /** * {@inheritDoc} */ @Override - public SettableBeanProperty withNullProvider(NullValueProvider nva) { - return new SettableBeanPropertyDelegate(delegate.withNullProvider(nva), anySetter, useAnySetter); + public boolean isIgnorable() { + return delegate.isIgnorable(); } /** * {@inheritDoc} */ @Override - public AnnotatedMember getMember() { - return delegate.getMember(); + public void setViews(Class[] views) { + delegate.setViews(views); } /** * {@inheritDoc} */ @Override - public A getAnnotation(Class acls) { - return delegate.getAnnotation(acls); + public A getContextAnnotation(Class acls) { + return delegate.getContextAnnotation(acls); } /** * {@inheritDoc} */ @Override - public void fixAccess(DeserializationConfig config) { - delegate.fixAccess(config); + public PropertyName getWrapperName() { + return delegate.getWrapperName(); } /** * {@inheritDoc} */ @Override - public void markAsIgnorable() { - delegate.markAsIgnorable(); + public NullValueProvider getNullValueProvider() { + return delegate.getNullValueProvider(); } /** * {@inheritDoc} */ @Override - public boolean isIgnorable() { - return delegate.isIgnorable(); + public void depositSchemaProperty(JsonObjectFormatVisitor objectVisitor, SerializerProvider provider) + throws JsonMappingException { + delegate.depositSchemaProperty(objectVisitor, provider); + } + + /** + * {@inheritDoc} + */ + @Override + public JavaType getType() { + return delegate.getType(); + } + + /** + * {@inheritDoc} + */ + @Override + public PropertyName getFullName() { + return delegate.getFullName(); + } + + /** + * {@inheritDoc} + */ + @Override + public void setManagedReferenceName(String n) { + delegate.setManagedReferenceName(n); + } + + /** + * {@inheritDoc} + */ + @Override + public SettableBeanProperty withSimpleName(String simpleName) { + return _with(delegate.withSimpleName(simpleName)); + } + + /** + * {@inheritDoc} + */ + @Override + public void setObjectIdInfo(ObjectIdInfo objectIdInfo) { + delegate.setObjectIdInfo(objectIdInfo); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return delegate.toString(); } /** @@ -151,23 +200,7 @@ public Object deserializeSetAndReturn(JsonParser p, DeserializationContext ctxt, } catch (MismatchedInputException ex) { deserializeAndSet(p, ctxt, instance); } - return null; - } - - /** - * {@inheritDoc} - */ - @Override - public void set(Object instance, Object value) throws IOException { - delegate.set(instance, value); - } - - /** - * {@inheritDoc} - */ - @Override - public Object setAndReturn(Object instance, Object value) throws IOException { - return delegate.setAndReturn(instance, value); + return instance; } private boolean shouldUseAnySetter() { diff --git a/kubernetes-model-generator/kubernetes-model-common/src/test/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegateTest.java b/kubernetes-model-generator/kubernetes-model-common/src/test/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegateTest.java index f61d6f5f42e..240ecb8e6f4 100644 --- a/kubernetes-model-generator/kubernetes-model-common/src/test/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegateTest.java +++ b/kubernetes-model-generator/kubernetes-model-common/src/test/java/io/fabric8/kubernetes/model/jackson/SettableBeanPropertyDelegateTest.java @@ -15,207 +15,520 @@ */ package io.fabric8.kubernetes.model.jackson; +import com.fasterxml.jackson.annotation.JsonAnyGetter; +import com.fasterxml.jackson.annotation.JsonAnySetter; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.PropertyName; +import com.fasterxml.jackson.databind.deser.BeanDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerFactory; +import com.fasterxml.jackson.databind.deser.CreatorProperty; +import com.fasterxml.jackson.databind.deser.DefaultDeserializationContext; +import com.fasterxml.jackson.databind.deser.NullValueProvider; import com.fasterxml.jackson.databind.deser.SettableAnyProperty; import com.fasterxml.jackson.databind.deser.SettableBeanProperty; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.deser.impl.FieldProperty; +import com.fasterxml.jackson.databind.deser.std.NumberDeserializers; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.AnnotatedMember; +import com.fasterxml.jackson.databind.introspect.BasicBeanDescription; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.databind.introspect.ObjectIdInfo; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.databind.util.SimpleBeanPropertyDefinition; +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Collectors; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.RETURNS_DEEP_STUBS; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class SettableBeanPropertyDelegateTest { - private SettableBeanProperty delegateMock; - private SettableAnyProperty anySetterMock; - private SettableBeanPropertyDelegate settableBeanPropertyDelegate; private AtomicBoolean useAnySetter; + private ObjectMapper objectMapper; + private DefaultDeserializationContext deserializationContext; + private SettableAnyProperty anySetter; + private SettableBeanProperty intFieldProperty; + private SettableBeanPropertyDelegate intFieldPropertyDelegating; @BeforeEach - void setUp() { - delegateMock = mock(SettableBeanProperty.class, RETURNS_DEEP_STUBS); - anySetterMock = mock(SettableAnyProperty.class); + void setUp() throws Exception { useAnySetter = new AtomicBoolean(false); - settableBeanPropertyDelegate = new SettableBeanPropertyDelegate(delegateMock, anySetterMock, useAnySetter::get); + // Required Jackson deserialization objects to set up the test components + objectMapper = new ObjectMapper(); + final DeserializationConfig deserializationConfig = objectMapper.getDeserializationConfig(); + deserializationContext = new DefaultDeserializationContext.Impl(objectMapper.getDeserializationContext().getFactory()) + .createDummyInstance(deserializationConfig); + final JavaType testBeanJavaType = objectMapper.constructType(TestBean.class); + final BeanDescription testBeanDescription = deserializationConfig.introspect(testBeanJavaType); + final BeanDeserializer testBeanDeserializer = (BeanDeserializer) ((BeanDeserializerFactory) deserializationContext + .getFactory()) + .buildBeanDeserializer(deserializationContext, testBeanJavaType, testBeanDescription); + // AnySetter used by delegator, real instance that will invoke the additionalProperties any setter in TestBean + final BeanPropertyDefinition anySetterDefinition = SimpleBeanPropertyDefinition.construct(deserializationConfig, + testBeanDescription.findAnySetterAccessor()); + final BeanProperty anySetterProperty = new BeanProperty.Std( + anySetterDefinition.getFullName(), anySetterDefinition.getPrimaryType(), anySetterDefinition.getWrapperName(), + anySetterDefinition.getPrimaryMember(), anySetterDefinition.getMetadata()); + final JavaType anySetterValueType = objectMapper.constructType(Object.class); + anySetter = SettableAnyProperty.constructForMethod( + deserializationContext, anySetterProperty, anySetterProperty.getMember(), anySetterValueType, + deserializationContext.findKeyDeserializer(objectMapper.constructType(String.class), anySetterProperty), + deserializationContext.findRootValueDeserializer(anySetterValueType), null); + // Delegated SettableBeanProperty + intFieldProperty = testBeanDeserializer.findProperty("intField") + .withValueDeserializer(NumberDeserializers.find(int.class, null)); + // Delegating SettableBeanProperty in test + intFieldPropertyDelegating = new SettableBeanPropertyDelegate(intFieldProperty, anySetter, useAnySetter::get); + } @Test @DisplayName("withValueDeserializer, should return a new instance") void withValueDeserializer() { - // Given - doReturn(delegateMock).when(delegateMock).withValueDeserializer(any()); // When - final SettableBeanProperty result = settableBeanPropertyDelegate.withValueDeserializer(null); + final SettableBeanProperty result = intFieldPropertyDelegating.withValueDeserializer(null); // Then assertThat(result) .isInstanceOf(SettableBeanPropertyDelegate.class) - .isNotSameAs(settableBeanPropertyDelegate) - .hasFieldOrPropertyWithValue("anySetter", anySetterMock) - .hasFieldOrPropertyWithValue("delegate", delegateMock); + .isNotSameAs(intFieldPropertyDelegating) + .hasFieldOrPropertyWithValue("anySetter", anySetter) + .asInstanceOf(InstanceOfAssertFactories.type(SettableBeanPropertyDelegate.class)) + .extracting(SettableBeanPropertyDelegate::getDelegate) + .isInstanceOf(CreatorProperty.class) + .isNotSameAs(intFieldProperty) + .hasFieldOrPropertyWithValue("name", "intField"); } @Test @DisplayName("withName, should return a new instance") void withName() { - // Given - doReturn(delegateMock).when(delegateMock).withName(any()); // When - final SettableBeanProperty result = settableBeanPropertyDelegate.withName(null); + final SettableBeanProperty result = intFieldPropertyDelegating.withName(new PropertyName("overriddenName")); // Then assertThat(result) .isInstanceOf(SettableBeanPropertyDelegate.class) - .isNotSameAs(settableBeanPropertyDelegate) - .hasFieldOrPropertyWithValue("anySetter", anySetterMock) - .hasFieldOrPropertyWithValue("delegate", delegateMock); + .isNotSameAs(intFieldPropertyDelegating) + .hasFieldOrPropertyWithValue("anySetter", anySetter) + .asInstanceOf(InstanceOfAssertFactories.type(SettableBeanPropertyDelegate.class)) + .extracting(SettableBeanPropertyDelegate::getDelegate) + .isInstanceOf(CreatorProperty.class) + .isNotSameAs(intFieldProperty) + .hasFieldOrPropertyWithValue("name", "overriddenName"); } @Test @DisplayName("withNullProvider, should return a new instance") void withNullProvider() { - // Given - doReturn(delegateMock).when(delegateMock).withNullProvider(any()); // When - final SettableBeanProperty result = settableBeanPropertyDelegate.withNullProvider(null); + final SettableBeanProperty result = intFieldPropertyDelegating.withNullProvider(null); // Then assertThat(result) .isInstanceOf(SettableBeanPropertyDelegate.class) - .isNotSameAs(settableBeanPropertyDelegate) - .hasFieldOrPropertyWithValue("anySetter", anySetterMock) - .hasFieldOrPropertyWithValue("delegate", delegateMock); + .isNotSameAs(intFieldPropertyDelegating) + .hasFieldOrPropertyWithValue("anySetter", anySetter) + .asInstanceOf(InstanceOfAssertFactories.type(SettableBeanPropertyDelegate.class)) + .extracting(SettableBeanPropertyDelegate::getDelegate) + .isInstanceOf(CreatorProperty.class) + .isNotSameAs(intFieldProperty) + .hasFieldOrPropertyWithValue("name", "intField"); } @Test @DisplayName("getMember, should return delegate's Member") void getMember() { - // Given - when(delegateMock.getMember().getName()).thenReturn("the-member"); // When - final String result = settableBeanPropertyDelegate.getMember().getName(); + final AnnotatedMember result = intFieldPropertyDelegating.getMember(); + // Then + assertThat(result) + .isSameAs(intFieldProperty.getMember()) + .extracting(am -> am.getAnnotation(JsonProperty.class).value()) + .isEqualTo("intField"); + } + + @Test + @DisplayName("getCreatorIndex, should return delegate's creator index") + void getCreatorIndex() { + // When + final int result = intFieldPropertyDelegating.getCreatorIndex(); // Then - assertThat(result).isEqualTo("the-member"); + assertThat(result).isZero(); } @Test @DisplayName("getAnnotation, should return delegate's Annotation") void getAnnotation() { // When - settableBeanPropertyDelegate.getAnnotation(null); + final JsonProperty result = intFieldPropertyDelegating.getAnnotation(JsonProperty.class); // Then - verify(delegateMock, times(1)).getAnnotation(null); + assertThat(result) + .isSameAs(intFieldProperty.getAnnotation(JsonProperty.class)); } @Test @DisplayName("fixAccess, should invoke fixAccess in delegate") void fixAccess() { + // Given + final JavaType testBeanJavaType = objectMapper.constructType(TestBean.class); + final BasicBeanDescription testBeanDescription = (BasicBeanDescription) deserializationContext.getConfig() + .introspect(testBeanJavaType); + final BeanPropertyDefinition testPropertyFieldDefinition = (testBeanDescription) + .findProperty(PropertyName.construct("intField")); + final SettableBeanProperty fieldProperty = new FieldProperty(testPropertyFieldDefinition, testBeanJavaType, null, + testBeanDescription.getClassAnnotations(), testPropertyFieldDefinition.getField()); + final SettableBeanProperty fieldPropertyDelegating = new SettableBeanPropertyDelegate(fieldProperty, anySetter, + useAnySetter::get); + assertThat(((AccessibleObject) fieldProperty.getMember().getMember()).isAccessible()).isFalse(); // When - settableBeanPropertyDelegate.fixAccess(null); + fieldPropertyDelegating.fixAccess(deserializationContext.getConfig()); // Then - verify(delegateMock, times(1)).fixAccess(null); + assertThat(((AccessibleObject) fieldProperty.getMember().getMember()).isAccessible()).isTrue(); } @Test @DisplayName("markAsIgnorable, should invoke markAsIgnorable in delegate") void markAsIgnorable() { + // Given + assertThat(intFieldProperty.isIgnorable()).isFalse(); // When - settableBeanPropertyDelegate.markAsIgnorable(); + intFieldPropertyDelegating.markAsIgnorable(); // Then - verify(delegateMock, times(1)).markAsIgnorable(); + assertThat(intFieldProperty.isIgnorable()).isTrue(); } @Test @DisplayName("isIgnorable, should return isIgnorable result in delegate") void isIgnorable() { - // Given - when(delegateMock.isIgnorable()).thenReturn(true); // When - final boolean result = settableBeanPropertyDelegate.isIgnorable(); + final boolean result = intFieldPropertyDelegating.isIgnorable(); // Then - assertThat(result).isTrue(); + assertThat(result) + .isFalse() + .isEqualTo(intFieldProperty.isIgnorable()); } @Test - @DisplayName("set, should set in delegate") - void set() throws IOException { + @DisplayName("setViews, should invoke setViews in delegate") + void setViews() { // Given - final Object o1 = new Object(); - final Object o2 = new Object(); + assertThat(intFieldProperty.visibleInView(String.class)).isTrue(); // When - settableBeanPropertyDelegate.set(o1, o2); + intFieldPropertyDelegating.setViews(new Class[] { Integer.class }); // Then - verify(delegateMock, times(1)).set(o1, o2); + assertThat(intFieldProperty.visibleInView(String.class)).isFalse(); } @Test - @DisplayName("setAndReturn, should setAndReturn in delegate") - void setAndReturn() throws IOException { + @DisplayName("getContextAnnotation, should return getContextAnnotation result in delegate") + void getContextAnnotation() { + // When + final JsonIgnoreProperties result = intFieldPropertyDelegating + .getContextAnnotation(JsonIgnoreProperties.class); + // Then + assertThat(result) + .isSameAs(intFieldProperty.getContextAnnotation(JsonIgnoreProperties.class)) + .extracting(JsonIgnoreProperties::ignoreUnknown) + .isEqualTo(true); + } + + @Test + @DisplayName("getWrapperName, should return getWrapperName result in delegate") + void getWrapperName() { // Given - final Object o1 = new Object(); - final Object o2 = new Object(); + final DeserializationConfig config = deserializationContext.getConfig() + .withAppendedAnnotationIntrospector(new JacksonAnnotationIntrospector() { + @Override + public PropertyName findWrapperName(Annotated ann) { + return PropertyName.construct("WrapperNameForTest"); + } + }); + final JavaType testBeanJavaType = objectMapper.constructType(TestBean.class); + final BasicBeanDescription testBeanDescription = (BasicBeanDescription) config + .introspect(testBeanJavaType); + final BeanPropertyDefinition testPropertyFieldDefinition = (testBeanDescription) + .findProperty(PropertyName.construct("intField")); + final SettableBeanProperty fieldProperty = new FieldProperty(testPropertyFieldDefinition, testBeanJavaType, null, + testBeanDescription.getClassAnnotations(), testPropertyFieldDefinition.getField()); + final SettableBeanProperty fieldPropertyDelegating = new SettableBeanPropertyDelegate(fieldProperty, anySetter, + useAnySetter::get); // When - settableBeanPropertyDelegate.setAndReturn(o1, o2); + final PropertyName result = fieldPropertyDelegating.getWrapperName(); // Then - verify(delegateMock, times(1)).setAndReturn(o1, o2); + assertThat(result) + .isSameAs(fieldProperty.getWrapperName()) + .hasFieldOrPropertyWithValue("simpleName", "WrapperNameForTest"); + } + + @Test + @DisplayName("getNullValueProvider, should return getNullValueProvider result in delegate") + void getNullValueProvider() { + // When + final NullValueProvider result = intFieldPropertyDelegating.getNullValueProvider(); + // Then + assertThat(result) + .isSameAs(intFieldProperty.getNullValueProvider()); } @Test - @DisplayName("deserializeSetAndReturn, should deserializeSetAndReturn in delegate") - void deserializeSetAndReturn() throws IOException { + @DisplayName("depositSchemaProperty, should invoke depositSchemaProperty in delegate") + void depositSchemaProperty() throws Exception { // Given - final Object instance = new Object(); - when(delegateMock.deserializeSetAndReturn(any(), any(), eq(instance))).thenReturn("the-set-value"); + final JsonObjectFormatVisitor visitor = new JsonObjectFormatVisitor.Base() { + @Override + public void optionalProperty(BeanProperty prop) { + ((CreatorProperty) prop).setManagedReferenceName("visited"); + } + }; + // When + intFieldPropertyDelegating.depositSchemaProperty(visitor, objectMapper.getSerializerProvider()); + // Then + assertThat(intFieldProperty.getManagedReferenceName()) + .isEqualTo("visited"); + } + + @Test + @DisplayName("getFullName, should return getNullValueProvider result in delegate") + void getFullName() { + // When + final PropertyName result = intFieldPropertyDelegating.getFullName(); + // Then + assertThat(result) + .isSameAs(intFieldProperty.getFullName()) + .hasFieldOrPropertyWithValue("simpleName", "intField"); + } + + @Test + @DisplayName("setManagedReferenceName, should invoke setManagedReferenceName in delegate") + void setManagedReferenceName() { // When - final Object result = settableBeanPropertyDelegate.deserializeSetAndReturn(null, null, instance); + intFieldPropertyDelegating.setManagedReferenceName("the-managed-reference-name"); // Then - assertThat(result).isEqualTo("the-set-value"); + assertThat(intFieldPropertyDelegating.getManagedReferenceName()) + .isEqualTo(intFieldProperty.getManagedReferenceName()) + .isEqualTo("the-managed-reference-name"); } @Test - @DisplayName("deserializeSetAndReturn, with anySetter enabled and throws Exception, should use anySetter") - void deserializeSetAndReturnWithExceptionUsingAnySetter() throws IOException { + @DisplayName("setObjectIdInfo, should invoke setObjectIdInfo in delegate") + void setObjectIdInfo() { + // When + intFieldPropertyDelegating.setObjectIdInfo( + new ObjectIdInfo(PropertyName.construct("objectId"), null, null, null)); + // Then + assertThat(intFieldProperty.getObjectIdInfo()) + .extracting(ObjectIdInfo::getPropertyName) + .hasFieldOrPropertyWithValue("simpleName", "objectId"); + } + + @Test + @DisplayName("withSimpleName, should invoke withSimpleName in delegate") + void withSimpleName() { + // When + final SettableBeanProperty result = intFieldPropertyDelegating + .withSimpleName("overridden-simple-name"); + // Then + assertThat(result) + .isNotSameAs(intFieldPropertyDelegating) + .returns("overridden-simple-name", SettableBeanProperty::getName) + .extracting("delegate") + .asInstanceOf(InstanceOfAssertFactories.type(CreatorProperty.class)) + .isNotSameAs(intFieldProperty) + .returns("overridden-simple-name", SettableBeanProperty::getName); + } + + @Test + @DisplayName("toString, should return toString result in delegate") + void toStringTest() { + // When + final String result = intFieldPropertyDelegating.toString(); + // Then + assertThat(result) + .isEqualTo(intFieldProperty.toString()) + .isNotBlank(); + } + + @Test + @DisplayName("set, should set in delegate") + void set() throws IOException { // Given - final Object instance = new Object(); - when(delegateMock.getName()).thenReturn("the-property"); - when(delegateMock.deserializeSetAndReturn(any(), any(), eq(instance))) - .thenThrow(MismatchedInputException.from(null, Integer.class, "The Mocked Exception")); - doThrow(MismatchedInputException.from(null, Integer.class, "The delegate deserializeAndSet Exception")) - .when(delegateMock).deserializeAndSet(any(), any(), eq(instance)); - useAnySetter.set(true); + final TestBean instance = new TestBean(1337); + intFieldProperty.fixAccess(objectMapper.getDeserializationConfig()); // When - final Object result = settableBeanPropertyDelegate.deserializeSetAndReturn(mock(JsonParser.class), null, instance); + intFieldPropertyDelegating.set(instance, 313373); // Then - assertThat(result).isNull(); - verify(anySetterMock, times(1)).set(eq(instance), eq("the-property"), any()); + assertThat(instance) + .hasFieldOrPropertyWithValue("intField", 313373); } @Test - @DisplayName("deserializeSetAndReturn, with anySetter disabled and throws Exception, should throw Exception") - void deserializeSetAndReturnWithExceptionNotUsingAnySetter() throws IOException { + @DisplayName("setAndReturn, should setAndReturn in delegate") + void setAndReturn() throws IOException { // Given - final Object instance = new Object(); - when(delegateMock.getName()).thenReturn("the-property"); - when(delegateMock.deserializeSetAndReturn(any(), any(), eq(instance))) - .thenThrow(MismatchedInputException.from(null, Integer.class, "The Mocked Exception")); - doThrow(MismatchedInputException.from(null, Integer.class, "The delegate deserializeAndSet Exception")) - .when(delegateMock).deserializeAndSet(any(), any(), eq(instance)); + final TestBean instance = new TestBean(1337); + intFieldProperty.fixAccess(objectMapper.getDeserializationConfig()); // When - final MismatchedInputException result = assertThrows(MismatchedInputException.class, - () -> settableBeanPropertyDelegate.deserializeSetAndReturn(mock(JsonParser.class), null, instance)); + final Object result = intFieldPropertyDelegating.setAndReturn(instance, 313373); // Then - assertThat(result) - .hasMessage("The delegate deserializeAndSet Exception"); + assertThat(instance) + .hasFieldOrPropertyWithValue("intField", 313373) + .isSameAs(result); + } + + @Nested + @DisplayName("deserializeSetAndReturn") + class DeserializeSetAndReturn { + + private TestBean instance; + + @BeforeEach + void setUp() { + intFieldProperty.fixAccess(objectMapper.getDeserializationConfig()); + instance = new TestBean(1337); + } + + @Test + @DisplayName("validValue, should deserializeSetAndReturn in delegate") + void validValue() throws IOException { + try (JsonParser parser = objectMapper.createParser("313373")) { + final DefaultDeserializationContext ctx = deserializationContext + .createInstance(deserializationContext.getConfig(), parser, null); + parser.nextToken(); + final Object result = intFieldPropertyDelegating.deserializeSetAndReturn(parser, ctx, instance); + assertThat(instance) + .hasFieldOrPropertyWithValue("intField", 313373) + .isEqualTo(result); + } + } + + @Test + @DisplayName("deserializeSetAndReturn, with anySetter enabled and throws Exception, should use anySetter") + void invalidValueWithExceptionUsingAnySetter() throws IOException { + useAnySetter.set(true); + try (JsonParser parser = objectMapper.createParser("\"${a-placeholder}\"")) { + final DefaultDeserializationContext ctx = deserializationContext + .createInstance(deserializationContext.getConfig(), parser, null); + parser.nextToken(); + final Object result = intFieldPropertyDelegating.deserializeSetAndReturn(parser, ctx, instance); + assertThat(instance) + .hasFieldOrPropertyWithValue("intField", 1337) + .hasFieldOrPropertyWithValue("additionalProperties", Collections.singletonMap("intField", "${a-placeholder}")) + .isEqualTo(result); + } + } + + @Test + @DisplayName("deserializeSetAndReturn, with anySetter disabled and throws Exception, should throw Exception") + void deserializeSetAndReturnWithExceptionNotUsingAnySetter() throws IOException { + try (JsonParser parser = objectMapper.createParser("\"${a-placeholder}\"")) { + final DefaultDeserializationContext ctx = deserializationContext + .createInstance(deserializationContext.getConfig(), parser, null); + parser.nextToken(); + assertThatThrownBy(() -> intFieldPropertyDelegating.deserializeSetAndReturn(parser, ctx, instance)) + .isInstanceOf(InvalidFormatException.class) + .hasMessageContainingAll( + "Cannot deserialize value of type `int`", "\"${a-placeholder}\""); + } + } + + @Test + @DisplayName("deserializeSetAndReturn, with anySetter=null and throws Exception, should throw Exception") + void deserializeSetAndReturnWithExceptionAndNullAnySetter() throws IOException { + intFieldPropertyDelegating = new SettableBeanPropertyDelegate(intFieldProperty, null, () -> true); + try (JsonParser parser = objectMapper.createParser("\"${a-placeholder}\"")) { + final DefaultDeserializationContext ctx = deserializationContext + .createInstance(deserializationContext.getConfig(), parser, null); + parser.nextToken(); + assertThatThrownBy(() -> intFieldPropertyDelegating.deserializeSetAndReturn(parser, ctx, instance)) + .isInstanceOf(InvalidFormatException.class) + .hasMessageContainingAll( + "Cannot deserialize value of type `int`", "\"${a-placeholder}\""); + } + } + } + + @Nested + class ReflectionTest { + + @Test + @DisplayName("all methods from superclass (SettableBeanProperty) are implemented by delegating class (SettableBeanPropertyDelegate)") + void allMethodsFromSuperclassAreImplementedByDelegatingClass() { + final Map superclassMethods = Stream.of(SettableBeanProperty.class.getDeclaredMethods()) + .filter(m -> !Modifier.isFinal(m.getModifiers())) + .filter(m -> !Modifier.isPrivate(m.getModifiers())) + .filter(m -> !Modifier.isAbstract(m.getModifiers())) + .filter(m -> !m.getName().startsWith("_")) + .map(MethodSignature::from) + .collect(Collectors.toMap(ms -> ms, ms -> false)); + Stream.concat( + Stream.of(SettableBeanProperty.Delegating.class.getDeclaredMethods()), + Stream.of(SettableBeanPropertyDelegate.class.getDeclaredMethods())) + .map(MethodSignature::from) + .forEach(ms -> superclassMethods.computeIfPresent(ms, (k, v) -> true)); + assertThat(superclassMethods) + .values() + .containsOnly(true); + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + private static final class TestBean { + + @JsonProperty("intField") + int intField; + private final Map additionalProperties; + + @JsonCreator + private TestBean(@JsonProperty("intField") int intField) { + this.intField = intField; + additionalProperties = new LinkedHashMap<>(); + } + + @JsonAnyGetter + private Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + private void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + } + + @AllArgsConstructor + @EqualsAndHashCode + private static final class MethodSignature { + private final Class returnType; + private final String name; + private final Class[] parameterTypes; + + private static MethodSignature from(Method m) { + return new MethodSignature(m.getReturnType(), m.getName(), m.getParameterTypes()); + } + } }