Skip to content

Commit eb08e79

Browse files
Avoid capturing lambdas, update javadoc and add tests.
Also allow direct usage of (at)Reference from data commons to define associations.
1 parent e2a8c95 commit eb08e79

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)