Skip to content

Commit

Permalink
Avoid capturing lambdas, update javadoc and add tests.
Browse files Browse the repository at this point in the history
Also allow direct usage of (at)Reference from data commons to define associations.

Original pull request: #3647.
Closes #3602.
  • Loading branch information
christophstrobl authored and mp911de committed May 21, 2021
1 parent 82af678 commit e96ef8e
Show file tree
Hide file tree
Showing 16 changed files with 747 additions and 189 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,40 +19,45 @@

import java.util.Collections;

import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.DocumentReference;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

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

private final ReferenceLoader referenceLoader;

private final LookupFunction collectionLookupFunction = (filter, ctx) -> getReferenceLoader().fetchMany(filter, ctx);
private final LookupFunction singleValueLookupFunction = (filter, ctx) -> {
Object target = getReferenceLoader().fetchOne(filter, ctx);
return target == null ? Collections.emptyList() : Collections.singleton(getReferenceLoader().fetchOne(filter, ctx));
};

/**
* Create a new instance of {@link DefaultReferenceResolver}.
*
* @param referenceLoader must not be {@literal null}.
*/
public DefaultReferenceResolver(ReferenceLoader referenceLoader) {

Assert.notNull(referenceLoader, "ReferenceLoader must not be null!");
this.referenceLoader = referenceLoader;
}

@Override
public ReferenceLoader getReferenceLoader() {
return referenceLoader;
}

@Nullable
@Override
public Object resolveReference(MongoPersistentProperty property, Object source,
ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) {

LookupFunction lookupFunction = (filter, ctx) -> {
if (property.isCollectionLike() || property.isMap()) {
return getReferenceLoader().fetchMany(filter, ctx);

}

Object target = getReferenceLoader().fetchOne(filter, ctx);
return target == null ? Collections.emptyList()
: Collections.singleton(getReferenceLoader().fetchOne(filter, ctx));
};
LookupFunction lookupFunction = (property.isCollectionLike() || property.isMap()) ? collectionLookupFunction
: singleValueLookupFunction;

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

private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction,
MongoEntityReader entityReader) {
return new LazyLoadingProxyFactory(referenceLookupDelegate).createLazyLoadingProxy(property, source, lookupFunction,
entityReader);
}

/**
* Check if the association expressed by the given {@link MongoPersistentProperty property} should be resolved lazily.
*
* @param property
* @return return {@literal true} if the defined association is lazy.
* @see DBRef#lazy()
* @see DocumentReference#lazy()
*/
protected boolean isLazyReference(MongoPersistentProperty property) {

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

return property.getDBRef() != null && property.getDBRef().lazy();
}

/**
* The {@link ReferenceLoader} executing the lookup.
*
* @return never {@literal null}.
*/
protected ReferenceLoader getReferenceLoader() {
return referenceLoader;
}

private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source,
ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) {
return new LazyLoadingProxyFactory(referenceLookupDelegate).createLazyLoadingProxy(property, source, lookupFunction,
entityReader);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,136 +15,223 @@
*/
package org.springframework.data.mongodb.core.convert;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.bson.Document;

import org.springframework.core.convert.ConversionService;
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.data.annotation.Reference;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.BeanWrapperPropertyAccessorFactory;
import org.springframework.data.mongodb.core.mapping.DocumentPointer;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;

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

private final ConversionService conversionService;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final Map<String, LinkageDocument> linkageMap;

public DocumentPointerFactory(ConversionService conversionService,
private final Map<String, LinkageDocument> cache;

/**
* A {@link Pattern} matching quoted and unquoted variants (with/out whitespaces) of
* <code>{'_id' : ?#{#target} }</code>.
*/
private static final Pattern DEFAULT_LOOKUP_PATTERN = Pattern.compile("\\{\\s?" + // document start (whitespace opt)
"['\"]?_id['\"]?" + // followed by an optionally quoted _id. Like: _id, '_id' or "_id"
"?\\s?:\\s?" + // then a colon optionally wrapped inside whitespaces
"['\"]?\\?#\\{#target\\}['\"]?" + // leading to the potentially quoted ?#{#target} expression
"\\s*}"); // some optional whitespaces and document close

DocumentPointerFactory(ConversionService conversionService,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {

this.conversionService = conversionService;
this.mappingContext = mappingContext;
this.linkageMap = new HashMap<>();
this.cache = new WeakHashMap<>();
}

public DocumentPointer<?> computePointer(MongoPersistentProperty property, Object value, Class<?> typeHint) {
DocumentPointer<?> computePointer(
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
MongoPersistentProperty property, Object value, Class<?> typeHint) {

if (value instanceof LazyLoadingProxy) {
return () -> ((LazyLoadingProxy) value).getSource();
}

if (conversionService.canConvert(typeHint, DocumentPointer.class)) {
return conversionService.convert(value, DocumentPointer.class);
} else {
}

MongoPersistentEntity<?> persistentEntity = mappingContext
.getRequiredPersistentEntity(property.getAssociationTargetType());
MongoPersistentEntity<?> persistentEntity = mappingContext
.getRequiredPersistentEntity(property.getAssociationTargetType());

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

MongoPersistentEntity<?> valueEntity = mappingContext.getPersistentEntity(value.getClass());
PersistentPropertyAccessor<Object> propertyAccessor;
if (valueEntity == null) {
propertyAccessor = BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(),
value);
} else {
propertyAccessor = valueEntity.getPropertyAccessor(value);
MongoPersistentEntity<?> valueEntity = mappingContext.getPersistentEntity(value.getClass());
PersistentPropertyAccessor<Object> propertyAccessor;
if (valueEntity == null) {
propertyAccessor = BeanWrapperPropertyAccessorFactory.INSTANCE.getPropertyAccessor(property.getOwner(), value);
} else {
propertyAccessor = valueEntity.getPropertyPathAccessor(value);
}

}
return cache.computeIfAbsent(property.getDocumentReference().lookup(), LinkageDocument::from)
.getDocumentPointer(mappingContext, persistentEntity, propertyAccessor);
}

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

// just take the id as a reference
return () -> persistentEntity.getIdentifierAccessor(value).getIdentifier();
if (property.isDocumentReference()) {
return DEFAULT_LOOKUP_PATTERN.matcher(property.getDocumentReference().lookup()).matches();
}

Reference atReference = property.findAnnotation(Reference.class);
if (atReference != null) {
return true;
}

throw new IllegalStateException(String.format("%s does not seem to be define Reference", property));
}

/**
* Value object that computes a document pointer from a given lookup query by identifying SpEL expressions and
* inverting it.
*
* <pre class="code">
* // source
* { 'firstname' : ?#{fn}, 'lastname' : '?#{ln} }
*
* // target
* { 'fn' : ..., 'ln' : ... }
* </pre>
*
* The actual pointer is the computed via
* {@link #getDocumentPointer(MappingContext, MongoPersistentEntity, PersistentPropertyAccessor)} applying values from
* the provided {@link PersistentPropertyAccessor} to the target document by looking at the keys of the expressions
* from the source.
*/
static class LinkageDocument {

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

String lookup;
org.bson.Document fetchDocument;
Map<Integer, String> mapMap;
private final String lookup;
private final org.bson.Document documentPointer;
private final Map<String, String> placeholderMap;

public LinkageDocument(String lookup) {
static LinkageDocument from(String lookup) {
return new LinkageDocument(lookup);
}

this.lookup = lookup;
String targetLookup = lookup;
private LinkageDocument(String lookup) {

this.lookup = lookup;
this.placeholderMap = new LinkedHashMap<>();

Matcher matcher = pattern.matcher(lookup);
int index = 0;
mapMap = new LinkedHashMap<>();
Matcher matcher = EXPRESSION_PATTERN.matcher(lookup);
String targetLookup = lookup;

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

String expr = matcher.group();
String sanitized = expr.substring(0, expr.length() - 1).replace("?#{#", "").replace("?#{", "")
.replace("target.", "").replaceAll("'", "");
mapMap.put(index, sanitized);
targetLookup = targetLookup.replace(expr, index + "");
String expression = matcher.group();
String fieldName = matcher.group("fieldName").replace("target.", "");

String placeholder = placeholder(index);
placeholderMap.put(placeholder, fieldName);
targetLookup = targetLookup.replace(expression, "'" + placeholder + "'");
index++;
}

fetchDocument = org.bson.Document.parse(targetLookup);
this.documentPointer = org.bson.Document.parse(targetLookup);
}

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

org.bson.Document targetDocument = new Document();
private boolean isPlaceholder(String key) {
return PLACEHOLDER_PATTERN.matcher(key).matches();
}

// TODO: recursive matching over nested Documents or would the parameter binding json parser be a thing?
// like we have it ordered by index values and could provide the parameter array from it.
DocumentPointer<Object> getDocumentPointer(
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {
return () -> updatePlaceholders(documentPointer, new Document(), mappingContext, persistentEntity,
propertyAccessor);
}

Document updatePlaceholders(org.bson.Document source, org.bson.Document target,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext,
MongoPersistentEntity<?> persistentEntity, PersistentPropertyAccessor<?> propertyAccessor) {

for (Entry<String, Object> entry : fetchDocument.entrySet()) {
for (Entry<String, Object> entry : source.entrySet()) {

if (entry.getKey().startsWith("$")) {
throw new InvalidDataAccessApiUsageException(String.format(
"Cannot derive document pointer from lookup '%s' using query operator (%s). Please consider registering a custom converter.",
lookup, entry.getKey()));
}

if (entry.getKey().equals("target")) {
if (entry.getValue() instanceof Document) {

String refKey = mapMap.get(entry.getValue());
MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(entry.getKey());
if (persistentProperty != null && persistentProperty.isEntity()) {

if (persistentEntity.hasIdProperty()) {
targetDocument.put(refKey, propertyAccessor.getProperty(persistentEntity.getIdProperty()));
MongoPersistentEntity<?> nestedEntity = mappingContext.getPersistentEntity(persistentProperty.getType());
target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext,
nestedEntity, nestedEntity.getPropertyAccessor(propertyAccessor.getProperty(persistentProperty))));
} else {
targetDocument.put(refKey, propertyAccessor.getBean());
target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext,
persistentEntity, propertyAccessor));
}
continue;
}

Object target = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(entry.getKey()));
String refKey = mapMap.get(entry.getValue());
targetDocument.put(refKey, target);
if (placeholderMap.containsKey(entry.getValue())) {

String attribute = placeholderMap.get(entry.getValue());
if (attribute.contains(".")) {
attribute = attribute.substring(attribute.lastIndexOf('.') + 1);
}

String fieldName = entry.getKey().equals("_id") ? "id" : entry.getKey();
if (!fieldName.contains(".")) {

Object targetValue = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(fieldName));
target.put(attribute, targetValue);
continue;
}

PersistentPropertyPath<?> path = mappingContext
.getPersistentPropertyPath(PropertyPath.from(fieldName, persistentEntity.getTypeInformation()));
Object targetValue = propertyAccessor.getProperty(path);
target.put(attribute, targetValue);
continue;
}

target.put(entry.getKey(), entry.getValue());
}
return targetDocument;
return target;
}
}
}
Loading

0 comments on commit e96ef8e

Please # to comment.