Skip to content

Commit e96ef8e

Browse files
christophstroblmp911de
authored andcommitted
Avoid capturing lambdas, update javadoc and add tests.
Also allow direct usage of (at)Reference from data commons to define associations. Original pull request: #3647. Closes #3602.
1 parent 82af678 commit e96ef8e

16 files changed

+747
-189
lines changed

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

+45-24
Original file line numberDiff line numberDiff line change
@@ -19,40 +19,45 @@
1919

2020
import java.util.Collections;
2121

22+
import org.springframework.data.mongodb.core.mapping.DBRef;
23+
import org.springframework.data.mongodb.core.mapping.DocumentReference;
2224
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
23-
import org.springframework.lang.Nullable;
25+
import org.springframework.util.Assert;
2426

2527
/**
28+
* {@link ReferenceResolver} implementation that uses a given {@link ReferenceLookupDelegate} to load and convert entity
29+
* associations expressed via a {@link MongoPersistentProperty persitent property}. Creates {@link LazyLoadingProxy
30+
* proxies} for associations that should be lazily loaded.
31+
*
2632
* @author Christoph Strobl
2733
*/
2834
public class DefaultReferenceResolver implements ReferenceResolver {
2935

3036
private final ReferenceLoader referenceLoader;
3137

38+
private final LookupFunction collectionLookupFunction = (filter, ctx) -> getReferenceLoader().fetchMany(filter, ctx);
39+
private final LookupFunction singleValueLookupFunction = (filter, ctx) -> {
40+
Object target = getReferenceLoader().fetchOne(filter, ctx);
41+
return target == null ? Collections.emptyList() : Collections.singleton(getReferenceLoader().fetchOne(filter, ctx));
42+
};
43+
44+
/**
45+
* Create a new instance of {@link DefaultReferenceResolver}.
46+
*
47+
* @param referenceLoader must not be {@literal null}.
48+
*/
3249
public DefaultReferenceResolver(ReferenceLoader referenceLoader) {
50+
51+
Assert.notNull(referenceLoader, "ReferenceLoader must not be null!");
3352
this.referenceLoader = referenceLoader;
3453
}
3554

36-
@Override
37-
public ReferenceLoader getReferenceLoader() {
38-
return referenceLoader;
39-
}
40-
41-
@Nullable
4255
@Override
4356
public Object resolveReference(MongoPersistentProperty property, Object source,
4457
ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) {
4558

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-
};
59+
LookupFunction lookupFunction = (property.isCollectionLike() || property.isMap()) ? collectionLookupFunction
60+
: singleValueLookupFunction;
5661

5762
if (isLazyReference(property)) {
5863
return createLazyLoadingProxy(property, source, referenceLookupDelegate, lookupFunction, entityReader);
@@ -61,13 +66,14 @@ public Object resolveReference(MongoPersistentProperty property, Object source,
6166
return referenceLookupDelegate.readReference(property, source, lookupFunction, entityReader);
6267
}
6368

64-
private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
65-
ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction,
66-
MongoEntityReader entityReader) {
67-
return new LazyLoadingProxyFactory(referenceLookupDelegate).createLazyLoadingProxy(property, source, lookupFunction,
68-
entityReader);
69-
}
70-
69+
/**
70+
* Check if the association expressed by the given {@link MongoPersistentProperty property} should be resolved lazily.
71+
*
72+
* @param property
73+
* @return return {@literal true} if the defined association is lazy.
74+
* @see DBRef#lazy()
75+
* @see DocumentReference#lazy()
76+
*/
7177
protected boolean isLazyReference(MongoPersistentProperty property) {
7278

7379
if (property.isDocumentReference()) {
@@ -76,4 +82,19 @@ protected boolean isLazyReference(MongoPersistentProperty property) {
7682

7783
return property.getDBRef() != null && property.getDBRef().lazy();
7884
}
85+
86+
/**
87+
* The {@link ReferenceLoader} executing the lookup.
88+
*
89+
* @return never {@literal null}.
90+
*/
91+
protected ReferenceLoader getReferenceLoader() {
92+
return referenceLoader;
93+
}
94+
95+
private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
96+
ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) {
97+
return new LazyLoadingProxyFactory(referenceLookupDelegate).createLazyLoadingProxy(property, source, lookupFunction,
98+
entityReader);
99+
}
79100
}

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

+144-57
Original file line numberDiff line numberDiff line change
@@ -15,136 +15,223 @@
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;
28+
import org.springframework.data.annotation.Reference;
2929
import org.springframework.data.mapping.PersistentPropertyAccessor;
30+
import org.springframework.data.mapping.PersistentPropertyPath;
31+
import org.springframework.data.mapping.PropertyPath;
3032
import org.springframework.data.mapping.context.MappingContext;
3133
import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory;
3234
import org.springframework.data.mongodb.core.mapping.DocumentPointer;
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 {
80+
}
6381

64-
MongoPersistentEntity<?> persistentEntity = mappingContext
65-
.getRequiredPersistentEntity(property.getAssociationTargetType());
82+
MongoPersistentEntity<?> persistentEntity = mappingContext
83+
.getRequiredPersistentEntity(property.getAssociationTargetType());
6684

67-
// TODO: Extract method
68-
if (!property.getDocumentReference().lookup().toLowerCase(Locale.ROOT).replaceAll("\\s", "").replaceAll("'", "")
69-
.equals("{_id:?#{#target}}")) {
85+
if (usesDefaultLookup(property)) {
86+
return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier();
87+
}
7088

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);
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+
}
7896

79-
}
97+
return cache.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::from)
98+
.getDocumentPointer(mappingContext, persistentEntity, propertyAccessor);
99+
}
80100

81-
return () -> linkageMap.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::new)
82-
.get(persistentEntity, propertyAccessor);
83-
}
101+
private boolean usesDefaultLookup(MongoPersistentProperty property) {
84102

85-
// just take the id as a reference
86-
return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier();
103+
if (property.isDocumentReference()) {
104+
return DEFAULT_LOOKUP_PATTERN.matcher(property.getDocumentReference().lookup()).matches();
105+
}
106+
107+
Reference atReference = property.findAnnotation(Reference.class);
108+
if (atReference != null) {
109+
return true;
87110
}
111+
112+
throw new IllegalStateException(String.format("%s does not seem to be define Reference", property));
88113
}
89114

115+
/**
116+
* Value object that computes a document pointer from a given lookup query by identifying SpEL expressions and
117+
* inverting it.
118+
*
119+
* <pre class="code">
120+
* // source
121+
* { 'firstname' : ?#{fn}, 'lastname' : '?#{ln} }
122+
*
123+
* // target
124+
* { 'fn' : ..., 'ln' : ... }
125+
* </pre>
126+
*
127+
* The actual pointer is the computed via
128+
* {@link #getDocumentPointer(MappingContext, MongoPersistentEntity, PersistentPropertyAccessor)} applying values from
129+
* the provided {@link PersistentPropertyAccessor} to the target document by looking at the keys of the expressions
130+
* from the source.
131+
*/
90132
static class LinkageDocument {
91133

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

94-
String lookup;
95-
org.bson.Document fetchDocument;
96-
Map<Integer, String> mapMap;
137+
private final String lookup;
138+
private final org.bson.Document documentPointer;
139+
private final Map<String, String> placeholderMap;
97140

98-
public LinkageDocument(String lookup) {
141+
static LinkageDocument from(String lookup) {
142+
return new LinkageDocument(lookup);
143+
}
99144

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

147+
this.lookup = lookup;
148+
this.placeholderMap = new LinkedHashMap<>();
103149

104-
Matcher matcher = pattern.matcher(lookup);
105150
int index = 0;
106-
mapMap = new LinkedHashMap<>();
151+
Matcher matcher = EXPRESSION_PATTERN.matcher(lookup);
152+
String targetLookup = lookup;
107153

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

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 + "");
156+
String expression = matcher.group();
157+
String fieldName = matcher.group("fieldName").replace("target.", "");
158+
159+
String placeholder = placeholder(index);
160+
placeholderMap.put(placeholder, fieldName);
161+
targetLookup = targetLookup.replace(expression, "'" + placeholder + "'");
116162
index++;
117163
}
118164

119-
fetchDocument = org.bson.Document.parse(targetLookup);
165+
this.documentPointer = org.bson.Document.parse(targetLookup);
120166
}
121167

122-
org.bson.Document get(MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {
168+
private String placeholder(int index) {
169+
return "###_" + index + "_###";
170+
}
123171

124-
org.bson.Document targetDocument = new Document();
172+
private boolean isPlaceholder(String key) {
173+
return PLACEHOLDER_PATTERN.matcher(key).matches();
174+
}
125175

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.
176+
DocumentPointer<Object> getDocumentPointer(
177+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
178+
MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {
179+
return () -> updatePlaceholders(documentPointer, new Document(), mappingContext, persistentEntity,
180+
propertyAccessor);
181+
}
182+
183+
Document updatePlaceholders(org.bson.Document source, org.bson.Document target,
184+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
185+
MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {
128186

129-
for (Entry<String, Object> entry : fetchDocument.entrySet()) {
187+
for (Entry<String, Object> entry : source.entrySet()) {
188+
189+
if (entry.getKey().startsWith("$")) {
190+
throw new InvalidDataAccessApiUsageException(String.format(
191+
"Cannot derive document pointer from lookup '%s' using query operator (%s). Please consider registering a custom converter.",
192+
lookup, entry.getKey()));
193+
}
130194

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

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

135-
if (persistentEntity.hasIdProperty()) {
136-
targetDocument.put(refKey, propertyAccessor.getProperty(persistentEntity.getIdProperty()));
200+
MongoPersistentEntity<?> nestedEntity = mappingContext.getPersistentEntity(persistentProperty.getType());
201+
target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext,
202+
nestedEntity, nestedEntity.getPropertyAccessor(propertyAccessor.getProperty(persistentProperty))));
137203
} else {
138-
targetDocument.put(refKey, propertyAccessor.getBean());
204+
target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext,
205+
persistentEntity, propertyAccessor));
139206
}
140207
continue;
141208
}
142209

143-
Object target = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(entry.getKey()));
144-
String refKey = mapMap.get(entry.getValue());
145-
targetDocument.put(refKey, target);
210+
if (placeholderMap.containsKey(entry.getValue())) {
211+
212+
String attribute = placeholderMap.get(entry.getValue());
213+
if (attribute.contains(".")) {
214+
attribute = attribute.substring(attribute.lastIndexOf('.') + 1);
215+
}
216+
217+
String fieldName = entry.getKey().equals("_id") ? "id" : entry.getKey();
218+
if (!fieldName.contains(".")) {
219+
220+
Object targetValue = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(fieldName));
221+
target.put(attribute, targetValue);
222+
continue;
223+
}
224+
225+
PersistentPropertyPath<?> path = mappingContext
226+
.getPersistentPropertyPath(PropertyPath.from(fieldName, persistentEntity.getTypeInformation()));
227+
Object targetValue = propertyAccessor.getProperty(path);
228+
target.put(attribute, targetValue);
229+
continue;
230+
}
231+
232+
target.put(entry.getKey(), entry.getValue());
146233
}
147-
return targetDocument;
234+
return target;
148235
}
149236
}
150237
}

0 commit comments

Comments
 (0)