Skip to content

Commit aa35aae

Browse files
kufdchristophstrobl
authored andcommitted
Add fullDocumentBeforeChange support for change streams.
Closes: #4187 Original Pull Request: #4193
1 parent a572580 commit aa35aae

File tree

7 files changed

+306
-16
lines changed

7 files changed

+306
-16
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java

+32-15
Original file line numberDiff line numberDiff line change
@@ -36,21 +36,29 @@
3636
*
3737
* @author Christoph Strobl
3838
* @author Mark Paluch
39+
* @author Myroslav Kosinskyi
3940
* @since 2.1
4041
*/
4142
public class ChangeStreamEvent<T> {
4243

4344
@SuppressWarnings("rawtypes") //
44-
private static final AtomicReferenceFieldUpdater<ChangeStreamEvent, Object> CONVERTED_UPDATER = AtomicReferenceFieldUpdater
45-
.newUpdater(ChangeStreamEvent.class, Object.class, "converted");
45+
private static final AtomicReferenceFieldUpdater<ChangeStreamEvent, Object> CONVERTED_FULL_DOCUMENT_UPDATER = AtomicReferenceFieldUpdater
46+
.newUpdater(ChangeStreamEvent.class, Object.class, "convertedFullDocument");
47+
48+
@SuppressWarnings("rawtypes") //
49+
private static final AtomicReferenceFieldUpdater<ChangeStreamEvent, Object> CONVERTED_FULL_DOCUMENT_BEFORE_CHANGE_UPDATER = AtomicReferenceFieldUpdater
50+
.newUpdater(ChangeStreamEvent.class, Object.class, "convertedFullDocumentBeforeChange");
4651

4752
private final @Nullable ChangeStreamDocument<Document> raw;
4853

4954
private final Class<T> targetType;
5055
private final MongoConverter converter;
5156

52-
// accessed through CONVERTED_UPDATER.
53-
private volatile @Nullable T converted;
57+
// accessed through CONVERTED_FULL_DOCUMENT_UPDATER.
58+
private volatile @Nullable T convertedFullDocument;
59+
60+
// accessed through CONVERTED_FULL_DOCUMENT_BEFORE_CHANGE_UPDATER.
61+
private volatile @Nullable T convertedFullDocumentBeforeChange;
5462

5563
/**
5664
* @param raw can be {@literal null}.
@@ -147,27 +155,36 @@ public String getCollectionName() {
147155
@Nullable
148156
public T getBody() {
149157

150-
if (raw == null) {
158+
if (raw == null || raw.getFullDocument() == null) {
151159
return null;
152160
}
153161

154-
Document fullDocument = raw.getFullDocument();
162+
return getConvertedFullDocument(raw.getFullDocument());
163+
}
164+
165+
@Nullable
166+
public T getBodyBeforeChange() {
155167

156-
if (fullDocument == null) {
157-
return targetType.cast(fullDocument);
168+
if (raw == null || raw.getFullDocumentBeforeChange() == null) {
169+
return null;
158170
}
159171

160-
return getConverted(fullDocument);
172+
return getConvertedFullDocumentBeforeChange(raw.getFullDocumentBeforeChange());
173+
}
174+
175+
@SuppressWarnings("unchecked")
176+
private T getConvertedFullDocumentBeforeChange(Document fullDocument) {
177+
return (T) doGetConverted(fullDocument, CONVERTED_FULL_DOCUMENT_BEFORE_CHANGE_UPDATER);
161178
}
162179

163180
@SuppressWarnings("unchecked")
164-
private T getConverted(Document fullDocument) {
165-
return (T) doGetConverted(fullDocument);
181+
private T getConvertedFullDocument(Document fullDocument) {
182+
return (T) doGetConverted(fullDocument, CONVERTED_FULL_DOCUMENT_UPDATER);
166183
}
167184

168-
private Object doGetConverted(Document fullDocument) {
185+
private Object doGetConverted(Document fullDocument, AtomicReferenceFieldUpdater<ChangeStreamEvent, Object> updater) {
169186

170-
Object result = CONVERTED_UPDATER.get(this);
187+
Object result = updater.get(this);
171188

172189
if (result != null) {
173190
return result;
@@ -176,13 +193,13 @@ private Object doGetConverted(Document fullDocument) {
176193
if (ClassUtils.isAssignable(Document.class, fullDocument.getClass())) {
177194

178195
result = converter.read(targetType, fullDocument);
179-
return CONVERTED_UPDATER.compareAndSet(this, null, result) ? result : CONVERTED_UPDATER.get(this);
196+
return updater.compareAndSet(this, null, result) ? result : updater.get(this);
180197
}
181198

182199
if (converter.getConversionService().canConvert(fullDocument.getClass(), targetType)) {
183200

184201
result = converter.getConversionService().convert(fullDocument, targetType);
185-
return CONVERTED_UPDATER.compareAndSet(this, null, result) ? result : CONVERTED_UPDATER.get(this);
202+
return updater.compareAndSet(this, null, result) ? result : updater.get(this);
186203
}
187204

188205
throw new IllegalArgumentException(

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java

+30
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Arrays;
2020
import java.util.Optional;
2121

22+
import com.mongodb.client.model.changestream.FullDocumentBeforeChange;
2223
import org.bson.BsonDocument;
2324
import org.bson.BsonTimestamp;
2425
import org.bson.BsonValue;
@@ -40,13 +41,15 @@
4041
*
4142
* @author Christoph Strobl
4243
* @author Mark Paluch
44+
* @author Myroslav Kosinskyi
4345
* @since 2.1
4446
*/
4547
public class ChangeStreamOptions {
4648

4749
private @Nullable Object filter;
4850
private @Nullable BsonValue resumeToken;
4951
private @Nullable FullDocument fullDocumentLookup;
52+
private @Nullable FullDocumentBeforeChange fullDocumentBeforeChangeLookup;
5053
private @Nullable Collation collation;
5154
private @Nullable Object resumeTimestamp;
5255
private Resume resume = Resume.UNDEFINED;
@@ -74,6 +77,13 @@ public Optional<FullDocument> getFullDocumentLookup() {
7477
return Optional.ofNullable(fullDocumentLookup);
7578
}
7679

80+
/**
81+
* @return {@link Optional#empty()} if not set.
82+
*/
83+
public Optional<FullDocumentBeforeChange> getFullDocumentBeforeChangeLookup() {
84+
return Optional.ofNullable(fullDocumentBeforeChangeLookup);
85+
}
86+
7787
/**
7888
* @return {@link Optional#empty()} if not set.
7989
*/
@@ -170,6 +180,9 @@ public boolean equals(Object o) {
170180
if (!ObjectUtils.nullSafeEquals(this.fullDocumentLookup, that.fullDocumentLookup)) {
171181
return false;
172182
}
183+
if (!ObjectUtils.nullSafeEquals(this.fullDocumentBeforeChangeLookup, that.fullDocumentBeforeChangeLookup)) {
184+
return false;
185+
}
173186
if (!ObjectUtils.nullSafeEquals(this.collation, that.collation)) {
174187
return false;
175188
}
@@ -184,6 +197,7 @@ public int hashCode() {
184197
int result = ObjectUtils.nullSafeHashCode(filter);
185198
result = 31 * result + ObjectUtils.nullSafeHashCode(resumeToken);
186199
result = 31 * result + ObjectUtils.nullSafeHashCode(fullDocumentLookup);
200+
result = 31 * result + ObjectUtils.nullSafeHashCode(fullDocumentBeforeChangeLookup);
187201
result = 31 * result + ObjectUtils.nullSafeHashCode(collation);
188202
result = 31 * result + ObjectUtils.nullSafeHashCode(resumeTimestamp);
189203
result = 31 * result + ObjectUtils.nullSafeHashCode(resume);
@@ -220,6 +234,7 @@ public static class ChangeStreamOptionsBuilder {
220234
private @Nullable Object filter;
221235
private @Nullable BsonValue resumeToken;
222236
private @Nullable FullDocument fullDocumentLookup;
237+
private @Nullable FullDocumentBeforeChange fullDocumentBeforeChangeLookup;
223238
private @Nullable Collation collation;
224239
private @Nullable Object resumeTimestamp;
225240
private Resume resume = Resume.UNDEFINED;
@@ -322,6 +337,20 @@ public ChangeStreamOptionsBuilder fullDocumentLookup(FullDocument lookup) {
322337
return this;
323338
}
324339

340+
/**
341+
* Set the {@link FullDocumentBeforeChange} lookup to use.
342+
*
343+
* @param lookup must not be {@literal null}.
344+
* @return this.
345+
*/
346+
public ChangeStreamOptionsBuilder fullDocumentBeforeChangeLookup(FullDocumentBeforeChange lookup) {
347+
348+
Assert.notNull(lookup, "Lookup must not be null");
349+
350+
this.fullDocumentBeforeChangeLookup = lookup;
351+
return this;
352+
}
353+
325354
/**
326355
* Set the cluster time to resume from.
327356
*
@@ -391,6 +420,7 @@ public ChangeStreamOptions build() {
391420
options.filter = this.filter;
392421
options.resumeToken = this.resumeToken;
393422
options.fullDocumentLookup = this.fullDocumentLookup;
423+
options.fullDocumentBeforeChangeLookup = this.fullDocumentBeforeChangeLookup;
394424
options.collation = this.collation;
395425
options.resumeTimestamp = this.resumeTimestamp;
396426
options.resume = this.resume;

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java

+16
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.time.Duration;
1919
import java.time.Instant;
2020

21+
import com.mongodb.client.model.changestream.FullDocumentBeforeChange;
2122
import org.bson.BsonValue;
2223
import org.bson.Document;
2324
import org.springframework.data.mongodb.core.ChangeStreamOptions;
@@ -90,6 +91,7 @@
9091
*
9192
* @author Christoph Strobl
9293
* @author Mark Paluch
94+
* @author Myroslav Kosinskyi
9395
* @since 2.1
9496
*/
9597
public class ChangeStreamRequest<T>
@@ -425,6 +427,20 @@ public ChangeStreamRequestBuilder<T> fullDocumentLookup(FullDocument lookup) {
425427
return this;
426428
}
427429

430+
/**
431+
* @return this.
432+
* @see #fullDocumentBeforeChangeLookup(FullDocumentBeforeChange) (FullDocumentBeforeChange)
433+
* @see ChangeStreamOptions#getFullDocumentBeforeChangeLookup()
434+
* @see ChangeStreamOptionsBuilder#fullDocumentBeforeChangeLookup(FullDocumentBeforeChange)
435+
*/
436+
public ChangeStreamRequestBuilder<T> fullDocumentBeforeChangeLookup(FullDocumentBeforeChange lookup) {
437+
438+
Assert.notNull(lookup, "FullDocumentBeforeChange not be null");
439+
440+
this.delegate.fullDocumentBeforeChangeLookup(lookup);
441+
return this;
442+
}
443+
428444
/**
429445
* Set the cursors maximum wait time on the server (for a new Document to be emitted).
430446
*

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java

+13
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.Set;
2424
import java.util.concurrent.TimeUnit;
2525

26+
import com.mongodb.client.model.changestream.FullDocumentBeforeChange;
2627
import org.bson.BsonDocument;
2728
import org.bson.BsonTimestamp;
2829
import org.bson.BsonValue;
@@ -58,6 +59,7 @@
5859
*
5960
* @author Christoph Strobl
6061
* @author Mark Paluch
62+
* @author Myroslav Kosinskyi
6163
* @since 2.1
6264
*/
6365
class ChangeStreamTask extends CursorReadingTask<ChangeStreamDocument<Document>, Object> {
@@ -86,6 +88,7 @@ protected MongoCursor<ChangeStreamDocument<Document>> initCursor(MongoTemplate t
8688
Collation collation = null;
8789
FullDocument fullDocument = ClassUtils.isAssignable(Document.class, targetType) ? FullDocument.DEFAULT
8890
: FullDocument.UPDATE_LOOKUP;
91+
FullDocumentBeforeChange fullDocumentBeforeChange = FullDocumentBeforeChange.DEFAULT;
8992
BsonTimestamp startAt = null;
9093
boolean resumeAfter = true;
9194

@@ -113,6 +116,9 @@ protected MongoCursor<ChangeStreamDocument<Document>> initCursor(MongoTemplate t
113116
.orElseGet(() -> ClassUtils.isAssignable(Document.class, targetType) ? FullDocument.DEFAULT
114117
: FullDocument.UPDATE_LOOKUP);
115118

119+
fullDocumentBeforeChange = changeStreamOptions.getFullDocumentBeforeChangeLookup()
120+
.orElse(FullDocumentBeforeChange.DEFAULT);
121+
116122
startAt = changeStreamOptions.getResumeBsonTimestamp().orElse(null);
117123
}
118124

@@ -152,6 +158,7 @@ protected MongoCursor<ChangeStreamDocument<Document>> initCursor(MongoTemplate t
152158
}
153159

154160
iterable = iterable.fullDocument(fullDocument);
161+
iterable = iterable.fullDocumentBeforeChange(fullDocumentBeforeChange);
155162

156163
return iterable.iterator();
157164
}
@@ -230,6 +237,12 @@ public T getBody() {
230237
return delegate.getBody();
231238
}
232239

240+
@Nullable
241+
@Override
242+
public T getBodyBeforeChange() {
243+
return delegate.getBodyBeforeChange();
244+
}
245+
233246
@Override
234247
public MessageProperties getProperties() {
235248
return this.messageProperties;

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java

+11
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
*
3232
* @author Christoph Strobl
3333
* @author Mark Paluch
34+
* @author Myroslav Kosinskyi
3435
* @see MessageProperties
3536
* @since 2.1
3637
*/
@@ -52,6 +53,16 @@ public interface Message<S, T> {
5253
@Nullable
5354
T getBody();
5455

56+
/**
57+
* The converted message body before change if available.
58+
*
59+
* @return can be {@literal null}.
60+
*/
61+
@Nullable
62+
default T getBodyBeforeChange() {
63+
return null;
64+
}
65+
5566
/**
5667
* {@link MessageProperties} containing information about the {@link Message} origin and other metadata.
5768
*

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTaskUnitTests.java

+3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242

4343
/**
4444
* @author Christoph Strobl
45+
* @author Myroslav Kosinskyi
4546
*/
4647
@ExtendWith(MockitoExtension.class)
4748
class ChangeStreamTaskUnitTests {
@@ -67,6 +68,8 @@ void setUp() {
6768
when(mongoCollection.watch(eq(Document.class))).thenReturn(changeStreamIterable);
6869

6970
when(changeStreamIterable.fullDocument(any())).thenReturn(changeStreamIterable);
71+
72+
when(changeStreamIterable.fullDocumentBeforeChange(any())).thenReturn(changeStreamIterable);
7073
}
7174

7275
@Test // DATAMONGO-2258

0 commit comments

Comments
 (0)