DATAMONGO-1322 - Add support for Criteria-based validator for collection creation.

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.

Original pull request: #511.
Related pull request: #525.
Related ticket: DATACMNS-1835.
This commit is contained in:
Andreas Zink
2017-10-22 19:06:12 +02:00
committed by Mark Paluch
parent f2bb46724c
commit 2024b30059
10 changed files with 521 additions and 8 deletions

View File

@@ -21,6 +21,7 @@ import java.util.Optional;
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 @@ import com.mongodb.client.model.ValidationLevel;
* @author Thomas Risberg
* @author Christoph Strobl
* @author Mark Paluch
* @author Andreas Zink
*/
public class CollectionOptions {
@@ -144,7 +146,11 @@ public class CollectionOptions {
* @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 class CollectionOptions {
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 class CollectionOptions {
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 class CollectionOptions {
* 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 class CollectionOptions {
return Optional.ofNullable(schema);
}
public Optional<ValidatorDefinition> getValidatorDefinition() {
return Optional.ofNullable(validatorDefinition);
}
/**
* Get the {@code validationLevel} to apply.
*
@@ -346,7 +358,7 @@ public class CollectionOptions {
* @return {@literal true} if no arguments set.
*/
boolean isEmpty() {
return !Optionals.isAnyPresent(getSchema(), getValidationAction(), getValidationLevel());
return !Optionals.isAnyPresent(getSchema(), getValidatorDefinition(), getValidationAction(), getValidationLevel());
}
}
}

View File

@@ -140,6 +140,9 @@ 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.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 @@ import com.mongodb.util.JSONParseException;
* @author Maninder Singh
* @author Borislav Rangelov
* @author duozhilin
* @author Andreas Zink
*/
@SuppressWarnings("deprecation")
public class MongoTemplate implements MongoOperations, ApplicationContextAware, IndexOperationsProvider {
@@ -2374,6 +2378,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
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 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
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;

View File

@@ -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();
}
}

View File

@@ -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 <a href="https://docs.mongodb.com/manual/reference/method/db.createCollection/">MongoDB Collection Options</a>
*/
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;
}
}

View File

@@ -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 <a href="https://docs.mongodb.com/manual/reference/method/db.createCollection/">MongoDB Collection Options</a>
*/
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;
}
}

View File

@@ -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 <a href="https://docs.mongodb.com/manual/core/document-validation/">MongoDB Document Validation</a>
* @see <a href="https://docs.mongodb.com/manual/reference/method/db.createCollection/">MongoDB Collection Options</a>
*/
@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<ValidationLevel> getValidationLevel() {
return Optional.ofNullable(validationLevel);
}
public Optional<ValidationAction> getValidationAction() {
return Optional.ofNullable(validationAction);
}
}

View File

@@ -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 <a href="https://docs.mongodb.com/manual/reference/method/db.createCollection/">MongoDB Collection Options</a>
*/
public interface ValidatorDefinition {
/**
* @return a MongoDB {@code validator} document
*/
public @NonNull Document toDocument();
}

View File

@@ -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;
}
}

View File

@@ -28,7 +28,6 @@ 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.DataRetrievalFailureException;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.CollectionOptions;
import org.springframework.data.mongodb.core.MongoTemplate;

View File

@@ -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);
}
}