diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java new file mode 100644 index 000000000..a9b1d19d8 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -0,0 +1,629 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.Map; + +import org.bson.Document; +import org.springframework.core.convert.ConversionService; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.mapping.IdentifierAccessor; +import org.springframework.data.mapping.MappingException; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mongodb.core.convert.MongoWriter; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import com.mongodb.util.JSONParseException; + +/** + * Common operations performed on an entity in the context of it's mapping metadata. + * + * @author Oliver Gierke + * @since 2.1 + * @see MongoTemplate + * @see ReactiveMongoTemplate + */ +@RequiredArgsConstructor +class EntityOperations { + + private static final String ID_FIELD = "_id"; + + private final @NonNull MappingContext, MongoPersistentProperty> context; + + /** + * Creates a new {@link Entity} for the given bean. + * + * @param entity must not be {@literal null}. + * @return + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Entity forEntity(T entity) { + + Assert.notNull(entity, "Bean must not be null!"); + + if (entity instanceof String) { + return new SimpleEntity(parse(entity.toString())); + } + + if (entity instanceof Map) { + return new SimpleEntity((Map) entity); + } + + return MappedEntity.of(entity, context); + } + + /** + * Creates a new {@link AdaptibleEntity} for the given bean and {@link ConversionService}. + * + * @param entity must not be {@literal null}. + * @param conversionService must not be {@literal null}. + * @return + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public AdaptibleEntity forEntity(T entity, ConversionService conversionService) { + + Assert.notNull(entity, "Bean must not be null!"); + Assert.notNull(conversionService, "ConversionService must not be null!"); + + if (entity instanceof String) { + return new SimpleEntity(parse(entity.toString())); + } + + if (entity instanceof Map) { + return new SimpleEntity((Map) entity); + } + + return AdaptibleMappedEntity.of(entity, context, conversionService); + } + + public String determineCollectionName(@Nullable Class entityClass) { + + if (entityClass == null) { + throw new InvalidDataAccessApiUsageException( + "No class parameter provided, entity collection can't be determined!"); + } + + return context.getRequiredPersistentEntity(entityClass).getCollection(); + } + + /** + * Returns the collection name to be used for the given entity. + * + * @param obj can be {@literal null}. + * @return + */ + @Nullable + public String determineEntityCollectionName(@Nullable Object obj) { + return null == obj ? null : determineCollectionName(obj.getClass()); + } + + public Query getByIdInQuery(Collection entities) { + + MultiValueMap byIds = new LinkedMultiValueMap<>(); + + entities.stream() // + .map(this::forEntity) // + .forEach(it -> byIds.add(it.getIdFieldName(), it.getId())); + + Criteria[] criterias = byIds.entrySet().stream() // + .map(it -> Criteria.where(it.getKey()).in(it.getValue())) // + .toArray(Criteria[]::new); + + return new Query(criterias.length == 1 ? criterias[0] : new Criteria().orOperator(criterias)); + } + + /** + * Returns the name of the identifier property. Considers mapping information but falls back to the MongoDB default of + * {@code _id} if no identifier property can be found. + * + * @param type must not be {@literal null}. + * @return + */ + public String getIdPropertyName(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + MongoPersistentEntity persistentEntity = context.getPersistentEntity(type); + + if (persistentEntity != null && persistentEntity.getIdProperty() != null) { + return persistentEntity.getRequiredIdProperty().getName(); + } + + return ID_FIELD; + } + + private static Document parse(String source) { + + try { + return Document.parse(source); + } catch (JSONParseException | org.bson.json.JsonParseException o_O) { + throw new MappingException("Could not parse given String to save into a JSON document!", o_O); + } + } + + /** + * A representation of information about an entity. + * + * @author Oliver Gierke + * @since 2.1 + */ + interface Entity { + + /** + * Returns the field name of the identifier of the entity. + * + * @return + */ + String getIdFieldName(); + + /** + * Returns the identifier of the entity. + * + * @return + */ + Object getId(); + + /** + * Returns the {@link Query} to find the entity by its identifier. + * + * @return + */ + Query getByIdQuery(); + + /** + * Returns the {@link Query} to find the entity in its current version. + * + * @return + */ + Query getQueryForVersion(); + + /** + * Maps the backing entity into a {@link MappedDocument} using the given {@link MongoWriter}. + * + * @param writer must not be {@literal null}. + * @return + */ + MappedDocument toMappedDocument(MongoWriter writer); + + /** + * Asserts that the identifier type is updatable in case its not already set. + */ + default void assertUpdateableIdIfNotSet() {} + + /** + * Returns whether the entity is versioned, i.e. if it contains a version property. + * + * @return + */ + default boolean isVersionedEntity() { + return false; + } + + /** + * Returns the value of the version if the entity has a version property, {@literal null} otherwise. + * + * @return + */ + @Nullable + Object getVersion(); + + /** + * Returns the underlying bean. + * + * @return + */ + T getBean(); + } + + /** + * Information and commands on an entity. + * + * @author Oliver Gierke + * @since 2.1 + */ + interface AdaptibleEntity extends Entity { + + /** + * Populates the identifier of the backing entity if it has an identifier property and there's no identifier + * currently present. + * + * @param id must not be {@literal null}. + * @return + */ + @Nullable + T populateIdIfNecessary(@Nullable Object id); + + /** + * Initializes the version property of the of the current entity if available. + * + * @return the entity with the version property updated if available. + */ + T initializeVersionProperty(); + + /** + * Increments the value of the version property if available. + * + * @return the entity with the version property incremented if available. + */ + T incrementVersion(); + + /** + * Returns the current version value if the entity has a version property. + * + * @return the current version or {@literal null} in case it's uninitialized or the entity doesn't expose a version + * property. + */ + @Nullable + Number getVersion(); + } + + @RequiredArgsConstructor + private static class SimpleEntity> implements AdaptibleEntity { + + private final T map; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getIdPropertyName() + */ + @Override + public String getIdFieldName() { + return ID_FIELD; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getId() + */ + @Override + public Object getId() { + return map.get(ID_FIELD); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getByIdQuery() + */ + @Override + public Query getByIdQuery() { + return Query.query(Criteria.where(ID_FIELD).is(map.get(ID_FIELD))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#populateIdIfNecessary(java.lang.Object) + */ + @Nullable + @Override + public T populateIdIfNecessary(@Nullable Object id) { + + map.put(ID_FIELD, id); + + return map; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getQueryForVersion() + */ + @Override + public Query getQueryForVersion() { + throw new MappingException("Cannot query for version on plain Documents!"); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#toMappedDocument(org.springframework.data.mongodb.core.convert.MongoWriter) + */ + @Override + public MappedDocument toMappedDocument(MongoWriter writer) { + return MappedDocument.of(map instanceof Document // + ? (Document) map // + : new Document(map)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#initializeVersionProperty() + */ + @Override + public T initializeVersionProperty() { + return map; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#getVersion() + */ + @Override + @Nullable + public Number getVersion() { + return null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.MutablePersistableSource#incrementVersion() + */ + @Override + public T incrementVersion() { + return map; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getBean() + */ + @Override + public T getBean() { + return map; + } + } + + @RequiredArgsConstructor(access = AccessLevel.PROTECTED) + private static class MappedEntity implements Entity { + + private final @NonNull MongoPersistentEntity entity; + private final @NonNull IdentifierAccessor idAccessor; + private final @NonNull PersistentPropertyAccessor propertyAccessor; + + private static MappedEntity of(T bean, + MappingContext, MongoPersistentProperty> context) { + + MongoPersistentEntity entity = context.getRequiredPersistentEntity(bean.getClass()); + IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean); + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(bean); + + return new MappedEntity<>(entity, identifierAccessor, propertyAccessor); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getIdPropertyName() + */ + @Override + public String getIdFieldName() { + return entity.getRequiredIdProperty().getFieldName(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getId() + */ + @Override + public Object getId() { + return idAccessor.getRequiredIdentifier(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getByIdQuery() + */ + @Override + public Query getByIdQuery() { + + if (!entity.hasIdProperty()) { + throw new MappingException("No id property found for object of type " + entity.getType() + "!"); + } + + MongoPersistentProperty idProperty = entity.getRequiredIdProperty(); + + return Query.query(Criteria.where(idProperty.getName()).is(getId())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getQueryForVersion(java.lang.Object) + */ + @Override + public Query getQueryForVersion() { + + MongoPersistentProperty idProperty = entity.getRequiredIdProperty(); + MongoPersistentProperty property = entity.getRequiredVersionProperty(); + + return new Query(Criteria.where(idProperty.getName()).is(getId())// + .and(property.getName()).is(getVersion())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#toMappedDocument(org.springframework.data.mongodb.core.convert.MongoWriter) + */ + @Override + public MappedDocument toMappedDocument(MongoWriter writer) { + + T bean = propertyAccessor.getBean(); + + Document document = new Document(); + writer.write(bean, document); + + if (document.containsKey(ID_FIELD) && document.get(ID_FIELD) == null) { + document.remove(ID_FIELD); + } + + return MappedDocument.of(document); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.Entity#assertUpdateableIdIfNotSet() + */ + public void assertUpdateableIdIfNotSet() { + + if (!entity.hasIdProperty()) { + return; + } + + MongoPersistentProperty property = entity.getRequiredIdProperty(); + Object propertyValue = idAccessor.getIdentifier(); + + if (propertyValue != null) { + return; + } + + if (!MongoSimpleTypes.AUTOGENERATED_ID_TYPES.contains(property.getType())) { + throw new InvalidDataAccessApiUsageException( + String.format("Cannot autogenerate id of type %s for entity of type %s!", property.getType().getName(), + entity.getType().getName())); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#isVersionedEntity() + */ + @Override + public boolean isVersionedEntity() { + return entity.hasVersionProperty(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getVersion() + */ + @Override + @Nullable + public Object getVersion() { + return propertyAccessor.getProperty(entity.getRequiredVersionProperty()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.PersistableSource#getBean() + */ + @Override + public T getBean() { + return propertyAccessor.getBean(); + } + } + + private static class AdaptibleMappedEntity extends MappedEntity implements AdaptibleEntity { + + private final MongoPersistentEntity entity; + private final ConvertingPropertyAccessor propertyAccessor; + private final IdentifierAccessor identifierAccessor; + + private AdaptibleMappedEntity(MongoPersistentEntity entity, IdentifierAccessor identifierAccessor, + ConvertingPropertyAccessor propertyAccessor) { + + super(entity, identifierAccessor, propertyAccessor); + + this.entity = entity; + this.propertyAccessor = propertyAccessor; + this.identifierAccessor = identifierAccessor; + } + + private static AdaptibleEntity of(T bean, + MappingContext, MongoPersistentProperty> context, + ConversionService conversionService) { + + MongoPersistentEntity entity = context.getRequiredPersistentEntity(bean.getClass()); + IdentifierAccessor identifierAccessor = entity.getIdentifierAccessor(bean); + PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(bean); + + return new AdaptibleMappedEntity<>(entity, identifierAccessor, + new ConvertingPropertyAccessor<>(propertyAccessor, conversionService)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity#populateIdIfNecessary(java.lang.Object) + */ + @Nullable + @Override + public T populateIdIfNecessary(@Nullable Object id) { + + if (id == null) { + return null; + } + + T bean = propertyAccessor.getBean(); + MongoPersistentProperty idProperty = entity.getIdProperty(); + + if (idProperty == null) { + return bean; + } + + if (identifierAccessor.getIdentifier() != null) { + return bean; + } + + propertyAccessor.setProperty(idProperty, id); + + return propertyAccessor.getBean(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.MappedEntity#getVersion() + */ + @Override + @Nullable + public Number getVersion() { + + MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty(); + + return propertyAccessor.getProperty(versionProperty, Number.class); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity#initializeVersionProperty() + */ + @Override + public T initializeVersionProperty() { + + if (!entity.hasVersionProperty()) { + return propertyAccessor.getBean(); + } + + propertyAccessor.setProperty(entity.getRequiredVersionProperty(), 0); + + return propertyAccessor.getBean(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity#incrementVersion() + */ + @Override + public T incrementVersion() { + + MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty(); + Number version = getVersion(); + Number nextVersion = version == null ? 0 : version.longValue() + 1; + + propertyAccessor.setProperty(versionProperty, nextVersion); + + return propertyAccessor.getBean(); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java index f0c7e6267..87ebb41ba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java @@ -119,11 +119,11 @@ class ExecutableAggregationOperationSupport implements ExecutableAggregationOper TypedAggregation typedAggregation = (TypedAggregation) aggregation; if (typedAggregation.getInputType() != null) { - return template.determineCollectionName(typedAggregation.getInputType()); + return template.getCollectionName(typedAggregation.getInputType()); } } - return template.determineCollectionName(domainType); + return template.getCollectionName(domainType); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java index cbfbf4d0f..ccc3ba66a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java @@ -230,7 +230,7 @@ class ExecutableFindOperationSupport implements ExecutableFindOperation { } private String getCollectionName() { - return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); + return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType); } private String asString() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java index ee75ad11c..22fd6d08b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java @@ -129,7 +129,7 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation { } private String getCollectionName() { - return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); + return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java index 989d7c4c2..c4bdaf425 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java @@ -15,10 +15,11 @@ */ package org.springframework.data.mongodb.core; -import java.util.List; - import lombok.NonNull; import lombok.RequiredArgsConstructor; + +import java.util.List; + import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.Query; import org.springframework.lang.Nullable; @@ -67,8 +68,9 @@ class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperatio private final @Nullable String reduceFunction; private final @Nullable MapReduceOptions options; - ExecutableMapReduceSupport(MongoTemplate template, Class domainType, Class returnType, @Nullable String collection, - Query query, @Nullable String mapFunction, @Nullable String reduceFunction, @Nullable MapReduceOptions options) { + ExecutableMapReduceSupport(MongoTemplate template, Class domainType, Class returnType, + @Nullable String collection, Query query, @Nullable String mapFunction, @Nullable String reduceFunction, + @Nullable MapReduceOptions options) { this.template = template; this.domainType = domainType; @@ -169,7 +171,7 @@ class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperatio } private String getCollectionName() { - return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); + return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java index 50b75ef55..7cc376680 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java @@ -123,7 +123,7 @@ class ExecutableRemoveOperationSupport implements ExecutableRemoveOperation { } private String getCollectionName() { - return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); + return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java index d5de68354..a07969d16 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java @@ -221,7 +221,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { } private String getCollectionName() { - return StringUtils.hasText(collection) ? collection : template.determineCollectionName(domainType); + return StringUtils.hasText(collection) ? collection : template.getCollectionName(domainType); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java new file mode 100644 index 000000000..bccaa50ee --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java @@ -0,0 +1,88 @@ +/* + * Copyright 2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.List; + +import org.bson.Document; +import org.bson.conversions.Bson; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.util.StreamUtils; + +import com.mongodb.client.model.Filters; + +/** + * A MongoDB document in its mapped state. I.e. after a source document has been mapped using mapping information of the + * entity the source document was supposed to represent. + * + * @author Oliver Gierke + * @since 2.1 + */ +@RequiredArgsConstructor(staticName = "of") +public class MappedDocument { + + private static final String ID_FIELD = "_id"; + private static final Document ID_ONLY_PROJECTION = new Document(ID_FIELD, 1); + + private final @Getter Document document; + + public static Document getIdOnlyProjection() { + return ID_ONLY_PROJECTION; + } + + public static Document getIdIn(Collection ids) { + return new Document(ID_FIELD, new Document("$in", ids)); + } + + public static List toIds(Collection documents) { + + return documents.stream()// + .map(it -> it.get(ID_FIELD))// + .collect(StreamUtils.toUnmodifiableList()); + } + + public boolean hasId() { + return document.containsKey(ID_FIELD); + } + + public boolean hasNonNullId() { + return hasId() && document.get(ID_FIELD) != null; + } + + public Object getId() { + return document.get(ID_FIELD); + } + + public T getId(Class type) { + return document.get(ID_FIELD, type); + } + + public boolean isIdPresent(Class type) { + return type.isInstance(getId()); + } + + public Bson getIdFilter() { + return Filters.eq(ID_FIELD, document.get(ID_FIELD)); + } + + public Update updateWithoutId() { + return Update.fromDocument(document, ID_FIELD); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 011d6845b..00bf92207 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -15,17 +15,28 @@ */ package org.springframework.data.mongodb.core; -import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.SerializationUtils.*; +import com.mongodb.client.model.*; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.NonNull; import lombok.RequiredArgsConstructor; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; +import java.util.Scanner; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -42,30 +53,26 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.convert.ConversionService; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.dao.support.PersistenceExceptionTranslator; -import org.springframework.data.annotation.Id; import org.springframework.data.convert.EntityReader; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metric; -import org.springframework.data.mapping.MappingException; -import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.SessionSynchronization; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext; +import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; @@ -73,7 +80,16 @@ import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.Fields; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; -import org.springframework.data.mongodb.core.convert.*; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; +import org.springframework.data.mongodb.core.convert.JsonSchemaMapper; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; +import org.springframework.data.mongodb.core.convert.MongoWriter; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.data.mongodb.core.index.IndexOperationsProvider; import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher; @@ -81,7 +97,6 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCre import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent; import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent; import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent; @@ -106,8 +121,6 @@ import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.Optionals; -import org.springframework.data.util.Pair; -import org.springframework.data.util.StreamUtils; import org.springframework.jca.cci.core.ConnectionCallback; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -135,10 +148,8 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; -import com.mongodb.client.model.*; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; -import com.mongodb.util.JSONParseException; /** * Primary implementation of {@link MongoOperations}. @@ -167,7 +178,6 @@ import com.mongodb.util.JSONParseException; public class MongoTemplate implements MongoOperations, ApplicationContextAware, IndexOperationsProvider { private static final Logger LOGGER = LoggerFactory.getLogger(MongoTemplate.class); - private static final String ID_FIELD = "_id"; private static final WriteResultChecking DEFAULT_WRITE_RESULT_CHECKING = WriteResultChecking.NONE; private static final Collection ITERABLE_CLASSES; @@ -189,6 +199,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private final UpdateMapper updateMapper; private final JsonSchemaMapper schemaMapper; private final SpelAwareProxyProjectionFactory projectionFactory; + private final EntityOperations operations; private @Nullable WriteConcern writeConcern; private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE; @@ -247,6 +258,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.updateMapper = new UpdateMapper(this.mongoConverter); this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter); this.projectionFactory = new SpelAwareProxyProjectionFactory(); + this.operations = new EntityOperations(this.mongoConverter.getMappingContext()); // We always have a mapping context in the converter, whether it's a simple one or not mappingContext = this.mongoConverter.getMappingContext(); @@ -272,6 +284,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.schemaMapper = that.schemaMapper; this.projectionFactory = that.projectionFactory; this.mappingContext = that.mappingContext; + this.operations = that.operations; } /** @@ -375,7 +388,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public CloseableIterator stream(final Query query, final Class entityType) { - return stream(query, entityType, determineCollectionName(entityType)); + return stream(query, entityType, operations.determineCollectionName(entityType)); } /* @@ -417,7 +430,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public String getCollectionName(Class entityClass) { - return this.determineCollectionName(entityClass); + return this.operations.determineCollectionName(entityClass); } /* @@ -445,13 +458,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(command, "Command must not be null!"); - Document result = execute(new DbCallback() { - public Document doInDB(MongoDatabase db) throws MongoException, DataAccessException { - return db.runCommand(command, Document.class); - } - }); - - return result; + return execute(db -> db.runCommand(command, Document.class)); } /* @@ -463,14 +470,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(command, "Command must not be null!"); - Document result = execute(new DbCallback() { - public Document doInDB(MongoDatabase db) throws MongoException, DataAccessException { - return readPreference != null ? db.runCommand(command, readPreference, Document.class) - : db.runCommand(command, Document.class); - } - }); - - return result; + return execute(db -> readPreference != null // + ? db.runCommand(command, readPreference, Document.class) // + : db.runCommand(command, Document.class)); } /* @@ -536,7 +538,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, public T execute(Class entityClass, CollectionCallback callback) { Assert.notNull(entityClass, "EntityClass must not be null!"); - return execute(determineCollectionName(entityClass), callback); + return execute(operations.determineCollectionName(entityClass), callback); } /* @@ -596,7 +598,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @see org.springframework.data.mongodb.core.MongoOperations#createCollection(java.lang.Class) */ public MongoCollection createCollection(Class entityClass) { - return createCollection(determineCollectionName(entityClass)); + return createCollection(operations.determineCollectionName(entityClass)); } /* @@ -607,7 +609,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable CollectionOptions collectionOptions) { Assert.notNull(entityClass, "EntityClass must not be null!"); - return doCreateCollection(determineCollectionName(entityClass), convertToDocument(collectionOptions, entityClass)); + return doCreateCollection(operations.determineCollectionName(entityClass), + convertToDocument(collectionOptions, entityClass)); } /* @@ -652,7 +655,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @see org.springframework.data.mongodb.core.ExecutableInsertOperation#getCollection(java.lang.Class) */ public boolean collectionExists(Class entityClass) { - return collectionExists(determineCollectionName(entityClass)); + return collectionExists(operations.determineCollectionName(entityClass)); } /* @@ -681,7 +684,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @see org.springframework.data.mongodb.core.ExecutableInsertOperation#dropCollection(java.lang.Class) */ public void dropCollection(Class entityClass) { - dropCollection(determineCollectionName(entityClass)); + dropCollection(operations.determineCollectionName(entityClass)); } /* @@ -717,7 +720,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @see org.springframework.data.mongodb.core.ExecutableInsertOperation#indexOps(java.lang.Class) */ public IndexOperations indexOps(Class entityClass) { - return new DefaultIndexOperations(this, determineCollectionName(entityClass), entityClass); + return new DefaultIndexOperations(this, operations.determineCollectionName(entityClass), entityClass); } /* @@ -733,7 +736,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @see org.springframework.data.mongodb.core.ExecutableInsertOperation#bulkOps(org.springframework.data.mongodb.core.BulkMode, java.lang.Class) */ public BulkOperations bulkOps(BulkMode bulkMode, Class entityClass) { - return bulkOps(bulkMode, entityClass, determineCollectionName(entityClass)); + return bulkOps(bulkMode, entityClass, operations.determineCollectionName(entityClass)); } /* @@ -768,7 +771,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable @Override public T findOne(Query query, Class entityClass) { - return findOne(query, entityClass, determineCollectionName(entityClass)); + return findOne(query, entityClass, operations.determineCollectionName(entityClass)); } @Nullable @@ -790,7 +793,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public boolean exists(Query query, Class entityClass) { - return exists(query, entityClass, determineCollectionName(entityClass)); + return exists(query, entityClass, operations.determineCollectionName(entityClass)); } @Override @@ -820,7 +823,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ @Override public List find(Query query, Class entityClass) { - return find(query, entityClass, determineCollectionName(entityClass)); + return find(query, entityClass, operations.determineCollectionName(entityClass)); } /* @@ -841,7 +844,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable @Override public T findById(Object id, Class entityClass) { - return findById(id, entityClass, determineCollectionName(entityClass)); + return findById(id, entityClass, operations.determineCollectionName(entityClass)); } @Nullable @@ -852,14 +855,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(entityClass, "EntityClass must not be null!"); Assert.notNull(collectionName, "CollectionName must not be null!"); - MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(entityClass); - String idKey = ID_FIELD; - - if (persistentEntity != null) { - if (persistentEntity.getIdProperty() != null) { - idKey = persistentEntity.getIdProperty().getName(); - } - } + String idKey = operations.getIdPropertyName(entityClass); return doFindOne(collectionName, new Document(idKey, id), new Document(), entityClass); } @@ -870,7 +866,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ @Override public List findDistinct(Query query, String field, Class entityClass, Class resultClass) { - return findDistinct(query, field, determineCollectionName(entityClass), entityClass, resultClass); + return findDistinct(query, field, operations.determineCollectionName(entityClass), entityClass, resultClass); } /* @@ -893,8 +889,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), entity); String mappedFieldName = queryMapper.getMappedFields(new Document(field, 1), entity).keySet().iterator().next(); - Class mongoDriverCompatibleType = getMongoDbFactory().getCodecFor(resultClass).map(Codec::getEncoderClass) - .orElse((Class) BsonValue.class); + Class mongoDriverCompatibleType = getMongoDbFactory().getCodecFor(resultClass) // + .map(Codec::getEncoderClass) // + .orElse((Class) BsonValue.class); MongoIterable result = execute(collectionName, (collection) -> { @@ -947,15 +944,15 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public GeoResults geoNear(NearQuery near, Class entityClass) { - return geoNear(near, entityClass, determineCollectionName(entityClass)); + return geoNear(near, entityClass, operations.determineCollectionName(entityClass)); } @Override - @SuppressWarnings("unchecked") public GeoResults geoNear(NearQuery near, Class domainType, String collectionName) { return geoNear(near, domainType, collectionName, domainType); } + @SuppressWarnings("unchecked") public GeoResults geoNear(NearQuery near, Class domainType, String collectionName, Class returnType) { if (near == null) { @@ -969,7 +966,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(collectionName, "CollectionName must not be null!"); Assert.notNull(returnType, "ReturnType must not be null!"); - String collection = StringUtils.hasText(collectionName) ? collectionName : determineCollectionName(domainType); + String collection = StringUtils.hasText(collectionName) ? collectionName + : operations.determineCollectionName(domainType); Document nearDocument = near.toDocument(); Document command = new Document("geoNear", collection); @@ -1022,7 +1020,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable @Override public T findAndModify(Query query, Update update, Class entityClass) { - return findAndModify(query, update, new FindAndModifyOptions(), entityClass, determineCollectionName(entityClass)); + return findAndModify(query, update, new FindAndModifyOptions(), entityClass, + operations.determineCollectionName(entityClass)); } @Nullable @@ -1034,7 +1033,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable @Override public T findAndModify(Query query, Update update, FindAndModifyOptions options, Class entityClass) { - return findAndModify(query, update, options, entityClass, determineCollectionName(entityClass)); + return findAndModify(query, update, options, entityClass, operations.determineCollectionName(entityClass)); } @Nullable @@ -1085,7 +1084,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document mappedFields = queryMapper.getMappedFields(query.getFieldsObject(), entity); Document mappedSort = queryMapper.getMappedSort(query.getSortObject(), entity); - Document mappedReplacement = toDocument(replacement, this.mongoConverter); + Document mappedReplacement = operations.forEntity(replacement).toMappedDocument(this.mongoConverter).getDocument(); return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, query.getCollation().map(Collation::toMongoCollation).orElse(null), entityType, mappedReplacement, options, @@ -1098,7 +1097,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable @Override public T findAndRemove(Query query, Class entityClass) { - return findAndRemove(query, entityClass, determineCollectionName(entityClass)); + return findAndRemove(query, entityClass, operations.determineCollectionName(entityClass)); } @Nullable @@ -1117,7 +1116,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, public long count(Query query, Class entityClass) { Assert.notNull(entityClass, "Entity class must not be null!"); - return count(query, entityClass, determineCollectionName(entityClass)); + return count(query, entityClass, operations.determineCollectionName(entityClass)); } @Override @@ -1153,7 +1152,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(objectToSave, "ObjectToSave must not be null!"); ensureNotIterable(objectToSave); - return insert(objectToSave, determineEntityCollectionName(objectToSave)); + return insert(objectToSave, operations.determineEntityCollectionName(objectToSave)); } /* @@ -1161,6 +1160,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @see org.springframework.data.mongodb.core.MongoOperations#insert(java.lang.Object, java.lang.String) */ @Override + @SuppressWarnings("unchecked") public T insert(T objectToSave, String collectionName) { Assert.notNull(objectToSave, "ObjectToSave must not be null!"); @@ -1223,93 +1223,54 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, protected T doInsert(String collectionName, T objectToSave, MongoWriter writer) { - T toSave = (T) initializeVersionProperty(objectToSave); - maybeEmitEvent(new BeforeConvertEvent<>(toSave, collectionName)); - assertUpdateableIdIfNotSet(toSave); + AdaptibleEntity entity = operations.forEntity(objectToSave, mongoConverter.getConversionService()); + T toSave = entity.initializeVersionProperty(); - Document dbDoc = toDocument(toSave, writer); + BeforeConvertEvent event = new BeforeConvertEvent<>(toSave, collectionName); + toSave = maybeEmitEvent(event).getSource(); + + entity.assertUpdateableIdIfNotSet(); + + Document dbDoc = entity.toMappedDocument(writer).getDocument(); maybeEmitEvent(new BeforeSaveEvent<>(toSave, dbDoc, collectionName)); Object id = insertDocument(collectionName, dbDoc, toSave.getClass()); - T saved = (T) populateIdIfNecessary(toSave, id); + T saved = populateIdIfNecessary(toSave, id); maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)); return saved; } - /** - * @param objectToSave - * @param writer - * @return - */ - private Document toDocument(T objectToSave, MongoWriter writer) { - - if (objectToSave instanceof Document) { - return (Document) objectToSave; - } - - if (!(objectToSave instanceof String)) { - Document dbDoc = new Document(); - writer.write(objectToSave, dbDoc); - - if (dbDoc.containsKey(ID_FIELD) && dbDoc.get(ID_FIELD) == null) { - dbDoc.remove(ID_FIELD); - } - return dbDoc; - } else { - try { - return Document.parse((String) objectToSave); - } catch (JSONParseException e) { - throw new MappingException("Could not parse given String to save into a JSON document!", e); - } catch (org.bson.json.JsonParseException e) { - throw new MappingException("Could not parse given String to save into a JSON document!", e); - } - } - } - - private Object initializeVersionProperty(Object entity) { - - MongoPersistentEntity persistentEntity = getPersistentEntity(entity.getClass()); - - if (persistentEntity != null && persistentEntity.hasVersionProperty()) { - - MongoPersistentProperty versionProperty = persistentEntity.getRequiredVersionProperty(); - - ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor(persistentEntity.getPropertyAccessor(entity), - mongoConverter.getConversionService()); - accessor.setProperty(versionProperty, 0); - - return accessor.getBean(); - } - - return entity; - } - @Override + @SuppressWarnings("unchecked") public Collection insert(Collection batchToSave, Class entityClass) { Assert.notNull(batchToSave, "BatchToSave must not be null!"); - return (Collection) doInsertBatch(determineCollectionName(entityClass), batchToSave, this.mongoConverter); + return (Collection) doInsertBatch(operations.determineCollectionName(entityClass), batchToSave, + this.mongoConverter); } @Override + @SuppressWarnings("unchecked") public Collection insert(Collection batchToSave, String collectionName) { Assert.notNull(batchToSave, "BatchToSave must not be null!"); Assert.notNull(collectionName, "CollectionName must not be null!"); - return (Collection) doInsertBatch(collectionName, batchToSave, this.mongoConverter); + return (Collection) doInsertBatch(collectionName, batchToSave, this.mongoConverter); } @Override + @SuppressWarnings("unchecked") public Collection insertAll(Collection objectsToSave) { Assert.notNull(objectsToSave, "ObjectsToSave must not be null!"); - return (Collection) doInsertAll(objectsToSave, this.mongoConverter); + return (Collection) doInsertAll(objectsToSave, this.mongoConverter); } + @SuppressWarnings("unchecked") protected Collection doInsertAll(Collection listToSave, MongoWriter writer) { Map> elementsByCollection = new HashMap<>(); @@ -1335,7 +1296,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } for (Map.Entry> entry : elementsByCollection.entrySet()) { - savedObjects.addAll((Collection) doInsertBatch(entry.getKey(), entry.getValue(), this.mongoConverter)); + savedObjects.addAll((Collection) doInsertBatch(entry.getKey(), entry.getValue(), this.mongoConverter)); } return savedObjects; @@ -1350,10 +1311,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, List initializedBatchToSave = new ArrayList<>(batchToSave.size()); for (T uninitialized : batchToSave) { - T toSave = (T) initializeVersionProperty(uninitialized); - maybeEmitEvent(new BeforeConvertEvent<>(toSave, collectionName)); + AdaptibleEntity entity = operations.forEntity(uninitialized, mongoConverter.getConversionService()); + T toSave = entity.initializeVersionProperty(); - Document document = toDocument(toSave, writer); + BeforeConvertEvent event = new BeforeConvertEvent<>(toSave, collectionName); + toSave = maybeEmitEvent(event).getSource(); + + Document document = entity.toMappedDocument(writer).getDocument(); maybeEmitEvent(new BeforeSaveEvent<>(toSave, document, collectionName)); documentList.add(document); @@ -1367,7 +1331,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, for (T obj : initializedBatchToSave) { if (i < ids.size()) { - T saved = (T) populateIdIfNecessary(obj, ids.get(i)); + T saved = populateIdIfNecessary(obj, ids.get(i)); maybeEmitEvent(new AfterSaveEvent<>(saved, documentList.get(i), collectionName)); savedObjects.add(saved); } else { @@ -1383,80 +1347,75 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, public T save(T objectToSave) { Assert.notNull(objectToSave, "Object to save must not be null!"); - return save(objectToSave, determineEntityCollectionName(objectToSave)); + return save(objectToSave, operations.determineEntityCollectionName(objectToSave)); } @Override + @SuppressWarnings("unchecked") public T save(T objectToSave, String collectionName) { Assert.notNull(objectToSave, "Object to save must not be null!"); Assert.hasText(collectionName, "Collection name must not be null or empty!"); - MongoPersistentEntity entity = getPersistentEntity(objectToSave.getClass()); + AdaptibleEntity source = operations.forEntity(objectToSave, mongoConverter.getConversionService()); - if (entity != null && entity.hasVersionProperty()) { - return doSaveVersioned(objectToSave, entity, collectionName); - } + return source.isVersionedEntity() // + ? doSaveVersioned(source, collectionName) // + : (T) doSave(collectionName, objectToSave, this.mongoConverter); - return (T) doSave(collectionName, objectToSave, this.mongoConverter); } - private T doSaveVersioned(T objectToSave, MongoPersistentEntity entity, String collectionName) { + @SuppressWarnings("unchecked") + private T doSaveVersioned(AdaptibleEntity source, String collectionName) { - ConvertingPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor( - entity.getPropertyAccessor(objectToSave), mongoConverter.getConversionService()); - - MongoPersistentProperty property = entity.getRequiredVersionProperty(); - Number number = (Number) convertingAccessor.getProperty(property, Number.class); + Number number = source.getVersion(); if (number != null) { - // Bump version number - convertingAccessor.setProperty(property, number.longValue() + 1); - - T toSave = (T) convertingAccessor.getBean(); - - maybeEmitEvent(new BeforeConvertEvent<>(toSave, collectionName)); - assertUpdateableIdIfNotSet(toSave); - - Document document = new Document(); - - this.mongoConverter.write(toSave, document); - - maybeEmitEvent(new BeforeSaveEvent<>(toSave, document, collectionName)); - Update update = Update.fromDocument(document, ID_FIELD); - // Create query for entity with the id and old version - MongoPersistentProperty idProperty = entity.getRequiredIdProperty(); - Object id = entity.getIdentifierAccessor(toSave).getRequiredIdentifier(); - Query query = new Query(Criteria.where(idProperty.getName()).is(id).and(property.getName()).is(number)); + Query query = source.getQueryForVersion(); + + // Bump version number + T toSave = source.incrementVersion(); + + toSave = maybeEmitEvent(new BeforeConvertEvent(toSave, collectionName)).getSource(); + + source.assertUpdateableIdIfNotSet(); + + MappedDocument mapped = source.toMappedDocument(mongoConverter); + + maybeEmitEvent(new BeforeSaveEvent<>(toSave, mapped.getDocument(), collectionName)); + Update update = mapped.updateWithoutId(); UpdateResult result = doUpdate(collectionName, query, update, toSave.getClass(), false, false); if (result.getModifiedCount() == 0) { throw new OptimisticLockingFailureException( - String.format("Cannot save entity %s with version %s to collection %s. Has it been modified meanwhile?", id, - number, collectionName)); + String.format("Cannot save entity %s with version %s to collection %s. Has it been modified meanwhile?", + source.getId(), number, collectionName)); } - maybeEmitEvent(new AfterSaveEvent<>(toSave, document, collectionName)); + maybeEmitEvent(new AfterSaveEvent<>(toSave, mapped.getDocument(), collectionName)); return toSave; } - return (T) doInsert(collectionName, objectToSave, this.mongoConverter); + return (T) doInsert(collectionName, source.getBean(), this.mongoConverter); } protected T doSave(String collectionName, T objectToSave, MongoWriter writer) { - maybeEmitEvent(new BeforeConvertEvent<>(objectToSave, collectionName)); - assertUpdateableIdIfNotSet(objectToSave); + objectToSave = maybeEmitEvent(new BeforeConvertEvent<>(objectToSave, collectionName)).getSource(); - Document dbDoc = toDocument(objectToSave, writer); + AdaptibleEntity entity = operations.forEntity(objectToSave, mongoConverter.getConversionService()); + entity.assertUpdateableIdIfNotSet(); + + MappedDocument mapped = entity.toMappedDocument(writer); + Document dbDoc = mapped.getDocument(); maybeEmitEvent(new BeforeSaveEvent<>(objectToSave, dbDoc, collectionName)); Object id = saveDocument(collectionName, dbDoc, objectToSave.getClass()); - T saved = (T) populateIdIfNecessary(objectToSave, id); + T saved = entity.populateIdIfNecessary(id); maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)); return saved; @@ -1479,7 +1438,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } else { collection.withWriteConcern(writeConcernToUse).insertOne(document); } - return document.get(ID_FIELD); + + return operations.forEntity(document).getId(); } }); } @@ -1509,9 +1469,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return null; }); - return documents.stream()// - .map(it -> it.get(ID_FIELD))// - .collect(StreamUtils.toUnmodifiableList()); + return MappedDocument.toIds(documents); } protected Object saveDocument(final String collectionName, final Document dbDoc, final Class entityClass) { @@ -1526,26 +1484,28 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, dbDoc, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - if (!dbDoc.containsKey(ID_FIELD)) { + MappedDocument mapped = MappedDocument.of(dbDoc); + + if (!mapped.hasId()) { if (writeConcernToUse == null) { collection.insertOne(dbDoc); } else { collection.withWriteConcern(writeConcernToUse).insertOne(dbDoc); } } else if (writeConcernToUse == null) { - collection.replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc, new ReplaceOptions().upsert(true)); + collection.replaceOne(mapped.getIdFilter(), dbDoc, new ReplaceOptions().upsert(true)); } else { - collection.withWriteConcern(writeConcernToUse).replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc, + collection.withWriteConcern(writeConcernToUse).replaceOne(mapped.getIdFilter(), dbDoc, new ReplaceOptions().upsert(true)); } - return dbDoc.get(ID_FIELD); + return mapped.getId(); } }); } @Override public UpdateResult upsert(Query query, Update update, Class entityClass) { - return doUpdate(determineCollectionName(entityClass), query, update, entityClass, true, false); + return doUpdate(operations.determineCollectionName(entityClass), query, update, entityClass, true, false); } @Override @@ -1563,7 +1523,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public UpdateResult updateFirst(Query query, Update update, Class entityClass) { - return doUpdate(determineCollectionName(entityClass), query, update, entityClass, false, false); + return doUpdate(operations.determineCollectionName(entityClass), query, update, entityClass, false, false); } @Override @@ -1581,7 +1541,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public UpdateResult updateMulti(Query query, Update update, Class entityClass) { - return doUpdate(determineCollectionName(entityClass), query, update, entityClass, false, true); + return doUpdate(operations.determineCollectionName(entityClass), query, update, entityClass, false, true); } @Override @@ -1623,8 +1583,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, query.getCollation().map(Collation::toMongoCollation).ifPresent(opts::collation); } - Document updateObj = update == null ? new Document() - : updateMapper.getMappedObject(update.getUpdateObject(), entity); + Document updateObj = updateMapper.getMappedObject(update.getUpdateObject(), entity); if (multi && update.isIsolated() && !queryObj.containsKey("$isolated")) { queryObj.put("$isolated", 1); @@ -1674,7 +1633,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(object, "Object must not be null!"); - return remove(getIdQueryFor(object), object.getClass()); + Query query = operations.forEntity(object).getByIdQuery(); + + return remove(query, object.getClass()); } @Override @@ -1683,91 +1644,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(object, "Object must not be null!"); Assert.hasText(collectionName, "Collection name must not be null or empty!"); - return doRemove(collectionName, getIdQueryFor(object), object.getClass(), false); - } + Query query = operations.forEntity(object).getByIdQuery(); - /** - * Returns {@link Entry} containing the field name of the id property as {@link Entry#getKey()} and the {@link Id}s - * property value as its {@link Entry#getValue()}. - * - * @param object - * @return - */ - private Pair extractIdPropertyAndValue(Object object) { - - Assert.notNull(object, "Id cannot be extracted from 'null'."); - - Class objectType = object.getClass(); - - if (object instanceof Document) { - return Pair.of(ID_FIELD, ((Document) object).get(ID_FIELD)); - } - - MongoPersistentEntity entity = mappingContext.getPersistentEntity(objectType); - - if (entity != null && entity.hasIdProperty()) { - - MongoPersistentProperty idProperty = entity.getIdProperty(); - return Pair.of(idProperty.getFieldName(), entity.getPropertyAccessor(object).getProperty(idProperty)); - } - - throw new MappingException("No id property found for object of type " + objectType); - } - - /** - * Returns a {@link Query} for the given entity by its id. - * - * @param object must not be {@literal null}. - * @return - */ - private Query getIdQueryFor(Object object) { - - Pair id = extractIdPropertyAndValue(object); - return new Query(where(id.getFirst()).is(id.getSecond())); - } - - /** - * Returns a {@link Query} for the given entities by their ids. - * - * @param objects must not be {@literal null} or {@literal empty}. - * @return - */ - private Query getIdInQueryFor(Collection objects) { - - Assert.notEmpty(objects, "Cannot create Query for empty collection."); - - Iterator it = objects.iterator(); - Pair pair = extractIdPropertyAndValue(it.next()); - - ArrayList ids = new ArrayList<>(objects.size()); - ids.add(pair.getSecond()); - - while (it.hasNext()) { - ids.add(extractIdPropertyAndValue(it.next()).getSecond()); - } - - return new Query(where(pair.getFirst()).in(ids)); - } - - private void assertUpdateableIdIfNotSet(Object value) { - - MongoPersistentEntity entity = mappingContext.getPersistentEntity(value.getClass()); - - if (entity != null && entity.hasIdProperty()) { - - MongoPersistentProperty property = entity.getRequiredIdProperty(); - Object propertyValue = entity.getPropertyAccessor(value).getProperty(property); - - if (propertyValue != null) { - return; - } - - if (!MongoSimpleTypes.AUTOGENERATED_ID_TYPES.contains(property.getType())) { - throw new InvalidDataAccessApiUsageException( - String.format("Cannot autogenerate id of type %s for entity of type %s!", property.getType().getName(), - value.getClass().getName())); - } - } + return doRemove(collectionName, query, object.getClass(), false); } @Override @@ -1777,7 +1656,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public DeleteResult remove(Query query, Class entityClass) { - return remove(query, entityClass, determineCollectionName(entityClass)); + return remove(query, entityClass, operations.determineCollectionName(entityClass)); } @Override @@ -1821,18 +1700,20 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, if (query.getLimit() > 0 || query.getSkip() > 0) { MongoCursor cursor = new QueryCursorPreparer(query, entityClass) - .prepare(collection.find(removeQuery).projection(new Document(ID_FIELD, 1))).iterator(); + .prepare(collection.find(removeQuery).projection(MappedDocument.getIdOnlyProjection())) // + .iterator(); Set ids = new LinkedHashSet<>(); while (cursor.hasNext()) { - ids.add(cursor.next().get(ID_FIELD)); + ids.add(MappedDocument.of(cursor.next()).getId()); } - removeQuery = new Document(ID_FIELD, new Document("$in", ids)); + removeQuery = MappedDocument.getIdIn(ids); } MongoCollection collectionToUse = writeConcernToUse != null - ? collection.withWriteConcern(writeConcernToUse) : collection; + ? collection.withWriteConcern(writeConcernToUse) + : collection; DeleteResult result = multi ? collectionToUse.deleteMany(removeQuery, options) : collection.deleteOne(removeQuery, options); @@ -1846,7 +1727,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public List findAll(Class entityClass) { - return findAll(entityClass, determineCollectionName(entityClass)); + return findAll(entityClass, operations.determineCollectionName(entityClass)); } @Override @@ -2029,7 +1910,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ @Override public AggregationResults aggregate(TypedAggregation aggregation, Class outputType) { - return aggregate(aggregation, determineCollectionName(aggregation.getInputType()), outputType); + return aggregate(aggregation, operations.determineCollectionName(aggregation.getInputType()), outputType); } /* (non-Javadoc) @@ -2052,7 +1933,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public AggregationResults aggregate(Aggregation aggregation, Class inputType, Class outputType) { - return aggregate(aggregation, determineCollectionName(inputType), outputType, + return aggregate(aggregation, operations.determineCollectionName(inputType), outputType, new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper)); } @@ -2083,7 +1964,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ @Override public CloseableIterator aggregateStream(TypedAggregation aggregation, Class outputType) { - return aggregateStream(aggregation, determineCollectionName(aggregation.getInputType()), outputType); + return aggregateStream(aggregation, operations.determineCollectionName(aggregation.getInputType()), outputType); } /* (non-Javadoc) @@ -2092,7 +1973,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Override public CloseableIterator aggregateStream(Aggregation aggregation, Class inputType, Class outputType) { - return aggregateStream(aggregation, determineCollectionName(inputType), outputType, + return aggregateStream(aggregation, operations.determineCollectionName(inputType), outputType, new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper)); } @@ -2108,6 +1989,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @see org.springframework.data.mongodb.core.MongoOperations#findAllAndRemove(org.springframework.data.mongodb.core.query.Query, java.lang.String) */ @Override + @SuppressWarnings("unchecked") public List findAllAndRemove(Query query, String collectionName) { return (List) findAllAndRemove(query, Object.class, collectionName); } @@ -2117,7 +1999,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ @Override public List findAllAndRemove(Query query, Class entityClass) { - return findAllAndRemove(query, entityClass, determineCollectionName(entityClass)); + return findAllAndRemove(query, entityClass, operations.determineCollectionName(entityClass)); } /* (non-Javadoc) @@ -2143,7 +2025,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, List result = find(query, entityClass, collectionName); if (!CollectionUtils.isEmpty(result)) { - remove(getIdInQueryFor(result), entityClass, collectionName); + + Query byIdInQuery = operations.getByIdInQuery(result); + + remove(byIdInQuery, entityClass, collectionName); } return result; @@ -2161,7 +2046,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return doAggregate(aggregation, collectionName, outputType, contextToUse); } - @SuppressWarnings("ConstantConditions") protected AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, AggregationOperationContext context) { @@ -2212,7 +2096,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, }); } - @SuppressWarnings("ConstantConditions") protected CloseableIterator aggregateStream(Aggregation aggregation, String collectionName, Class outputType, @Nullable AggregationOperationContext context) { @@ -2247,7 +2130,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, cursor = cursor.collation(options.getCollation().map(Collation::toMongoCollation).get()); } - return new CloseableIterableCursorAdapter<>(cursor.iterator(), exceptionTranslator, readCallback); + return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator, readCallback); }); } @@ -2362,10 +2245,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return database; } - protected void maybeEmitEvent(MongoMappingEvent event) { + protected , T> E maybeEmitEvent(E event) { + if (null != eventPublisher) { eventPublisher.publishEvent(event); } + + return event; } /** @@ -2683,38 +2569,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @param savedObject * @param id */ - @SuppressWarnings("unchecked") - protected Object populateIdIfNecessary(Object savedObject, Object id) { + protected T populateIdIfNecessary(T savedObject, Object id) { - if (id == null) { - return null; - } - - if (savedObject instanceof Map) { - - Map map = (Map) savedObject; - map.put(ID_FIELD, id); - - return map; - } - - MongoPersistentProperty idProperty = getIdPropertyFor(savedObject.getClass()); - - if (idProperty != null) { - - ConversionService conversionService = mongoConverter.getConversionService(); - MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(savedObject.getClass()); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(savedObject); - - Object value = accessor.getProperty(idProperty); - if (value == null) { - new ConvertingPropertyAccessor(accessor, conversionService).setProperty(idProperty, id); - } - - return accessor.getBean(); - } - - return savedObject; + return operations.forEntity(savedObject, mongoConverter.getConversionService()) // + .populateIdIfNecessary(id); } private MongoCollection getAndPrepareCollection(MongoDatabase db, String collectionName) { @@ -2848,32 +2706,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, return type != null ? mappingContext.getPersistentEntity(type) : null; } - @Nullable - private MongoPersistentProperty getIdPropertyFor(Class type) { - - MongoPersistentEntity persistentEntity = getPersistentEntity(type); - return persistentEntity != null ? persistentEntity.getIdProperty() : null; - } - - @Nullable - private String determineEntityCollectionName(@Nullable T obj) { - if (null != obj) { - return determineCollectionName(obj.getClass()); - } - - return null; - } - - String determineCollectionName(@Nullable Class entityClass) { - - if (entityClass == null) { - throw new InvalidDataAccessApiUsageException( - "No class parameter provided, entity collection can't be determined!"); - } - - return mappingContext.getRequiredPersistentEntity(entityClass).getCollection(); - } - private static MongoConverter getDefaultMongoConverter(MongoDbFactory factory) { DbRefResolver dbRefResolver = new DefaultDbRefResolver(factory); @@ -2993,10 +2825,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, private final Document query; private final Document fields; - public FindCallback(Document query) { - this(query, new Document()); - } - public FindCallback(Document query, Document fields) { Assert.notNull(query, "Query must not be null!"); @@ -3421,7 +3249,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @param exceptionTranslator * @param objectReadCallback */ - public CloseableIterableCursorAdapter(FindIterable cursor, + public CloseableIterableCursorAdapter(MongoIterable cursor, PersistenceExceptionTranslator exceptionTranslator, DocumentCallback objectReadCallback) { this.cursor = cursor.iterator(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveCollectionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveCollectionCallback.java index 86f22ac8a..ac138e357 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveCollectionCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveCollectionCallback.java @@ -30,5 +30,4 @@ import com.mongodb.reactivestreams.client.MongoCollection; public interface ReactiveCollectionCallback { Publisher doInCollection(MongoCollection collection) throws MongoException, DataAccessException; - } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index c037d1e96..0b7308a7d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -15,7 +15,6 @@ */ package org.springframework.data.mongodb.core; -import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.SerializationUtils.*; import lombok.AccessLevel; @@ -24,9 +23,20 @@ import lombok.RequiredArgsConstructor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; @@ -49,32 +59,39 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.ApplicationListener; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.convert.ConversionService; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.dao.support.PersistenceExceptionTranslator; -import org.springframework.data.annotation.Id; import org.springframework.data.convert.EntityReader; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.Metric; -import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContextEvent; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; +import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.PrefixingDelegatingAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; +import org.springframework.data.mongodb.core.convert.DbRefProxyHandler; +import org.springframework.data.mongodb.core.convert.DbRefResolver; +import org.springframework.data.mongodb.core.convert.DbRefResolverCallback; +import org.springframework.data.mongodb.core.convert.JsonSchemaMapper; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; +import org.springframework.data.mongodb.core.convert.MongoWriter; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.convert.DbRefResolver; import org.springframework.data.mongodb.core.convert.JsonSchemaMapper; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -112,7 +129,6 @@ import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.util.Optionals; -import org.springframework.data.util.Pair; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -129,6 +145,15 @@ import com.mongodb.Mongo; import com.mongodb.MongoException; import com.mongodb.ReadPreference; import com.mongodb.WriteConcern; +import com.mongodb.client.model.CountOptions; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.DeleteOptions; +import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.FindOneAndUpdateOptions; +import com.mongodb.client.model.ReplaceOptions; +import com.mongodb.client.model.ReturnDocument; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.ValidationOptions; import com.mongodb.client.model.*; import com.mongodb.client.model.changestream.FullDocument; import com.mongodb.client.result.DeleteResult; @@ -143,8 +168,6 @@ import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoCollection; import com.mongodb.reactivestreams.client.MongoDatabase; import com.mongodb.reactivestreams.client.Success; -import com.mongodb.util.JSONParseException; -import reactor.util.function.Tuples; /** * Primary implementation of {@link ReactiveMongoOperations}. It simplifies the use of Reactive MongoDB usage and helps @@ -165,7 +188,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati public static final DbRefResolver NO_OP_REF_RESOLVER = NoOpDbRefResolver.INSTANCE; private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveMongoTemplate.class); - private static final String ID_FIELD = "_id"; private static final WriteResultChecking DEFAULT_WRITE_RESULT_CHECKING = WriteResultChecking.NONE; private static final Collection> ITERABLE_CLASSES; @@ -189,6 +211,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private final JsonSchemaMapper schemaMapper; private final SpelAwareProxyProjectionFactory projectionFactory; private final ApplicationListener> indexCreatorListener; + private final EntityOperations operations; private @Nullable WriteConcern writeConcern; private WriteConcernResolver writeConcernResolver = DefaultWriteConcernResolver.INSTANCE; @@ -253,6 +276,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati // We always have a mapping context in the converter, whether it's a simple one or not this.mappingContext = this.mongoConverter.getMappingContext(); + this.operations = new EntityOperations(this.mappingContext); // We create indexes based on mapping events if (this.mappingContext instanceof MongoMappingContext) { @@ -279,6 +303,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.indexCreator = that.indexCreator; this.indexCreatorListener = that.indexCreatorListener; this.mappingContext = that.mappingContext; + this.operations = that.operations; } private void onCheckForIndexes(MongoPersistentEntity entity, Consumer subscriptionExceptionHandler) { @@ -820,10 +845,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati */ public Mono findById(Object id, Class entityClass, String collectionName) { - MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(entityClass); - MongoPersistentProperty idProperty = persistentEntity != null ? persistentEntity.getIdProperty() : null; - - String idKey = idProperty == null ? ID_FIELD : idProperty.getName(); + String idKey = operations.getIdPropertyName(entityClass); return doFindOne(collectionName, new Document(idKey, id), null, entityClass, null); } @@ -855,8 +877,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), entity); String mappedFieldName = queryMapper.getMappedFields(new Document(field, 1), entity).keySet().iterator().next(); - Class mongoDriverCompatibleType = mongoDatabaseFactory.getCodecFor(resultClass).map(Codec::getEncoderClass) - .orElse((Class) BsonValue.class); + Class mongoDriverCompatibleType = mongoDatabaseFactory.getCodecFor(resultClass) // + .map(Codec::getEncoderClass) // + .orElse((Class) BsonValue.class); Flux result = execute(collectionName, collection -> { @@ -1006,11 +1029,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#geoNear(org.springframework.data.mongodb.core.query.NearQuery, java.lang.Class, java.lang.String) */ @Override - @SuppressWarnings("unchecked") public Flux> geoNear(NearQuery near, Class entityClass, String collectionName) { return geoNear(near, entityClass, collectionName, entityClass); } + @SuppressWarnings("unchecked") protected Flux> geoNear(NearQuery near, Class entityClass, String collectionName, Class returnType) { @@ -1045,11 +1068,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return executeCommand(command, this.readPreference).flatMapMany(document -> { - List l = document.get("results", List.class); - if (l == null) { - return Flux.empty(); - } - return Flux.fromIterable(l); + List results = document.get("results", List.class); + + return results == null ? Flux.empty() : Flux.fromIterable(results); + }).skip(near.getSkip() != null ? near.getSkip() : 0).map(callback::doWith); }); } @@ -1122,7 +1144,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Document mappedFields = queryMapper.getMappedFields(query.getFieldsObject(), entity); Document mappedSort = queryMapper.getMappedSort(query.getSortObject(), entity); - Document mappedReplacement = toDocument(replacement, this.mongoConverter); + Document mappedReplacement = operations.forEntity(replacement).toMappedDocument(this.mongoConverter).getDocument(); return doFindAndReplace(collectionName, mappedQuery, mappedFields, mappedSort, query.getCollation().map(Collation::toMongoCollation).orElse(null), entityType, mappedReplacement, options, @@ -1253,16 +1275,18 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return Mono.defer(() -> { - T toSave = (T) initializeVersionProperty(objectToSave); + AdaptibleEntity entity = operations.forEntity(objectToSave, mongoConverter.getConversionService()); + T toSave = entity.initializeVersionProperty(); + maybeEmitEvent(new BeforeConvertEvent<>(toSave, collectionName)); - Document dbDoc = toDocument(toSave, writer); + Document dbDoc = entity.toMappedDocument(writer).getDocument(); maybeEmitEvent(new BeforeSaveEvent<>(toSave, dbDoc, collectionName)); Mono afterInsert = insertDBObject(collectionName, dbDoc, toSave.getClass()).map(id -> { - T saved = (T) populateIdIfNecessary(toSave, id); + T saved = entity.populateIdIfNecessary(id); maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)); return saved; }); @@ -1327,19 +1351,21 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.notNull(writer, "MongoWriter must not be null!"); - Mono>> prepareDocuments = Flux.fromIterable(batchToSave) - .map(o -> { + Mono, Document>>> prepareDocuments = Flux.fromIterable(batchToSave).map(o -> { - T toSave = (T) initializeVersionProperty(o); - maybeEmitEvent(new BeforeConvertEvent<>(toSave, collectionName)); + AdaptibleEntity entity = operations.forEntity(o, mongoConverter.getConversionService()); + T toSave = entity.initializeVersionProperty(); - Document dbDoc = toDocument(toSave, writer); + BeforeConvertEvent event = new BeforeConvertEvent<>(toSave, collectionName); + toSave = maybeEmitEvent(event).getSource(); - maybeEmitEvent(new BeforeSaveEvent<>(toSave, dbDoc, collectionName)); - return Tuples.of(toSave, dbDoc); - }).collectList(); + Document dbDoc = entity.toMappedDocument(writer).getDocument(); - Flux> insertDocuments = prepareDocuments.flatMapMany(tuples -> { + maybeEmitEvent(new BeforeSaveEvent<>(toSave, dbDoc, collectionName)); + return Tuples.of(entity, dbDoc); + }).collectList(); + + Flux, Document>> insertDocuments = prepareDocuments.flatMapMany(tuples -> { List dbObjects = tuples.stream().map(Tuple2::getT2).collect(Collectors.toList()); @@ -1348,7 +1374,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return insertDocuments.map(tuple -> { - T saved = (T) populateIdIfNecessary(tuple.getT1(), tuple.getT2().get(ID_FIELD)); + Object id = MappedDocument.of(tuple.getT2()).getId(); + + T saved = tuple.getT1().populateIdIfNecessary(id); maybeEmitEvent(new AfterSaveEvent<>(saved, tuple.getT2(), collectionName)); return saved; }); @@ -1409,48 +1437,33 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private Mono doSaveVersioned(T objectToSave, MongoPersistentEntity entity, String collectionName) { + AdaptibleEntity forEntity = operations.forEntity(objectToSave, mongoConverter.getConversionService()); + return createMono(collectionName, collection -> { - ConvertingPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor( - entity.getPropertyAccessor(objectToSave), mongoConverter.getConversionService()); - - MongoPersistentProperty idProperty = entity.getRequiredIdProperty(); - MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty(); - - Object version = convertingAccessor.getProperty(versionProperty); - Number versionNumber = convertingAccessor.getProperty(versionProperty, Number.class); + Number versionNumber = forEntity.getVersion(); // Fresh instance -> initialize version property - if (version == null) { + if (versionNumber == null) { return doInsert(collectionName, objectToSave, mongoConverter); } - assertUpdateableIdIfNotSet(objectToSave); + forEntity.assertUpdateableIdIfNotSet(); - // Create query for entity with the id and old version - Object id = convertingAccessor.getProperty(idProperty); - Query query = new Query(Criteria.where(idProperty.getName()).is(id).and(versionProperty.getName()).is(version)); + Query query = forEntity.getQueryForVersion(); - if (versionNumber == null) { - versionNumber = 0; - } - // Bump version number - convertingAccessor.setProperty(versionProperty, versionNumber.longValue() + 1); + T toSave = forEntity.incrementVersion(); - T toSave = (T) convertingAccessor.getBean(); + BeforeConvertEvent event = new BeforeConvertEvent<>(toSave, collectionName); + T afterEvent = ReactiveMongoTemplate.this.maybeEmitEvent(event).getSource(); - ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeConvertEvent(toSave, collectionName)); + MappedDocument mapped = operations.forEntity(toSave).toMappedDocument(mongoConverter); + Document document = mapped.getDocument(); - Document document = ReactiveMongoTemplate.this.toDocument(toSave, mongoConverter); + ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeSaveEvent<>(afterEvent, document, collectionName)); - ReactiveMongoTemplate.this.maybeEmitEvent(new BeforeSaveEvent<>(toSave, document, collectionName)); - Update update = Update.fromDocument(document, ID_FIELD); - - return doUpdate(collectionName, query, update, toSave.getClass(), false, false).map(updateResult -> { - - maybeEmitEvent(new AfterSaveEvent<>(toSave, document, collectionName)); - return toSave; - }); + return doUpdate(collectionName, query, mapped.updateWithoutId(), afterEvent.getClass(), false, false) + .map(updateResult -> maybeEmitEvent(new AfterSaveEvent(afterEvent, document, collectionName)).getSource()); }); } @@ -1460,15 +1473,16 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return createMono(collectionName, collection -> { - maybeEmitEvent(new BeforeConvertEvent<>(objectToSave, collectionName)); - Document dbDoc = toDocument(objectToSave, writer); - maybeEmitEvent(new BeforeSaveEvent<>(objectToSave, dbDoc, collectionName)); + T toSave = maybeEmitEvent(new BeforeConvertEvent(objectToSave, collectionName)).getSource(); - return saveDocument(collectionName, dbDoc, objectToSave.getClass()).map(id -> { + AdaptibleEntity entity = operations.forEntity(toSave, mongoConverter.getConversionService()); + Document dbDoc = entity.toMappedDocument(writer).getDocument(); + maybeEmitEvent(new BeforeSaveEvent(toSave, dbDoc, collectionName)); - T saved = (T) populateIdIfNecessary(objectToSave, id); - maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)); - return saved; + return saveDocument(collectionName, dbDoc, toSave.getClass()).map(id -> { + + T saved = entity.populateIdIfNecessary(id); + return maybeEmitEvent(new AfterSaveEvent<>(saved, dbDoc, collectionName)).getSource(); }); }); } @@ -1479,7 +1493,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati LOGGER.debug("Inserting Document containing fields: " + dbDoc.keySet() + " in collection: " + collectionName); } - final Document document = new Document(dbDoc); + Document document = new Document(dbDoc); + Flux execute = execute(collectionName, collection -> { MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName, entityClass, @@ -1491,7 +1506,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return collectionToUse.insertOne(document); }); - return Flux.from(execute).last().map(success -> document.get(ID_FIELD)); + return Flux.from(execute).last().map(success -> MappedDocument.of(document).getId()); } protected Flux insertDocumentList(final String collectionName, final List dbDocList) { @@ -1516,12 +1531,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati documents.addAll(toDocuments(dbDocList)); return collectionToUse.insertMany(documents); + }).flatMap(s -> { - List documentsWithIds = documents.stream() - .filter(document -> document.get(ID_FIELD) instanceof ObjectId).collect(Collectors.toList()); - return Flux.fromIterable(documentsWithIds); - }).map(document -> document.get(ID_FIELD, ObjectId.class)); + return Flux.fromStream(documents.stream() // + .map(MappedDocument::of) // + .filter(it -> it.isIdPresent(ObjectId.class)) // + .map(it -> it.getId(ObjectId.class))); + }); } private MongoCollection prepareCollection(MongoCollection collection, @@ -1546,24 +1563,19 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.SAVE, collectionName, entityClass, document, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); + MappedDocument mapped = MappedDocument.of(document); - Publisher publisher; - if (!document.containsKey(ID_FIELD)) { - if (writeConcernToUse == null) { - publisher = collection.insertOne(document); - } else { - publisher = collection.withWriteConcern(writeConcernToUse).insertOne(document); - } - } else if (writeConcernToUse == null) { - publisher = collection.replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document, - new ReplaceOptions().upsert(true)); - } else { - publisher = collection.withWriteConcern(writeConcernToUse) - .replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document, new ReplaceOptions().upsert(true)); - } + MongoCollection collectionToUse = writeConcernToUse == null // + ? collection // + : collection.withWriteConcern(writeConcernToUse); - return Mono.from(publisher).map(o -> document.get(ID_FIELD)); + Publisher publisher = !mapped.hasId() // + ? collectionToUse.insertOne(document) // + : collectionToUse.replaceOne(mapped.getIdFilter(), document, new ReplaceOptions().upsert(true)); + + return Mono.from(publisher).map(o -> mapped.getId()); }); + } /* @@ -1639,7 +1651,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return doUpdate(collectionName, query, update, entityClass, false, true); } - protected Mono doUpdate(final String collectionName, @Nullable Query query, @Nullable Update update, + protected Mono doUpdate(final String collectionName, Query query, @Nullable Update update, @Nullable Class entityClass, final boolean upsert, final boolean multi) { MongoPersistentEntity entity = entityClass == null ? null : getPersistentEntity(entityClass); @@ -1648,7 +1660,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati increaseVersionForUpdateIfNecessary(entity, update); - Document queryObj = query == null ? new Document() : queryMapper.getMappedObject(query.getQueryObject(), entity); + Document queryObj = queryMapper.getMappedObject(query.getQueryObject(), entity); Document updateObj = update == null ? new Document() : updateMapper.getMappedObject(update.getUpdateObject(), entity); @@ -1742,7 +1754,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.notNull(object, "Object must not be null!"); - return remove(getIdQueryFor(object), object.getClass()); + return remove(operations.forEntity(object).getByIdQuery(), object.getClass()); } /* @@ -1754,72 +1766,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.notNull(object, "Object must not be null!"); Assert.hasText(collectionName, "Collection name must not be null or empty!"); - return doRemove(collectionName, getIdQueryFor(object), object.getClass()); - } - - /** - * Returns {@link Entry} containing the field name of the id property as {@link Entry#getKey()} and the {@link Id}s - * property value as its {@link Entry#getValue()}. - * - * @param object - * @return - */ - private Pair extractIdPropertyAndValue(Object object) { - - Assert.notNull(object, "Id cannot be extracted from 'null'."); - - Assert.notNull(object, "Id cannot be extracted from 'null'."); - - Class objectType = object.getClass(); - - if (object instanceof Document) { - return Pair.of(ID_FIELD, ((Document) object).get(ID_FIELD)); - } - - MongoPersistentEntity entity = mappingContext.getPersistentEntity(objectType); - - if (entity != null && entity.hasIdProperty()) { - - MongoPersistentProperty idProperty = entity.getIdProperty(); - return Pair.of(idProperty.getFieldName(), entity.getPropertyAccessor(object).getProperty(idProperty)); - } - - throw new MappingException("No id property found for object of type " + objectType); - } - - /** - * Returns a {@link Query} for the given entity by its id. - * - * @param object must not be {@literal null}. - * @return - */ - private Query getIdQueryFor(Object object) { - - Pair id = extractIdPropertyAndValue(object); - return new Query(where(id.getFirst()).is(id.getSecond())); - } - - /** - * Returns a {@link Query} for the given entities by their ids. - * - * @param objects must not be {@literal null} or {@literal empty}. - * @return - */ - private Query getIdInQueryFor(Collection objects) { - - Assert.notEmpty(objects, "Cannot create Query for empty collection."); - - Iterator it = objects.iterator(); - Pair firstEntry = extractIdPropertyAndValue(it.next()); - - ArrayList ids = new ArrayList<>(objects.size()); - ids.add(firstEntry.getSecond()); - - while (it.hasNext()) { - ids.add(extractIdPropertyAndValue(it.next()).getSecond()); - } - - return new Query(where(firstEntry.getFirst()).in(ids)); + return doRemove(collectionName, operations.forEntity(object).getByIdQuery(), object.getClass()); } private void assertUpdateableIdIfNotSet(Object value) { @@ -1902,13 +1849,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati FindPublisher cursor = new QueryFindPublisherPreparer(query, entityClass) .prepare(collection.find(removeQuey)) // - .projection(new Document(ID_FIELD, 1)); + .projection(MappedDocument.getIdOnlyProjection()); return Flux.from(cursor) // - .map(doc -> doc.get(ID_FIELD)) // + .map(MappedDocument::of) // + .map(MappedDocument::getId) // .collectList() // .flatMapMany(val -> { - return collectionToUse.deleteMany(new Document(ID_FIELD, new Document("$in", val)), deleteOptions); + return collectionToUse.deleteMany(MappedDocument.getIdIn(val), deleteOptions); }); } else { return collectionToUse.deleteMany(removeQuey, deleteOptions); @@ -2019,8 +1967,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati .map(publisher::startAtOperationTime).orElse(publisher); publisher = publisher.fullDocument(options.getFullDocumentLookup().orElse(fullDocument)); - return Flux.from(publisher).map(document -> new ChangeStreamEvent<>(document, targetType, getConverter())); - } + return Flux.from( + publisher ).map(document -> new ChangeStreamEvent<>(document, targetType, getConverter())); + } List prepareFilter(ChangeStreamOptions options) { @@ -2223,7 +2172,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Flux flux = find(query, entityClass, collectionName); return Flux.from(flux).collectList() - .flatMapMany(list -> Flux.from(remove(getIdInQueryFor(list), entityClass, collectionName)) + .flatMapMany(list -> Flux.from(remove(operations.getByIdInQuery(list), entityClass, collectionName)) .flatMap(deleteResult -> Flux.fromIterable(list))); } @@ -2509,50 +2458,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati }); } - protected void maybeEmitEvent(MongoMappingEvent event) { + protected , T> E maybeEmitEvent(E event) { + if (null != eventPublisher) { eventPublisher.publishEvent(event); } - } - /** - * Populates the id property of the saved object, if it's not set already. - * - * @param savedObject - * @param id - */ - @SuppressWarnings("unchecked") - private Object populateIdIfNecessary(Object savedObject, @Nullable Object id) { - - if (id == null) { - return null; - } - - if (savedObject instanceof Map) { - - Map map = (Map) savedObject; - map.put(ID_FIELD, id); - - return map; - } - - MongoPersistentProperty idProp = getIdPropertyFor(savedObject.getClass()); - - if (idProp == null) { - return savedObject; - } - - ConversionService conversionService = mongoConverter.getConversionService(); - MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(savedObject.getClass()); - PersistentPropertyAccessor accessor = entity.getPropertyAccessor(savedObject); - - if (accessor.getProperty(idProp) != null) { - return accessor.getBean(); - } - - new ConvertingPropertyAccessor(accessor, conversionService).setProperty(idProp, id); - - return accessor.getBean(); + return event; } private MongoCollection getAndPrepareCollection(MongoDatabase db, String collectionName) { @@ -2583,10 +2495,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati * @param collection */ protected MongoCollection prepareCollection(MongoCollection collection) { - if (this.readPreference != null) { - return collection.withReadPreference(readPreference); - } - return collection; + return this.readPreference != null ? collection.withReadPreference(readPreference) : collection; } /** @@ -2782,49 +2691,6 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati return queryMapper.getMappedSort(query.getSortObject(), mappingContext.getPersistentEntity(type)); } - /** - * @param objectToSave - * @param writer - * @return - */ - private Document toDocument(T objectToSave, MongoWriter writer) { - - if (objectToSave instanceof Document) { - return (Document) objectToSave; - } - - if (!(objectToSave instanceof String)) { - Document dbDoc = new Document(); - writer.write(objectToSave, dbDoc); - - if (dbDoc.containsKey(ID_FIELD) && dbDoc.get(ID_FIELD) == null) { - dbDoc.remove(ID_FIELD); - } - return dbDoc; - } else { - try { - return Document.parse((String) objectToSave); - } catch (JSONParseException | org.bson.json.JsonParseException e) { - throw new MappingException("Could not parse given String to save into a JSON document!", e); - } - } - } - - private Object initializeVersionProperty(Object entity) { - - MongoPersistentEntity mongoPersistentEntity = getPersistentEntity(entity.getClass()); - - if (mongoPersistentEntity != null && mongoPersistentEntity.hasVersionProperty()) { - ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor( - mongoPersistentEntity.getPropertyAccessor(entity), mongoConverter.getConversionService()); - accessor.setProperty(mongoPersistentEntity.getRequiredVersionProperty(), 0); - - return accessor.getBean(); - } - - return entity; - } - // Callback implementations /** @@ -3135,13 +3001,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private final @NonNull String collectionName; @Nullable + @SuppressWarnings("unchecked") public T doWith(@Nullable Document object) { if (object == null) { return null; } - Class typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) ? entityType + Class typeToRead = targetType.isInterface() || targetType.isAssignableFrom(entityType) // + ? entityType // : targetType; if (null != object) { @@ -3208,6 +3076,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati this.type = type; } + @SuppressWarnings("deprecation") public FindPublisher prepare(FindPublisher findPublisher) { if (query == null) { @@ -3226,12 +3095,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } try { + if (query.getSkip() > 0) { findPublisherToUse = findPublisherToUse.skip((int) query.getSkip()); } + if (query.getLimit() > 0) { findPublisherToUse = findPublisherToUse.limit(query.getLimit()); } + if (!ObjectUtils.isEmpty(query.getSortObject())) { Document sort = type != null ? getMappedSortObject(query, type) : query.getSortObject(); findPublisherToUse = findPublisherToUse.sort(sort); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index cc1045ea7..23df7f06e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -147,7 +147,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App */ public void setTypeMapper(@Nullable MongoTypeMapper typeMapper) { this.typeMapper = typeMapper == null - ? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext) : typeMapper; + ? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext) + : typeMapper; } /* @@ -272,7 +273,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App EntityInstantiator instantiator = instantiators.getInstantiatorFor(entity); S instance = instantiator.createInstance(entity, provider); - PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor(entity.getPropertyAccessor(instance), + PersistentPropertyAccessor accessor = new ConvertingPropertyAccessor<>(entity.getPropertyAccessor(instance), conversionService); MongoPersistentProperty idProperty = entity.getIdProperty(); @@ -296,7 +297,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App MappingMongoConverter.this); readProperties(entity, accessor, idProperty, documentAccessor, valueProvider, callback); - return (S) accessor.getBean(); + return accessor.getBean(); } private Object readIdValue(ObjectPath path, DefaultSpELExpressionEvaluator evaluator, @@ -557,7 +558,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } MongoPersistentEntity entity = isSubtype(prop.getType(), obj.getClass()) - ? mappingContext.getRequiredPersistentEntity(obj.getClass()) : mappingContext.getRequiredPersistentEntity(type); + ? mappingContext.getRequiredPersistentEntity(obj.getClass()) + : mappingContext.getRequiredPersistentEntity(type); Object existingValue = accessor.get(prop); Document document = existingValue instanceof Document ? (Document) existingValue : new Document(); @@ -779,7 +781,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } return conversions.hasCustomWriteTarget(key.getClass(), String.class) - ? (String) getPotentiallyConvertedSimpleWrite(key) : key.toString(); + ? (String) getPotentiallyConvertedSimpleWrite(key) + : key.toString(); } /** @@ -1481,7 +1484,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } List referencedRawDocuments = dbrefs.size() == 1 - ? Collections.singletonList(readRef(dbrefs.iterator().next())) : bulkReadRefs(dbrefs); + ? Collections.singletonList(readRef(dbrefs.iterator().next())) + : bulkReadRefs(dbrefs); String collectionName = dbrefs.iterator().next().getCollectionName(); List targeList = new ArrayList<>(dbrefs.size()); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java index 47cea2823..4d0c369c2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java @@ -16,8 +16,7 @@ package org.springframework.data.mongodb.core; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; @@ -78,13 +77,13 @@ public class ExecutableAggregationOperationSupportUnitTests { @Test // DATAMONGO-1563 public void aggregateWithUntypedAggregation() { - when(template.determineCollectionName(any(Class.class))).thenReturn("person"); + when(template.getCollectionName(any(Class.class))).thenReturn("person"); opSupport.aggregateAndReturn(Person.class).by(newAggregation(project("foo"))).all(); ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).determineCollectionName(captor.capture()); + verify(template).getCollectionName(captor.capture()); verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture()); assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class); @@ -93,13 +92,13 @@ public class ExecutableAggregationOperationSupportUnitTests { @Test // DATAMONGO-1563 public void aggregateWithTypeAggregation() { - when(template.determineCollectionName(any(Class.class))).thenReturn("person"); + when(template.getCollectionName(any(Class.class))).thenReturn("person"); opSupport.aggregateAndReturn(Jedi.class).by(newAggregation(Person.class, project("foo"))).all(); ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).determineCollectionName(captor.capture()); + verify(template).getCollectionName(captor.capture()); verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture()); assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class); @@ -118,13 +117,13 @@ public class ExecutableAggregationOperationSupportUnitTests { @Test // DATAMONGO-1563 public void aggregateStreamWithUntypedAggregation() { - when(template.determineCollectionName(any(Class.class))).thenReturn("person"); + when(template.getCollectionName(any(Class.class))).thenReturn("person"); opSupport.aggregateAndReturn(Person.class).by(newAggregation(project("foo"))).stream(); ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).determineCollectionName(captor.capture()); + verify(template).getCollectionName(captor.capture()); verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture()); assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class); @@ -133,13 +132,13 @@ public class ExecutableAggregationOperationSupportUnitTests { @Test // DATAMONGO-1563 public void aggregateStreamWithTypeAggregation() { - when(template.determineCollectionName(any(Class.class))).thenReturn("person"); + when(template.getCollectionName(any(Class.class))).thenReturn("person"); opSupport.aggregateAndReturn(Jedi.class).by(newAggregation(Person.class, project("foo"))).stream(); ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).determineCollectionName(captor.capture()); + verify(template).getCollectionName(captor.capture()); verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture()); assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupportUnitTests.java index d4f981528..6d27b7a78 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupportUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupportUnitTests.java @@ -16,10 +16,8 @@ package org.springframework.data.mongodb.core; import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; -import static org.mockito.Mockito.anyList; import lombok.Data; @@ -56,7 +54,7 @@ public class ExecutableInsertOperationSupportUnitTests { public void setUp() { when(template.bulkOps(any(), any(), any())).thenReturn(bulkOperations); - when(template.determineCollectionName(any(Class.class))).thenReturn(STAR_WARS); + when(template.getCollectionName(any(Class.class))).thenReturn(STAR_WARS); when(bulkOperations.insert(anyList())).thenReturn(bulkOperations); ops = new ExecutableInsertOperationSupport(template); @@ -88,7 +86,7 @@ public class ExecutableInsertOperationSupportUnitTests { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).determineCollectionName(captor.capture()); + verify(template).getCollectionName(captor.capture()); verify(template).insert(eq(luke), eq(STAR_WARS)); assertThat(captor.getAllValues()).containsExactly(Person.class); @@ -99,7 +97,7 @@ public class ExecutableInsertOperationSupportUnitTests { ops.insert(Person.class).inCollection(STAR_WARS).one(luke); - verify(template, never()).determineCollectionName(any(Class.class)); + verify(template, never()).getCollectionName(any(Class.class)); verify(template).insert(eq(luke), eq(STAR_WARS)); } @@ -108,7 +106,7 @@ public class ExecutableInsertOperationSupportUnitTests { ops.insert(Person.class).all(Arrays.asList(luke, han)); - verify(template).determineCollectionName(any(Class.class)); + verify(template).getCollectionName(any(Class.class)); verify(template).insert(anyList(), eq(STAR_WARS)); } @@ -119,7 +117,7 @@ public class ExecutableInsertOperationSupportUnitTests { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).determineCollectionName(any(Class.class)); + verify(template).getCollectionName(any(Class.class)); verify(template).bulkOps(eq(BulkMode.ORDERED), captor.capture(), eq(STAR_WARS)); verify(bulkOperations).insert(anyList()); verify(bulkOperations).execute(); @@ -132,7 +130,7 @@ public class ExecutableInsertOperationSupportUnitTests { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).determineCollectionName(any(Class.class)); + verify(template).getCollectionName(any(Class.class)); verify(template).bulkOps(eq(BulkMode.UNORDERED), captor.capture(), eq(STAR_WARS)); verify(bulkOperations).insert(anyList()); verify(bulkOperations).execute(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupportUnitTests.java index a68b72a16..ab35a4c88 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupportUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupportUnitTests.java @@ -15,9 +15,7 @@ */ package org.springframework.data.mongodb.core; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import lombok.AllArgsConstructor; @@ -55,7 +53,7 @@ public class ExecutableMapReduceOperationSupportUnitTests { @Before public void setUp() { - when(template.determineCollectionName(eq(Person.class))).thenReturn(STAR_WARS); + when(template.getCollectionName(eq(Person.class))).thenReturn(STAR_WARS); mapReduceOpsSupport = new ExecutableMapReduceOperationSupport(template); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 4edfcd4f9..9a2aa3b50 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -30,16 +30,28 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; +import lombok.Value; +import lombok.experimental.Wither; import java.lang.reflect.InvocationTargetException; import java.math.BigDecimal; import java.math.BigInteger; import java.time.Instant; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; import java.util.stream.Collectors; import java.util.stream.IntStream; -import lombok.experimental.Wither; import org.bson.types.ObjectId; import org.hamcrest.collection.IsMapContaining; import org.joda.time.DateTime; @@ -58,8 +70,10 @@ import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.PersistenceConstructor; import org.springframework.data.annotation.Version; +import org.springframework.data.auditing.IsNewAwareAuditingHandler; import org.springframework.data.convert.CustomConversions; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Sort; @@ -79,6 +93,7 @@ import org.springframework.data.mongodb.core.index.IndexInfo; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener; +import org.springframework.data.mongodb.core.mapping.event.AuditingEventListener; import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; import org.springframework.data.mongodb.core.query.BasicQuery; @@ -140,6 +155,8 @@ public class MongoTemplateTests { this.context = context; context.addApplicationListener(new PersonWithIdPropertyOfTypeUUIDListener()); + context.addApplicationListener( + new AuditingEventListener(() -> new IsNewAwareAuditingHandler(template.getConverter().getMappingContext()))); } @Autowired @@ -219,6 +236,7 @@ public class MongoTemplateTests { template.dropCollection(DocumentWithCollectionOfSamples.class); template.dropCollection(WithGeoJson.class); template.dropCollection(DocumentWithNestedTypeHavingStringIdProperty.class); + template.dropCollection(ImmutableAudited.class); } @Test @@ -353,7 +371,6 @@ public class MongoTemplateTests { } @Test - @SuppressWarnings("deprecation") public void testEnsureIndex() throws Exception { Person p1 = new Person("Oliver"); @@ -762,7 +779,7 @@ public class MongoTemplateTests { assertThat(template.findDistinct(new BasicQuery("{'address.state' : 'PA'}"), "name", MyPerson.class, String.class)) .containsExactlyInAnyOrder(person1.getName(), person2.getName()); assertThat(template.findDistinct(new BasicQuery("{'address.state' : 'PA'}"), "name", - template.determineCollectionName(MyPerson.class), MyPerson.class, String.class)) + template.getCollectionName(MyPerson.class), MyPerson.class, String.class)) .containsExactlyInAnyOrder(person1.getName(), person2.getName()); } @@ -1259,14 +1276,14 @@ public class MongoTemplateTests { template.setWriteConcern(WriteConcern.UNACKNOWLEDGED); template.save(person); - UpdateResult result = template.updateFirst(query(where("id").is(person.getId())), update("firstName", "Carter"), + template.updateFirst(query(where("id").is(person.getId())), update("firstName", "Carter"), PersonWithIdPropertyOfTypeObjectId.class); FsyncSafeWriteConcernResolver resolver = new FsyncSafeWriteConcernResolver(); template.setWriteConcernResolver(resolver); Query q = query(where("_id").is(person.getId())); Update u = update("firstName", "Carter"); - result = template.updateFirst(q, u, PersonWithIdPropertyOfTypeObjectId.class); + template.updateFirst(q, u, PersonWithIdPropertyOfTypeObjectId.class); MongoAction lastMongoAction = resolver.getMongoAction(); assertThat(lastMongoAction.getCollectionName(), is("personWithIdPropertyOfTypeObjectId")); @@ -1283,7 +1300,7 @@ public class MongoTemplateTests { public WriteConcern resolve(MongoAction action) { this.mongoAction = action; - return WriteConcern.FSYNC_SAFE; + return WriteConcern.JOURNALED; } public MongoAction getMongoAction() { @@ -1530,7 +1547,7 @@ public class MongoTemplateTests { org.bson.Document document = new org.bson.Document(); document.put("firstName", "Oliver"); - template.insert(document, template.determineCollectionName(PersonWithVersionPropertyOfTypeInteger.class)); + template.insert(document, template.getCollectionName(PersonWithVersionPropertyOfTypeInteger.class)); } @Test // DATAMONGO-1617 @@ -1685,7 +1702,7 @@ public class MongoTemplateTests { @Test(expected = DuplicateKeyException.class) // DATAMONGO-622 public void preventsDuplicateInsert() { - template.setWriteConcern(WriteConcern.SAFE); + template.setWriteConcern(WriteConcern.ACKNOWLEDGED); PersonWithVersionPropertyOfTypeInteger person = new PersonWithVersionPropertyOfTypeInteger(); person.firstName = "Dave"; @@ -3527,11 +3544,11 @@ public class MongoTemplateTests { template.save(rickon); List result = template.findAllAndRemove(query(where("field").regex(".*stark$")), - template.determineCollectionName(Sample.class)); + template.getCollectionName(Sample.class)); assertThat(result, hasSize(2)); assertThat(result, containsInAnyOrder(bran, rickon)); - assertThat(template.count(new BasicQuery("{}"), template.determineCollectionName(Sample.class)), is(equalTo(1L))); + assertThat(template.count(new BasicQuery("{}"), template.getCollectionName(Sample.class)), is(equalTo(1L))); } @Test // DATAMONGO-1779 @@ -3594,6 +3611,20 @@ public class MongoTemplateTests { assertThat(target).isEqualTo(source); } + @Test // DATAMONGO-1992 + public void writesAuditingMetadataForImmutableTypes() { + + ImmutableAudited source = new ImmutableAudited(null, null); + ImmutableAudited result = template.save(source); + + assertThat(result).isNotSameAs(source).describedAs("Expected a different instances to be returned!"); + assertThat(result.modified).isNotNull().describedAs("Auditing field must not be null!"); + + ImmutableAudited read = template.findOne(query(where("id").is(result.getId())), ImmutableAudited.class); + + assertThat(read.modified).isEqualTo(result.modified).describedAs("Expected auditing information to be read!"); + } + static class TypeWithNumbers { @Id String id; @@ -4074,6 +4105,8 @@ public class MongoTemplateTests { } + // DATAMONGO-1992 + @AllArgsConstructor @Wither static class ImmutableVersioned { @@ -4086,4 +4119,11 @@ public class MongoTemplateTests { version = null; } } + + @Value + @Wither + static class ImmutableAudited { + @Id String id; + @LastModifiedDate Instant modified; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/AuditingEventListenerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/AuditingEventListenerUnitTests.java index de0c32fab..961c9e347 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/AuditingEventListenerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/AuditingEventListenerUnitTests.java @@ -15,17 +15,25 @@ */ package org.springframework.data.mongodb.core.mapping.event; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Value; +import lombok.experimental.Wither; + import java.util.Arrays; import java.util.Date; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.AdditionalAnswers; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.core.Ordered; import org.springframework.data.annotation.CreatedDate; @@ -54,8 +62,9 @@ public class AuditingEventListenerUnitTests { mappingContext.getPersistentEntity(Sample.class); handler = spy(new IsNewAwareAuditingHandler(new PersistentEntities(Arrays.asList(mappingContext)))); - doNothing().when(handler).markCreated(any()); - doNothing().when(handler).markModified(any()); + + doAnswer(AdditionalAnswers.returnsArgAt(0)).when(handler).markCreated(any()); + doAnswer(AdditionalAnswers.returnsArgAt(0)).when(handler).markModified(any()); listener = new AuditingEventListener(() -> handler); } @@ -93,10 +102,37 @@ public class AuditingEventListenerUnitTests { assertThat(listener.getOrder(), is(100)); } + @Test // DATAMONGO-1992 + public void propagatesChangedInstanceToEvent() { + + ImmutableSample sample = new ImmutableSample(); + BeforeConvertEvent event = new BeforeConvertEvent<>(sample, "collection"); + + ImmutableSample newSample = new ImmutableSample(); + IsNewAwareAuditingHandler handler = mock(IsNewAwareAuditingHandler.class); + doReturn(newSample).when(handler).markAudited(eq(sample)); + + AuditingEventListener listener = new AuditingEventListener(() -> handler); + listener.onApplicationEvent(event); + + assertThat(event.getSource()).isSameAs(newSample); + } + static class Sample { @Id String id; @CreatedDate Date created; @LastModifiedDate Date modified; } + + @Value + @Wither + @AllArgsConstructor + @NoArgsConstructor(force = true) + static class ImmutableSample { + + @Id String id; + @CreatedDate Date created; + @LastModifiedDate Date modified; + } }