From 594798005208d243c0f24fee80a6ef9cf2b2a8de Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 15 Jan 2018 07:20:56 +0100 Subject: [PATCH 1/3] DATAMONGO-1322 - Prepare issue branch --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-cross-store/pom.xml | 4 ++-- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index 0f5c0b18a7..a18a3ec6c4 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1322-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 9baccaa905..5c7e462f47 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1322-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index 8ba393d38b..51780ba3fa 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1322-SNAPSHOT ../pom.xml @@ -49,7 +49,7 @@ org.springframework.data spring-data-mongodb - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1322-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index e5c865ea08..ef5aa1473b 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1322-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 41b711f7c8..9ac2f3f6c7 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1322-SNAPSHOT ../pom.xml From 0ce34d8edc8c30f564cd8fb5fba35f4365bd351f Mon Sep 17 00:00:00 2001 From: Andreas Zink Date: Sun, 22 Oct 2017 19:06:12 +0200 Subject: [PATCH 2/3] DATAMONGO-1322 - Add support for validator when creating collection. Extended the CollectionOptions with a ValidationOptions property which corresponds to the MongoDB createCollection() parameters. A validator object can be defined using the Criteria API, or by writing a custom provider. --- .../data/mongodb/core/CollectionOptions.java | 22 ++- .../data/mongodb/core/MongoTemplate.java | 10 +- .../core/validation/CriteriaValidator.java | 65 +++++++ .../core/validation/ValidationAction.java | 46 +++++ .../core/validation/ValidationLevel.java | 50 +++++ .../core/validation/ValidationOptions.java | 74 +++++++ .../core/validation/ValidatorDefinition.java | 35 ++++ .../core/MongoTemplateValidationTests.java | 183 ++++++++++++++++++ .../core/schema/MongoJsonSchemaTests.java | 1 - .../validation/CriteriaValidatorTest.java | 43 ++++ 10 files changed, 521 insertions(+), 8 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationAction.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationLevel.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationOptions.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidatorDefinition.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/CriteriaValidatorTest.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index b1a7d8214b..0f28588d5c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -21,6 +21,7 @@ import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; +import org.springframework.data.mongodb.core.validation.ValidatorDefinition; import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -34,6 +35,7 @@ * @author Thomas Risberg * @author Christoph Strobl * @author Mark Paluch + * @author Andreas Zink */ public class CollectionOptions { @@ -144,7 +146,11 @@ public CollectionOptions collation(@Nullable Collation collation) { * @since 2.1 */ public CollectionOptions schema(@Nullable MongoJsonSchema schema) { - return validation(new Validator(schema, validator.validationLevel, validator.validationAction)); + return validation(new Validator(schema, null, validator.validationLevel, validator.validationAction)); + } + + public CollectionOptions validatorDefinition(@Nullable ValidatorDefinition definition) { + return validation(new Validator(null, definition, validator.validationLevel, validator.validationAction)); } /** @@ -213,7 +219,7 @@ public CollectionOptions failOnValidationError() { public CollectionOptions schemaValidationLevel(ValidationLevel validationLevel) { Assert.notNull(validationLevel, "ValidationLevel must not be null!"); - return validation(new Validator(validator.schema, validationLevel, validator.validationAction)); + return validation(new Validator(validator.schema, validator.validatorDefinition, validationLevel, validator.validationAction)); } /** @@ -227,7 +233,7 @@ public CollectionOptions schemaValidationLevel(ValidationLevel validationLevel) public CollectionOptions schemaValidationAction(ValidationAction validationAction) { Assert.notNull(validationAction, "ValidationAction must not be null!"); - return validation(new Validator(validator.schema, validator.validationLevel, validationAction)); + return validation(new Validator(validator.schema, validator.validatorDefinition, validator.validationLevel, validationAction)); } /** @@ -295,14 +301,16 @@ public Optional getValidator() { * Encapsulation of Validator options. * * @author Christoph Strobl + * @author Andreas Zink * @since 2.1 */ @RequiredArgsConstructor public static class Validator { - private static final Validator NONE = new Validator(null, null, null); + private static final Validator NONE = new Validator(null, null, null, null); private final @Nullable MongoJsonSchema schema; + private final @Nullable ValidatorDefinition validatorDefinition; private final @Nullable ValidationLevel validationLevel; private final @Nullable ValidationAction validationAction; @@ -324,6 +332,10 @@ public Optional getSchema() { return Optional.ofNullable(schema); } + public Optional getValidatorDefinition() { + return Optional.ofNullable(validatorDefinition); + } + /** * Get the {@code validationLevel} to apply. * @@ -346,7 +358,7 @@ public Optional getValidationAction() { * @return {@literal true} if no arguments set. */ boolean isEmpty() { - return !Optionals.isAnyPresent(getSchema(), getValidationAction(), getValidationLevel()); + return !Optionals.isAnyPresent(getSchema(), getValidatorDefinition(), getValidationAction(), getValidationLevel()); } } } 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 6b0612d554..2d2f7d5a22 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 @@ -140,6 +140,9 @@ import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; import com.mongodb.client.model.*; +import com.mongodb.client.model.ValidationAction; +import com.mongodb.client.model.ValidationLevel; +import com.mongodb.client.model.ValidationOptions; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; import com.mongodb.util.JSONParseException; @@ -165,6 +168,7 @@ * @author Maninder Singh * @author Borislav Rangelov * @author duozhilin + * @author Andreas Zink */ @SuppressWarnings("deprecation") public class MongoTemplate implements MongoOperations, ApplicationContextAware, IndexOperationsProvider { @@ -2374,6 +2378,7 @@ protected Document convertToDocument(@Nullable CollectionOptions collectionOptio Document doc = convertToDocument(collectionOptions); if (collectionOptions != null && collectionOptions.getValidator().isPresent()) { + Validator v = collectionOptions.getValidator().get(); v.getSchema().ifPresent(val -> doc.put("validator", schemaMapper.mapSchema(val.toDocument(), targetType))); } @@ -2398,10 +2403,11 @@ protected Document convertToDocument(@Nullable CollectionOptions collectionOptio if (collectionOptions.getValidator().isPresent()) { Validator v = collectionOptions.getValidator().get(); - v.getValidationLevel().ifPresent(val -> document.append("validationLevel", val)); - v.getValidationAction().ifPresent(val -> document.append("validationAction", val)); + v.getValidationLevel().ifPresent(val -> document.append("validationLevel", val.getValue())); + v.getValidationAction().ifPresent(val -> document.append("validationAction", val.getValue())); v.getSchema().ifPresent(val -> document.append("validator", new MongoJsonSchemaMapper(getConverter()).mapSchema(val.toDocument(), Object.class))); + v.getValidatorDefinition().ifPresent(val -> document.put("validator", val.toDocument())); } } return document; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java new file mode 100644 index 0000000000..c6cc303859 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java @@ -0,0 +1,65 @@ +/* + * Copyright 2017 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.validation; + +import lombok.EqualsAndHashCode; + +import org.bson.Document; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.CriteriaDefinition; +import org.springframework.lang.NonNull; +import org.springframework.util.Assert; + +/** + * Utility to build a MongoDB {@code validator} based on a {@link CriteriaDefinition}. + * + * @author Andreas Zink + * @since 2.1 + * @see Criteria + */ +@EqualsAndHashCode +public class CriteriaValidator implements ValidatorDefinition { + + private final Document document; + + private CriteriaValidator(Document document) { + Assert.notNull(document, "Document must not be null!"); + this.document = document; + } + + /** + * Builds a {@code validator} object, which is basically setup of query operators, based on a + * {@link CriteriaDefinition} instance. + * + * @param criteria the criteria to build the {@code validator} from + * @return + */ + public static CriteriaValidator fromCriteria(@NonNull CriteriaDefinition criteria) { + Assert.notNull(criteria, "Criteria must not be null!"); + return new CriteriaValidator(criteria.getCriteriaObject()); + } + + @Override + public Document toDocument() { + return this.document; + } + + @Override + public String toString() { + return document.toString(); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationAction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationAction.java new file mode 100644 index 0000000000..199e00a7d6 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationAction.java @@ -0,0 +1,46 @@ +/* + * Copyright 2017 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.validation; + +import lombok.Getter; + +/** + * Determines whether to error on invalid documents or just warn about the violations but allow invalid documents to be + * inserted. + * + * @author Andreas Zink + * @since 2.1 + * @see MongoDB Collection Options + */ +public enum ValidationAction { + + /** + * Documents must pass validation before the write occurs. Otherwise, the write operation fails. (MongoDB default) + */ + ERROR("error"), + + /** + * Documents do not have to pass validation. If the document fails validation, the write operation logs the validation + * failure. + */ + WARN("warn"); + + @Getter private String value; + + private ValidationAction(String value) { + this.value = value; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationLevel.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationLevel.java new file mode 100644 index 0000000000..187c63ca15 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationLevel.java @@ -0,0 +1,50 @@ +/* + * Copyright 2017 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.validation; + +import lombok.Getter; + +/** + * Determines how strictly MongoDB applies the validation rules to existing documents during an update. + * + * @author Andreas Zink + * @since 2.1 + * @see MongoDB Collection Options + */ +public enum ValidationLevel { + + /** + * No validation for inserts or updates. + */ + OFF("off"), + + /** + * Apply validation rules to all inserts and all updates. (MongoDB default) + */ + STRICT("strict"), + + /** + * Apply validation rules to inserts and to updates on existing valid documents. Do not apply rules to updates on + * existing invalid documents. + */ + MODERATE("moderate"); + + @Getter private String value; + + private ValidationLevel(String value) { + this.value = value; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationOptions.java new file mode 100644 index 0000000000..ebb9c0baa8 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationOptions.java @@ -0,0 +1,74 @@ +/* + * Copyright 2017 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.validation; + +import lombok.EqualsAndHashCode; +import lombok.ToString; + +import java.util.Optional; + +import org.springframework.data.mongodb.core.CollectionOptions; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Wraps the collection validation options. + * + * @author Andreas Zink + * @since 2.1 + * @see {@link CollectionOptions} + * @see MongoDB Document Validation + * @see MongoDB Collection Options + */ +@EqualsAndHashCode +@ToString +public class ValidationOptions { + private ValidatorDefinition validator; + private ValidationLevel validationLevel; + private ValidationAction validationAction; + + private ValidationOptions(ValidatorDefinition validator) { + Assert.notNull(validator, "ValidatorDefinition must not be null!"); + this.validator = validator; + } + + public static ValidationOptions validator(@NonNull ValidatorDefinition validator) { + return new ValidationOptions(validator); + } + + public ValidationOptions validationLevel(@Nullable ValidationLevel validationLevel) { + this.validationLevel = validationLevel; + return this; + } + + public ValidationOptions validationAction(@Nullable ValidationAction validationAction) { + this.validationAction = validationAction; + return this; + } + + public ValidatorDefinition getValidator() { + return validator; + } + + public Optional getValidationLevel() { + return Optional.ofNullable(validationLevel); + } + + public Optional getValidationAction() { + return Optional.ofNullable(validationAction); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidatorDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidatorDefinition.java new file mode 100644 index 0000000000..50bcbb579a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidatorDefinition.java @@ -0,0 +1,35 @@ +/* + * Copyright 2017 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.validation; + +import org.bson.Document; +import org.springframework.lang.NonNull; + +/** + * Provides a {@code validator} object to be used for collection validation. + * + * @author Andreas Zink + * @since 2.1 + * @see MongoDB Collection Options + */ +public interface ValidatorDefinition { + + /** + * @return a MongoDB {@code validator} document + */ + public @NonNull Document toDocument(); + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java new file mode 100644 index 0000000000..10db45c824 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java @@ -0,0 +1,183 @@ +/* + * Copyright 2017 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 static org.assertj.core.api.Assertions.assertThat; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +import javax.validation.constraints.Max; +import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; + +import org.bson.Document; +import org.junit.Assert; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.AbstractMongoConfiguration; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.validation.CriteriaValidator; +import org.springframework.data.mongodb.core.validation.ValidationAction; +import org.springframework.data.mongodb.core.validation.ValidationLevel; +import org.springframework.data.mongodb.core.validation.ValidationOptions; +import org.springframework.data.mongodb.test.util.MongoVersionRule; +import org.springframework.data.util.Version; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import com.mongodb.MongoClient; + +/** + * @author Andreas Zink + */ +@RunWith(SpringJUnit4ClassRunner.class) +public class MongoTemplateValidationTests { + + public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_2_0 = MongoVersionRule.atLeast(Version.parse("3.2.0")); + public static final String COLLECTION_NAME = "validation-1"; + + @Configuration + static class Config extends AbstractMongoConfiguration { + + @Override + public MongoClient mongoClient() { + return new MongoClient(); + } + + @Override + protected String getDatabaseName() { + return "validation-tests"; + } + } + + @Autowired MongoTemplate template; + + @Before + public void setUp() { + template.dropCollection(COLLECTION_NAME); + } + + @Test // DATAMONGO-1322 + public void testCollectionWithSimpleCriteriaBasedValidation() { + Criteria criteria = Criteria.where("nonNullString").ne(null).type(2).and("rangedInteger").ne(null).type(16).gte(0) + .lte(122); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().validatorDefinition(CriteriaValidator.fromCriteria(criteria))); + + Document validator = getValidatorInfo(COLLECTION_NAME); + assertThat(validator.get("nonNullString")).isEqualTo(new Document("$ne", null).append("$type", 2)); + assertThat(validator.get("rangedInteger")) + .isEqualTo(new Document("$ne", null).append("$type", 16).append("$gte", 0).append("$lte", 122)); + + template.save(new SimpleBean("hello", 101), COLLECTION_NAME); + try { + template.save(new SimpleBean(null, 101), COLLECTION_NAME); + Assert.fail("The collection validation was setup to check for non-null string"); + } catch (Exception e) { + // ignore + } + try { + template.save(new SimpleBean("hello", -1), COLLECTION_NAME); + Assert.fail("The collection validation was setup to check for non-negative int"); + } catch (Exception e) { + // ignore + } + } + + @Test // DATAMONGO-1322 + public void testCollectionValidationActionError() { + Criteria criteria = Criteria.where("name").type(2); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationAction(com.mongodb.client.model.ValidationAction.ERROR).validatorDefinition(CriteriaValidator.fromCriteria(criteria))); + + String validationAction = getValidationActionInfo(COLLECTION_NAME); + assertThat(ValidationAction.ERROR.getValue()).isEqualTo(validationAction); + } + + @Test // DATAMONGO-1322 + public void testCollectionValidationActionWarn() { + Criteria criteria = Criteria.where("name").type(2); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationAction(com.mongodb.client.model.ValidationAction.WARN).validatorDefinition(CriteriaValidator.fromCriteria(criteria))); + + String validationAction = getValidationActionInfo(COLLECTION_NAME); + assertThat(ValidationAction.WARN.getValue()).isEqualTo(validationAction); + } + + @Test // DATAMONGO-1322 + public void testCollectionValidationLevelOff() { + Criteria criteria = Criteria.where("name").type(2); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationLevel(com.mongodb.client.model.ValidationLevel.OFF).validatorDefinition(CriteriaValidator.fromCriteria(criteria))); + + String validationAction = getValidationLevelInfo(COLLECTION_NAME); + assertThat(ValidationLevel.OFF.getValue()).isEqualTo(validationAction); + } + + @Test // DATAMONGO-1322 + public void testCollectionValidationLevelModerate() { + Criteria criteria = Criteria.where("name").type(2); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationLevel(com.mongodb.client.model.ValidationLevel.MODERATE).validatorDefinition(CriteriaValidator.fromCriteria(criteria))); + + String validationAction = getValidationLevelInfo(COLLECTION_NAME); + assertThat(ValidationLevel.MODERATE.getValue()).isEqualTo(validationAction); + } + + @Test // DATAMONGO-1322 + public void testCollectionValidationLevelStrict() { + Criteria criteria = Criteria.where("name").type(2); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationLevel(com.mongodb.client.model.ValidationLevel.STRICT).validatorDefinition(CriteriaValidator.fromCriteria(criteria))); + + String validationAction = getValidationLevelInfo(COLLECTION_NAME); + assertThat(ValidationLevel.STRICT.getValue()).isEqualTo(validationAction); + } + + private Document getCollectionOptions(String collectionName) { + return getCollectionInfo(collectionName).get("options", Document.class); + } + + private Document getValidatorInfo(String collectionName) { + return getCollectionOptions(collectionName).get("validator", Document.class); + } + + private String getValidationActionInfo(String collectionName) { + return getCollectionOptions(collectionName).get("validationAction", String.class); + } + + private String getValidationLevelInfo(String collectionName) { + return getCollectionOptions(collectionName).get("validationLevel", String.class); + } + + private Document getCollectionInfo(String collectionName) { + return template.execute(db -> { + Document result = db.runCommand( + new Document().append("listCollections", 1).append("filter", new Document("name", collectionName))); + return (Document) result.get("cursor", Document.class).get("firstBatch", List.class).get(0); + }); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class SimpleBean { + @NotNull private String nonNullString; + @NotNull @Min(0) @Max(122) private Integer rangedInteger; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java index 0eabe3bd42..0ce070ae22 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java @@ -28,7 +28,6 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; -import org.springframework.dao.DataRetrievalFailureException; import org.springframework.data.mongodb.config.AbstractMongoConfiguration; import org.springframework.data.mongodb.core.CollectionOptions; import org.springframework.data.mongodb.core.MongoTemplate; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/CriteriaValidatorTest.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/CriteriaValidatorTest.java new file mode 100644 index 0000000000..1279680435 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/CriteriaValidatorTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 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.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.bson.Document; +import org.junit.Test; +import org.springframework.data.mongodb.core.query.Criteria; + +/** + * @author Andreas Zink + */ +public class CriteriaValidatorTest { + + @Test // DATAMONGO-1322 + public void testSimpleCriteria() { + Criteria criteria = Criteria.where("nonNullString").ne(null).type(2).and("rangedInteger").type(16).gte(0).lte(122); + Document validator = CriteriaValidator.fromCriteria(criteria).toDocument(); + + assertThat(validator.get("nonNullString")).isEqualTo(new Document("$ne", null).append("$type", 2)); + assertThat(validator.get("rangedInteger")) + .isEqualTo(new Document("$type", 16).append("$gte", 0).append("$lte", 122)); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1322 + public void testFailOnNull() { + CriteriaValidator.fromCriteria(null); + } +} From b00054f04963742888f03406c8fb9190574acac4 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 15 Jan 2018 10:32:21 +0100 Subject: [PATCH 3/3] DATAMONGO-1322 - Polishing Remove ValidationAction/Level and ValidationOptions duplicates. Rename ValidatorDefinition to Validator and Validator to ValidationOptions. Update and rename some of the tests. Also make sure to run the created validator document through the QueryMapper for value conversion and potential field mappings. Streamline Validator usage by providing plain Document and JsonSchema validator options next to the Criteria based one. Finally update the reference documentation. --- .../data/mongodb/core/CollectionOptions.java | 109 +++++++++----- .../data/mongodb/core/MongoTemplate.java | 42 ++++-- .../mongodb/core/ReactiveMongoTemplate.java | 29 +++- .../core/validation/CriteriaValidator.java | 48 +++--- .../core/validation/DocumentValidator.java | 70 +++++++++ .../core/validation/JsonSchemaValidator.java | 67 +++++++++ .../core/validation/ValidationAction.java | 46 ------ .../core/validation/ValidationLevel.java | 50 ------- .../core/validation/ValidationOptions.java | 74 ---------- .../mongodb/core/validation/Validator.java | 81 +++++++++++ .../core/validation/ValidatorDefinition.java | 35 ----- .../mongodb/core/validation/package-info.java | 6 + .../core/MongoTemplateValidationTests.java | 137 +++++++++++------- .../core/schema/MongoJsonSchemaTests.java | 16 ++ ...t.java => CriteriaValidatorUnitTests.java} | 10 +- src/main/asciidoc/reference/mongo-3.adoc | 17 +++ 16 files changed, 504 insertions(+), 333 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/DocumentValidator.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/JsonSchemaValidator.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationAction.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationLevel.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationOptions.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/Validator.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidatorDefinition.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/package-info.java rename spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/{CriteriaValidatorTest.java => CriteriaValidatorUnitTests.java} (84%) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index 0f28588d5c..c1c430117f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -21,7 +21,7 @@ import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; -import org.springframework.data.mongodb.core.validation.ValidatorDefinition; +import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -43,7 +43,7 @@ public class CollectionOptions { private @Nullable Long size; private @Nullable Boolean capped; private @Nullable Collation collation; - private Validator validator; + private ValidationOptions validationOptions; /** * Constructs a new CollectionOptions instance. @@ -56,17 +56,17 @@ public class CollectionOptions { */ @Deprecated public CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped) { - this(size, maxDocuments, capped, null, Validator.none()); + this(size, maxDocuments, capped, null, ValidationOptions.none()); } private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped, - @Nullable Collation collation, Validator validator) { + @Nullable Collation collation, ValidationOptions validationOptions) { this.maxDocuments = maxDocuments; this.size = size; this.capped = capped; this.collation = collation; - this.validator = validator; + this.validationOptions = validationOptions; } /** @@ -80,7 +80,7 @@ public static CollectionOptions just(Collation collation) { Assert.notNull(collation, "Collation must not be null!"); - return new CollectionOptions(null, null, null, collation, Validator.none()); + return new CollectionOptions(null, null, null, collation, ValidationOptions.none()); } /** @@ -90,7 +90,7 @@ public static CollectionOptions just(Collation collation) { * @since 2.0 */ public static CollectionOptions empty() { - return new CollectionOptions(null, null, null, null, Validator.none()); + return new CollectionOptions(null, null, null, null, ValidationOptions.none()); } /** @@ -101,7 +101,7 @@ public static CollectionOptions empty() { * @since 2.0 */ public CollectionOptions capped() { - return new CollectionOptions(size, maxDocuments, true, collation, validator); + return new CollectionOptions(size, maxDocuments, true, collation, validationOptions); } /** @@ -112,7 +112,7 @@ public CollectionOptions capped() { * @since 2.0 */ public CollectionOptions maxDocuments(long maxDocuments) { - return new CollectionOptions(size, maxDocuments, capped, collation, validator); + return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions); } /** @@ -123,7 +123,7 @@ public CollectionOptions maxDocuments(long maxDocuments) { * @since 2.0 */ public CollectionOptions size(long size) { - return new CollectionOptions(size, maxDocuments, capped, collation, validator); + return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions); } /** @@ -134,11 +134,11 @@ public CollectionOptions size(long size) { * @since 2.0 */ public CollectionOptions collation(@Nullable Collation collation) { - return new CollectionOptions(size, maxDocuments, capped, collation, validator); + return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions); } /** - * Create new {@link CollectionOptions} with already given settings and {@code validator} set to given + * Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given * {@link MongoJsonSchema}. * * @param schema can be {@literal null}. @@ -146,11 +146,19 @@ public CollectionOptions collation(@Nullable Collation collation) { * @since 2.1 */ public CollectionOptions schema(@Nullable MongoJsonSchema schema) { - return validation(new Validator(schema, null, validator.validationLevel, validator.validationAction)); + return validator(Validator.schema(schema)); } - public CollectionOptions validatorDefinition(@Nullable ValidatorDefinition definition) { - return validation(new Validator(null, definition, validator.validationLevel, validator.validationAction)); + /** + * /** Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given + * {@link Validator}. + * + * @param validator can be {@literal null}. + * @return new {@link CollectionOptions}. + * @since 2.1 + */ + public CollectionOptions validator(@Nullable Validator validator) { + return validation(validationOptions.validator(validator)); } /** @@ -219,7 +227,7 @@ public CollectionOptions failOnValidationError() { public CollectionOptions schemaValidationLevel(ValidationLevel validationLevel) { Assert.notNull(validationLevel, "ValidationLevel must not be null!"); - return validation(new Validator(validator.schema, validator.validatorDefinition, validationLevel, validator.validationAction)); + return validation(validationOptions.validationLevel(validationLevel)); } /** @@ -233,20 +241,20 @@ public CollectionOptions schemaValidationLevel(ValidationLevel validationLevel) public CollectionOptions schemaValidationAction(ValidationAction validationAction) { Assert.notNull(validationAction, "ValidationAction must not be null!"); - return validation(new Validator(validator.schema, validator.validatorDefinition, validator.validationLevel, validationAction)); + return validation(validationOptions.validationAction(validationAction)); } /** - * Create new {@link CollectionOptions} with the given {@link Validator}. + * Create new {@link CollectionOptions} with the given {@link ValidationOptions}. * - * @param validator must not be {@literal null}. Use {@link Validator#none()} to remove validation. + * @param validationOptions must not be {@literal null}. Use {@link ValidationOptions#none()} to remove validation. * @return new {@link CollectionOptions}. * @since 2.1 */ - public CollectionOptions validation(Validator validator) { + public CollectionOptions validation(ValidationOptions validationOptions) { - Assert.notNull(validator, "Validator must not be null!"); - return new CollectionOptions(size, maxDocuments, capped, collation, validator); + Assert.notNull(validationOptions, "ValidationOptions must not be null!"); + return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions); } /** @@ -293,47 +301,72 @@ public Optional getCollation() { * @return {@link Optional#empty()} if not set. * @since 2.1 */ - public Optional getValidator() { - return validator.isEmpty() ? Optional.empty() : Optional.of(validator); + public Optional getValidationOptions() { + return validationOptions.isEmpty() ? Optional.empty() : Optional.of(validationOptions); } /** - * Encapsulation of Validator options. + * Encapsulation of ValidationOptions options. * * @author Christoph Strobl * @author Andreas Zink * @since 2.1 */ @RequiredArgsConstructor - public static class Validator { + public static class ValidationOptions { - private static final Validator NONE = new Validator(null, null, null, null); + private static final ValidationOptions NONE = new ValidationOptions(null, null, null); - private final @Nullable MongoJsonSchema schema; - private final @Nullable ValidatorDefinition validatorDefinition; + private final @Nullable Validator validator; private final @Nullable ValidationLevel validationLevel; private final @Nullable ValidationAction validationAction; /** - * Create an empty {@link Validator}. + * Create an empty {@link ValidationOptions}. * * @return never {@literal null}. */ - public static Validator none() { + public static ValidationOptions none() { return NONE; } /** - * Get the {@code $jsonSchema} used for validation. + * Define the {@link Validator} to be used for document validation. * - * @return {@link Optional#empty()} if not set. + * @param validator can be {@literal null}. + * @return new instance of {@link ValidationOptions}. + */ + public ValidationOptions validator(@Nullable Validator validator) { + return new ValidationOptions(validator, validationLevel, validationAction); + } + + /** + * Define the validation level to apply. + * + * @param validationLevel can be {@literal null}. + * @return new instance of {@link ValidationOptions}. + */ + public ValidationOptions validationLevel(ValidationLevel validationLevel) { + return new ValidationOptions(validator, validationLevel, validationAction); + } + + /** + * Define the validation action to take. + * + * @param validationAction can be {@literal null}. + * @return new instance of {@link ValidationOptions}. */ - public Optional getSchema() { - return Optional.ofNullable(schema); + public ValidationOptions validationAction(ValidationAction validationAction) { + return new ValidationOptions(validator, validationLevel, validationAction); } - public Optional getValidatorDefinition() { - return Optional.ofNullable(validatorDefinition); + /** + * Get the {@link Validator} to use. + * + * @return never {@literal null}. + */ + public Optional getValidator() { + return Optional.ofNullable(validator); } /** @@ -358,7 +391,7 @@ public Optional getValidationAction() { * @return {@literal true} if no arguments set. */ boolean isEmpty() { - return !Optionals.isAnyPresent(getSchema(), getValidatorDefinition(), getValidationAction(), getValidationLevel()); + return !Optionals.isAnyPresent(getValidator(), getValidationAction(), getValidationLevel()); } } } 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 2d2f7d5a22..b83a668786 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 @@ -63,7 +63,7 @@ import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; -import org.springframework.data.mongodb.core.CollectionOptions.Validator; +import org.springframework.data.mongodb.core.CollectionOptions.ValidationOptions; import org.springframework.data.mongodb.core.DefaultBulkOperations.BulkOperationContext; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; @@ -108,6 +108,8 @@ import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.validation.JsonSchemaValidator; +import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -139,10 +141,16 @@ import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; -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.Filters; +import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.FindOneAndUpdateOptions; +import com.mongodb.client.model.ReturnDocument; +import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.ValidationAction; import com.mongodb.client.model.ValidationLevel; -import com.mongodb.client.model.ValidationOptions; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; import com.mongodb.util.JSONParseException; @@ -2236,7 +2244,7 @@ public MongoCollection doInDB(MongoDatabase db) throws MongoException, if (collectionOptions.containsKey("validator")) { - ValidationOptions options = new ValidationOptions(); + com.mongodb.client.model.ValidationOptions options = new com.mongodb.client.model.ValidationOptions(); if (collectionOptions.containsKey("validationLevel")) { options.validationLevel(ValidationLevel.fromString(collectionOptions.getString("validationLevel"))); @@ -2377,10 +2385,10 @@ protected Document convertToDocument(@Nullable CollectionOptions collectionOptio Document doc = convertToDocument(collectionOptions); - if (collectionOptions != null && collectionOptions.getValidator().isPresent()) { + if (collectionOptions != null && collectionOptions.getValidationOptions().isPresent()) { - Validator v = collectionOptions.getValidator().get(); - v.getSchema().ifPresent(val -> doc.put("validator", schemaMapper.mapSchema(val.toDocument(), targetType))); + ValidationOptions v = collectionOptions.getValidationOptions().get(); + v.getValidator().ifPresent(val -> doc.put("validator", getMappedValidator(val, targetType))); } return doc; } @@ -2401,18 +2409,28 @@ protected Document convertToDocument(@Nullable CollectionOptions collectionOptio collectionOptions.getMaxDocuments().ifPresent(val -> document.put("max", val)); collectionOptions.getCollation().ifPresent(val -> document.append("collation", val.toDocument())); - if (collectionOptions.getValidator().isPresent()) { - Validator v = collectionOptions.getValidator().get(); + if (collectionOptions.getValidationOptions().isPresent()) { + + CollectionOptions.ValidationOptions v = collectionOptions.getValidationOptions().get(); v.getValidationLevel().ifPresent(val -> document.append("validationLevel", val.getValue())); v.getValidationAction().ifPresent(val -> document.append("validationAction", val.getValue())); - v.getSchema().ifPresent(val -> document.append("validator", - new MongoJsonSchemaMapper(getConverter()).mapSchema(val.toDocument(), Object.class))); - v.getValidatorDefinition().ifPresent(val -> document.put("validator", val.toDocument())); + v.getValidator().ifPresent(val -> document.append("validator", getMappedValidator(val, Object.class))); } } return document; } + Document getMappedValidator(Validator validator, Class domainType) { + + Document validationRules = validator.toDocument(); + + if (validator instanceof JsonSchemaValidator || validationRules.containsKey("$jsonSchema")) { + return schemaMapper.mapSchema(validationRules, domainType); + } + + return queryMapper.getMappedObject(validationRules, mappingContext.getPersistentEntity(domainType)); + } + /** * Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter. * The first document that matches the query is returned and also removed from the collection in the database. 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 71fffaa920..3a70e89c0a 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 @@ -24,8 +24,17 @@ import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; -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.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; +import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -91,6 +100,8 @@ import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.validation.JsonSchemaValidator; +import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -2007,15 +2018,14 @@ protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Col collectionOptions.getMaxDocuments().ifPresent(result::maxDocuments); collectionOptions.getCollation().map(Collation::toMongoCollation).ifPresent(result::collation); - collectionOptions.getValidator().ifPresent(it -> { + collectionOptions.getValidationOptions().ifPresent(it -> { ValidationOptions validationOptions = new ValidationOptions(); it.getValidationAction().ifPresent(validationOptions::validationAction); it.getValidationLevel().ifPresent(validationOptions::validationLevel); - it.getSchema() - .ifPresent(val -> validationOptions.validator(schemaMapper.mapSchema(val.toDocument(), entityType))); + it.getValidator().ifPresent(val -> validationOptions.validator(getMappedValidator(val, entityType))); result.validationOptions(validationOptions); }); @@ -2023,6 +2033,17 @@ protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Col return result; } + Document getMappedValidator(Validator validator, Class domainType) { + + Document validationRules = validator.toDocument(); + + if (validator instanceof JsonSchemaValidator || validationRules.containsKey("$jsonSchema")) { + return schemaMapper.mapSchema(validationRules, domainType); + } + + return queryMapper.getMappedObject(validationRules, mappingContext.getPersistentEntity(domainType)); + } + /** * Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter. * The first document that matches the query is returned and also removed from the collection in the database. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java index c6cc303859..c6d150a2b1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/CriteriaValidator.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * 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. @@ -15,51 +15,61 @@ */ package org.springframework.data.mongodb.core.validation; +import lombok.AccessLevel; import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; import org.bson.Document; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.CriteriaDefinition; -import org.springframework.lang.NonNull; +import org.springframework.data.mongodb.core.query.SerializationUtils; import org.springframework.util.Assert; /** - * Utility to build a MongoDB {@code validator} based on a {@link CriteriaDefinition}. - * + * {@link Validator} implementation based on {@link CriteriaDefinition query expressions}. + * * @author Andreas Zink + * @author Christoph Strobl * @since 2.1 * @see Criteria + * @see Schema Validation */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) @EqualsAndHashCode -public class CriteriaValidator implements ValidatorDefinition { - - private final Document document; +public class CriteriaValidator implements Validator { - private CriteriaValidator(Document document) { - Assert.notNull(document, "Document must not be null!"); - this.document = document; - } + private final CriteriaDefinition criteria; /** - * Builds a {@code validator} object, which is basically setup of query operators, based on a + * Creates a new {@link Validator} object, which is basically setup of query operators, based on a * {@link CriteriaDefinition} instance. - * - * @param criteria the criteria to build the {@code validator} from - * @return + * + * @param criteria the criteria to build the {@code validator} from. Must not be {@literal null}. + * @return new instance of {@link CriteriaValidator}. + * @throws IllegalArgumentException when criteria is {@literal null}. */ - public static CriteriaValidator fromCriteria(@NonNull CriteriaDefinition criteria) { + public static CriteriaValidator of(CriteriaDefinition criteria) { + Assert.notNull(criteria, "Criteria must not be null!"); - return new CriteriaValidator(criteria.getCriteriaObject()); + return new CriteriaValidator(criteria); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.validation.Validator#toDocument() + */ @Override public Document toDocument() { - return this.document; + return criteria.getCriteriaObject(); } + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ @Override public String toString() { - return document.toString(); + return SerializationUtils.serializeToJsonSafely(toDocument()); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/DocumentValidator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/DocumentValidator.java new file mode 100644 index 0000000000..870973c0f6 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/DocumentValidator.java @@ -0,0 +1,70 @@ +/* + * 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.validation; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; + +import org.bson.Document; +import org.springframework.data.mongodb.core.query.SerializationUtils; +import org.springframework.util.Assert; + +/** + * Most trivial {@link Validator} implementation using plain {@link Document} to describe the desired document structure + * which can be either a {@code $jsonSchema} or query expression. + * + * @author Christoph Strobl + * @since 2.1 + * @see Schema Validation + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode +public class DocumentValidator implements Validator { + + private final Document validatorObject; + + /** + * Create new {@link org.springframework.data.mongodb.core.validation.DocumentValidator} defining validation rules via + * a plain {@link Document}. + * + * @param validatorObject must not be {@literal null}. + * @throws IllegalArgumentException if validatorObject is {@literal null}. + */ + public static DocumentValidator of(Document validatorObject) { + + Assert.notNull(validatorObject, "ValidatorObject must not be null!"); + return new DocumentValidator(new Document(validatorObject)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.validation.Validator#toDocument() + */ + @Override + public Document toDocument() { + return new Document(validatorObject); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return SerializationUtils.serializeToJsonSafely(validatorObject); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/JsonSchemaValidator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/JsonSchemaValidator.java new file mode 100644 index 0000000000..3c1fbbf9c2 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/JsonSchemaValidator.java @@ -0,0 +1,67 @@ +/* + * 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.validation; + +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.RequiredArgsConstructor; + +import org.bson.Document; +import org.springframework.data.mongodb.core.query.SerializationUtils; +import org.springframework.data.mongodb.core.schema.MongoJsonSchema; +import org.springframework.util.Assert; + +/** + * {@link Validator} implementation based on {@link MongoJsonSchema JSON Schema}. + * + * @author Christoph Strobl + * @since 2.1 + * @see Schema Validation + */ +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode +public class JsonSchemaValidator implements Validator { + + private final MongoJsonSchema schema; + + public static JsonSchemaValidator of(MongoJsonSchema schema) { + + Assert.notNull(schema, "Schema must not be null!"); + return new JsonSchemaValidator(schema); + } + + public MongoJsonSchema getSchema() { + return schema; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.validation.Validator#toDocument() + */ + @Override + public Document toDocument() { + return schema.toDocument(); + } + + /* + * (non-Javadoc) + * @see java.lang.Object#toString() + */ + @Override + public String toString() { + return SerializationUtils.serializeToJsonSafely(toDocument()); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationAction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationAction.java deleted file mode 100644 index 199e00a7d6..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationAction.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2017 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.validation; - -import lombok.Getter; - -/** - * Determines whether to error on invalid documents or just warn about the violations but allow invalid documents to be - * inserted. - * - * @author Andreas Zink - * @since 2.1 - * @see MongoDB Collection Options - */ -public enum ValidationAction { - - /** - * Documents must pass validation before the write occurs. Otherwise, the write operation fails. (MongoDB default) - */ - ERROR("error"), - - /** - * Documents do not have to pass validation. If the document fails validation, the write operation logs the validation - * failure. - */ - WARN("warn"); - - @Getter private String value; - - private ValidationAction(String value) { - this.value = value; - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationLevel.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationLevel.java deleted file mode 100644 index 187c63ca15..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationLevel.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2017 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.validation; - -import lombok.Getter; - -/** - * Determines how strictly MongoDB applies the validation rules to existing documents during an update. - * - * @author Andreas Zink - * @since 2.1 - * @see MongoDB Collection Options - */ -public enum ValidationLevel { - - /** - * No validation for inserts or updates. - */ - OFF("off"), - - /** - * Apply validation rules to all inserts and all updates. (MongoDB default) - */ - STRICT("strict"), - - /** - * Apply validation rules to inserts and to updates on existing valid documents. Do not apply rules to updates on - * existing invalid documents. - */ - MODERATE("moderate"); - - @Getter private String value; - - private ValidationLevel(String value) { - this.value = value; - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationOptions.java deleted file mode 100644 index ebb9c0baa8..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidationOptions.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2017 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.validation; - -import lombok.EqualsAndHashCode; -import lombok.ToString; - -import java.util.Optional; - -import org.springframework.data.mongodb.core.CollectionOptions; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * Wraps the collection validation options. - * - * @author Andreas Zink - * @since 2.1 - * @see {@link CollectionOptions} - * @see MongoDB Document Validation - * @see MongoDB Collection Options - */ -@EqualsAndHashCode -@ToString -public class ValidationOptions { - private ValidatorDefinition validator; - private ValidationLevel validationLevel; - private ValidationAction validationAction; - - private ValidationOptions(ValidatorDefinition validator) { - Assert.notNull(validator, "ValidatorDefinition must not be null!"); - this.validator = validator; - } - - public static ValidationOptions validator(@NonNull ValidatorDefinition validator) { - return new ValidationOptions(validator); - } - - public ValidationOptions validationLevel(@Nullable ValidationLevel validationLevel) { - this.validationLevel = validationLevel; - return this; - } - - public ValidationOptions validationAction(@Nullable ValidationAction validationAction) { - this.validationAction = validationAction; - return this; - } - - public ValidatorDefinition getValidator() { - return validator; - } - - public Optional getValidationLevel() { - return Optional.ofNullable(validationLevel); - } - - public Optional getValidationAction() { - return Optional.ofNullable(validationAction); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/Validator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/Validator.java new file mode 100644 index 0000000000..40dfd2994d --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/Validator.java @@ -0,0 +1,81 @@ +/* + * 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.validation; + +import org.bson.Document; +import org.springframework.data.mongodb.core.query.CriteriaDefinition; +import org.springframework.data.mongodb.core.schema.MongoJsonSchema; +import org.springframework.util.Assert; + +/** + * Provides a {@code validator} object to be used for collection validation via + * {@link org.springframework.data.mongodb.core.CollectionOptions.ValidationOptions}. + * + * @author Andreas Zink + * @author Christoph Strobl + * @since 2.1 + * @see MongoDB Collection Options + */ +public interface Validator { + + /** + * Get the {@link Document} containing the validation specific rules. The document may contain fields that may require + * type and/or field name mapping. + * + * @return a MongoDB {@code validator} {@link Document}. Never {@literal null}. + */ + Document toDocument(); + + /** + * Creates a basic {@link Validator} checking documents against a given set of rules. + * + * @param validationRules must not be {@literal null}. + * @return new instance of {@link Validator}. + * @throws IllegalArgumentException if validationRules is {@literal null}. + */ + static Validator document(Document validationRules) { + + Assert.notNull(validationRules, "ValidationRules must not be null!"); + return DocumentValidator.of(validationRules); + } + + /** + * Creates a new {@link Validator} checking documents against the structure defined in {@link MongoJsonSchema}. + * + * @param schema must not be {@literal null}. + * @return new instance of {@link Validator}. + * @throws IllegalArgumentException if schema is {@literal null}. + */ + static Validator schema(MongoJsonSchema schema) { + + Assert.notNull(schema, "Schema must not be null!"); + return JsonSchemaValidator.of(schema); + } + + /** + * Creates a new {@link Validator} checking documents against a given query structure expressed by + * {@link CriteriaDefinition}.
+ * + * @param criteria must not be {@literal null}. + * @return new instance of {@link Validator}. + * @throws IllegalArgumentException if criteria is {@literal null}. + */ + static Validator criteria(CriteriaDefinition criteria) { + + Assert.notNull(criteria, "Criteria must not be null!"); + return CriteriaValidator.of(criteria); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidatorDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidatorDefinition.java deleted file mode 100644 index 50bcbb579a..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/ValidatorDefinition.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2017 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.validation; - -import org.bson.Document; -import org.springframework.lang.NonNull; - -/** - * Provides a {@code validator} object to be used for collection validation. - * - * @author Andreas Zink - * @since 2.1 - * @see MongoDB Collection Options - */ -public interface ValidatorDefinition { - - /** - * @return a MongoDB {@code validator} document - */ - public @NonNull Document toDocument(); - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/package-info.java new file mode 100644 index 0000000000..002a4ee1fb --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/validation/package-info.java @@ -0,0 +1,6 @@ +/** + * MongoDB schema validation specifics. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.data.mongodb.core.validation; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java index 10db45c824..a2449427b0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * 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. @@ -15,46 +15,50 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.validation.Validator.*; import lombok.AllArgsConstructor; import lombok.Data; -import lombok.NoArgsConstructor; import java.util.List; -import javax.validation.constraints.Max; -import javax.validation.constraints.Min; -import javax.validation.constraints.NotNull; - import org.bson.Document; -import org.junit.Assert; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.mongodb.config.AbstractMongoConfiguration; +import org.springframework.data.mongodb.core.CollectionOptions.ValidationOptions; +import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.validation.CriteriaValidator; -import org.springframework.data.mongodb.core.validation.ValidationAction; -import org.springframework.data.mongodb.core.validation.ValidationLevel; -import org.springframework.data.mongodb.core.validation.ValidationOptions; +import org.springframework.data.mongodb.core.validation.DocumentValidator; import org.springframework.data.mongodb.test.util.MongoVersionRule; import org.springframework.data.util.Version; +import org.springframework.lang.Nullable; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import com.mongodb.MongoClient; +import com.mongodb.client.model.ValidationAction; +import com.mongodb.client.model.ValidationLevel; /** + * Integration tests for {@link CollectionOptions#validation(ValidationOptions)} using + * {@link org.springframework.data.mongodb.core.validation.CriteriaValidator} and {@link DocumentValidator}. + * * @author Andreas Zink + * @author Christoph Strobl */ @RunWith(SpringJUnit4ClassRunner.class) public class MongoTemplateValidationTests { public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_2_0 = MongoVersionRule.atLeast(Version.parse("3.2.0")); - public static final String COLLECTION_NAME = "validation-1"; + + static final String COLLECTION_NAME = "validation-1"; @Configuration static class Config extends AbstractMongoConfiguration { @@ -79,73 +83,99 @@ public void setUp() { @Test // DATAMONGO-1322 public void testCollectionWithSimpleCriteriaBasedValidation() { - Criteria criteria = Criteria.where("nonNullString").ne(null).type(2).and("rangedInteger").ne(null).type(16).gte(0) - .lte(122); - template.createCollection(COLLECTION_NAME, CollectionOptions.empty().validatorDefinition(CriteriaValidator.fromCriteria(criteria))); + + Criteria criteria = where("nonNullString").ne(null).type(2).and("rangedInteger").ne(null).type(16).gte(0).lte(122); + + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().validator(criteria(criteria))); Document validator = getValidatorInfo(COLLECTION_NAME); + assertThat(validator.get("nonNullString")).isEqualTo(new Document("$ne", null).append("$type", 2)); assertThat(validator.get("rangedInteger")) .isEqualTo(new Document("$ne", null).append("$type", 16).append("$gte", 0).append("$lte", 122)); - template.save(new SimpleBean("hello", 101), COLLECTION_NAME); - try { - template.save(new SimpleBean(null, 101), COLLECTION_NAME); - Assert.fail("The collection validation was setup to check for non-null string"); - } catch (Exception e) { - // ignore - } - try { - template.save(new SimpleBean("hello", -1), COLLECTION_NAME); - Assert.fail("The collection validation was setup to check for non-negative int"); - } catch (Exception e) { - // ignore - } + template.save(new SimpleBean("hello", 101, null), COLLECTION_NAME); + + assertThatExceptionOfType(DataIntegrityViolationException.class) + .isThrownBy(() -> template.save(new SimpleBean(null, 101, null), COLLECTION_NAME)); + + assertThatExceptionOfType(DataIntegrityViolationException.class) + .isThrownBy(() -> template.save(new SimpleBean("hello", -1, null), COLLECTION_NAME)); } @Test // DATAMONGO-1322 public void testCollectionValidationActionError() { - Criteria criteria = Criteria.where("name").type(2); - template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationAction(com.mongodb.client.model.ValidationAction.ERROR).validatorDefinition(CriteriaValidator.fromCriteria(criteria))); - String validationAction = getValidationActionInfo(COLLECTION_NAME); - assertThat(ValidationAction.ERROR.getValue()).isEqualTo(validationAction); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationAction(ValidationAction.ERROR) + .validator(criteria(where("name").type(2)))); + + assertThat(getValidationActionInfo(COLLECTION_NAME)).isEqualTo(ValidationAction.ERROR.getValue()); } @Test // DATAMONGO-1322 public void testCollectionValidationActionWarn() { - Criteria criteria = Criteria.where("name").type(2); - template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationAction(com.mongodb.client.model.ValidationAction.WARN).validatorDefinition(CriteriaValidator.fromCriteria(criteria))); - String validationAction = getValidationActionInfo(COLLECTION_NAME); - assertThat(ValidationAction.WARN.getValue()).isEqualTo(validationAction); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationAction(ValidationAction.WARN) + .validator(criteria(where("name").type(2)))); + + assertThat(getValidationActionInfo(COLLECTION_NAME)).isEqualTo(ValidationAction.WARN.getValue()); } @Test // DATAMONGO-1322 public void testCollectionValidationLevelOff() { - Criteria criteria = Criteria.where("name").type(2); - template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationLevel(com.mongodb.client.model.ValidationLevel.OFF).validatorDefinition(CriteriaValidator.fromCriteria(criteria))); - String validationAction = getValidationLevelInfo(COLLECTION_NAME); - assertThat(ValidationLevel.OFF.getValue()).isEqualTo(validationAction); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationLevel(ValidationLevel.OFF) + .validator(criteria(where("name").type(2)))); + + assertThat(getValidationLevelInfo(COLLECTION_NAME)).isEqualTo(ValidationLevel.OFF.getValue()); } @Test // DATAMONGO-1322 public void testCollectionValidationLevelModerate() { - Criteria criteria = Criteria.where("name").type(2); - template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationLevel(com.mongodb.client.model.ValidationLevel.MODERATE).validatorDefinition(CriteriaValidator.fromCriteria(criteria))); - String validationAction = getValidationLevelInfo(COLLECTION_NAME); - assertThat(ValidationLevel.MODERATE.getValue()).isEqualTo(validationAction); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationLevel(ValidationLevel.MODERATE) + .validator(criteria(where("name").type(2)))); + + assertThat(getValidationLevelInfo(COLLECTION_NAME)).isEqualTo(ValidationLevel.MODERATE.getValue()); } @Test // DATAMONGO-1322 public void testCollectionValidationLevelStrict() { - Criteria criteria = Criteria.where("name").type(2); - template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationLevel(com.mongodb.client.model.ValidationLevel.STRICT).validatorDefinition(CriteriaValidator.fromCriteria(criteria))); - String validationAction = getValidationLevelInfo(COLLECTION_NAME); - assertThat(ValidationLevel.STRICT.getValue()).isEqualTo(validationAction); + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().schemaValidationLevel(ValidationLevel.STRICT) + .validator(criteria(where("name").type(2)))); + + assertThat(getValidationLevelInfo(COLLECTION_NAME)).isEqualTo(ValidationLevel.STRICT.getValue()); + } + + @Test // DATAMONGO-1322 + public void mapsFieldNameCorrectlyWhenGivenDomainTypeInformation() { + + template.createCollection(SimpleBean.class, + CollectionOptions.empty().validator(criteria(where("customFieldName").type(8)))); + + assertThat(getValidatorInfo(COLLECTION_NAME)).isEqualTo(new Document("customName", new Document("$type", 8))); + } + + @Test // DATAMONGO-1322 + public void usesDocumentValidatorCorrectly() { + + Document rules = new Document("customFieldName", new Document("$type", "bool")); + + template.createCollection(COLLECTION_NAME, CollectionOptions.empty().validator(document(rules))); + + assertThat(getValidatorInfo(COLLECTION_NAME)) + .isEqualTo(new Document("customFieldName", new Document("$type", "bool"))); + } + + @Test // DATAMONGO-1322 + public void mapsDocumentValidatorFieldsCorrectly() { + + Document rules = new Document("customFieldName", new Document("$type", "bool")); + + template.createCollection(SimpleBean.class, CollectionOptions.empty().validator(document(rules))); + + assertThat(getValidatorInfo(COLLECTION_NAME)).isEqualTo(new Document("customName", new Document("$type", "bool"))); } private Document getCollectionOptions(String collectionName) { @@ -165,6 +195,7 @@ private String getValidationLevelInfo(String collectionName) { } private Document getCollectionInfo(String collectionName) { + return template.execute(db -> { Document result = db.runCommand( new Document().append("listCollections", 1).append("filter", new Document("name", collectionName))); @@ -174,10 +205,12 @@ private Document getCollectionInfo(String collectionName) { @Data @AllArgsConstructor - @NoArgsConstructor + @org.springframework.data.mongodb.core.mapping.Document(collection = COLLECTION_NAME) static class SimpleBean { - @NotNull private String nonNullString; - @NotNull @Min(0) @Max(122) private Integer rangedInteger; + + private @Nullable String nonNullString; + private @Nullable Integer rangedInteger; + private @Field("customName") Object customFieldName; } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java index 0ce070ae22..ff8b9ff795 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java @@ -19,6 +19,7 @@ import lombok.Data; +import java.util.Collections; import java.util.List; import org.bson.Document; @@ -33,6 +34,7 @@ import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.mongodb.test.util.MongoVersionRule; import org.springframework.data.util.Version; import org.springframework.test.context.ContextConfiguration; @@ -99,6 +101,20 @@ public void writeSchemaViaTemplate() { assertThat(fromDb).isEqualTo($jsonSchema); } + @Test // DATAMONGO-1835 + public void writeSchemaInDocumentValidatorCorrectly() { + + Document unmappedSchema = new Document("$jsonSchema", + new Document("type", "object").append("required", Collections.singletonList("firstname"))); + + Document mappedSchema = new Document("$jsonSchema", + new Document("type", "object").append("required", Collections.singletonList("first_name"))); + + template.createCollection(Person.class, CollectionOptions.empty().validator(Validator.document(unmappedSchema))); + + assertThat(readSchemaFromDatabase("persons")).isEqualTo(mappedSchema); + } + @Test // DATAMONGO-1835 public void nonMappedSchema() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/CriteriaValidatorTest.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/CriteriaValidatorUnitTests.java similarity index 84% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/CriteriaValidatorTest.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/CriteriaValidatorUnitTests.java index 1279680435..2a4c8dd11b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/CriteriaValidatorTest.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/validation/CriteriaValidatorUnitTests.java @@ -22,14 +22,18 @@ import org.springframework.data.mongodb.core.query.Criteria; /** + * Unit tests for {@link CriteriaValidator}. + * * @author Andreas Zink + * @author Christoph Strobl */ -public class CriteriaValidatorTest { +public class CriteriaValidatorUnitTests { @Test // DATAMONGO-1322 public void testSimpleCriteria() { + Criteria criteria = Criteria.where("nonNullString").ne(null).type(2).and("rangedInteger").type(16).gte(0).lte(122); - Document validator = CriteriaValidator.fromCriteria(criteria).toDocument(); + Document validator = CriteriaValidator.of(criteria).toDocument(); assertThat(validator.get("nonNullString")).isEqualTo(new Document("$ne", null).append("$type", 2)); assertThat(validator.get("rangedInteger")) @@ -38,6 +42,6 @@ public void testSimpleCriteria() { @Test(expected = IllegalArgumentException.class) // DATAMONGO-1322 public void testFailOnNull() { - CriteriaValidator.fromCriteria(null); + CriteriaValidator.of(null); } } diff --git a/src/main/asciidoc/reference/mongo-3.adoc b/src/main/asciidoc/reference/mongo-3.adoc index 281a63a602..37ac56a932 100644 --- a/src/main/asciidoc/reference/mongo-3.adoc +++ b/src/main/asciidoc/reference/mongo-3.adoc @@ -96,6 +96,23 @@ MongoDB 3.6 allows validation and querying of documents using JSON schema draft Spring Data MongoDB supports MongoDB's specific JSON schema implementation to define and use schemas. See <> for further details. +[[mongo.mongo-3.validation.query-expression]] +==== Query Expression Validation + +Next to the <> as of version 3.2 MongoDB supports validating documents against a given query structure. The structure can be built using `Criteria` objects just the same way as they are used for defining queries. + +[source,java] +---- +Criteria queryExpression = Criteria.where("lastname").ne(null).type(2) + .and("age").ne(null).type(16).gt(0).lte(150); + +Validator validator = Validator.criteria(queryExpression); + +template.createCollection(Person.class, CollectionOptions.empty().validator(validator)); +---- + +NOTE: Field names used within the query expression are mapped to the domain types property names taking potential `@Field` annotations into account. + [[mongo.mongo-3.misc]] === Other things to be aware of