Skip to content

Commit 64a4de2

Browse files
committed
Publish link-save events with the value being saved.
createPropertyReference(…) published BeforeLinkSaveEvent and AfterLinkSaveEvent with the property value captured before the submitted links were applied to the owning aggregate. For PUT on collection and map associations (which replace the value with a freshly created container) and for singular associations, that captured value was the previously linked one rather than the one being saved. Only the augmenting POST/PATCH case happened to reflect the new state, as it mutates the existing container in place. This left listeners (e.g. methods annotated with @HandleBeforeLinkSave) inspecting the event's linked argument unable to see the value actually being persisted. We now re-read the property after the links have been applied, so both events carry the value that is going to be saved, independent of the association's cardinality or the HTTP method used. Closes GH-1600, GH-1419.
1 parent 3c6b8f1 commit 64a4de2

2 files changed

Lines changed: 90 additions & 2 deletions

File tree

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,14 @@ public ResponseEntity<?> createPropertyReference(
293293
loadPropertyValue(prop.propertyType, source.getLinks().toList().get(0)));
294294
}
295295

296-
publishEvent(new BeforeLinkSaveEvent(prop.accessor.getBean(), prop.propertyValue));
296+
// Re-read the property after the mutation above so that the events carry the value actually being
297+
// saved rather than the one captured before the assignment. The latter would be stale for PUT
298+
// (which replaces collections/maps with a new container) and for singular references.
299+
var linked = prop.accessor.getProperty(prop.property);
300+
301+
publishEvent(new BeforeLinkSaveEvent(prop.accessor.getBean(), linked));
297302
var result = invoker.invokeSave(prop.accessor.getBean());
298-
publishEvent(new AfterLinkSaveEvent(result, prop.propertyValue));
303+
publishEvent(new AfterLinkSaveEvent(result, linked));
299304

300305
return null;
301306
};

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,23 @@
1515
*/
1616
package org.springframework.data.rest.webmvc;
1717

18+
import static org.assertj.core.api.Assertions.*;
1819
import static org.mockito.ArgumentMatchers.*;
1920
import static org.mockito.Mockito.*;
2021

2122
import java.util.ArrayList;
23+
import java.util.Collection;
2224
import java.util.Collections;
2325
import java.util.List;
2426
import java.util.Optional;
2527

2628
import org.junit.jupiter.api.Test;
2729
import org.junit.jupiter.api.extension.ExtendWith;
30+
import org.mockito.ArgumentCaptor;
2831
import org.mockito.Mock;
2932
import org.mockito.junit.jupiter.MockitoExtension;
3033
import org.springframework.context.ApplicationEventPublisher;
34+
import org.springframework.data.rest.core.event.BeforeLinkSaveEvent;
3135
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity;
3236
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
3337
import org.springframework.data.mapping.PersistentProperty;
@@ -90,9 +94,88 @@ void usesRepositoryInvokerToLookupRelatedInstance() throws Exception {
9094
verify(invoker).invokeFindById("some-id");
9195
}
9296

97+
@Test // Link-save events must carry the value actually being saved, not the pre-mutation one.
98+
void publishesLinkSaveEventWithNewSingularReference() throws Exception {
99+
100+
KeyValuePersistentEntity<?, ?> entity = mappingContext.getRequiredPersistentEntity(Sample.class);
101+
102+
ResourceMappings mappings = new PersistentEntitiesResourceMappings(
103+
new PersistentEntities(Collections.singleton(mappingContext)));
104+
ResourceMetadata metadata = spy(mappings.getMetadataFor(Sample.class));
105+
when(metadata.getSupportedHttpMethods()).thenReturn(AllSupportedHttpMethods.INSTANCE);
106+
107+
RepositoryPropertyReferenceController controller = new RepositoryPropertyReferenceController(repositories,
108+
invokerFactory);
109+
controller.setApplicationEventPublisher(publisher);
110+
111+
Sample owner = new Sample();
112+
owner.reference = new Reference(); // the previously linked reference
113+
Reference newReference = new Reference(); // the one being linked now
114+
115+
doReturn(invoker).when(invokerFactory).getInvokerFor(Reference.class);
116+
doReturn(Optional.of(owner)).when(invoker).invokeFindById(4711);
117+
doReturn(Optional.of(newReference)).when(invoker).invokeFindById("new-id");
118+
doReturn(owner).when(invoker).invokeSave(any(Object.class));
119+
120+
RootResourceInformation information = new RootResourceInformation(metadata, entity, invoker);
121+
CollectionModel<Object> request = CollectionModel.empty(Link.of("/reference/new-id"));
122+
123+
controller.createPropertyReference(information, HttpMethod.PUT, request, 4711, "reference");
124+
125+
assertThat(captureLinkSaveEvent().getLinked()).isSameAs(newReference);
126+
}
127+
128+
@Test // Link-save events must carry the value actually being saved, not the pre-mutation one.
129+
void publishesLinkSaveEventWithReplacedCollection() throws Exception {
130+
131+
KeyValuePersistentEntity<?, ?> entity = mappingContext.getRequiredPersistentEntity(Sample.class);
132+
133+
ResourceMappings mappings = new PersistentEntitiesResourceMappings(
134+
new PersistentEntities(Collections.singleton(mappingContext)));
135+
ResourceMetadata metadata = spy(mappings.getMetadataFor(Sample.class));
136+
when(metadata.getSupportedHttpMethods()).thenReturn(AllSupportedHttpMethods.INSTANCE);
137+
138+
RepositoryPropertyReferenceController controller = new RepositoryPropertyReferenceController(repositories,
139+
invokerFactory);
140+
controller.setApplicationEventPublisher(publisher);
141+
142+
Sample owner = new Sample();
143+
Reference oldReference = new Reference();
144+
owner.references.add(oldReference); // the previously linked element
145+
Reference newReference = new Reference(); // the one replacing it via PUT
146+
147+
doReturn(invoker).when(invokerFactory).getInvokerFor(Reference.class);
148+
doReturn(Optional.of(owner)).when(invoker).invokeFindById(4711);
149+
doReturn(Optional.of(newReference)).when(invoker).invokeFindById("new-id");
150+
doReturn(owner).when(invoker).invokeSave(any(Object.class));
151+
152+
RootResourceInformation information = new RootResourceInformation(metadata, entity, invoker);
153+
CollectionModel<Object> request = CollectionModel.empty(Link.of("/reference/new-id"));
154+
155+
controller.createPropertyReference(information, HttpMethod.PUT, request, 4711, "references");
156+
157+
@SuppressWarnings("unchecked")
158+
Collection<Reference> linked = (Collection<Reference>) captureLinkSaveEvent().getLinked();
159+
160+
assertThat(linked).containsExactly(newReference).doesNotContain(oldReference);
161+
}
162+
163+
private BeforeLinkSaveEvent captureLinkSaveEvent() {
164+
165+
ArgumentCaptor<Object> events = ArgumentCaptor.forClass(Object.class);
166+
verify(publisher, atLeastOnce()).publishEvent(events.capture());
167+
168+
return events.getAllValues().stream() //
169+
.filter(BeforeLinkSaveEvent.class::isInstance) //
170+
.map(BeforeLinkSaveEvent.class::cast) //
171+
.findFirst() //
172+
.orElseThrow(() -> new AssertionError("No BeforeLinkSaveEvent published"));
173+
}
174+
93175
@RestResource
94176
static class Sample {
95177
@org.springframework.data.annotation.Reference List<Reference> references = new ArrayList<Reference>();
178+
@org.springframework.data.annotation.Reference Reference reference;
96179
}
97180

98181
@RestResource

0 commit comments

Comments
 (0)