Skip to content

Commit b4b8fdf

Browse files
committed
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.
1 parent 21ed682 commit b4b8fdf

File tree

4 files changed

+163
-26
lines changed

4 files changed

+163
-26
lines changed

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/config/RepositoryRestMvcConfiguration.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,9 @@ protected ObjectMapper basicObjectMapper() {
921921
configurerDelegate.get().configureJacksonObjectMapper(objectMapper);
922922

923923
objectMapper.registerModule(geoModule.getObject());
924+
objectMapper.registerModule(new AggregateReferenceResolvingModule(
925+
new UriToEntityConverter(persistentEntities.get(), repositoryInvokerFactory.get(), repositories.get()),
926+
resourceMappings.get()));
924927

925928
if (repositoryRestConfiguration.get().isEnableEnumTranslation()) {
926929
objectMapper.registerModule(new JacksonSerializers(enumTranslator.get()));
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.rest.webmvc.json;
17+
18+
import java.util.Iterator;
19+
20+
import org.springframework.data.rest.core.UriToEntityConverter;
21+
import org.springframework.data.rest.core.mapping.ResourceMappings;
22+
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.AssociationUriResolvingDeserializerModifier.ValueInstantiatorCustomizer;
23+
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.CollectionValueInstantiator;
24+
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.UriStringDeserializer;
25+
import org.springframework.data.util.ClassTypeInformation;
26+
import org.springframework.data.util.TypeInformation;
27+
import org.springframework.util.Assert;
28+
29+
import com.fasterxml.jackson.databind.BeanDescription;
30+
import com.fasterxml.jackson.databind.DeserializationConfig;
31+
import com.fasterxml.jackson.databind.JsonDeserializer;
32+
import com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder;
33+
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
34+
import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
35+
import com.fasterxml.jackson.databind.deser.std.CollectionDeserializer;
36+
import com.fasterxml.jackson.databind.module.SimpleModule;
37+
import com.fasterxml.jackson.databind.type.CollectionLikeType;
38+
39+
/**
40+
* Jackson module to enable aggregate reference resolution for non-aggregate root types. This is primarily useful for
41+
* any kind of payload mapping DTO that is supposed to be able to map URIs to aggregate roots.
42+
*
43+
* @author Oliver Drotbohm
44+
* @since 3.5
45+
*/
46+
public class AggregateReferenceResolvingModule extends SimpleModule {
47+
48+
private static final long serialVersionUID = 6002883434719869173L;
49+
50+
/**
51+
* Creates a new {@link AggregateReferenceResolvingModule} using the given {@link UriToEntityConverter} and
52+
* {@link ResourceMappings}.
53+
*
54+
* @param converter must not be {@literal null}.
55+
* @param mappings must not be {@literal null}.
56+
*/
57+
public AggregateReferenceResolvingModule(UriToEntityConverter converter, ResourceMappings mappings) {
58+
setDeserializerModifier(new AggregateReferenceDeserializerModifier(converter, mappings));
59+
}
60+
61+
/**
62+
* {@link BeanDeserializerModifier} implementation to support URI deserialization into aggregate roots
63+
*
64+
* @author Oliver Drotbohm
65+
*/
66+
static class AggregateReferenceDeserializerModifier extends BeanDeserializerModifier {
67+
68+
private final UriToEntityConverter converter;
69+
private final ResourceMappings mappings;
70+
71+
/**
72+
* Creates a new {@link AggregateReferenceDeserializerModifier} for the given {@link UriToEntityConverter} and
73+
* {@link ResourceMappings}.
74+
*
75+
* @param converter must not be {@literal null}.
76+
* @param mappings must not be {@literal null}.
77+
*/
78+
public AggregateReferenceDeserializerModifier(UriToEntityConverter converter, ResourceMappings mappings) {
79+
80+
Assert.notNull(converter, "UriToEntityConverter must not be null!");
81+
Assert.notNull(mappings, "ResourceMappings must not be null!");
82+
83+
this.converter = converter;
84+
this.mappings = mappings;
85+
}
86+
87+
/*
88+
* (non-Javadoc)
89+
* @see com.fasterxml.jackson.databind.deser.BeanDeserializerModifier#updateBuilder(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.BeanDescription, com.fasterxml.jackson.databind.deser.BeanDeserializerBuilder)
90+
*/
91+
@Override
92+
public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanDescription beanDesc,
93+
BeanDeserializerBuilder builder) {
94+
95+
// Type is aggregate itself, already handled by AssociationUriResolvingDeserializerModifier
96+
if (mappings.hasMappingFor(beanDesc.getBeanClass())) {
97+
return builder;
98+
}
99+
100+
TypeInformation<?> type = ClassTypeInformation.from(beanDesc.getBeanClass());
101+
ValueInstantiatorCustomizer customizer = new ValueInstantiatorCustomizer(builder.getValueInstantiator(), config);
102+
Iterator<SettableBeanProperty> properties = builder.getProperties();
103+
104+
while (properties.hasNext()) {
105+
106+
SettableBeanProperty property = properties.next();
107+
108+
TypeInformation<?> propertyType = type.getProperty(property.getName());
109+
TypeInformation<?> actualType = propertyType.getActualType();
110+
111+
if (!mappings.exportsMappingFor(actualType.getType())) {
112+
continue;
113+
}
114+
115+
UriStringDeserializer uriStringDeserializer = new UriStringDeserializer(actualType.getType(), converter);
116+
JsonDeserializer<?> deserializer = wrapIfCollection(propertyType, uriStringDeserializer, config);
117+
118+
customizer.replacePropertyIfNeeded(builder, property.withValueDeserializer(deserializer));
119+
}
120+
121+
return builder;
122+
}
123+
124+
private static JsonDeserializer<?> wrapIfCollection(TypeInformation<?> type,
125+
JsonDeserializer<Object> elementDeserializer, DeserializationConfig config) {
126+
127+
if (!type.isCollectionLike()) {
128+
return elementDeserializer;
129+
}
130+
131+
CollectionLikeType collectionType = config.getTypeFactory() //
132+
.constructCollectionLikeType(type.getType(), type.getActualType().getType());
133+
CollectionValueInstantiator instantiator = new CollectionValueInstantiator(type);
134+
135+
return new CollectionDeserializer(collectionType, elementDeserializer, null, instantiator);
136+
}
137+
}
138+
}

spring-data-rest-webmvc/src/main/java/org/springframework/data/rest/webmvc/json/PersistentEntityJackson2Module.java

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.data.rest.webmvc.mapping.Associations;
4949
import org.springframework.data.rest.webmvc.mapping.LinkCollector;
5050
import org.springframework.data.util.CastUtils;
51+
import org.springframework.data.util.TypeInformation;
5152
import org.springframework.hateoas.EntityModel;
5253
import org.springframework.hateoas.Link;
5354
import org.springframework.hateoas.Links;
@@ -427,7 +428,6 @@ public static class AssociationUriResolvingDeserializerModifier extends BeanDese
427428
private final UriToEntityConverter converter;
428429
private final RepositoryInvokerFactory factory;
429430

430-
@java.lang.SuppressWarnings("all")
431431
public AssociationUriResolvingDeserializerModifier(PersistentEntities entities, Associations associations,
432432
UriToEntityConverter converter, RepositoryInvokerFactory factory) {
433433

@@ -451,27 +451,26 @@ public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanD
451451
BeanDeserializerBuilder builder) {
452452

453453
ValueInstantiatorCustomizer customizer = new ValueInstantiatorCustomizer(builder.getValueInstantiator(), config);
454-
455454
Iterator<SettableBeanProperty> properties = builder.getProperties();
456455

457456
entities.getPersistentEntity(beanDesc.getBeanClass()).ifPresent(entity -> {
458457

459458
while (properties.hasNext()) {
460459

461460
SettableBeanProperty property = properties.next();
462-
463461
PersistentProperty<?> persistentProperty = entity.getPersistentProperty(property.getName());
464462

465463
if (persistentProperty == null) {
466464
continue;
467465
}
468466

467+
TypeInformation<?> propertyType = persistentProperty.getTypeInformation();
468+
469469
if (associationLinks.isLookupType(persistentProperty)) {
470470

471471
RepositoryInvokingDeserializer repositoryInvokingDeserializer = new RepositoryInvokingDeserializer(factory,
472472
persistentProperty);
473-
JsonDeserializer<?> deserializer = wrapIfCollection(persistentProperty, repositoryInvokingDeserializer,
474-
config);
473+
JsonDeserializer<?> deserializer = wrapIfCollection(propertyType, repositoryInvokingDeserializer, config);
475474

476475
builder.addOrReplaceProperty(property.withValueDeserializer(deserializer), false);
477476
continue;
@@ -481,8 +480,9 @@ public BeanDeserializerBuilder updateBuilder(DeserializationConfig config, BeanD
481480
continue;
482481
}
483482

484-
UriStringDeserializer uriStringDeserializer = new UriStringDeserializer(persistentProperty, converter);
485-
JsonDeserializer<?> deserializer = wrapIfCollection(persistentProperty, uriStringDeserializer, config);
483+
Class<?> actualPropertyType = persistentProperty.getActualType();
484+
UriStringDeserializer uriStringDeserializer = new UriStringDeserializer(actualPropertyType, converter);
485+
JsonDeserializer<?> deserializer = wrapIfCollection(propertyType, uriStringDeserializer, config);
486486

487487
customizer.replacePropertyIfNeeded(builder, property.withValueDeserializer(deserializer));
488488
}
@@ -519,7 +519,7 @@ static class ValueInstantiatorCustomizer {
519519

520520
/**
521521
* Replaces the logically same property with the given {@link SettableBeanProperty} on the given
522-
* {@link BeanDeserializerBuilder}. In case we get a {@link CreatorProperty} we als register that one to be later
522+
* {@link BeanDeserializerBuilder}. In case we get a {@link CreatorProperty} we also register that one to be later
523523
* exposed via the {@link ValueInstantiator} backing the {@link BeanDeserializerBuilder}.
524524
*
525525
* @param builder must not be {@literal null}.
@@ -559,15 +559,15 @@ BeanDeserializerBuilder conclude(BeanDeserializerBuilder builder) {
559559
}
560560
}
561561

562-
private static JsonDeserializer<?> wrapIfCollection(PersistentProperty<?> property,
562+
private static JsonDeserializer<?> wrapIfCollection(TypeInformation<?> property,
563563
JsonDeserializer<Object> elementDeserializer, DeserializationConfig config) {
564564

565565
if (!property.isCollectionLike()) {
566566
return elementDeserializer;
567567
}
568568

569569
CollectionLikeType collectionType = config.getTypeFactory().constructCollectionLikeType(property.getType(),
570-
property.getActualType());
570+
property.getActualType().getType());
571571
CollectionValueInstantiator instantiator = new CollectionValueInstantiator(property);
572572
return new CollectionDeserializer(collectionType, elementDeserializer, null, instantiator);
573573
}
@@ -580,26 +580,26 @@ private static JsonDeserializer<?> wrapIfCollection(PersistentProperty<?> proper
580580
* @author Oliver Gierke
581581
* @author Valentin Rentschler
582582
*/
583-
static class UriStringDeserializer extends StdDeserializer<Object> {
583+
public static class UriStringDeserializer extends StdDeserializer<Object> {
584584

585585
private static final long serialVersionUID = -2175900204153350125L;
586586
private static final String UNEXPECTED_VALUE = "Expected URI cause property %s points to the managed domain type!";
587587

588-
private final PersistentProperty<?> property;
588+
private final Class<?> type;
589589
private final UriToEntityConverter converter;
590590

591591
/**
592592
* Creates a new {@link UriStringDeserializer} for the given {@link PersistentProperty} using the given
593593
* {@link UriToEntityConverter}.
594594
*
595-
* @param property must not be {@literal null}.
595+
* @param type must not be {@literal null}.
596596
* @param converter must not be {@literal null}.
597597
*/
598-
public UriStringDeserializer(PersistentProperty<?> property, UriToEntityConverter converter) {
598+
public UriStringDeserializer(Class<?> type, UriToEntityConverter converter) {
599599

600-
super(property.getActualType());
600+
super(type);
601601

602-
this.property = property;
602+
this.type = type;
603603
this.converter = converter;
604604
}
605605

@@ -618,11 +618,11 @@ public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOE
618618

619619
try {
620620
URI uri = UriTemplate.of(source).expand();
621-
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(property.getActualType());
621+
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(type);
622622

623623
return converter.convert(uri, URI_DESCRIPTOR, typeDescriptor);
624624
} catch (IllegalArgumentException o_O) {
625-
throw ctxt.weirdStringException(source, URI.class, String.format(UNEXPECTED_VALUE, property));
625+
throw ctxt.weirdStringException(source, URI.class, String.format(UNEXPECTED_VALUE, type));
626626
}
627627
}
628628

@@ -813,16 +813,16 @@ public JsonSerializer<ProjectionResourceContent> unwrappingSerializer(NameTransf
813813
*
814814
* @author Oliver Gierke
815815
*/
816-
private static class CollectionValueInstantiator extends ValueInstantiator {
816+
static class CollectionValueInstantiator extends ValueInstantiator {
817817

818-
private final PersistentProperty<?> property;
818+
private final TypeInformation<?> property;
819819

820820
/**
821821
* Creates a new {@link CollectionValueInstantiator} for the given {@link PersistentProperty}.
822822
*
823823
* @param property must not be {@literal null} and must be a collection.
824824
*/
825-
public CollectionValueInstantiator(PersistentProperty<?> property) {
825+
public CollectionValueInstantiator(TypeInformation<?> property) {
826826

827827
Assert.notNull(property, "Property must not be null!");
828828
Assert.isTrue(property.isCollectionLike() || property.isMap(), "Property must be a collection or map property!");

spring-data-rest-webmvc/src/test/java/org/springframework/data/rest/webmvc/json/UriStringDeserializerUnitTests.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
import org.mockito.Mockito;
3131
import org.mockito.junit.MockitoJUnitRunner;
3232
import org.springframework.core.convert.TypeDescriptor;
33-
import org.springframework.data.mapping.PersistentProperty;
3433
import org.springframework.data.rest.core.UriToEntityConverter;
3534
import org.springframework.data.rest.webmvc.json.PersistentEntityJackson2Module.UriStringDeserializer;
3635
import org.springframework.test.util.ReflectionTestUtils;
@@ -51,7 +50,6 @@ public class UriStringDeserializerUnitTests {
5150
public @Rule ExpectedException exception = ExpectedException.none();
5251

5352
@Mock UriToEntityConverter converter;
54-
@Mock PersistentProperty<?> property;
5553

5654
@Mock JsonParser parser;
5755
DeserializationContext context;
@@ -61,7 +59,7 @@ public class UriStringDeserializerUnitTests {
6159
@Before
6260
public void setUp() {
6361

64-
this.deserializer = new UriStringDeserializer(property, converter);
62+
this.deserializer = new UriStringDeserializer(Object.class, converter);
6563

6664
// Need to hack the context as there's virtually no way wo set up a combined parser and context easily
6765
this.context = new ObjectMapper().getDeserializationContext();
@@ -94,10 +92,8 @@ public void rejectsNonUriValue() throws Exception {
9492
invokeConverterWith("{ \"foo\" : \"bar\" }");
9593
}
9694

97-
@SuppressWarnings({ "unchecked", "rawtypes" })
9895
private Object invokeConverterWith(String source) throws Exception {
9996

100-
when(property.getActualType()).thenReturn((Class) Object.class);
10197
when(parser.getValueAsString()).thenReturn(source);
10298

10399
return deserializer.deserialize(parser, context);

0 commit comments

Comments
 (0)