DATAMONGO-1835 - Add support for JSON Schema.

We now can create a $jsonSchema that can be used as a validator when creating collections and as predicate for queries. Required fields and properties get mapped according to the @Field annotation on domain objects.

MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname")
  .properties(string("firstname").possibleValues("luke", "han"),
              object("address").properties(string("postCode").minLength(4).maxLength(5)))
  .build();

resulting in the following schema:

{
  "type": "object",
  "required": [ "firstname", "lastname" ],
  "properties": {
    "firstname": {
      "type": "string", "enum": [ "luke", "han" ],
    },
    "address": {
      "type": "object",
      "properties": {
        "postCode": { "type": "string", "minLength": 4, "maxLength": 5 }
      }
    }
  }
}

Query usage:

MongoJsonSchema schema = MongoJsonSchema.builder()
  .required("address")
  .property(object("address").properties(string("street").matching("^Apple.*"))).build();

List<Person> person = template.find(query(matchingDocumentStructure(schema)), Person.class));

Collection validation:

MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname")
  .properties(string("firstname").possibleValues("luke", "han"),
              object("address").properties(string("postCode").minLength(4).maxLength(5)))
  .build();

template.createCollection(Person.class, CollectionOptions.empty()
  .schema(schema)
  .failOnValidationError());

Original pull request: #524.
This commit is contained in:
Christoph Strobl
2017-12-14 07:49:41 +01:00
committed by Mark Paluch
parent ddc6e4a219
commit 14ccb5152a
21 changed files with 4840 additions and 17 deletions

View File

@@ -18,9 +18,14 @@ package org.springframework.data.mongodb.core;
import java.util.Optional;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.util.Optionals;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import com.mongodb.client.model.ValidationAction;
import com.mongodb.client.model.ValidationLevel;
/**
* Provides a simple wrapper to encapsulate the variety of settings you can use when creating a collection.
*
@@ -34,6 +39,7 @@ public class CollectionOptions {
private @Nullable Long size;
private @Nullable Boolean capped;
private @Nullable Collation collation;
private Validator validator;
/**
* Constructs a new <code>CollectionOptions</code> instance.
@@ -46,16 +52,17 @@ public class CollectionOptions {
*/
@Deprecated
public CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped) {
this(size, maxDocuments, capped, null);
this(size, maxDocuments, capped, null, Validator.none());
}
private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped,
@Nullable Collation collation) {
@Nullable Collation collation, Validator validator) {
this.maxDocuments = maxDocuments;
this.size = size;
this.capped = capped;
this.collation = collation;
this.validator = validator;
}
/**
@@ -69,7 +76,7 @@ public class CollectionOptions {
Assert.notNull(collation, "Collation must not be null!");
return new CollectionOptions(null, null, null, collation);
return new CollectionOptions(null, null, null, collation, Validator.none());
}
/**
@@ -79,7 +86,7 @@ public class CollectionOptions {
* @since 2.0
*/
public static CollectionOptions empty() {
return new CollectionOptions(null, null, null, null);
return new CollectionOptions(null, null, null, null, Validator.none());
}
/**
@@ -90,7 +97,7 @@ public class CollectionOptions {
* @since 2.0
*/
public CollectionOptions capped() {
return new CollectionOptions(size, maxDocuments, true, collation);
return new CollectionOptions(size, maxDocuments, true, collation, validator);
}
/**
@@ -101,7 +108,7 @@ public class CollectionOptions {
* @since 2.0
*/
public CollectionOptions maxDocuments(long maxDocuments) {
return new CollectionOptions(size, maxDocuments, capped, collation);
return new CollectionOptions(size, maxDocuments, capped, collation, validator);
}
/**
@@ -112,7 +119,7 @@ public class CollectionOptions {
* @since 2.0
*/
public CollectionOptions size(long size) {
return new CollectionOptions(size, maxDocuments, capped, collation);
return new CollectionOptions(size, maxDocuments, capped, collation, validator);
}
/**
@@ -123,7 +130,111 @@ public class CollectionOptions {
* @since 2.0
*/
public CollectionOptions collation(@Nullable Collation collation) {
return new CollectionOptions(size, maxDocuments, capped, collation);
return new CollectionOptions(size, maxDocuments, capped, collation, validator);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validator} set to given
* {@link MongoJsonSchema}.
*
* @param schema can be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions schema(@Nullable MongoJsonSchema schema) {
return validation(new Validator(schema, validator.validationLevel, validator.validationAction));
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to {@code off}.
*
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions disableValidation() {
return schemaValidationLevel(ValidationLevel.OFF);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to {@code strict}.
*
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions strictValidation() {
return schemaValidationLevel(ValidationLevel.STRICT);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to
* {@code moderate}.
*
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions moderateValidation() {
return schemaValidationLevel(ValidationLevel.MODERATE);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationAction} set to {@code warn}.
*
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions warnOnValidationError() {
return schemaValidationAction(ValidationAction.WARN);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationAction} set to {@code error}.
*
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions failOnValidationError() {
return schemaValidationAction(ValidationAction.ERROR);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set given
* {@link ValidationLevel}.
*
* @param validationLevel must not be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions schemaValidationLevel(ValidationLevel validationLevel) {
Assert.notNull(validationLevel, "ValidationLevel must not be null!");
return validation(new Validator(validator.schema, validationLevel, validator.validationAction));
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationAction} set given
* {@link ValidationAction}.
*
* @param validationAction must not be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions schemaValidationAction(ValidationAction validationAction) {
Assert.notNull(validationAction, "ValidationAction must not be null!");
return validation(new Validator(validator.schema, validator.validationLevel, validationAction));
}
/**
* Create new {@link CollectionOptions} with the given {@link Validator}.
*
* @param validator must not be {@literal null}. Use {@link Validator#none()} to remove validation.
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions validation(Validator validator) {
Assert.notNull(validator, "Validator must not be null!");
return new CollectionOptions(size, maxDocuments, capped, collation, validator);
}
/**
@@ -163,4 +274,83 @@ public class CollectionOptions {
public Optional<Collation> getCollation() {
return Optional.ofNullable(collation);
}
/**
* Get the {@link MongoJsonSchema} for the collection.
*
* @return {@link Optional#empty()} if not set.
* @since 2.1
*/
public Optional<Validator> getValidator() {
return validator.isEmpty() ? Optional.empty() : Optional.of(validator);
}
/**
* Encapsulation of Validator options.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class Validator {
private static final Validator NONE = new Validator(null, null, null);
private @Nullable MongoJsonSchema schema;
private @Nullable ValidationLevel validationLevel;
private @Nullable ValidationAction validationAction;
private Validator(@Nullable MongoJsonSchema schema, @Nullable ValidationLevel validationLevel,
@Nullable ValidationAction validationAction) {
this.schema = schema;
this.validationLevel = validationLevel;
this.validationAction = validationAction;
}
/**
* Create an empty {@link Validator}.
*
* @return never {@literal null}.
*/
public static Validator none() {
return NONE;
}
/**
* Get the {@code $jsonSchema} used for validation.
*
* @return {@link Optional#empty()} if not set.
*/
@Nullable
public Optional<MongoJsonSchema> getSchema() {
return Optional.ofNullable(schema);
}
/**
* Get the {@code validationLevel} to apply.
*
* @return {@link Optional#empty()} if not set.
*/
@Nullable
public Optional<ValidationLevel> getValidationLevel() {
return Optional.ofNullable(validationLevel);
}
/**
* Get the {@code validationAction} to perform.
*
* @return @return {@link Optional#empty()} if not set.
*/
@Nullable
public Optional<ValidationAction> getValidationAction() {
return Optional.ofNullable(validationAction);
}
/**
* @return {@literal true} if no arguments set.
*/
boolean isEmpty() {
return !Optionals.isAnyPresent(getSchema(), getValidationAction(), getValidationLevel());
}
}
}

View File

@@ -63,6 +63,7 @@ import org.springframework.data.mapping.context.MappingContext;
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.DefaultBulkOperations.BulkOperationContext;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
@@ -73,9 +74,11 @@ import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOpe
import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.DbRefResolver;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.JsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
import org.springframework.data.mongodb.core.convert.MongoWriter;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.convert.UpdateMapper;
@@ -133,14 +136,7 @@ import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.MongoIterable;
import com.mongodb.client.model.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.*;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
import com.mongodb.util.JSONParseException;
@@ -191,6 +187,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
private final PersistenceExceptionTranslator exceptionTranslator;
private final QueryMapper queryMapper;
private final UpdateMapper updateMapper;
private final JsonSchemaMapper schemaMapper;
private final SpelAwareProxyProjectionFactory projectionFactory;
private @Nullable WriteConcern writeConcern;
@@ -235,6 +232,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter(mongoDbFactory) : mongoConverter;
this.queryMapper = new QueryMapper(this.mongoConverter);
this.updateMapper = new UpdateMapper(this.mongoConverter);
this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter);
this.projectionFactory = new SpelAwareProxyProjectionFactory();
// We always have a mapping context in the converter, whether it's a simple one or not
@@ -545,7 +543,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*/
public <T> MongoCollection<Document> createCollection(Class<T> entityClass,
@Nullable CollectionOptions collectionOptions) {
return createCollection(determineCollectionName(entityClass), collectionOptions);
Assert.notNull(entityClass, "EntityClass must not be null!");
return doCreateCollection(determineCollectionName(entityClass), convertToDocument(collectionOptions, entityClass));
}
/*
@@ -2227,6 +2227,21 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
co.collation(IndexConverters.fromDocument(collectionOptions.get("collation", Document.class)));
}
if (collectionOptions.containsKey("validator")) {
ValidationOptions options = new ValidationOptions();
if (collectionOptions.containsKey("validationLevel")) {
options.validationLevel(ValidationLevel.fromString(collectionOptions.getString("validationLevel")));
}
if (collectionOptions.containsKey("validationAction")) {
options.validationAction(ValidationAction.fromString(collectionOptions.getString("validationAction")));
}
options.validator(collectionOptions.get("validator", Document.class));
co.validationOptions(options);
}
db.createCollection(collectionName, co);
MongoCollection<Document> coll = db.getCollection(collectionName, Document.class);
@@ -2339,6 +2354,35 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
new ProjectingReadCallback<>(mongoConverter, sourceClass, targetClass, collectionName), collectionName);
}
/**
* Convert given {@link CollectionOptions} to a document and take the domain type information into account when
* creating a mapped schema for validation. <br />
* This method calls {@link #convertToDocument(CollectionOptions)} for backwards compatibility and potentially
* overwrites the validator with the mapped validator document. In the long run
* {@link #convertToDocument(CollectionOptions)} will be removed so that this one becomes the only source of truth.
*
* @param collectionOptions can be {@literal null}.
* @param targetType must not be {@literal null}. Use {@link Object} type instead.
* @return never {@literal null}.
* @since 2.1
*/
protected Document convertToDocument(@Nullable CollectionOptions collectionOptions, Class<?> targetType) {
Document doc = convertToDocument(collectionOptions);
if (collectionOptions.getValidator().isPresent()) {
Validator v = collectionOptions.getValidator().get();
v.getSchema().ifPresent(val -> doc.put("validator", schemaMapper.mapSchema(val.toDocument(), targetType)));
}
return doc;
}
/**
* @param collectionOptions can be {@literal null}.
* @return never {@literal null}.
* @deprecated since 2.1 in favor of {@link #convertToDocument(CollectionOptions, Class)}.
*/
@Deprecated
protected Document convertToDocument(@Nullable CollectionOptions collectionOptions) {
Document document = new Document();
@@ -2348,6 +2392,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
collectionOptions.getSize().ifPresent(val -> document.put("size", val));
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();
v.getValidationLevel().ifPresent(val -> document.append("validationLevel", val));
v.getValidationAction().ifPresent(val -> document.append("validationAction", val));
v.getSchema().ifPresent(val -> document.append("validator",
new MongoJsonSchemaMapper(getConverter()).mapSchema(val.toDocument(), Object.class)));
}
}
return document;
}

View File

@@ -0,0 +1,41 @@
/*
* 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.convert;
import org.bson.Document;
/**
* {@link JsonSchemaMapper} allows mapping a given {@link Document} containing a {@literal $jsonSchema} to the fields of
* a given domain type. The mapping considers {@link org.springframework.data.mongodb.core.mapping.Field} annotations
* and other Spring Data specifics.
*
* @author Christoph Strobl
* @since 2.1
*/
public interface JsonSchemaMapper {
/**
* Map the {@literal required} and {@literal properties} fields the given {@link Document} containing the
* {@literal $jsonSchema} against the given domain type. <br />
* The source document remains untouched, fields that do not require mapping are simply copied over to the mapped
* instance.
*
* @param jsonSchema the {@link Document} holding the raw schema representation. Must not be {@literal null}.
* @param type the target type to map against. Must not be {@literal null}.
* @return a <strong>new</strong> {@link Document} containing the mapped {@literal $jsonSchema} never {@literal null}.
*/
Document mapSchema(Document jsonSchema, Class<?> type);
}

View File

@@ -0,0 +1,162 @@
/*
* 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.convert;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.bson.Document;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* {@link JsonSchemaMapper} implementation using the conversion and mapping infrastructure for mapping fields to the
* provided domain type.
*
* @author Christoph Strobl
* @since 2.1
*/
public class MongoJsonSchemaMapper implements JsonSchemaMapper {
private static final String $JSON_SCHEMA = "$jsonSchema";
private static final String REQUIRED_FIELD = "required";
private static final String PROPERTIES_FIELD = "properties";
private static final String ENUM_FIELD = "enum";
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final MongoConverter converter;
/**
* Create a new {@link MongoJsonSchemaMapper} facilitating the given {@link MongoConverter}.
*
* @param converter must not be {@literal null}.
*/
public MongoJsonSchemaMapper(MongoConverter converter) {
Assert.notNull(converter, "Converter must not be null!");
this.converter = converter;
this.mappingContext = converter.getMappingContext();
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.convert.JsonSchemaMapper#mapSchema(org.springframework.data.mongodb.core.schema.MongoJsonSchema, java.lang.Class)
*/
public Document mapSchema(Document jsonSchema, Class<?> type) {
Assert.notNull(jsonSchema, "Schema must not be null!");
Assert.notNull(type, "Type must not be null! Please consider Object.class.");
Assert.isTrue(jsonSchema.containsKey($JSON_SCHEMA),
() -> String.format("Document does not contain $jsonSchema field. Found %s.", jsonSchema));
if (Object.class.equals(type)) {
return new Document(jsonSchema);
}
return new Document($JSON_SCHEMA,
mapSchemaObject(mappingContext.getPersistentEntity(type), jsonSchema.get($JSON_SCHEMA, Document.class)));
}
private Document mapSchemaObject(@Nullable PersistentEntity entity, Document source) {
Document sink = new Document(source);
if (source.containsKey(REQUIRED_FIELD)) {
sink.replace(REQUIRED_FIELD, mapRequiredProperties(entity, source.get(REQUIRED_FIELD, Collection.class)));
}
if (source.containsKey(PROPERTIES_FIELD)) {
sink.replace(PROPERTIES_FIELD, mapProperties(entity, source.get(PROPERTIES_FIELD, Document.class)));
}
mapEnumValuesIfNecessary(sink);
return sink;
}
private Document mapProperties(@Nullable PersistentEntity<?, MongoPersistentProperty> entity, Document source) {
Document sink = new Document();
for (String fieldName : source.keySet()) {
String mappedFieldName = getFieldName(entity, fieldName);
Document mappedProperty = mapProperty(entity, fieldName, source.get(fieldName, Document.class));
sink.append(mappedFieldName, mappedProperty);
}
return sink;
}
private List<String> mapRequiredProperties(@Nullable PersistentEntity<?, MongoPersistentProperty> entity,
Collection<String> sourceFields) {
return sourceFields.stream() ///
.map(fieldName -> getFieldName(entity, fieldName)) //
.collect(Collectors.toList());
}
private Document mapProperty(@Nullable PersistentEntity<?, MongoPersistentProperty> entity, String sourceFieldName,
Document source) {
Document sink = new Document(source);
if (entity != null && sink.containsKey(Type.objectType().representation())) {
MongoPersistentProperty property = entity.getPersistentProperty(sourceFieldName);
if (property != null && property.isEntity()) {
sink = mapSchemaObject(mappingContext.getPersistentEntity(property.getActualType()), source);
}
}
return mapEnumValuesIfNecessary(sink);
}
private Document mapEnumValuesIfNecessary(Document source) {
Document sink = new Document(source);
if (source.containsKey(ENUM_FIELD)) {
sink.replace(ENUM_FIELD, mapEnumValues(source.get(ENUM_FIELD, Iterable.class)));
}
return sink;
}
private List<Object> mapEnumValues(Iterable<?> values) {
List<Object> converted = new ArrayList<>();
for (Object val : values) {
converted.add(converter.convertToMongoType(val));
}
return converted;
}
private String getFieldName(@Nullable PersistentEntity<?, MongoPersistentProperty> entity, String sourceField) {
if (entity == null) {
return sourceField;
}
MongoPersistentProperty property = entity.getPersistentProperty(sourceField);
return property != null ? property.getFieldName() : sourceField;
}
}

View File

@@ -80,6 +80,7 @@ public class QueryMapper {
private final MongoConverter converter;
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
private final MongoExampleMapper exampleMapper;
private final MongoJsonSchemaMapper schemaMapper;
/**
* Creates a new {@link QueryMapper} with the given {@link MongoConverter}.
@@ -94,6 +95,7 @@ public class QueryMapper {
this.converter = converter;
this.mappingContext = converter.getMappingContext();
this.exampleMapper = new MongoExampleMapper(converter);
this.schemaMapper = new MongoJsonSchemaMapper(converter);
}
public Document getMappedObject(Bson query, Optional<? extends MongoPersistentEntity<?>> entity) {
@@ -272,6 +274,10 @@ public class QueryMapper {
return exampleMapper.getMappedExample(keyword.<Example<?>> getValue(), entity);
}
if (keyword.isJsonSchema()) {
return schemaMapper.mapSchema(new Document(keyword.getKey(), keyword.getValue()), entity.getType());
}
return new Document(keyword.getKey(), convertSimpleOrDocument(keyword.getValue(), entity));
}
@@ -599,6 +605,7 @@ public class QueryMapper {
* Value object to capture a query keyword representation.
*
* @author Oliver Gierke
* @author Christoph Strobl
*/
static class Keyword {
@@ -666,6 +673,16 @@ public class QueryMapper {
public <T> T getValue() {
return (T) value;
}
/**
* Returns whether the current keyword indicates a {@literal $jsonSchema} object.
*
* @return {@literal true} if {@code key} equals {@literal $jsonSchema}.
* @since 2.1
*/
public boolean isJsonSchema() {
return "$jsonSchema".equalsIgnoreCase(key);
}
}
/**

View File

@@ -24,6 +24,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.bson.BSON;
import org.bson.BsonRegularExpression;
@@ -35,6 +36,9 @@ import org.springframework.data.geo.Shape;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
import org.springframework.data.mongodb.core.geo.GeoJson;
import org.springframework.data.mongodb.core.geo.Sphere;
import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type;
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -115,6 +119,20 @@ public class Criteria implements CriteriaDefinition {
return new Criteria().alike(example);
}
/**
* Static factory method to create a {@link Criteria} matching documents against a given structure defined by the
* {@link MongoJsonSchema} using ({@code $jsonSchema}) operator.
*
* @param schema must not be {@literal null}.
* @return this
* @since 2.1
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/">MongoDB Query operator:
* $jsonSchema</a>
*/
public static Criteria matchingDocumentStructure(MongoJsonSchema schema) {
return new Criteria().andDocumentStructureMatches(schema);
}
/**
* Static factory method to create a Criteria using the provided key
*
@@ -335,6 +353,23 @@ public class Criteria implements CriteriaDefinition {
return this;
}
/**
* Creates a criterion using the {@literal $type} operator.
*
* @param type must not be {@literal null}.
* @return this
* @since 2.1
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/type/">MongoDB Query operator: $type</a>
*/
public Criteria type(Type... types) {
Assert.notNull(types, "Types must not be null!");
Assert.noNullElements(types, "Types must not contain null.");
criteria.put("$type", Arrays.asList(types).stream().map(Type::value).collect(Collectors.toList()));
return this;
}
/**
* Creates a criterion using the {@literal $not} meta operator which affects the clause directly following
*
@@ -563,6 +598,30 @@ public class Criteria implements CriteriaDefinition {
return this;
}
/**
* Creates a criterion ({@code $jsonSchema}) matching documents against a given structure defined by the
* {@link MongoJsonSchema}. <br />
* <strong>NOTE:</strong> {@code $jsonSchema} cannot be used on field/property level but defines the whole document
* structure. Please use
* {@link org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder#properties(JsonSchemaProperty...)}
* to specify nested fields or query them using the {@link #type(Type...) $type} operator.
*
* @param schema must not be {@literal null}.
* @return this
* @since 2.1
* @see <a href="https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/">MongoDB Query operator:
* $jsonSchema</a>
*/
public Criteria andDocumentStructureMatches(MongoJsonSchema schema) {
Assert.notNull(schema, "Schema must not be null!");
Criteria schemaCriteria = new Criteria();
schemaCriteria.criteria.putAll(schema.toDocument());
return registerCriteriaChainElement(schemaCriteria);
}
/**
* Creates an 'or' criteria using the $or operator for all of the provided criteria
* <p>

View File

@@ -0,0 +1,882 @@
/*
* 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.schema;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import org.bson.Document;
import org.springframework.data.domain.Range;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NullJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject;
import org.springframework.util.Assert;
/**
* {@link JsonSchemaProperty} implementation.
*
* @author Christoph Strobl
* @since 2.1
*/
public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implements JsonSchemaProperty {
protected String identifier;
protected T jsonSchemaObjectDelegate;
public IdentifiableJsonSchemaProperty(String identifier, T jsonSchemaObject) {
Assert.notNull(identifier, "Identifier must not be null!");
Assert.notNull(jsonSchemaObject, "JsonSchemaObject must not be null!");
this.identifier = identifier;
this.jsonSchemaObjectDelegate = jsonSchemaObject;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaProperty#getIdentifier()
*/
@Override
public String getIdentifier() {
return identifier;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument()
*/
@Override
public Document toDocument() {
return new Document(identifier, jsonSchemaObjectDelegate.toDocument());
}
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#getTypes()
*/
@Override
public Set<Type> getTypes() {
return jsonSchemaObjectDelegate.getTypes();
}
/**
* Convenience {@link JsonSchemaProperty} implementation without a {@code type} property.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class UntypedJsonSchemaProperty extends IdentifiableJsonSchemaProperty<UntypedJsonSchemaObject> {
public UntypedJsonSchemaProperty(String identifier, UntypedJsonSchemaObject jsonSchemaObject) {
super(identifier, jsonSchemaObject);
}
/**
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#possibleValues(Collection)
*/
public UntypedJsonSchemaProperty possibleValues(Object... possibleValues) {
return possibleValues(Arrays.asList(possibleValues));
}
/**
* @param allOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#allOf(Collection)
*/
public UntypedJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
}
/**
* @param anyOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#anyOf(Collection)
*/
public UntypedJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
}
/**
* @param oneOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#oneOf(Collection)
*/
public UntypedJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
}
/**
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#possibleValues(Collection)
*/
public UntypedJsonSchemaProperty possibleValues(Collection<Object> possibleValues) {
return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
}
/**
* @param allOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#allOf(Collection)
*/
public UntypedJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
}
/**
* @param anyOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#anyOf(Collection)
*/
public UntypedJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
}
/**
* @param oneOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#oneOf(Collection)
*/
public UntypedJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
}
/**
* @param notMatch must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#notMatch(JsonSchemaObject)
*/
public UntypedJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
}
/**
* @param description must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#description(String)
*/
public UntypedJsonSchemaProperty description(String description) {
return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
}
/**
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#generateDescription()
*/
public UntypedJsonSchemaProperty generatedDescription() {
return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
}
}
/**
* Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'string'} property.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class StringJsonSchemaProperty extends IdentifiableJsonSchemaProperty<StringJsonSchemaObject> {
/**
* @param identifier identifier the {@literal property} name or {@literal patternProperty} regex. Must not be
* {@literal null} nor {@literal empty}.
* @param schemaObject must not be {@literal null}.
*/
public StringJsonSchemaProperty(String identifier, StringJsonSchemaObject schemaObject) {
super(identifier, schemaObject);
}
/**
* @param length
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#minLength(int)
*/
public StringJsonSchemaProperty minLength(int length) {
return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minLength(length));
}
/**
* @param length
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#maxLength(int)
*/
public StringJsonSchemaProperty maxLength(int length) {
return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxLength(length));
}
/**
* @param pattern must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#matching(String)
*/
public StringJsonSchemaProperty matching(String pattern) {
return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.matching(pattern));
}
/**
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#possibleValues(Collection)
*/
public StringJsonSchemaProperty possibleValues(String... possibleValues) {
return possibleValues(Arrays.asList(possibleValues));
}
/**
* @param allOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#allOf(Collection)
*/
public StringJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
}
/**
* @param anyOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#anyOf(Collection)
*/
public StringJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
}
/**
* @param oneOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#oneOf(Collection)
*/
public StringJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
}
/**
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#possibleValues(Collection)
*/
public StringJsonSchemaProperty possibleValues(Collection<String> possibleValues) {
return new StringJsonSchemaProperty(identifier,
jsonSchemaObjectDelegate.possibleValues((Collection) possibleValues));
}
/**
* @param allOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#allOf(Collection)
*/
public StringJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
}
/**
* @param anyOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#anyOf(Collection)
*/
public StringJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
}
/**
* @param oneOf must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#oneOf(Collection)
*/
public StringJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
}
/**
* @param notMatch must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#notMatch(JsonSchemaObject)
*/
public StringJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
}
/**
* @param description must not be {@literal null}.
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#description(String)
*/
public StringJsonSchemaProperty description(String description) {
return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
}
/**
* @return new instance of {@link StringJsonSchemaProperty}.
* @see StringJsonSchemaObject#generateDescription()
*/
public StringJsonSchemaProperty generatedDescription() {
return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
}
}
/**
* Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'object'} property.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class ObjectJsonSchemaProperty extends IdentifiableJsonSchemaProperty<ObjectJsonSchemaObject> {
/**
* @param identifier identifier the {@literal property} name or {@literal patternProperty} regex. Must not be
* {@literal null} nor {@literal empty}.
* @param schemaObject must not be {@literal null}.
*/
public ObjectJsonSchemaProperty(String identifier, ObjectJsonSchemaObject schemaObject) {
super(identifier, schemaObject);
}
/**
* @param range must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#nrProperties
*/
public ObjectJsonSchemaProperty nrProperties(Range<Integer> range) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.nrProperties(range));
}
/**
* @param nrProperties must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#minNrProperties(int)
*/
public ObjectJsonSchemaProperty minNrProperties(int nrProperties) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minNrProperties(nrProperties));
}
/**
* @param nrProperties must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#maxNrProperties(int)
*/
public ObjectJsonSchemaProperty maxNrProperties(int nrProperties) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxNrProperties(nrProperties));
}
/**
* @param properties must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#required(String...)
*/
public ObjectJsonSchemaProperty required(String... properties) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.required(properties));
}
/**
* @param additionalPropertiesAllowed
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#additionalProperties(boolean)
*/
public ObjectJsonSchemaProperty additionalProperties(boolean additionalPropertiesAllowed) {
return new ObjectJsonSchemaProperty(identifier,
jsonSchemaObjectDelegate.additionalProperties(additionalPropertiesAllowed));
}
/**
* @param additionalProperties must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject)
*/
public ObjectJsonSchemaProperty additionalProperties(ObjectJsonSchemaObject additionalProperties) {
return new ObjectJsonSchemaProperty(identifier,
jsonSchemaObjectDelegate.additionalProperties(additionalProperties));
}
/**
* @param properties must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...)
*/
public ObjectJsonSchemaProperty properties(JsonSchemaProperty... properties) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.properties(properties));
}
/**
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#possibleValues(Collection)
*/
public ObjectJsonSchemaProperty possibleValues(Object... possibleValues) {
return possibleValues(Arrays.asList(possibleValues));
}
/**
* @param allOf must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#allOf(Collection)
*/
public ObjectJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
}
/**
* @param anyOf must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#anyOf(Collection)
*/
public ObjectJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
}
/**
* @param oneOf must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#oneOf(Collection)
*/
public ObjectJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
}
/**
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#possibleValues(Collection)
*/
public ObjectJsonSchemaProperty possibleValues(Collection<Object> possibleValues) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
}
/**
* @param allOf must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#allOf(Collection)
*/
public ObjectJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
}
/**
* @param anyOf must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#anyOf(Collection)
*/
public ObjectJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
}
/**
* @param oneOf must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#oneOf(Collection)
*/
public ObjectJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
}
/**
* @param notMatch must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#notMatch(JsonSchemaObject)
*/
public ObjectJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
}
/**
* @param description must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#description(String)
*/
public ObjectJsonSchemaProperty description(String description) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
}
/**
* @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#generateDescription()
*/
public ObjectJsonSchemaProperty generatedDescription() {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
}
}
/**
* Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'number'} property.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class NumericJsonSchemaProperty extends IdentifiableJsonSchemaProperty<NumericJsonSchemaObject> {
/**
* @param identifier identifier the {@literal property} name or {@literal patternProperty} regex. Must not be
* {@literal null} nor {@literal empty}.
* @param schemaObject must not be {@literal null}.
*/
public NumericJsonSchemaProperty(String identifier, NumericJsonSchemaObject schemaObject) {
super(identifier, schemaObject);
}
/**
* @param value must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#multipleOf
*/
public NumericJsonSchemaProperty multipleOf(Number value) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.multipleOf(value));
}
/**
* @param range must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#within(Range)
*/
public NumericJsonSchemaProperty within(Range<? extends Number> range) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.within(range));
}
/**
* @param min must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#gt(Number)
*/
public NumericJsonSchemaProperty gt(Number min) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.gt(min));
}
/**
* @param min must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#gte(Number)
*/
public NumericJsonSchemaProperty gte(Number min) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.gte(min));
}
/**
* @param max must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#lt(Number)
*/
public NumericJsonSchemaProperty lt(Number max) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.lt(max));
}
/**
* @param max must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#lte(Number)
*/
public NumericJsonSchemaProperty lte(Number max) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.lte(max));
}
/**
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#possibleValues(Collection)
*/
public NumericJsonSchemaProperty possibleValues(Number... possibleValues) {
return possibleValues(new LinkedHashSet<>(Arrays.asList(possibleValues)));
}
/**
* @param allOf must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#allOf(Collection)
*/
public NumericJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
return allOf(Arrays.asList(allOf));
}
/**
* @param anyOf must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#anyOf(Collection)
*/
public NumericJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
}
/**
* @param oneOf must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#oneOf(Collection)
*/
public NumericJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
}
/**
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#possibleValues(Collection)
*/
public NumericJsonSchemaProperty possibleValues(Collection<Number> possibleValues) {
return new NumericJsonSchemaProperty(identifier,
jsonSchemaObjectDelegate.possibleValues((Collection) possibleValues));
}
/**
* @param allOf must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#allOf(Collection)
*/
public NumericJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
}
/**
* @param anyOf must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#anyOf(Collection)
*/
public NumericJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
}
/**
* @param oneOf must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#oneOf(Collection)
*/
public NumericJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
}
/**
* @param notMatch must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#notMatch(JsonSchemaObject)
*/
public NumericJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
}
/**
* @param description must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#description(String)
*/
public NumericJsonSchemaProperty description(String description) {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
}
/**
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NumericJsonSchemaObject#generateDescription()
*/
public NumericJsonSchemaProperty generatedDescription() {
return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
}
}
/**
* Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'array'} property.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class ArrayJsonSchemaProperty extends IdentifiableJsonSchemaProperty<ArrayJsonSchemaObject> {
/**
* @param identifier identifier the {@literal property} name or {@literal patternProperty} regex. Must not be
* {@literal null} nor {@literal empty}.
* @param schemaObject must not be {@literal null}.
*/
public ArrayJsonSchemaProperty(String identifier, ArrayJsonSchemaObject schemaObject) {
super(identifier, schemaObject);
}
/**
* @param uniqueItems
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#uniqueItems(boolean)
*/
public ArrayJsonSchemaProperty uniqueItems(boolean uniqueItems) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.uniqueItems(uniqueItems));
}
/**
* @param range must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#range(Range)
*/
public ArrayJsonSchemaProperty range(Range<Integer> range) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.range(range));
}
/**
* @param items must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#items(Collection)
*/
public ArrayJsonSchemaProperty items(Collection<JsonSchemaObject> items) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(items));
}
/**
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#possibleValues(Collection)
*/
public ArrayJsonSchemaProperty possibleValues(Object... possibleValues) {
return possibleValues(new LinkedHashSet<>(Arrays.asList(possibleValues)));
}
/**
* @param allOf must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#allOf(Collection)
*/
public ArrayJsonSchemaProperty allOf(JsonSchemaObject... allOf) {
return allOf(new LinkedHashSet<>(Arrays.asList(allOf)));
}
/**
* @param anyOf must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#anyOf(Collection)
*/
public ArrayJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) {
return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf)));
}
/**
* @param oneOf must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#oneOf(Collection)
*/
public ArrayJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) {
return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf)));
}
/**
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#possibleValues(Collection)
*/
public ArrayJsonSchemaProperty possibleValues(Collection<Object> possibleValues) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
}
/**
* @param allOf must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#allOf(Collection)
*/
public ArrayJsonSchemaProperty allOf(Collection<JsonSchemaObject> allOf) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf));
}
/**
* @param anyOf must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#anyOf(Collection)
*/
public ArrayJsonSchemaProperty anyOf(Collection<JsonSchemaObject> anyOf) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf));
}
/**
* @param oneOf must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#oneOf(Collection)
*/
public ArrayJsonSchemaProperty oneOf(Collection<JsonSchemaObject> oneOf) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf));
}
/**
* @param notMatch must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#notMatch(JsonSchemaObject)
*/
public ArrayJsonSchemaProperty notMatch(JsonSchemaObject notMatch) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch));
}
/**
* @param description must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see ArrayJsonSchemaObjecty#description(String)
*/
public ArrayJsonSchemaProperty description(String description) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
}
/**
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#generateDescription()
*/
public ArrayJsonSchemaProperty generatedDescription() {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
}
}
/**
* Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'boolean'} property.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class BooleanJsonSchemaProperty extends IdentifiableJsonSchemaProperty<BooleanJsonSchemaObject> {
public BooleanJsonSchemaProperty(String identifier, BooleanJsonSchemaObject schemaObject) {
super(identifier, schemaObject);
}
/**
* @param description must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see BooleanJsonSchemaObject#description(String)
*/
public BooleanJsonSchemaProperty description(String description) {
return new BooleanJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
}
/**
* @return new instance of {@link BooleanJsonSchemaProperty}.
* @see BooleanJsonSchemaObject#generateDescription()
*/
public BooleanJsonSchemaProperty generatedDescription() {
return new BooleanJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription());
}
}
/**
* Convenience {@link JsonSchemaProperty} implementation for a {@code type : 'null'} property.
*
* @author Christoph Strobl
* @since 2.1
*/
public static class NullJsonSchemaProperty extends IdentifiableJsonSchemaProperty<NullJsonSchemaObject> {
public NullJsonSchemaProperty(String identifier, NullJsonSchemaObject schemaObject) {
super(identifier, schemaObject);
}
/**
* @param description must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}.
* @see NullJsonSchemaObject#description(String)
*/
public NullJsonSchemaProperty description(String description) {
return new NullJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
}
}
}

View File

@@ -0,0 +1,427 @@
/*
* 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.schema;
import lombok.RequiredArgsConstructor;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import org.bson.Document;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NullJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/**
* @author Christoph Strobl
* @since 2.1
*/
public interface JsonSchemaObject {
/**
* Get the set of types defined for this schema element.<br />
* The {@link Set} is likely to contain only one element in most cases.
*
* @return never {@literal null}.
*/
Set<Type> getTypes();
/**
* Get the MongoDB specific representation.<br />
* The Document may contain fields (eg. like {@literal bsonType}) not contained in the JsonSchema specification. It
* may also contain types not directly processable by the MongoDB java driver. Make sure to run the produced
* {@link Document} through the mapping infrastructure.
*
* @return never {@literal null}.
*/
Document toDocument();
/**
* Create a new {@link JsonSchemaObject} of {@code type : 'object'}.
*
* @return never {@literal null}.
*/
static ObjectJsonSchemaObject object() {
return new ObjectJsonSchemaObject();
}
/**
* Create a new {@link JsonSchemaObject} of {@code type : 'string'}.
*
* @return never {@literal null}.
*/
static StringJsonSchemaObject string() {
return new StringJsonSchemaObject();
}
/**
* Create a new {@link JsonSchemaObject} of {@code type : 'number'}.
*
* @return never {@literal null}.
*/
static NumericJsonSchemaObject number() {
return new NumericJsonSchemaObject();
}
/**
* Create a new {@link JsonSchemaObject} of {@code type : 'array'}.
*
* @return never {@literal null}.
*/
static ArrayJsonSchemaObject array() {
return new ArrayJsonSchemaObject();
}
/**
* Create a new {@link JsonSchemaObject} of {@code type : 'boolean'}.
*
* @return never {@literal null}.
*/
static BooleanJsonSchemaObject bool() {
return new BooleanJsonSchemaObject();
}
/**
* Create a new {@link JsonSchemaObject} of {@code type : 'null'}.
*
* @return never {@literal null}.
*/
static NullJsonSchemaObject nil() {
return new NullJsonSchemaObject();
}
/**
* Create a new {@link JsonSchemaObject} of given {@link Type}.
*
* @return never {@literal null}.
*/
static TypedJsonSchemaObject of(Type type) {
return TypedJsonSchemaObject.of(type);
}
/**
* Create a new {@link UntypedJsonSchemaObject}.
*
* @return never {@literal null}.
*/
static UntypedJsonSchemaObject untyped() {
return new UntypedJsonSchemaObject(null, null, false);
}
/**
* Create a new {@link JsonSchemaObject} matching the given {@code type}.
*
* @return never {@literal null}.
*/
static TypedJsonSchemaObject of(@Nullable Class<?> type) {
if (type == null) {
return of(Type.nullType());
}
if (type.isArray()) {
if (type.equals(byte[].class)) {
return of(Type.binaryType());
}
return of(Type.arrayType());
}
if (type.equals(Object.class)) {
return of(Type.objectType());
}
if (type.equals(ObjectId.class)) {
return of(Type.objectIdType());
}
if (ClassUtils.isAssignable(String.class, type)) {
return of(Type.stringType());
}
if (ClassUtils.isAssignable(Date.class, type)) {
return of(Type.dateType());
}
if (ClassUtils.isAssignable(Pattern.class, type)) {
return of(Type.regexType());
}
if (ClassUtils.isAssignable(Boolean.class, type)) {
return of(Type.booleanType());
}
if (ClassUtils.isAssignable(Number.class, type)) {
if (type.equals(Long.class)) {
return of(Type.longType());
}
if (type.equals(Float.class)) {
return of(Type.doubleType());
}
if (type.equals(Double.class)) {
return of(Type.doubleType());
}
if (type.equals(Integer.class)) {
return of(Type.intType());
}
if (type.equals(BigDecimal.class)) {
return of(Type.bigDecimalType());
}
return of(Type.numberType());
}
throw new IllegalArgumentException(String.format("No json schema type found for %s.", type));
}
/**
* Type represents either a json schema {@literal type} or a MongoDB specific {@literal bsonType}.
*
* @author Christoph Strobl
* @since 2.1
*/
interface Type {
// BSON TYPES
final Type OBJECT_ID = bsonTypeOf("objectId");
final Type REGULAR_EXPRESSION = bsonTypeOf("regex");
final Type DOUBLE = bsonTypeOf("double");
final Type BINARY_DATA = bsonTypeOf("binData");
final Type DATE = bsonTypeOf("date");
final Type JAVA_SCRIPT = bsonTypeOf("javascript");
final Type INT_32 = bsonTypeOf("int");
final Type INT_64 = bsonTypeOf("long");
final Type DECIMAL_128 = bsonTypeOf("decimal");
final Type TIMESTAMP = bsonTypeOf("timestamp");
final Set<Type> BSON_TYPES = new HashSet<>(Arrays.asList(OBJECT_ID, REGULAR_EXPRESSION, DOUBLE, BINARY_DATA, DATE,
JAVA_SCRIPT, INT_32, INT_64, DECIMAL_128, TIMESTAMP));
// JSON SCHEMA TYPES
final Type OBJECT = jsonTypeOf("object");
final Type ARRAY = jsonTypeOf("array");
final Type NUMBER = jsonTypeOf("number");
final Type BOOLEAN = jsonTypeOf("boolean");
final Type STRING = jsonTypeOf("string");
final Type NULL = jsonTypeOf("null");
final Set<Type> JSON_TYPES = new HashSet<>(Arrays.asList(OBJECT, ARRAY, NUMBER, BOOLEAN, STRING, NULL));
/**
* @return a constant {@link Type} representing {@code bsonType : 'objectId' }.
*/
static Type objectIdType() {
return OBJECT_ID;
}
/**
* @return a constant {@link Type} representing {@code bsonType : 'regex' }.
*/
static Type regexType() {
return REGULAR_EXPRESSION;
}
/**
* @return a constant {@link Type} representing {@code bsonType : 'double' }.
*/
static Type doubleType() {
return DOUBLE;
}
/**
* @return a constant {@link Type} representing {@code bsonType : 'binData' }.
*/
static Type binaryType() {
return BINARY_DATA;
}
/**
* @return a constant {@link Type} representing {@code bsonType : 'date' }.
*/
static Type dateType() {
return DATE;
}
/**
* @return a constant {@link Type} representing {@code bsonType : 'javascript' }.
*/
static Type javascriptType() {
return JAVA_SCRIPT;
}
/**
* @return a constant {@link Type} representing {@code bsonType : 'int' }.
*/
static Type intType() {
return INT_32;
}
/**
* @return a constant {@link Type} representing {@code bsonType : 'long' }.
*/
static Type longType() {
return INT_64;
}
/**
* @return a constant {@link Type} representing {@code bsonType : 'decimal128' }.
*/
static Type bigDecimalType() {
return DECIMAL_128;
}
/**
* @return a constant {@link Type} representing {@code bsonType : 'timestamp' }.
*/
static Type timestampType() {
return TIMESTAMP;
}
/**
* @return a constant {@link Type} representing {@code type : 'object' }.
*/
static Type objectType() {
return OBJECT;
}
/**
* @return a constant {@link Type} representing {@code type : 'array' }.
*/
static Type arrayType() {
return ARRAY;
}
/**
* @return a constant {@link Type} representing {@code type : 'number' }.
*/
static Type numberType() {
return NUMBER;
}
/**
* @return a constant {@link Type} representing {@code type : 'boolean' }.
*/
static Type booleanType() {
return BOOLEAN;
}
/**
* @return a constant {@link Type} representing {@code type : 'string' }.
*/
static Type stringType() {
return STRING;
}
/**
* @return a constant {@link Type} representing {@code type : 'null' }.
*/
static Type nullType() {
return NULL;
}
/**
* @return new {@link Type} representing the given {@code bsonType}.
*/
static Type bsonTypeOf(String name) {
return new BsonType(name);
}
/**
* @return new {@link Type} representing the given {@code type}.
*/
static Type jsonTypeOf(String name) {
return new JsonType(name);
}
static Set<Type> jsonTypes() {
return JSON_TYPES;
}
static Set<Type> bsonTypes() {
return BSON_TYPES;
}
/**
* Get the {@link Type} representation. Either {@code type} or {@code bsonType}.
*
* @return never {@literal null}.
*/
String representation();
/**
* Get the {@link Type} value. Like {@literal string}, {@literal number},...
*
* @return never {@literal null}.
*/
Object value();
/**
* @author Christpoh Strobl
* @since 2.1
*/
@RequiredArgsConstructor
static class JsonType implements Type {
private final String name;
@Override
public String representation() {
return "type";
}
@Override
public String value() {
return name;
}
}
/**
* @author Christpoh Strobl
* @since 2.1
*/
@RequiredArgsConstructor
static class BsonType implements Type {
private final String name;
@Override
public String representation() {
return "bsonType";
}
@Override
public String value() {
return name;
}
}
}
}

View File

@@ -0,0 +1,197 @@
/*
* 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.schema;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.BooleanJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NullJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NumericJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.StringJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.UntypedJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
/**
* A {@literal property} or {@literal patternProperty} within a {@link JsonSchemaObject} of {@code type : 'object'}.
*
* @author Christoph Strobl
* @since 2.1
*/
public interface JsonSchemaProperty extends JsonSchemaObject {
/**
* The identifier can be either the property name or the regex expression properties have to match when used along
* with {@link ObjectJsonSchemaObject#patternProperties(JsonSchemaProperty...)}.
*
* @return never {@literal null}.
*/
String getIdentifier();
/**
* Creates a new {@link UntypedJsonSchemaProperty} with given {@literal identifier} without {@code type}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link UntypedJsonSchemaProperty}.
*/
static UntypedJsonSchemaProperty untyped(String identifier) {
return new UntypedJsonSchemaProperty(identifier, JsonSchemaObject.untyped());
}
/**
* Creates a new {@link StringJsonSchemaProperty} with given {@literal identifier} of {@code type : 'string'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link StringJsonSchemaProperty}.
*/
static StringJsonSchemaProperty string(String identifier) {
return new StringJsonSchemaProperty(identifier, JsonSchemaObject.string());
}
/**
* Creates a new {@link ObjectJsonSchemaProperty} with given {@literal identifier} of {@code type : 'object'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link ObjectJsonSchemaProperty}.
*/
static ObjectJsonSchemaProperty object(String identifier) {
return new ObjectJsonSchemaProperty(identifier, JsonSchemaObject.object());
}
/**
* Creates a new {@link NumericJsonSchemaProperty} with given {@literal identifier} of {@code type : 'number'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link NumericJsonSchemaProperty}.
*/
static NumericJsonSchemaProperty number(String identifier) {
return new NumericJsonSchemaProperty(identifier, JsonSchemaObject.number());
}
/**
* Creates a new {@link NumericJsonSchemaProperty} with given {@literal identifier} of {@code bsonType : 'int'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link NumericJsonSchemaProperty}.
*/
static NumericJsonSchemaProperty int32(String identifier) {
return new NumericJsonSchemaProperty(identifier, new NumericJsonSchemaObject(Type.intType()));
}
/**
* Creates a new {@link NumericJsonSchemaProperty} with given {@literal identifier} of {@code bsonType : 'long'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link NumericJsonSchemaProperty}.
*/
static NumericJsonSchemaProperty int64(String identifier) {
return new NumericJsonSchemaProperty(identifier, new NumericJsonSchemaObject(Type.longType()));
}
/**
* Creates a new {@link NumericJsonSchemaProperty} with given {@literal identifier} of {@code bsonType : 'double'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link NumericJsonSchemaProperty}.
*/
static NumericJsonSchemaProperty float64(String identifier) {
return new NumericJsonSchemaProperty(identifier, new NumericJsonSchemaObject(Type.doubleType()));
}
/**
* Creates a new {@link NumericJsonSchemaProperty} with given {@literal identifier} of
* {@code bsonType : 'decimal128'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link NumericJsonSchemaProperty}.
*/
static NumericJsonSchemaProperty decimal128(String identifier) {
return new NumericJsonSchemaProperty(identifier, new NumericJsonSchemaObject(Type.bigDecimalType()));
}
/**
* Creates a new {@link ArrayJsonSchemaProperty} with given {@literal identifier} of {@code type : 'array'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
*/
static ArrayJsonSchemaProperty array(String identifier) {
return new ArrayJsonSchemaProperty(identifier, JsonSchemaObject.array());
}
/**
* Creates a new {@link BooleanJsonSchemaProperty} with given {@literal identifier} of {@code type : 'boolean'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
*/
static BooleanJsonSchemaProperty bool(String identifier) {
return new BooleanJsonSchemaProperty(identifier, JsonSchemaObject.bool());
}
/**
* Creates a new {@link BooleanJsonSchemaProperty} with given {@literal identifier} of {@code type : 'null'}.
*
* @param identifier the {@literal property} name or {@literal patternProperty} regex. Must not be {@literal null} nor
* {@literal empty}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
*/
static NullJsonSchemaProperty nil(String identifier) {
return new NullJsonSchemaProperty(identifier, JsonSchemaObject.nil());
}
/**
* Obtain a builder to create a {@link JsonSchemaProperty}.
*
* @param identifier
* @return
*/
static JsonSchemaPropertyBuilder named(String identifier) {
return new JsonSchemaPropertyBuilder(identifier);
}
class JsonSchemaPropertyBuilder {
private String identifier;
public JsonSchemaPropertyBuilder(String identifier) {
this.identifier = identifier;
}
public IdentifiableJsonSchemaProperty<TypedJsonSchemaObject> ofType(Type type) {
return new IdentifiableJsonSchemaProperty(identifier, TypedJsonSchemaObject.of(type));
}
public IdentifiableJsonSchemaProperty<TypedJsonSchemaObject> with(TypedJsonSchemaObject schemaObject) {
return new IdentifiableJsonSchemaProperty(identifier, schemaObject);
}
public IdentifiableJsonSchemaProperty<UntypedJsonSchemaObject> withoutType() {
return new IdentifiableJsonSchemaProperty(identifier, UntypedJsonSchemaObject.newInstance());
}
}
}

View File

@@ -0,0 +1,232 @@
/*
* 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.schema;
import java.util.Collection;
import java.util.Set;
import org.bson.Document;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject;
import org.springframework.util.Assert;
/**
* @author Christoph Strobl
* @since 2.1
**/
public class MongoJsonSchema {
private final JsonSchemaObject root;
private MongoJsonSchema(JsonSchemaObject root) {
Assert.notNull(root, "Root must not be null!");
this.root = root;
}
/**
* Create the {@link Document} containing the specified {@code $jsonSchema}. <br />
* Property and field names still need to be mapped to the domain type ones by running the {@link Document} through a
* {@link org.springframework.data.mongodb.core.convert.JsonSchemaMapper}.
*
* @return never {@literal null}.
*/
public Document toDocument() {
return new Document("$jsonSchema", root.toDocument());
}
/**
* Create a new {@link MongoJsonSchema} for a given root object.
*
* @param root must not be {@literal null}.
* @return
*/
public static MongoJsonSchema of(JsonSchemaObject root) {
return new MongoJsonSchema(root);
}
/**
* Obtain a new {@link MongoJsonSchemaBuilder} to fluently define the schema.
*
* @return new instance of {@link MongoJsonSchemaBuilder}.
*/
public static MongoJsonSchemaBuilder builder() {
return new MongoJsonSchemaBuilder();
}
/**
* {@link MongoJsonSchemaBuilder} provides a fluent API for defining a {@link MongoJsonSchema}.
*
* @since 2.1
*/
public static class MongoJsonSchemaBuilder {
private ObjectJsonSchemaObject root;
MongoJsonSchemaBuilder() {
root = new ObjectJsonSchemaObject();
}
/**
* @param nrProperties
* @return this
* @see ObjectJsonSchemaObject#minNrProperties(int)
*/
public MongoJsonSchemaBuilder minNrProperties(int nrProperties) {
root = root.minNrProperties(nrProperties);
return this;
}
/**
* @param nrProperties
* @return this
* @see ObjectJsonSchemaObject#maxNrProperties(int)
*/
public MongoJsonSchemaBuilder maxNrProperties(int nrProperties) {
root = root.maxNrProperties(nrProperties);
return this;
}
/**
* @param properties
* @return
* @see ObjectJsonSchemaObject#required(String...)
*/
public MongoJsonSchemaBuilder required(String... properties) {
root = root.required(properties);
return this;
}
/**
* @param additionalPropertiesAllowed
* @return this
* @see ObjectJsonSchemaObject#additionalProperties(boolean)
*/
public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesAllowed) {
root = root.additionalProperties(additionalPropertiesAllowed);
return this;
}
/**
* @param schema
* @return this
* @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject)
*/
public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema) {
root = root.additionalProperties(schema);
return this;
}
/**
* @param properties
* @return this
* @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...)
*/
public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) {
root = root.properties(properties);
return this;
}
/**
* @param properties
* @return this
* @see ObjectJsonSchemaObject#patternProperties(JsonSchemaProperty...)
*/
public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties) {
root = root.patternProperties(properties);
return this;
}
/**
* @param property
* @return this
* @see ObjectJsonSchemaObject#property(JsonSchemaProperty)
*/
public MongoJsonSchemaBuilder property(JsonSchemaProperty property) {
root = root.property(property);
return this;
}
/**
* @param possibleValues
* @return this
* @see ObjectJsonSchemaObject#possibleValues(Collection)
*/
public MongoJsonSchemaBuilder possibleValues(Set<Object> possibleValues) {
root = root.possibleValues(possibleValues);
return this;
}
/**
* @param allOf
* @return this
* @see UntypedJsonSchemaObject#allOf(Collection)
*/
public MongoJsonSchemaBuilder allOf(Set<JsonSchemaObject> allOf) {
root = root.allOf(allOf);
return this;
}
/**
* @param anyOf
* @return this
* @see UntypedJsonSchemaObject#anyOf(Collection)
*/
public MongoJsonSchemaBuilder anyOf(Set<JsonSchemaObject> anyOf) {
root = root.anyOf(anyOf);
return this;
}
/**
* @param oneOf
* @return this
* @see UntypedJsonSchemaObject#oneOf(Collection)
*/
public MongoJsonSchemaBuilder oneOf(Set<JsonSchemaObject> oneOf) {
root = root.oneOf(oneOf);
return this;
}
/**
* @param notMatch
* @return this
* @see UntypedJsonSchemaObject#notMatch(JsonSchemaObject)
*/
public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) {
root = root.notMatch(notMatch);
return this;
}
/**
* @param description
* @return this
* @see UntypedJsonSchemaObject#description(String)
*/
public MongoJsonSchemaBuilder description(String description) {
root = root.description(description);
return this;
}
/**
* Obtain the {@link MongoJsonSchema}.
*
* @return new instance of {@link MongoJsonSchema}.
*/
public MongoJsonSchema build() {
return MongoJsonSchema.of(root);
}
}
}

View File

@@ -0,0 +1,277 @@
/*
* 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.schema;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.bson.Document;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Common base for {@link JsonSchemaObject} with shared types and {@link JsonSchemaObject#toDocument()} implementation.
*
* @author Christoph Strobl
* @since 2.1
*/
public class UntypedJsonSchemaObject implements JsonSchemaObject {
protected final @Nullable String description;
protected final Restrictions restrictions;
protected final boolean generateDescription;
protected UntypedJsonSchemaObject(Restrictions restrictions, @Nullable String description, boolean generateDescription) {
this.description = description;
this.restrictions = restrictions != null ? restrictions : Restrictions.empty();
this.generateDescription = generateDescription;
}
public static UntypedJsonSchemaObject newInstance() {
return new UntypedJsonSchemaObject(null, null, false);
}
@Override
public Set<Type> getTypes() {
return Collections.emptySet();
}
/**
* Set the {@literal description}.
*
* @param description must not be {@literal null}.
* @return new instance of {@link TypedJsonSchemaObject}.
*/
public UntypedJsonSchemaObject description(String description) {
return new UntypedJsonSchemaObject(restrictions, description, generateDescription);
}
/**
* Auto generate the {@literal description} if not explicitly set.
*
* @param description must not be {@literal null}.
* @return new instance of {@link TypedJsonSchemaObject}.
*/
public UntypedJsonSchemaObject generatedDescription() {
return new UntypedJsonSchemaObject(restrictions, description, true);
}
/**
* {@literal enum}erates all possible values of the field.
*
* @param possibleValues must not be {@literal null}.
* @return new instance of {@link TypedJsonSchemaObject}.
*/
public UntypedJsonSchemaObject possibleValues(Collection<Object> possibleValues) {
return new UntypedJsonSchemaObject(restrictions.possibleValues(possibleValues), description, generateDescription);
}
/**
* The field value must match all specified schemas.
*
* @param allOf must not be {@literal null}.
* @return new instance of {@link TypedJsonSchemaObject}.
*/
public UntypedJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
return new UntypedJsonSchemaObject(restrictions.allOf(allOf), description, generateDescription);
}
/**
* The field value must match at least one of the specified schemas.
*
* @param anyOf must not be {@literal null}.
* @return new instance of {@link TypedJsonSchemaObject}.
*/
public UntypedJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
return new UntypedJsonSchemaObject(restrictions.anyOf(anyOf), description, generateDescription);
}
/**
* The field value must match exactly one of the specified schemas.
*
* @param oneOf must not be {@literal null}.
* @return new instance of {@link TypedJsonSchemaObject}.
*/
public UntypedJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
return new UntypedJsonSchemaObject(restrictions.oneOf(oneOf), description, generateDescription);
}
/**
* The field value must not match the specified schemas.
*
* @param oneOf must not be {@literal null}.
* @return new instance of {@link TypedJsonSchemaObject}.
*/
public UntypedJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
return new UntypedJsonSchemaObject(restrictions.notMatch(notMatch), description, generateDescription);
}
/**
* Create the json schema complying {@link Document} representation. This includes {@literal type},
* {@literal description} and the fields of {@link Restrictions#toDocument()} if set.
*/
@Override
public Document toDocument() {
Document document = new Document();
getOrCreateDescription().ifPresent(val -> document.append("description", val));
if (restrictions != null) {
document.putAll(restrictions.toDocument());
}
return document;
}
private Optional<String> getOrCreateDescription() {
if (description != null) {
return description.isEmpty() ? Optional.empty() : Optional.of(description);
}
return generateDescription ? Optional.ofNullable(generateDescription()) : Optional.empty();
}
/**
* Customization hook for creating description out of defined values.<br />
* Called by {@link #toDocument()} when no explicit {@link #description} is set.
*
* @return can be {@literal null}.
*/
@Nullable
protected String generateDescription() {
return null;
}
/**
* {@link Restrictions} encapsulate common json schema restrictions like {@literal enum}, {@literal allOf}, ...
*
* @author Christoph Strobl
* @since 2.1
*/
static class Restrictions {
private final Collection<Object> possibleValues;
private final Collection<JsonSchemaObject> allOf;
private final Collection<JsonSchemaObject> anyOf;
private final Collection<JsonSchemaObject> oneOf;
private final @Nullable JsonSchemaObject notMatch;
Restrictions(Collection<Object> possibleValues, Collection<JsonSchemaObject> allOf,
Collection<JsonSchemaObject> anyOf, Collection<JsonSchemaObject> oneOf, @Nullable JsonSchemaObject notMatch) {
this.possibleValues = possibleValues;
this.allOf = allOf;
this.anyOf = anyOf;
this.oneOf = oneOf;
this.notMatch = notMatch;
}
/**
* @return new empty {@link Restrictions}.
*/
static Restrictions empty() {
return new Restrictions(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(),
Collections.emptySet(), null);
}
/**
* @param possibleValues must not be {@literal null}.
* @return
*/
Restrictions possibleValues(Collection<Object> possibleValues) {
Assert.notNull(possibleValues, "PossibleValues must not be null!");
return new Restrictions(possibleValues, allOf, anyOf, oneOf, notMatch);
}
/**
* @param allOf must not be {@literal null}.
* @return
*/
Restrictions allOf(Collection<JsonSchemaObject> allOf) {
Assert.notNull(allOf, "AllOf must not be null!");
return new Restrictions(possibleValues, allOf, anyOf, oneOf, notMatch);
}
/**
* @param anyOf must not be {@literal null}.
* @return
*/
Restrictions anyOf(Collection<JsonSchemaObject> anyOf) {
Assert.notNull(anyOf, "AnyOf must not be null!");
return new Restrictions(possibleValues, allOf, anyOf, oneOf, notMatch);
}
/**
* @param oneOf must not be {@literal null}.
* @return
*/
Restrictions oneOf(Collection<JsonSchemaObject> oneOf) {
Assert.notNull(oneOf, "OneOf must not be null!");
return new Restrictions(possibleValues, allOf, anyOf, oneOf, notMatch);
}
/**
* @param notMatch must not be {@literal null}.
* @return
*/
Restrictions notMatch(JsonSchemaObject notMatch) {
Assert.notNull(notMatch, "NotMatch must not be null!");
return new Restrictions(possibleValues, allOf, anyOf, oneOf, notMatch);
}
/**
* Create the json schema complying {@link Document} representation. This includes {@literal enum},
* {@literal allOf}, {@literal anyOf}, {@literal oneOf}, {@literal notMatch} if set.
*
* @return never {@literal null}
*/
Document toDocument() {
Document document = new Document();
if (!CollectionUtils.isEmpty(possibleValues)) {
document.append("enum", possibleValues);
}
if (!CollectionUtils.isEmpty(allOf)) {
document.append("allOf", allOf.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList()));
}
if (!CollectionUtils.isEmpty(anyOf)) {
document.append("anyOf", anyOf.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList()));
}
if (!CollectionUtils.isEmpty(oneOf)) {
document.append("oneOf", oneOf.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList()));
}
if (notMatch != null) {
document.append("not", notMatch.toDocument());
}
return document;
}
}
}

View File

@@ -0,0 +1,18 @@
/*
* 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.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.data.mongodb.core.schema;

View File

@@ -0,0 +1,204 @@
/*
* Copyright 2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*;
import lombok.Data;
import reactor.test.StepVerifier;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import com.mongodb.MongoClient;
import com.mongodb.reactivestreams.client.MongoClients;
/**
* @author Christoph Strobl
*/
public class JsonSchemaQueryTests {
public static final String DATABASE_NAME = "json-schema-query-tests";
public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_6_0 = MongoVersionRule.atLeast(Version.parse("3.6.0"));
MongoTemplate template;
Person jellyBelly, roseSpringHeart, kazmardBoombub;
@Before
public void setUp() {
template = new MongoTemplate(new MongoClient(), DATABASE_NAME);
jellyBelly = new Person();
jellyBelly.id = "1";
jellyBelly.name = "Jelly Belly";
jellyBelly.gender = Gender.PIXY;
jellyBelly.address = new Address();
jellyBelly.address.city = "Candy Hill";
jellyBelly.address.street = "Apple Mint Street";
jellyBelly.value = 42;
roseSpringHeart = new Person();
roseSpringHeart.id = "2";
roseSpringHeart.name = "Rose SpringHeart";
roseSpringHeart.gender = Gender.UNICORN;
roseSpringHeart.address = new Address();
roseSpringHeart.address.city = "Rainbow Valley";
roseSpringHeart.address.street = "Twinkle Ave.";
roseSpringHeart.value = 42L;
kazmardBoombub = new Person();
kazmardBoombub.id = "3";
kazmardBoombub.name = "Kazmard Boombub";
kazmardBoombub.gender = Gender.GOBLIN;
kazmardBoombub.value = "green";
template.save(jellyBelly);
template.save(roseSpringHeart);
template.save(kazmardBoombub);
}
@Test // DATAMONGO-1835
public void findsDocumentsWithRequiredFieldsCorrectly() {
MongoJsonSchema schema = MongoJsonSchema.builder().required("address").build();
assertThat(template.find(query(matchingDocumentStructure(schema)), Person.class))
.containsExactlyInAnyOrder(jellyBelly, roseSpringHeart);
}
@Test // DATAMONGO-1835
public void findsDocumentsWithRequiredFieldsReactively() {
MongoJsonSchema schema = MongoJsonSchema.builder().required("address").build();
StepVerifier.create(new ReactiveMongoTemplate(MongoClients.create(), DATABASE_NAME)
.find(query(matchingDocumentStructure(schema)), Person.class)).expectNextCount(2).verifyComplete();
}
@Test // DATAMONGO-1835
public void findsDocumentsWithBsonFieldTypesCorrectly() {
MongoJsonSchema schema = MongoJsonSchema.builder().property(int32("value")).build();
assertThat(template.find(query(matchingDocumentStructure(schema)), Person.class))
.containsExactlyInAnyOrder(jellyBelly);
}
@Test // DATAMONGO-1835
public void findsDocumentsWithJsonFieldTypesCorrectly() {
MongoJsonSchema schema = MongoJsonSchema.builder().property(number("value")).build();
assertThat(template.find(query(matchingDocumentStructure(schema)), Person.class))
.containsExactlyInAnyOrder(jellyBelly, roseSpringHeart);
}
@Test // DATAMONGO-1835
public void combineSchemaWithOtherCriteria() {
MongoJsonSchema schema = MongoJsonSchema.builder().property(number("value")).build();
assertThat(
template.find(query(matchingDocumentStructure(schema).and("name").is(roseSpringHeart.name)), Person.class))
.containsExactlyInAnyOrder(roseSpringHeart);
}
@Test // DATAMONGO-1835
public void usesMappedFieldNameForRequiredProperties() {
MongoJsonSchema schema = MongoJsonSchema.builder().required("name").build();
assertThat(template.find(query(matchingDocumentStructure(schema)), Person.class))
.containsExactlyInAnyOrder(jellyBelly, roseSpringHeart, kazmardBoombub);
}
@Test // DATAMONGO-1835
public void usesMappedFieldNameForProperties() {
MongoJsonSchema schema = MongoJsonSchema.builder().property(string("name").matching("^R.*")).build();
assertThat(template.find(query(matchingDocumentStructure(schema)), Person.class))
.containsExactlyInAnyOrder(roseSpringHeart);
}
@Test // DATAMONGO-1835
public void mapsNestedFieldName() {
MongoJsonSchema schema = MongoJsonSchema.builder() //
.required("address") //
.property(object("address").properties(string("street").matching("^Apple.*"))).build();
assertThat(template.find(query(matchingDocumentStructure(schema)), Person.class))
.containsExactlyInAnyOrder(jellyBelly);
}
@Test // DATAMONGO-1835
public void mapsEnumValuesCorrectly() {
MongoJsonSchema schema = MongoJsonSchema.builder()
.property(untyped("gender").possibleValues(Gender.PIXY, Gender.GOBLIN)).build();
assertThat(template.find(query(matchingDocumentStructure(schema)), Person.class))
.containsExactlyInAnyOrder(jellyBelly, kazmardBoombub);
}
@Test // DATAMONGO-1835
public void useTypeOperatorOnFieldLevel() {
assertThat(template.find(query(where("value").type(Type.intType())), Person.class)).containsExactly(jellyBelly);
}
@Test // DATAMONGO-1835
public void useTypeOperatorWithMultipleTypesOnFieldLevel() {
assertThat(template.find(query(where("value").type(Type.intType(), Type.stringType())), Person.class))
.containsExactlyInAnyOrder(jellyBelly, kazmardBoombub);
}
@Data
static class Person {
@Id String id;
@Field("full_name") String name;
Gender gender;
Address address;
Object value;
}
@Data
static class Address {
String city;
@Field("str") String street;
}
static enum Gender {
PIXY, UNICORN, GOBLIN
}
}

View File

@@ -0,0 +1,218 @@
/*
* 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.convert;
import static org.mockito.Mockito.*;
import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import java.util.Arrays;
import java.util.List;
import org.bson.Document;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
/**
* Unit tests for {@link MongoJsonSchemaMapper}.
*
* @author Christoph Strobl
*/
public class MongoJsonSchemaMapperUnitTests {
public @Rule ExpectedException exception = ExpectedException.none();
MongoJsonSchemaMapper mapper;
Document addressProperty = new Document("type", "object").append("required", Arrays.asList("street", "postCode"))
.append("properties",
new Document("street", new Document("type", "string")).append("postCode", new Document("type", "string")));
Document mappedAddressProperty = new Document("type", "object")
.append("required", Arrays.asList("street", "post_code")).append("properties",
new Document("street", new Document("type", "string")).append("post_code", new Document("type", "string")));
Document nameProperty = new Document("type", "string");
Document gradePointAverageProperty = new Document("bsonType", "double");
Document yearProperty = new Document("bsonType", "int").append("minimum", 2017).append("maximum", 3017)
.append("exclusiveMaximum", true);
Document properties = new Document("name", nameProperty) //
.append("gradePointAverage", gradePointAverageProperty) //
.append("year", yearProperty);
Document mappedProperties = new Document("name", new Document(nameProperty)) //
.append("gpa", new Document(gradePointAverageProperty)) //
.append("year", new Document(yearProperty));
List<String> requiredProperties = Arrays.asList("name", "gradePointAverage");
List<String> mappedRequiredProperties = Arrays.asList("name", "gpa");
Document $jsonSchema = new Document("type", "object") //
.append("required", requiredProperties) //
.append("properties", properties);
Document mapped$jsonSchema = new Document("type", "object") //
.append("required", mappedRequiredProperties) //
.append("properties", mappedProperties);
Document sourceSchemaDocument = new Document("$jsonSchema", $jsonSchema);
Document mappedSchemaDocument = new Document("$jsonSchema", mapped$jsonSchema);
String complexSchemaJsonString = "{ $jsonSchema: {" + //
" type: \"object\"," + //
" required: [ \"name\", \"year\", \"major\", \"gpa\" ]," + //
" properties: {" + //
" name: {" + //
" type: \"string\"," + //
" description: \"must be a string and is required\"" + //
" }," + //
" gender: {" + //
" type: \"string\"," + //
" description: \"must be a string and is not required\"" + //
" }," + //
" year: {" + //
" bsonType: \"int\"," + //
" minimum: 2017," + //
" maximum: 3017," + //
" exclusiveMaximum: true," + //
" description: \"must be an integer in [ 2017, 3017 ] and is required\"" + //
" }," + //
" major: {" + //
" type: \"string\"," + //
" enum: [ \"Math\", \"English\", \"Computer Science\", \"History\", null ]," + //
" description: \"can only be one of the enum values and is required\"" + //
" }," + //
" gpa: {" + //
" bsonType: \"double\"," + //
" description: \"must be a double and is required\"" + //
" }" + //
" }" + //
" } }";
@Before
public void setUp() {
mapper = new MongoJsonSchemaMapper(new MappingMongoConverter(mock(DbRefResolver.class), new MongoMappingContext()));
}
@Test // DATAMONGO-1835
public void noNullSchemaAllowed() {
exception.expect(IllegalArgumentException.class);
mapper.mapSchema(null, Object.class);
}
@Test // DATAMONGO-1835
public void noNullDomainTypeAllowed() {
exception.expect(IllegalArgumentException.class);
mapper.mapSchema(new Document("$jsonSchema", new Document()), null);
}
@Test // DATAMONGO-1835
public void schemaDocumentMustContain$jsonSchemaField() {
exception.expect(IllegalArgumentException.class);
exception.expectMessage("contain $jsonSchema");
mapper.mapSchema(new Document("foo", new Document()), Object.class);
}
@Test // DATAMONGO-1835
public void objectTypeSkipsFieldMapping() {
assertThat(mapper.mapSchema(sourceSchemaDocument, Object.class)).isEqualTo(sourceSchemaDocument);
}
@Test // DATAMONGO-1835
public void mapSchemaProducesNewDocument() {
assertThat(mapper.mapSchema(sourceSchemaDocument, Object.class)).isNotSameAs(sourceSchemaDocument);
}
@Test // DATAMONGO-1835
public void mapSchemaMapsPropertiesToFieldNames() {
assertThat(mapper.mapSchema(sourceSchemaDocument, Student.class)).isEqualTo(mappedSchemaDocument);
}
@Test // DATAMONGO-1835
public void mapSchemaLeavesSourceDocumentUntouched() {
Document source = Document.parse(complexSchemaJsonString);
mapper.mapSchema(source, Student.class);
assertThat(source).isEqualTo(Document.parse(complexSchemaJsonString));
}
@Test // DATAMONGO-1835
public void mapsNestedPropertiesCorrectly() {
Document schema = new Document("$jsonSchema", new Document("type", "object") //
.append("properties", new Document(properties).append("address", addressProperty)));
Document expectedSchema = new Document("$jsonSchema", new Document("type", "object") //
.append("properties", new Document(mappedProperties).append("address", mappedAddressProperty)));
assertThat(mapper.mapSchema(schema, Student.class)).isEqualTo(expectedSchema);
}
@Test // DATAMONGO-1835
public void constructReferenceSchemaCorrectly() {
MongoJsonSchema schema = MongoJsonSchema.builder() //
.required("name", "year", "major", "gradePointAverage").description("") //
.properties(string("name").description("must be a string and is required"), //
string("gender").description("must be a string and is not required"), //
int32("year").description("must be an integer in [ 2017, 3017 ] and is required").gte(2017).lt(3017), //
string("major").description("can only be one of the enum values and is required").possibleValues("Math",
"English", "Computer Science", "History", null), //
float64("gradePointAverage").description("must be a double and is required") //
).build();
assertThat(mapper.mapSchema(schema.toDocument(), Student.class)).isEqualTo(Document.parse(complexSchemaJsonString));
}
static class Student {
String name;
Gender gender;
Integer year;
String major;
@Field("gpa") //
Double gradePointAverage;
Address address;
}
static class Address {
String city;
String street;
@Field("post_code") //
String postCode;
}
static enum Gender {
M, F
}
}

View File

@@ -19,12 +19,15 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import java.util.Collections;
import org.bson.Document;
import org.junit.Test;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
import org.springframework.data.mongodb.core.geo.GeoJsonLineString;
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
/**
* @author Oliver Gierke
@@ -213,4 +216,24 @@ public class CriteriaTests {
assertThat(document, isBsonObject().containing("foo.$geoIntersects.$geometry", lineString));
}
@Test // DATAMONGO-1835
public void extractsJsonSchemaInChainCorrectly() {
MongoJsonSchema schema = MongoJsonSchema.builder().required("name").build();
Criteria critera = Criteria.where("foo").is("bar").andDocumentStructureMatches(schema);
assertThat(critera.getCriteriaObject(), is(equalTo(new Document("foo", "bar").append("$jsonSchema",
new Document("type", "object").append("required", Collections.singletonList("name"))))));
}
@Test // DATAMONGO-1835
public void extractsJsonSchemaFromFactoryMethodCorrectly() {
MongoJsonSchema schema = MongoJsonSchema.builder().required("name").build();
Criteria critera = Criteria.matchingDocumentStructure(schema);
assertThat(critera.getCriteriaObject(), is(equalTo(new Document("$jsonSchema",
new Document("type", "object").append("required", Collections.singletonList("name"))))));
}
}

View File

@@ -0,0 +1,269 @@
/*
* 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.schema;
import static org.springframework.data.domain.Range.from;
import static org.springframework.data.domain.Range.Bound.*;
import static org.springframework.data.mongodb.core.schema.JsonSchemaObject.*;
import static org.springframework.data.mongodb.core.schema.JsonSchemaObject.of;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import java.util.Arrays;
import org.bson.Document;
import org.junit.Test;
import org.springframework.data.domain.Range;
import org.springframework.data.domain.Range.*;
/**
* Tests verifying {@link org.bson.Document} representation of {@link JsonSchemaObject}s.
*
* @author Christoph Strobl
*/
public class JsonSchemaObjectUnitTests {
// -----------------
// type : 'object'
// -----------------
@Test // DATAMONGO-1835
public void objectObjectShouldRenderTypeCorrectly() {
assertThat(object().generatedDescription().toDocument())
.isEqualTo(new Document("type", "object").append("description", "Must be an object."));
}
@Test // DATAMONGO-1835
public void objectObjectShouldRenderNrPropertiesCorrectly() {
assertThat(object().nrProperties(from(inclusive(10)).to(inclusive(20))).generatedDescription().toDocument())
.isEqualTo(new Document("type", "object").append("description", "Must be an object with [10-20] properties.")
.append("minProperties", 10).append("maxProperties", 20));
}
@Test // DATAMONGO-1835
public void objectObjectShouldRenderRequiredPropertiesCorrectly() {
assertThat(object().required("spring", "data", "mongodb").generatedDescription().toDocument()).isEqualTo(
new Document("type", "object")
.append("description", "Must be an object where spring, data, mongodb are mandatory.").append("required",
Arrays.asList("spring", "data", "mongodb")));
}
@Test // DATAMONGO-1835
public void objectObjectShouldRenderAdditionalPropertiesCorrectlyWhenBoolean() {
assertThat(object().additionalProperties(true).generatedDescription().toDocument()).isEqualTo(
new Document("type", "object").append("description", "Must be an object allowing additional properties.")
.append("additionalProperties", true));
assertThat(object().additionalProperties(false).generatedDescription().toDocument()).isEqualTo(
new Document("type", "object").append("description", "Must be an object not allowing additional properties.")
.append("additionalProperties", false));
}
@Test // DATAMONGO-1835
public void objectObjectShouldRenderPropertiesCorrectly() {
Document expected = new Document("type", "object")
.append("description", "Must be an object defining restrictions for name, active.").append("properties",
new Document("name", new Document("type", "string")
.append("description", "Must be a string with length unbounded-10].").append("maxLength", 10))
.append("active", new Document("type", "boolean")));
assertThat(object().generatedDescription()
.properties(JsonSchemaProperty.string("name").maxLength(10).generatedDescription(),
JsonSchemaProperty.bool("active"))
.generatedDescription().toDocument()).isEqualTo(expected);
}
@Test // DATAMONGO-1835
public void objectObjectShouldRenderNestedObjectPropertiesCorrectly() {
Document expected = new Document("type", "object")
.append("description", "Must be an object defining restrictions for address.")
.append("properties", new Document("address",
new Document("type", "object").append("description", "Must be an object defining restrictions for city.")
.append("properties", new Document("city", new Document("type", "string")
.append("description", "Must be a string with length [3-unbounded.").append("minLength", 3)))));
assertThat(object()
.properties(JsonSchemaProperty.object("address")
.properties(JsonSchemaProperty.string("city").minLength(3).generatedDescription()).generatedDescription())
.generatedDescription().toDocument()).isEqualTo(expected);
}
@Test // DATAMONGO-1835
public void objectObjectShouldRenderPatternPropertiesCorrectly() {
Document expected = new Document("type", "object")
.append("description", "Must be an object defining restrictions for patterns na.*.")
.append("patternProperties", new Document("na.*", new Document("type", "string")
.append("description", "Must be a string with length unbounded-10].").append("maxLength", 10)));
assertThat(object().patternProperties(JsonSchemaProperty.string("na.*").maxLength(10).generatedDescription())
.generatedDescription().toDocument()).isEqualTo(expected);
}
// -----------------
// type : 'string'
// -----------------
@Test // DATAMONGO-1835
public void stringObjectShouldRenderTypeCorrectly() {
assertThat(string().generatedDescription().toDocument())
.isEqualTo(new Document("type", "string").append("description", "Must be a string."));
}
@Test // DATAMONGO-1835
public void stringObjectShouldRenderDescriptionCorrectly() {
assertThat(string().description("error msg").toDocument())
.isEqualTo(new Document("type", "string").append("description", "error msg"));
}
@Test // DATAMONGO-1835
public void stringObjectShouldRenderRangeCorrectly() {
assertThat(string().length(from(inclusive(10)).to(inclusive(20))).generatedDescription().toDocument())
.isEqualTo(new Document("type", "string").append("description", "Must be a string with length [10-20].")
.append("minLength", 10).append("maxLength", 20));
}
@Test // DATAMONGO-1835
public void stringObjectShouldRenderPatternCorrectly() {
assertThat(string().matching("^spring$").generatedDescription().toDocument())
.isEqualTo(new Document("type", "string").append("description", "Must be a string matching ^spring$.")
.append("pattern", "^spring$"));
}
// -----------------
// type : 'number'
// -----------------
@Test // DATAMONGO-1835
public void numberObjectShouldRenderMultipleOfCorrectly() {
assertThat(number().multipleOf(3.141592F).generatedDescription().toDocument())
.isEqualTo(new Document("type", "number").append("description", "Must be a numeric value multiple of 3.141592.")
.append("multipleOf", 3.141592F));
}
@Test // DATAMONGO-1835
public void numberObjectShouldRenderMaximumCorrectly() {
assertThat(
number().within(Range.of(Bound.unbounded(), Bound.inclusive(3.141592F))).generatedDescription().toDocument())
.isEqualTo(new Document("type", "number")
.append("description", "Must be a numeric value within range unbounded-3.141592].")
.append("maximum", 3.141592F));
assertThat(
number().within(Range.of(Bound.unbounded(), Bound.exclusive(3.141592F))).generatedDescription().toDocument())
.isEqualTo(new Document("type", "number")
.append("description", "Must be a numeric value within range unbounded-3.141592).")
.append("maximum", 3.141592F).append("exclusiveMaximum", true));
}
@Test // DATAMONGO-1835
public void numberObjectShouldRenderMinimumCorrectly() {
assertThat(
number().within(Range.of(Bound.inclusive(3.141592F), Bound.unbounded())).generatedDescription().toDocument())
.isEqualTo(new Document("type", "number")
.append("description", "Must be a numeric value within range [3.141592-unbounded.")
.append("minimum", 3.141592F));
assertThat(
number().within(Range.of(Bound.exclusive(3.141592F), Bound.unbounded())).generatedDescription().toDocument())
.isEqualTo(new Document("type", "number")
.append("description", "Must be a numeric value within range (3.141592-unbounded.")
.append("minimum", 3.141592F).append("exclusiveMinimum", true));
}
// -----------------
// type : 'arrays'
// -----------------
@Test // DATAMONGO-1835
public void arrayObjectShouldRenderItemsCorrectly() {
assertThat(array().items(Arrays.asList(string(), bool())).toDocument()).isEqualTo(new Document("type", "array")
.append("items", Arrays.asList(new Document("type", "string"), new Document("type", "boolean"))));
}
@Test // DATAMONGO-1835
public void arrayObjectShouldRenderMaxItemsCorrectly() {
assertThat(array().maxItems(5).generatedDescription().toDocument()).isEqualTo(new Document("type", "array")
.append("description", "Must be an array having size unbounded-5].").append("maxItems", 5));
}
@Test // DATAMONGO-1835
public void arrayObjectShouldRenderMinItemsCorrectly() {
assertThat(array().minItems(5).generatedDescription().toDocument()).isEqualTo(new Document("type", "array")
.append("description", "Must be an array having size [5-unbounded.").append("minItems", 5));
}
@Test // DATAMONGO-1835
public void arrayObjectShouldRenderUniqueItemsCorrectly() {
assertThat(array().uniqueItems(true).generatedDescription().toDocument()).isEqualTo(new Document("type", "array")
.append("description", "Must be an array of unique values.").append("uniqueItems", true));
}
// -----------------
// type : 'any'
// -----------------
@Test // DATAMONGO-1835
public void typedObjectShouldRenderEnumCorrectly() {
assertThat(of(String.class).possibleValues(Arrays.asList("one", "two")).toDocument())
.isEqualTo(new Document("type", "string").append("enum", Arrays.asList("one", "two")));
}
@Test // DATAMONGO-1835
public void typedObjectShouldRenderAllOfCorrectly() {
assertThat(of(Object.class).allOf(Arrays.asList(string())).toDocument())
.isEqualTo(new Document("type", "object").append("allOf", Arrays.asList(new Document("type", "string"))));
}
@Test // DATAMONGO-1835
public void typedObjectShouldRenderAnyOfCorrectly() {
assertThat(of(String.class).anyOf(Arrays.asList(string())).toDocument())
.isEqualTo(new Document("type", "string").append("anyOf", Arrays.asList(new Document("type", "string"))));
}
@Test // DATAMONGO-1835
public void typedObjectShouldRenderOneOfCorrectly() {
assertThat(of(String.class).oneOf(Arrays.asList(string())).toDocument())
.isEqualTo(new Document("type", "string").append("oneOf", Arrays.asList(new Document("type", "string"))));
}
@Test // DATAMONGO-1835
public void typedObjectShouldRenderNotCorrectly() {
assertThat(untyped().notMatch(string()).toDocument())
.isEqualTo(new Document("not", new Document("type", "string")));
}
}

View File

@@ -0,0 +1,190 @@
/*
* 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.schema;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import lombok.Data;
import java.util.List;
import org.bson.Document;
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.DataRetrievalFailureException;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.CollectionOptions;
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.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.mongodb.MongoClient;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.ValidationAction;
import com.mongodb.client.model.ValidationLevel;
import com.mongodb.client.model.ValidationOptions;
/**
* @author Christoph Strobl
* @since 2017/12
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class MongoJsonSchemaTests {
public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_6_0 = MongoVersionRule.atLeast(Version.parse("3.6.0"));
@Configuration
static class Config extends AbstractMongoConfiguration {
@Override
public MongoClient mongoClient() {
return new MongoClient();
}
@Override
protected String getDatabaseName() {
return "json-schema-tests";
}
}
@Autowired MongoTemplate template;
@Before
public void setUp() {
template.dropCollection(Person.class);
}
@Test // DATAMONGO-1835
public void writeSchemaViaTemplate() {
MongoJsonSchema schema = MongoJsonSchema.builder() //
.required("firstname", "lastname") //
.properties( //
JsonSchemaProperty.string("firstname").possibleValues("luke", "han").maxLength(10), //
JsonSchemaProperty.object("address") //
.properties(JsonSchemaProperty.string("postCode").minLength(4).maxLength(5))
).build();
template.createCollection(Person.class, CollectionOptions.empty().schema(schema));
Document $jsonSchema = new MongoJsonSchemaMapper(template.getConverter()).mapSchema(schema.toDocument(),
Person.class);
Document fromDb = readSchemaFromDatabase("persons");
assertThat(fromDb).isEqualTo($jsonSchema);
}
@Test // DATAMONGO-1835
public void nonMappedSchema() {
MongoJsonSchema schema = MongoJsonSchema.builder() //
.required("firstname", "lastname") //
.properties( //
JsonSchemaProperty.string("firstname").possibleValues("luke", "han").maxLength(10), //
JsonSchemaProperty.object("address") //
.properties(JsonSchemaProperty.string("postCode").minLength(4).maxLength(5))
).build();
template.createCollection("persons", CollectionOptions.empty().schema(schema));
Document fromDb = readSchemaFromDatabase("persons");
assertThat(fromDb)
.isNotEqualTo(new MongoJsonSchemaMapper(template.getConverter()).mapSchema(schema.toDocument(), Person.class));
}
@Test // DATAMONGO-1835
public void writeSchemaManually() {
MongoJsonSchema schema = MongoJsonSchema.builder() //
.required("firstname", "lastname") //
.properties( //
JsonSchemaProperty.string("firstname").possibleValues("luke", "han").maxLength(10), //
JsonSchemaProperty.object("address") //
.properties(JsonSchemaProperty.string("postCode").minLength(4).maxLength(5))
).build();
Document $jsonSchema = new MongoJsonSchemaMapper(template.getConverter()).mapSchema(schema.toDocument(),
Person.class);
ValidationOptions options = new ValidationOptions();
options.validationLevel(ValidationLevel.MODERATE);
options.validationAction(ValidationAction.ERROR);
options.validator($jsonSchema);
CreateCollectionOptions cco = new CreateCollectionOptions();
cco.validationOptions(options);
MongoDatabase db = template.getDb();
db.createCollection("persons", cco);
Document fromDb = readSchemaFromDatabase("persons");
assertThat(fromDb).isEqualTo($jsonSchema);
}
Document readSchemaFromDatabase(String collectionName) {
Document collectionInfo = template
.executeCommand(new Document("listCollections", 1).append("filter", new Document("name", collectionName)));
if (collectionInfo == null) {
throw new DataRetrievalFailureException(String.format("Collection %s was not found."));
}
if (collectionInfo.containsKey("cursor")) {
collectionInfo = (Document) collectionInfo.get("cursor", Document.class).get("firstBatch", List.class).iterator()
.next();
}
if (!collectionInfo.containsKey("options")) {
return new Document();
}
return collectionInfo.get("options", Document.class).get("validator", Document.class);
}
@Data
@org.springframework.data.mongodb.core.mapping.Document(collection = "persons")
static class Person {
@Field("first_name") String firstname;
String lastname;
Address address;
}
static class Address {
String city;
String street;
@Field("post_code") String postCode;
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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.schema;
import static org.springframework.data.mongodb.test.util.Assertions.*;
import java.util.Arrays;
import org.bson.Document;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
/**
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class MongoJsonSchemaUnitTests {
@Test // DATAMONGO-1835
public void toDocumentRendersSchemaCorrectly() {
MongoJsonSchema schema = MongoJsonSchema.builder() //
.required("firstname", "lastname") //
.build();
assertThat(schema.toDocument()).isEqualTo(new Document("$jsonSchema",
new Document("type", "object").append("required", Arrays.asList("firstname", "lastname"))));
}
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1835
public void throwsExceptionOnNullRoot() {
MongoJsonSchema.of(null);
}
}

View File

@@ -5,6 +5,8 @@
== What's new in Spring Data MongoDB 2.1
* Cursor-based aggregation execution.
* <<mongo-template.query.distinct,Distinct queries>> for imperative and reactive Template API.
* `validator` support for collections.
* `$jsonSchema` support for queries.
[[new-features.2-0-0]]
== What's new in Spring Data MongoDB 2.0

View File

@@ -1464,6 +1464,33 @@ AggregationResults<TagCount> results = template.aggregate(aggregation, "tags", T
WARNING: Indexes are only used if the collation used for the operation and the index collation matches.
[[mongo.jsonSchema]]
=== JSON Schema
As of version 3.6 MongoDB supports collections that validate ``Document``s against a provided JSON Schema. The schema itself and both validation action and level can be defined when creating the collection.
`CollectionOptions` provides the entry point to schema support for collections.
.Create collection with `$jsonSchema`
====
[source,java]
----
// TODO: add sample here!
----
====
Additionally it is also possible to query any collection for documents that match a given structure defined by a JSON Schema.
.Query collation for Documents matching a `$jsonSchema`
====
[source,java]
----
// TODO: add sample here!
----
====
NOTE: `$jsonSchema` can only be applied on the top level of a query and not property specific. Use the `properties` attribute of the schema to match against nested fields.
[[mongo.query.fluent-template-api]]
=== Fluent Template API