Support aggregate reference resolution for DTOs.
Introduce AggregateReferenceResolvingModule to be registered with the default ObjectMapper instance that will allow to materialize aggregate instances from URIs for incoming web requests. We do not apply this for aggregate roots themselves as they're already handled by the AssociationUriResolvingDeserializerModifier.
This commit is contained in:
@@ -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()));
|
||||
|
||||
@@ -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<SettableBeanProperty> 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<Object> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<SettableBeanProperty> 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<Object> 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<Object> {
|
||||
public static class UriStringDeserializer extends StdDeserializer<Object> {
|
||||
|
||||
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!");
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user