From fda07d9999c0c1b82eb2e67edc6feb9dd9a77e99 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 11 Mar 2022 15:02:54 +0100 Subject: [PATCH] Polishing. Extract CreateCollectionOptions conversion to EntityOperations to unify collection creation. Adopt tests. See #3984 Original pull request: #3990. --- .../data/mongodb/core/EntityOperations.java | 95 +++++++++++++- .../data/mongodb/core/MongoTemplate.java | 123 ++++++++++-------- .../mongodb/core/ReactiveMongoTemplate.java | 61 ++------- .../core/EntityOperationUnitTests.java | 59 --------- .../core/EntityOperationsUnitTests.java | 20 +++ .../core/UpdateOperationsUnitTests.java | 2 +- 6 files changed, 193 insertions(+), 167 deletions(-) delete mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java 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 index 9db573add..6908d331f 100644 --- 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 @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Optional; import org.bson.Document; + import org.springframework.core.convert.ConversionService; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.convert.CustomConversions; @@ -32,7 +33,9 @@ import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions; import org.springframework.data.mongodb.core.convert.MongoConverter; +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.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; @@ -41,9 +44,11 @@ import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.timeseries.Granularity; +import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.EntityProjectionIntrospector; import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -52,6 +57,10 @@ import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.TimeSeriesGranularity; +import com.mongodb.client.model.ValidationOptions; + /** * Common operations performed on an entity in the context of it's mapping metadata. * @@ -67,20 +76,31 @@ class EntityOperations { private static final String ID_FIELD = "_id"; private final MappingContext, MongoPersistentProperty> context; + private final QueryMapper queryMapper; private final EntityProjectionIntrospector introspector; + private final MongoJsonSchemaMapper schemaMapper; + EntityOperations(MongoConverter converter) { - this(converter.getMappingContext(), converter.getCustomConversions(), converter.getProjectionFactory()); + this(converter, new QueryMapper(converter)); } - EntityOperations(MappingContext, MongoPersistentProperty> context, - CustomConversions conversions, ProjectionFactory projectionFactory) { + EntityOperations(MongoConverter converter, QueryMapper queryMapper) { + this(converter, converter.getMappingContext(), converter.getCustomConversions(), converter.getProjectionFactory(), + queryMapper); + } + + EntityOperations(MongoConverter converter, + MappingContext, MongoPersistentProperty> context, + CustomConversions conversions, ProjectionFactory projectionFactory, QueryMapper queryMapper) { this.context = context; + this.queryMapper = queryMapper; this.introspector = EntityProjectionIntrospector.create(projectionFactory, EntityProjectionIntrospector.ProjectionPredicate.typeHierarchy() .and(((target, underlyingType) -> !conversions.isSimpleType(target))), context); + this.schemaMapper = new MongoJsonSchemaMapper(converter); } /** @@ -259,6 +279,75 @@ class EntityOperations { return introspector.introspect(resultType, entityType); } + /** + * Convert given {@link CollectionOptions} to a document and take the domain type information into account when + * creating a mapped schema for validation. + * + * @param collectionOptions can be {@literal null}. + * @param entityType must not be {@literal null}. Use {@link Object} type instead. + * @return the converted {@link CreateCollectionOptions}. + * @since 3.4 + */ + public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions, + Class entityType) { + + Optional collation = Optionals.firstNonEmpty( + () -> Optional.ofNullable(collectionOptions).flatMap(CollectionOptions::getCollation), + () -> forType(entityType).getCollation());// + + CreateCollectionOptions result = new CreateCollectionOptions(); + collation.map(Collation::toMongoCollation).ifPresent(result::collation); + + if (collectionOptions == null) { + return result; + } + + collectionOptions.getCapped().ifPresent(result::capped); + collectionOptions.getSize().ifPresent(result::sizeInBytes); + collectionOptions.getMaxDocuments().ifPresent(result::maxDocuments); + collectionOptions.getCollation().map(Collation::toMongoCollation).ifPresent(result::collation); + + collectionOptions.getValidationOptions().ifPresent(it -> { + + ValidationOptions validationOptions = new ValidationOptions(); + + it.getValidationAction().ifPresent(validationOptions::validationAction); + it.getValidationLevel().ifPresent(validationOptions::validationLevel); + + it.getValidator().ifPresent(val -> validationOptions.validator(getMappedValidator(val, entityType))); + + result.validationOptions(validationOptions); + }); + + collectionOptions.getTimeSeriesOptions().map(forType(entityType)::mapTimeSeriesOptions).ifPresent(it -> { + + com.mongodb.client.model.TimeSeriesOptions options = new com.mongodb.client.model.TimeSeriesOptions( + it.getTimeField()); + + if (StringUtils.hasText(it.getMetaField())) { + options.metaField(it.getMetaField()); + } + if (!Granularity.DEFAULT.equals(it.getGranularity())) { + options.granularity(TimeSeriesGranularity.valueOf(it.getGranularity().name().toUpperCase())); + } + + result.timeSeriesOptions(options); + }); + + return result; + } + + private Document getMappedValidator(Validator validator, Class domainType) { + + Document validationRules = validator.toDocument(); + + if (validationRules.containsKey("$jsonSchema")) { + return schemaMapper.mapSchema(validationRules, domainType); + } + + return queryMapper.getMappedObject(validationRules, context.getPersistentEntity(domainType)); + } + /** * A representation of information about an entity. * 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 f5a22f974..d081a4ac8 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 @@ -648,13 +648,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(entityClass, "EntityClass must not be null!"); - CollectionOptions options = collectionOptions != null ? collectionOptions : CollectionOptions.empty(); - options = Optionals - .firstNonEmpty(() -> Optional.ofNullable(collectionOptions).flatMap(CollectionOptions::getCollation), - () -> operations.forType(entityClass).getCollation()) // - .map(options::collation).orElse(options); - - return doCreateCollection(getCollectionName(entityClass), convertToDocument(options, entityClass)); + return doCreateCollection(getCollectionName(entityClass), + operations.convertToCreateCollectionOptions(collectionOptions, entityClass)); } /* @@ -676,7 +671,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, @Nullable CollectionOptions collectionOptions) { Assert.notNull(collectionName, "CollectionName must not be null!"); - return doCreateCollection(collectionName, convertToDocument(collectionOptions, Object.class)); + return doCreateCollection(collectionName, + operations.convertToCreateCollectionOptions(collectionOptions, Object.class)); } /* @@ -2475,56 +2471,23 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, * @param collectionOptions * @return the collection that was created */ - @SuppressWarnings("ConstantConditions") protected MongoCollection doCreateCollection(String collectionName, Document collectionOptions) { + return doCreateCollection(collectionName, getCreateCollectionOptions(collectionOptions)); + } + + /** + * Create the specified collection using the provided options + * + * @param collectionName + * @param collectionOptions + * @return the collection that was created + * @since 3.3.3 + */ + @SuppressWarnings("ConstantConditions") + protected MongoCollection doCreateCollection(String collectionName, + CreateCollectionOptions collectionOptions) { return execute(db -> { - - CreateCollectionOptions co = new CreateCollectionOptions(); - - if (collectionOptions.containsKey("capped")) { - co.capped((Boolean) collectionOptions.get("capped")); - } - if (collectionOptions.containsKey("size")) { - co.sizeInBytes(((Number) collectionOptions.get("size")).longValue()); - } - if (collectionOptions.containsKey("max")) { - co.maxDocuments(((Number) collectionOptions.get("max")).longValue()); - } - - if (collectionOptions.containsKey("collation")) { - co.collation(IndexConverters.fromDocument(collectionOptions.get("collation", Document.class))); - } - - if (collectionOptions.containsKey("validator")) { - - com.mongodb.client.model.ValidationOptions options = new com.mongodb.client.model.ValidationOptions(); - - if (collectionOptions.containsKey("validationLevel")) { - options.validationLevel(ValidationLevel.fromString(collectionOptions.getString("validationLevel"))); - } - if (collectionOptions.containsKey("validationAction")) { - options.validationAction(ValidationAction.fromString(collectionOptions.getString("validationAction"))); - } - - options.validator(collectionOptions.get("validator", Document.class)); - co.validationOptions(options); - } - - if (collectionOptions.containsKey("timeseries")) { - - Document timeSeries = collectionOptions.get("timeseries", Document.class); - com.mongodb.client.model.TimeSeriesOptions options = new com.mongodb.client.model.TimeSeriesOptions( - timeSeries.getString("timeField")); - if (timeSeries.containsKey("metaField")) { - options.metaField(timeSeries.getString("metaField")); - } - if (timeSeries.containsKey("granularity")) { - options.granularity(TimeSeriesGranularity.valueOf(timeSeries.getString("granularity").toUpperCase())); - } - co.timeSeriesOptions(options); - } - - db.createCollection(collectionName, co); + db.createCollection(collectionName, collectionOptions); MongoCollection coll = db.getCollection(collectionName, Document.class); @@ -2537,6 +2500,54 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, }); } + private CreateCollectionOptions getCreateCollectionOptions(Document collectionOptions) { + + CreateCollectionOptions co = new CreateCollectionOptions(); + + if (collectionOptions.containsKey("capped")) { + co.capped((Boolean) collectionOptions.get("capped")); + } + if (collectionOptions.containsKey("size")) { + co.sizeInBytes(((Number) collectionOptions.get("size")).longValue()); + } + if (collectionOptions.containsKey("max")) { + co.maxDocuments(((Number) collectionOptions.get("max")).longValue()); + } + + if (collectionOptions.containsKey("collation")) { + co.collation(IndexConverters.fromDocument(collectionOptions.get("collation", Document.class))); + } + + if (collectionOptions.containsKey("validator")) { + + ValidationOptions options = new ValidationOptions(); + + if (collectionOptions.containsKey("validationLevel")) { + options.validationLevel(ValidationLevel.fromString(collectionOptions.getString("validationLevel"))); + } + if (collectionOptions.containsKey("validationAction")) { + options.validationAction(ValidationAction.fromString(collectionOptions.getString("validationAction"))); + } + + options.validator(collectionOptions.get("validator", Document.class)); + co.validationOptions(options); + } + + if (collectionOptions.containsKey("timeseries")) { + + Document timeSeries = collectionOptions.get("timeseries", Document.class); + TimeSeriesOptions options = new TimeSeriesOptions(timeSeries.getString("timeField")); + if (timeSeries.containsKey("metaField")) { + options.metaField(timeSeries.getString("metaField")); + } + if (timeSeries.containsKey("granularity")) { + options.granularity(TimeSeriesGranularity.valueOf(timeSeries.getString("granularity").toUpperCase())); + } + co.timeSeriesOptions(options); + } + return co; + } + /** * Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter. * The query document is specified as a standard {@link Document} and so is the fields specification. 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 cfb3b9294..f31388773 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 @@ -112,7 +112,6 @@ import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; -import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.EntityProjection; @@ -131,7 +130,16 @@ import com.mongodb.CursorType; import com.mongodb.MongoException; import com.mongodb.ReadPreference; import com.mongodb.WriteConcern; -import com.mongodb.client.model.*; +import com.mongodb.client.model.CountOptions; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.DeleteOptions; +import com.mongodb.client.model.EstimatedDocumentCountOptions; +import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.FindOneAndReplaceOptions; +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.changestream.FullDocument; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.InsertOneResult; @@ -718,13 +726,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.notNull(entityClass, "EntityClass must not be null!"); - CollectionOptions options = collectionOptions != null ? collectionOptions : CollectionOptions.empty(); - options = Optionals - .firstNonEmpty(() -> Optional.ofNullable(collectionOptions).flatMap(CollectionOptions::getCollation), - () -> operations.forType(entityClass).getCollation()) // - .map(options::collation).orElse(options); - - return doCreateCollection(getCollectionName(entityClass), convertToCreateCollectionOptions(options, entityClass)); + return doCreateCollection(getCollectionName(entityClass), + operations.convertToCreateCollectionOptions(collectionOptions, entityClass)); } /* @@ -2542,45 +2545,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions, Class entityType) { - - CreateCollectionOptions result = new CreateCollectionOptions(); - - if (collectionOptions == null) { - return result; - } - - collectionOptions.getCapped().ifPresent(result::capped); - collectionOptions.getSize().ifPresent(result::sizeInBytes); - collectionOptions.getMaxDocuments().ifPresent(result::maxDocuments); - collectionOptions.getCollation().map(Collation::toMongoCollation).ifPresent(result::collation); - - collectionOptions.getValidationOptions().ifPresent(it -> { - - ValidationOptions validationOptions = new ValidationOptions(); - - it.getValidationAction().ifPresent(validationOptions::validationAction); - it.getValidationLevel().ifPresent(validationOptions::validationLevel); - - it.getValidator().ifPresent(val -> validationOptions.validator(getMappedValidator(val, entityType))); - - result.validationOptions(validationOptions); - }); - - collectionOptions.getTimeSeriesOptions().map(operations.forType(entityType)::mapTimeSeriesOptions).ifPresent(it -> { - - TimeSeriesOptions options = new TimeSeriesOptions(it.getTimeField()); - - if (StringUtils.hasText(it.getMetaField())) { - options.metaField(it.getMetaField()); - } - if (!Granularity.DEFAULT.equals(it.getGranularity())) { - options.granularity(TimeSeriesGranularity.valueOf(it.getGranularity().name().toUpperCase())); - } - - result.timeSeriesOptions(options); - }); - - return result; + return operations.convertToCreateCollectionOptions(collectionOptions, entityType); } private Document getMappedValidator(Validator validator, Class domainType) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java deleted file mode 100644 index 26b478a60..000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationUnitTests.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2019-2022 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 - * - * https://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 static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.data.annotation.Id; -import org.springframework.data.mongodb.core.EntityOperations.AdaptibleEntity; -import org.springframework.data.mongodb.core.convert.MappingMongoConverter; -import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; -import org.springframework.data.mongodb.core.mapping.MongoMappingContext; - -/** - * @author Christoph Strobl - */ -public class EntityOperationUnitTests { - - EntityOperations ops; - MongoMappingContext mappingContext = new MongoMappingContext(); - ConversionService conversionService = new DefaultConversionService(); - - @BeforeEach - public void setUp() { - ops = new EntityOperations(new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext)); - } - - @Test // DATAMONGO-2293 - public void populateIdShouldReturnTargetBeanWhenIdIsNull() { - assertThat(initAdaptibleEntity(new DomainTypeWithIdProperty()).populateIdIfNecessary(null)).isNotNull(); - } - - AdaptibleEntity initAdaptibleEntity(T source) { - return ops.forEntity(source, conversionService); - } - - private static class DomainTypeWithIdProperty { - - @Id String id; - String value; - } -} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java index 7f895e695..33ae30aa6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/EntityOperationsUnitTests.java @@ -21,6 +21,9 @@ import java.time.Instant; import org.junit.jupiter.api.Test; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.annotation.Id; import org.springframework.data.mapping.MappingException; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; @@ -34,6 +37,8 @@ import org.springframework.data.mongodb.test.util.MongoTestMappingContext; */ class EntityOperationsUnitTests { + ConversionService conversionService = new DefaultConversionService(); + EntityOperations operations = new EntityOperations( new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, MongoTestMappingContext.newTestContext())); @@ -51,6 +56,21 @@ class EntityOperationsUnitTests { .withMessageContaining("Meta field 'foo' does not exist"); } + @Test // DATAMONGO-2293 + void populateIdShouldReturnTargetBeanWhenIdIsNull() { + assertThat(initAdaptibleEntity(new DomainTypeWithIdProperty()).populateIdIfNecessary(null)).isNotNull(); + } + + EntityOperations.AdaptibleEntity initAdaptibleEntity(T source) { + return operations.forEntity(source, conversionService); + } + + private static class DomainTypeWithIdProperty { + + @Id String id; + String value; + } + @TimeSeries(timeField = "foo") static class InvalidTimeField { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java index a7d4d5550..f416c406e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java @@ -48,7 +48,7 @@ class UpdateOperationsUnitTests { MongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext); QueryMapper queryMapper = new QueryMapper(mongoConverter); UpdateMapper updateMapper = new UpdateMapper(mongoConverter); - EntityOperations entityOperations = new EntityOperations(mongoConverter); + EntityOperations entityOperations = new EntityOperations(mongoConverter, this.queryMapper); PropertyOperations propertyOperations = new PropertyOperations(mongoConverter.getMappingContext()); ExtendedQueryOperations queryOperations = new ExtendedQueryOperations(queryMapper, updateMapper, entityOperations, propertyOperations,