Skip to content

Commit 8d4306b

Browse files
Avoid capturing lambdas, update javadoc and add tests.
1 parent 140dbf9 commit 8d4306b

13 files changed

+454
-147
lines changed

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

+13-17
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,23 @@ public class DefaultReferenceResolver implements ReferenceResolver {
2929

3030
private final ReferenceLoader referenceLoader;
3131

32+
private final LookupFunction collectionLookupFunction = (filter, ctx) -> getReferenceLoader().fetchMany(filter, ctx);
33+
private final LookupFunction singleValueLookupFunction = (filter, ctx) -> {
34+
Object target = getReferenceLoader().fetchOne(filter, ctx);
35+
return target == null ? Collections.emptyList() : Collections.singleton(getReferenceLoader().fetchOne(filter, ctx));
36+
};
37+
3238
public DefaultReferenceResolver(ReferenceLoader referenceLoader) {
3339
this.referenceLoader = referenceLoader;
3440
}
3541

36-
@Override
37-
public ReferenceLoader getReferenceLoader() {
38-
return referenceLoader;
39-
}
40-
4142
@Nullable
4243
@Override
4344
public Object resolveReference(MongoPersistentProperty property, Object source,
4445
ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) {
4546

46-
LookupFunction lookupFunction = (filter, ctx) -> {
47-
if (property.isCollectionLike() || property.isMap()) {
48-
return getReferenceLoader().fetchMany(filter, ctx);
49-
50-
}
51-
52-
Object target = getReferenceLoader().fetchOne(filter, ctx);
53-
return target == null ? Collections.emptyList()
54-
: Collections.singleton(getReferenceLoader().fetchOne(filter, ctx));
55-
};
47+
LookupFunction lookupFunction = (property.isCollectionLike() || property.isMap()) ? collectionLookupFunction
48+
: singleValueLookupFunction;
5649

5750
if (isLazyReference(property)) {
5851
return createLazyLoadingProxy(property, source, referenceLookupDelegate, lookupFunction, entityReader);
@@ -61,9 +54,12 @@ public Object resolveReference(MongoPersistentProperty property, Object source,
6154
return referenceLookupDelegate.readReference(property, source, lookupFunction, entityReader);
6255
}
6356

57+
protected ReferenceLoader getReferenceLoader() {
58+
return referenceLoader;
59+
}
60+
6461
private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
65-
ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction,
66-
MongoEntityReader entityReader) {
62+
ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) {
6763
return new LazyLoadingProxyFactory(referenceLookupDelegate).createLazyLoadingProxy(property, source, lookupFunction,
6864
entityReader);
6965
}

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

+135-59
Original file line numberDiff line numberDiff line change
@@ -15,136 +15,212 @@
1515
*/
1616
package org.springframework.data.mongodb.core.convert;
1717

18-
import java.util.HashMap;
1918
import java.util.LinkedHashMap;
20-
import java.util.Locale;
2119
import java.util.Map;
2220
import java.util.Map.Entry;
21+
import java.util.WeakHashMap;
2322
import java.util.regex.Matcher;
2423
import java.util.regex.Pattern;
2524

2625
import org.bson.Document;
27-
2826
import org.springframework.core.convert.ConversionService;
27+
import org.springframework.dao.InvalidDataAccessApiUsageException;
2928
import org.springframework.data.mapping.PersistentPropertyAccessor;
29+
import org.springframework.data.mapping.PersistentPropertyPath;
30+
import org.springframework.data.mapping.PropertyPath;
3031
import org.springframework.data.mapping.context.MappingContext;
3132
import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory;
3233
import org.springframework.data.mongodb.core.mapping.DocumentPointer;
34+
import org.springframework.data.mongodb.core.mapping.DocumentReference;
3335
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
3436
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
3537

3638
/**
39+
* Internal API to construct {@link DocumentPointer} for a given property. Considers {@link LazyLoadingProxy},
40+
* registered {@link Object} to {@link DocumentPointer} {@link org.springframework.core.convert.converter.Converter},
41+
* simple {@literal _id} lookups and cases where the {@link DocumentPointer} needs to be computed via a lookup query.
42+
*
3743
* @author Christoph Strobl
3844
* @since 3.3
3945
*/
4046
class DocumentPointerFactory {
4147

4248
private final ConversionService conversionService;
4349
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
44-
private final Map<String, LinkageDocument> linkageMap;
45-
46-
public DocumentPointerFactory(ConversionService conversionService,
50+
private final Map<String, LinkageDocument> cache;
51+
52+
/**
53+
* A {@link Pattern} matching quoted and unquoted variants (with/out whitespaces) of
54+
* <code>{'_id' : ?#{#target} }</code>.
55+
*/
56+
private static final Pattern DEFAULT_LOOKUP_PATTERN = Pattern.compile("\\{\\s?" + // document start (whitespace opt)
57+
"['\"]?_id['\"]?" + // followed by an optionally quoted _id. Like: _id, '_id' or "_id"
58+
"?\\s?:\\s?" + // then a colon optionally wrapped inside whitespaces
59+
"['\"]?\\?#\\{#target\\}['\"]?" + // leading to the potentially quoted ?#{#target} expression
60+
"\\s*}"); // some optional whitespaces and document close
61+
62+
DocumentPointerFactory(ConversionService conversionService,
4763
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
4864

4965
this.conversionService = conversionService;
5066
this.mappingContext = mappingContext;
51-
this.linkageMap = new HashMap<>();
67+
this.cache = new WeakHashMap<>();
5268
}
5369

54-
public DocumentPointer<?> computePointer(MongoPersistentProperty property, Object value, Class<?> typeHint) {
70+
DocumentPointer<?> computePointer(
71+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
72+
MongoPersistentProperty property, Object value, Class<?> typeHint) {
5573

5674
if (value instanceof LazyLoadingProxy) {
5775
return () -> ((LazyLoadingProxy) value).getSource();
5876
}
5977

6078
if (conversionService.canConvert(typeHint, DocumentPointer.class)) {
6179
return conversionService.convert(value, DocumentPointer.class);
62-
} else {
63-
64-
MongoPersistentEntity<?> persistentEntity = mappingContext
65-
.getRequiredPersistentEntity(property.getAssociationTargetType());
80+
}
6681

67-
// TODO: Extract method
68-
if (!property.getDocumentReference().lookup().toLowerCase(Locale.ROOT).replaceAll("\\s", "").replaceAll("'", "")
69-
.equals("{_id:?#{#target}}")) {
82+
MongoPersistentEntity<?> persistentEntity = mappingContext
83+
.getRequiredPersistentEntity(property.getAssociationTargetType());
7084

71-
MongoPersistentEntity<?> valueEntity = mappingContext.getPersistentEntity(value.getClass());
72-
PersistentPropertyAccessor<Object> propertyAccessor;
73-
if (valueEntity == null) {
74-
propertyAccessor = BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(),
75-
value);
76-
} else {
77-
propertyAccessor = valueEntity.getPropertyAccessor(value);
85+
if (usesDefaultLookup(property.getDocumentReference())) {
86+
return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier();
87+
}
7888

79-
}
89+
MongoPersistentEntity<?> valueEntity = mappingContext.getPersistentEntity(value.getClass());
90+
PersistentPropertyAccessor<Object> propertyAccessor;
91+
if (valueEntity == null) {
92+
propertyAccessor = BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), value);
93+
} else {
94+
propertyAccessor = valueEntity.getPropertyPathAccessor(value);
95+
}
8096

81-
return () -> linkageMap.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::new)
82-
.get(persistentEntity, propertyAccessor);
83-
}
97+
return cache.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::from)
98+
.getDocumentPointer(mappingContext, persistentEntity, propertyAccessor);
99+
}
84100

85-
// just take the id as a reference
86-
return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier();
87-
}
101+
private boolean usesDefaultLookup(DocumentReference documentReference) {
102+
return DEFAULT_LOOKUP_PATTERN.matcher(documentReference.lookup()).matches();
88103
}
89104

105+
/**
106+
* Value object that computes a document pointer from a given lookup query by identifying SpEL expressions and
107+
* inverting it.
108+
*
109+
* <pre class="code">
110+
* // source
111+
* { 'firstname' : ?#{fn}, 'lastname' : '?#{ln} }
112+
*
113+
* // target
114+
* { 'fn' : ..., 'ln' : ... }
115+
* </pre>
116+
*
117+
* The actual pointer is the computed via
118+
* {@link #getDocumentPointer(MappingContext, MongoPersistentEntity, PersistentPropertyAccessor)} applying values from
119+
* the provided {@link PersistentPropertyAccessor} to the target document by looking at the keys of the expressions
120+
* from the source.
121+
*/
90122
static class LinkageDocument {
91123

92-
static final Pattern pattern = Pattern.compile("\\?#\\{#?[\\w\\d]*\\}");
124+
static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\?#\\{#?(?<fieldName>[\\w\\d\\.\\-)]*)\\}");
125+
static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("###_(?<index>\\d*)_###");
93126

94-
String lookup;
95-
org.bson.Document fetchDocument;
96-
Map<Integer, String> mapMap;
127+
private final String lookup;
128+
private final org.bson.Document documentPointer;
129+
private final Map<String, String> placeholderMap;
97130

98-
public LinkageDocument(String lookup) {
131+
static LinkageDocument from(String lookup) {
132+
return new LinkageDocument(lookup);
133+
}
99134

100-
this.lookup = lookup;
101-
String targetLookup = lookup;
135+
private LinkageDocument(String lookup) {
102136

137+
this.lookup = lookup;
138+
this.placeholderMap = new LinkedHashMap<>();
103139

104-
Matcher matcher = pattern.matcher(lookup);
105140
int index = 0;
106-
mapMap = new LinkedHashMap<>();
141+
Matcher matcher = EXPRESSION_PATTERN.matcher(lookup);
142+
String targetLookup = lookup;
107143

108-
// TODO: Make explicit what's happening here
109144
while (matcher.find()) {
110145

111-
String expr = matcher.group();
112-
String sanitized = expr.substring(0, expr.length() - 1).replace("?#{#", "").replace("?#{", "")
113-
.replace("target.", "").replaceAll("'", "");
114-
mapMap.put(index, sanitized);
115-
targetLookup = targetLookup.replace(expr, index + "");
146+
String expression = matcher.group();
147+
String fieldName = matcher.group("fieldName").replace("target.", "");
148+
149+
String placeholder = placeholder(index);
150+
placeholderMap.put(placeholder, fieldName);
151+
targetLookup = targetLookup.replace(expression, "'" + placeholder + "'");
116152
index++;
117153
}
118154

119-
fetchDocument = org.bson.Document.parse(targetLookup);
155+
this.documentPointer = org.bson.Document.parse(targetLookup);
120156
}
121157

122-
org.bson.Document get(MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {
158+
private String placeholder(int index) {
159+
return "###_" + index + "_###";
160+
}
123161

124-
org.bson.Document targetDocument = new Document();
162+
private boolean isPlaceholder(String key) {
163+
return PLACEHOLDER_PATTERN.matcher(key).matches();
164+
}
125165

126-
// TODO: recursive matching over nested Documents or would the parameter binding json parser be a thing?
127-
// like we have it ordered by index values and could provide the parameter array from it.
166+
DocumentPointer<Object> getDocumentPointer(
167+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
168+
MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {
169+
return () -> updatePlaceholders(documentPointer, new Document(), mappingContext, persistentEntity,
170+
propertyAccessor);
171+
}
172+
173+
Document updatePlaceholders(org.bson.Document source, org.bson.Document target,
174+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
175+
MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {
128176

129-
for (Entry<String, Object> entry : fetchDocument.entrySet()) {
177+
for (Entry<String, Object> entry : source.entrySet()) {
178+
179+
if (entry.getKey().startsWith("$")) {
180+
throw new InvalidDataAccessApiUsageException(String.format(
181+
"Cannot derive document pointer from lookup '%s' using query operator (%s). Please consider registering a custom converter.", lookup, entry.getKey()));
182+
}
130183

131-
if (entry.getKey().equals("target")) {
184+
if (entry.getValue() instanceof Document) {
132185

133-
String refKey = mapMap.get(entry.getValue());
186+
MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(entry.getKey());
187+
if (persistentProperty != null && persistentProperty.isEntity()) {
134188

135-
if (persistentEntity.hasIdProperty()) {
136-
targetDocument.put(refKey, propertyAccessor.getProperty(persistentEntity.getIdProperty()));
189+
MongoPersistentEntity<?> nestedEntity = mappingContext.getPersistentEntity(persistentProperty.getType());
190+
target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext,
191+
nestedEntity, nestedEntity.getPropertyAccessor(propertyAccessor.getProperty(persistentProperty))));
137192
} else {
138-
targetDocument.put(refKey, propertyAccessor.getBean());
193+
target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext,
194+
persistentEntity, propertyAccessor));
139195
}
140196
continue;
141197
}
142198

143-
Object target = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(entry.getKey()));
144-
String refKey = mapMap.get(entry.getValue());
145-
targetDocument.put(refKey, target);
199+
if (placeholderMap.containsKey(entry.getValue())) {
200+
201+
String attribute = placeholderMap.get(entry.getValue());
202+
if (attribute.contains(".")) {
203+
attribute = attribute.substring(attribute.lastIndexOf('.') + 1);
204+
}
205+
206+
String fieldName = entry.getKey().equals("_id") ? "id" : entry.getKey();
207+
if (!fieldName.contains(".")) {
208+
209+
Object targetValue = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(fieldName));
210+
target.put(attribute, targetValue);
211+
continue;
212+
}
213+
214+
PersistentPropertyPath<?> path = mappingContext
215+
.getPersistentPropertyPath(PropertyPath.from(fieldName, persistentEntity.getTypeInformation()));
216+
Object targetValue = propertyAccessor.getProperty(path);
217+
target.put(attribute, targetValue);
218+
continue;
219+
}
220+
221+
target.put(entry.getKey(), entry.getValue());
146222
}
147-
return targetDocument;
223+
return target;
148224
}
149225
}
150226
}

0 commit comments

Comments
 (0)