diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java index 6b67bf9e0..add729a44 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java @@ -921,6 +921,9 @@ public class RepositoryRestMvcConfiguration extends HateoasAwareSpringDataWebCon configurerDelegate.get().configureJacksonObjectMapper(objectMapper); objectMapper.registerModule(geoModule.getObject()); + objectMapper.registerModule(new AggregateReferenceResolvingModule( + new UriToEntityConverter(persistentEntities.get(), repositoryInvokerFactory.get(), repositories.get()), + resourceMappings.get())); if (repositoryRestConfiguration.get().isEnableEnumTranslation()) { objectMapper.registerModule(new JacksonSerializers(enumTranslator.get())); diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/AggregateReferenceResolvingModule.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/AggregateReferenceResolvingModule.java new file mode 100644 index 000000000..c02f91411 --- /dev/null +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/AggregateReferenceResolvingModule.java @@ -0,0 +1,138 @@ +/* + * Copyright 2021 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.rest.webmvc.json; + +import java.util.Iterator; + +import org.springframework.data.rest.core.UriToEntityConverter; +import org.springframework.data.rest.core.mapping.ResourceMappings; +import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.AssociationUriResolvingDeserializerModifier.ValueInstantiatorCustomizer; +import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.CollectionValueInstantiator; +import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.UriStringDeserializer; +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.databind.BeanDescription; +import com.fasterxml.jackson.databind.DeserializationConfig; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.deser.SettableBeanProperty; +import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.type.CollectionLikeType; + +/** + * Jackson module to enable aggregate reference resolution for non-aggregate root types. This is primarily useful for + * any kind of payload mapping DTO that is supposed to be able to map URIs to aggregate roots. + * + * @author Oliver Drotbohm + * @since 3.5 + */ +public class AggregateReferenceResolvingModule extends SimpleModule { + + private static final long serialVersionUID = 6002883434719869173L; + + /** + * Creates a new {@link AggregateReferenceResolvingModule} using the given {@link UriToEntityConverter} and + * {@link ResourceMappings}. + * + * @param converter must not be {@literal null}. + * @param mappings must not be {@literal null}. + */ + public AggregateReferenceResolvingModule(UriToEntityConverter converter, ResourceMappings mappings) { + setDeserializerModifier(new AggregateReferenceDeserializerModifier(converter, mappings)); + } + + /** + * {@link BeanDeserializerModifier} implementation to support URI deserialization into aggregate roots + * + * @author Oliver Drotbohm + */ + static class AggregateReferenceDeserializerModifier extends BeanDeserializerModifier { + + private final UriToEntityConverter converter; + private final ResourceMappings mappings; + + /** + * Creates a new {@link AggregateReferenceDeserializerModifier} for the given {@link UriToEntityConverter} and + * {@link ResourceMappings}. + * + * @param converter must not be {@literal null}. + * @param mappings must not be {@literal null}. + */ + public AggregateReferenceDeserializerModifier(UriToEntityConverter converter, ResourceMappings mappings) { + + Assert.notNull(converter, "UriToEntityConverter must not be null!"); + Assert.notNull(mappings, "ResourceMappings must not be null!"); + + this.converter = converter; + this.mappings = mappings; + } + + /* + * (non-Javadoc) + * @see com.fasterxml.jackson.databind.deser.BeanDeserializerModifier#updateBuilder(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.BeanDescription, com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder) + */ + @Override + public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc, + BeanDeserializerBuilder builder) { + + // Type is aggregate itself, already handled by AssociationUriResolvingDeserializerModifier + if (mappings.hasMappingFor(beanDesc.getBeanClass())) { + return builder; + } + + TypeInformation type = ClassTypeInformation.from(beanDesc.getBeanClass()); + ValueInstantiatorCustomizer customizer = new ValueInstantiatorCustomizer(builder.getValueInstantiator(), config); + Iterator properties = builder.getProperties(); + + while (properties.hasNext()) { + + SettableBeanProperty property = properties.next(); + + TypeInformation propertyType = type.getProperty(property.getName()); + TypeInformation actualType = propertyType.getActualType(); + + if (!mappings.exportsMappingFor(actualType.getType())) { + continue; + } + + UriStringDeserializer uriStringDeserializer = new UriStringDeserializer(actualType.getType(), converter); + JsonDeserializer deserializer = wrapIfCollection(propertyType, uriStringDeserializer, config); + + customizer.replacePropertyIfNeeded(builder, property.withValueDeserializer(deserializer)); + } + + return builder; + } + + private static JsonDeserializer wrapIfCollection(TypeInformation type, + JsonDeserializer elementDeserializer, DeserializationConfig config) { + + if (!type.isCollectionLike()) { + return elementDeserializer; + } + + CollectionLikeType collectionType = config.getTypeFactory() // + .constructCollectionLikeType(type.getType(), type.getActualType().getType()); + CollectionValueInstantiator instantiator = new CollectionValueInstantiator(type); + + return new CollectionDeserializer(collectionType, elementDeserializer, null, instantiator); + } + } +} diff --git a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java index a7aca2675..4b25f15e7 100644 --- a/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java +++ b/spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java @@ -48,6 +48,7 @@ import org.springframework.data.rest.webmvc.PersistentEntityResource; import org.springframework.data.rest.webmvc.mapping.Associations; import org.springframework.data.rest.webmvc.mapping.LinkCollector; import org.springframework.data.util.CastUtils; +import org.springframework.data.util.TypeInformation; import org.springframework.hateoas.EntityModel; import org.springframework.hateoas.Link; import org.springframework.hateoas.Links; @@ -427,7 +428,6 @@ public class PersistentEntityJackson2Module extends SimpleModule { private final UriToEntityConverter converter; private final RepositoryInvokerFactory factory; - @java.lang.SuppressWarnings("all") public AssociationUriResolvingDeserializerModifier(PersistentEntities entities, Associations associations, UriToEntityConverter converter, RepositoryInvokerFactory factory) { @@ -451,7 +451,6 @@ public class PersistentEntityJackson2Module extends SimpleModule { BeanDeserializerBuilder builder) { ValueInstantiatorCustomizer customizer = new ValueInstantiatorCustomizer(builder.getValueInstantiator(), config); - Iterator properties = builder.getProperties(); entities.getPersistentEntity(beanDesc.getBeanClass()).ifPresent(entity -> { @@ -459,19 +458,19 @@ public class PersistentEntityJackson2Module extends SimpleModule { while (properties.hasNext()) { SettableBeanProperty property = properties.next(); - PersistentProperty persistentProperty = entity.getPersistentProperty(property.getName()); if (persistentProperty == null) { continue; } + TypeInformation propertyType = persistentProperty.getTypeInformation(); + if (associationLinks.isLookupType(persistentProperty)) { RepositoryInvokingDeserializer repositoryInvokingDeserializer = new RepositoryInvokingDeserializer(factory, persistentProperty); - JsonDeserializer deserializer = wrapIfCollection(persistentProperty, repositoryInvokingDeserializer, - config); + JsonDeserializer deserializer = wrapIfCollection(propertyType, repositoryInvokingDeserializer, config); builder.addOrReplaceProperty(property.withValueDeserializer(deserializer), false); continue; @@ -481,8 +480,9 @@ public class PersistentEntityJackson2Module extends SimpleModule { continue; } - UriStringDeserializer uriStringDeserializer = new UriStringDeserializer(persistentProperty, converter); - JsonDeserializer deserializer = wrapIfCollection(persistentProperty, uriStringDeserializer, config); + Class actualPropertyType = persistentProperty.getActualType(); + UriStringDeserializer uriStringDeserializer = new UriStringDeserializer(actualPropertyType, converter); + JsonDeserializer deserializer = wrapIfCollection(propertyType, uriStringDeserializer, config); customizer.replacePropertyIfNeeded(builder, property.withValueDeserializer(deserializer)); } @@ -519,7 +519,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { /** * Replaces the logically same property with the given {@link SettableBeanProperty} on the given - * {@link BeanDeserializerBuilder}. In case we get a {@link CreatorProperty} we als register that one to be later + * {@link BeanDeserializerBuilder}. In case we get a {@link CreatorProperty} we also register that one to be later * exposed via the {@link ValueInstantiator} backing the {@link BeanDeserializerBuilder}. * * @param builder must not be {@literal null}. @@ -559,7 +559,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { } } - private static JsonDeserializer wrapIfCollection(PersistentProperty property, + private static JsonDeserializer wrapIfCollection(TypeInformation property, JsonDeserializer elementDeserializer, DeserializationConfig config) { if (!property.isCollectionLike()) { @@ -567,7 +567,7 @@ public class PersistentEntityJackson2Module extends SimpleModule { } CollectionLikeType collectionType = config.getTypeFactory().constructCollectionLikeType(property.getType(), - property.getActualType()); + property.getActualType().getType()); CollectionValueInstantiator instantiator = new CollectionValueInstantiator(property); return new CollectionDeserializer(collectionType, elementDeserializer, null, instantiator); } @@ -580,26 +580,26 @@ public class PersistentEntityJackson2Module extends SimpleModule { * @author Oliver Gierke * @author Valentin Rentschler */ - static class UriStringDeserializer extends StdDeserializer { + public static class UriStringDeserializer extends StdDeserializer { private static final long serialVersionUID = -2175900204153350125L; private static final String UNEXPECTED_VALUE = "Expected URI cause property %s points to the managed domain type!"; - private final PersistentProperty property; + private final Class type; private final UriToEntityConverter converter; /** * Creates a new {@link UriStringDeserializer} for the given {@link PersistentProperty} using the given * {@link UriToEntityConverter}. * - * @param property must not be {@literal null}. + * @param type must not be {@literal null}. * @param converter must not be {@literal null}. */ - public UriStringDeserializer(PersistentProperty property, UriToEntityConverter converter) { + public UriStringDeserializer(Class type, UriToEntityConverter converter) { - super(property.getActualType()); + super(type); - this.property = property; + this.type = type; this.converter = converter; } @@ -618,11 +618,11 @@ public class PersistentEntityJackson2Module extends SimpleModule { try { URI uri = UriTemplate.of(source).expand(); - TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(property.getActualType()); + TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(type); return converter.convert(uri, URI_DESCRIPTOR, typeDescriptor); } catch (IllegalArgumentException o_O) { - throw ctxt.weirdStringException(source, URI.class, String.format(UNEXPECTED_VALUE, property)); + throw ctxt.weirdStringException(source, URI.class, String.format(UNEXPECTED_VALUE, type)); } } @@ -813,16 +813,16 @@ public class PersistentEntityJackson2Module extends SimpleModule { * * @author Oliver Gierke */ - private static class CollectionValueInstantiator extends ValueInstantiator { + static class CollectionValueInstantiator extends ValueInstantiator { - private final PersistentProperty property; + private final TypeInformation property; /** * Creates a new {@link CollectionValueInstantiator} for the given {@link PersistentProperty}. * * @param property must not be {@literal null} and must be a collection. */ - public CollectionValueInstantiator(PersistentProperty property) { + public CollectionValueInstantiator(TypeInformation property) { Assert.notNull(property, "Property must not be null!"); Assert.isTrue(property.isCollectionLike() || property.isMap(), "Property must be a collection or map property!"); diff --git a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/UriStringDeserializerUnitTests.java b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/UriStringDeserializerUnitTests.java index 41e4fe078..bcd8fc23b 100755 --- a/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/UriStringDeserializerUnitTests.java +++ b/spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/UriStringDeserializerUnitTests.java @@ -30,7 +30,6 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.rest.core.UriToEntityConverter; import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.UriStringDeserializer; import org.springframework.test.util.ReflectionTestUtils; @@ -51,7 +50,6 @@ public class UriStringDeserializerUnitTests { public @Rule ExpectedException exception = ExpectedException.none(); @Mock UriToEntityConverter converter; - @Mock PersistentProperty property; @Mock JsonParser parser; DeserializationContext context; @@ -61,7 +59,7 @@ public class UriStringDeserializerUnitTests { @Before public void setUp() { - this.deserializer = new UriStringDeserializer(property, converter); + this.deserializer = new UriStringDeserializer(Object.class, converter); // Need to hack the context as there's virtually no way wo set up a combined parser and context easily this.context = new ObjectMapper().getDeserializationContext(); @@ -94,10 +92,8 @@ public class UriStringDeserializerUnitTests { invokeConverterWith("{ \"foo\" : \"bar\" }"); } - @SuppressWarnings({ "unchecked", "rawtypes" }) private Object invokeConverterWith(String source) throws Exception { - when(property.getActualType()).thenReturn((Class) Object.class); when(parser.getValueAsString()).thenReturn(source); return deserializer.deserialize(parser, context);