diff --git a/pom.xml b/pom.xml index 25a7ba08f5..971750afab 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1835-SNAPSHOT pom Spring Data MongoDB @@ -28,7 +28,7 @@ multi spring-data-mongodb 2.1.0.BUILD-SNAPSHOT - 3.5.0 + 3.6.0 1.6.0 1.19 diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 9baccaa905..711efc3a3c 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1835-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index 8ba393d38b..68a48a4d29 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1835-SNAPSHOT ../pom.xml @@ -49,7 +49,7 @@ org.springframework.data spring-data-mongodb - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1835-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index e5c865ea08..194d23a40b 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1835-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 41b711f7c8..423fcdd79b 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAMONGO-1835-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index 53cb2348c1..b1a7d8214b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -15,12 +15,19 @@ */ package org.springframework.data.mongodb.core; +import lombok.RequiredArgsConstructor; + 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 +41,7 @@ public class CollectionOptions { private @Nullable Long size; private @Nullable Boolean capped; private @Nullable Collation collation; + private Validator validator; /** * Constructs a new CollectionOptions instance. @@ -46,16 +54,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 +78,7 @@ public static CollectionOptions just(Collation collation) { 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 +88,7 @@ public static CollectionOptions just(Collation collation) { * @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 +99,7 @@ public static CollectionOptions empty() { * @since 2.0 */ public CollectionOptions capped() { - return new CollectionOptions(size, maxDocuments, true, collation); + return new CollectionOptions(size, maxDocuments, true, collation, validator); } /** @@ -101,7 +110,7 @@ public CollectionOptions capped() { * @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 +121,7 @@ public CollectionOptions maxDocuments(long maxDocuments) { * @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 +132,115 @@ public CollectionOptions size(long size) { * @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 + * {@link ValidationLevel#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 + * {@link ValidationLevel#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 + * {@link ValidationLevel#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 + * {@link ValidationAction#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 + * {@link ValidationAction#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 +280,73 @@ public Optional getCapped() { public Optional getCollation() { return Optional.ofNullable(collation); } + + /** + * Get the {@link MongoJsonSchema} for the collection. + * + * @return {@link Optional#empty()} if not set. + * @since 2.1 + */ + public Optional getValidator() { + return validator.isEmpty() ? Optional.empty() : Optional.of(validator); + } + + /** + * Encapsulation of Validator options. + * + * @author Christoph Strobl + * @since 2.1 + */ + @RequiredArgsConstructor + public static class Validator { + + private static final Validator NONE = new Validator(null, null, null); + + private final @Nullable MongoJsonSchema schema; + private final @Nullable ValidationLevel validationLevel; + private final @Nullable 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. + */ + public Optional getSchema() { + return Optional.ofNullable(schema); + } + + /** + * Get the {@code validationLevel} to apply. + * + * @return {@link Optional#empty()} if not set. + */ + public Optional getValidationLevel() { + return Optional.ofNullable(validationLevel); + } + + /** + * Get the {@code validationAction} to perform. + * + * @return @return {@link Optional#empty()} if not set. + */ + public Optional getValidationAction() { + return Optional.ofNullable(validationAction); + } + + /** + * @return {@literal true} if no arguments set. + */ + boolean isEmpty() { + return !Optionals.isAnyPresent(getSchema(), getValidationAction(), getValidationLevel()); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 338389d434..6b0612d554 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -63,6 +63,7 @@ import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; +import org.springframework.data.mongodb.core.CollectionOptions.Validator; import org.springframework.data.mongodb.core.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.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; @@ -121,6 +124,9 @@ import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; +import com.mongodb.DBCollection; +import com.mongodb.DBCursor; +import com.mongodb.Mongo; import com.mongodb.MongoClient; import com.mongodb.MongoException; import com.mongodb.ReadPreference; @@ -133,14 +139,7 @@ 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 +190,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 +235,7 @@ public MongoTemplate(MongoDbFactory mongoDbFactory, @Nullable MongoConverter mon 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 +546,9 @@ public MongoCollection createCollection(Class entityClass) { */ public MongoCollection createCollection(Class 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 +2230,21 @@ public MongoCollection doInDB(MongoDatabase db) throws MongoException, 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 coll = db.getCollection(collectionName, Document.class); @@ -2339,6 +2357,35 @@ List doFind(String collectionName, Document query, Document fields, Cl 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.
+ * 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 != null && 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 +2395,14 @@ protected Document convertToDocument(@Nullable CollectionOptions collectionOptio 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; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 46e6ea434f..71fffaa920 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -69,15 +69,7 @@ import org.springframework.data.mongodb.core.aggregation.AggregationOptions; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; -import org.springframework.data.mongodb.core.convert.DbRefProxyHandler; -import org.springframework.data.mongodb.core.convert.DbRefResolver; -import org.springframework.data.mongodb.core.convert.DbRefResolverCallback; -import org.springframework.data.mongodb.core.convert.MappingMongoConverter; -import org.springframework.data.mongodb.core.convert.MongoConverter; -import org.springframework.data.mongodb.core.convert.MongoCustomConversions; -import org.springframework.data.mongodb.core.convert.MongoWriter; -import org.springframework.data.mongodb.core.convert.QueryMapper; -import org.springframework.data.mongodb.core.convert.UpdateMapper; +import org.springframework.data.mongodb.core.convert.*; import org.springframework.data.mongodb.core.index.IndexOperationsAdapter; import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher; import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator; @@ -125,6 +117,7 @@ import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.ReturnDocument; import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.ValidationOptions; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; import com.mongodb.reactivestreams.client.AggregatePublisher; @@ -176,6 +169,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati private final PersistenceExceptionTranslator exceptionTranslator; private final QueryMapper queryMapper; private final UpdateMapper updateMapper; + private final JsonSchemaMapper schemaMapper; private final SpelAwareProxyProjectionFactory projectionFactory; private @Nullable WriteConcern writeConcern; @@ -220,6 +214,7 @@ public ReactiveMongoTemplate(ReactiveMongoDatabaseFactory mongoDatabaseFactory, this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter() : 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 @@ -486,14 +481,15 @@ public Mono> createCollection(Class entityClass */ public Mono> createCollection(Class entityClass, @Nullable CollectionOptions collectionOptions) { - return createCollection(determineCollectionName(entityClass), collectionOptions); + return doCreateCollection(determineCollectionName(entityClass), + convertToCreateCollectionOptions(collectionOptions, entityClass)); } /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#createCollection(java.lang.String) */ - public Mono> createCollection(final String collectionName) { + public Mono> createCollection(String collectionName) { return doCreateCollection(collectionName, new CreateCollectionOptions()); } @@ -501,8 +497,8 @@ public Mono> createCollection(final String collectionN * (non-Javadoc) * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#createCollection(java.lang.String, org.springframework.data.mongodb.core.CollectionOptions) */ - public Mono> createCollection(final String collectionName, - final CollectionOptions collectionOptions) { + public Mono> createCollection(String collectionName, + @Nullable CollectionOptions collectionOptions) { return doCreateCollection(collectionName, convertToCreateCollectionOptions(collectionOptions)); } @@ -814,8 +810,7 @@ protected Flux aggregate(Aggregation aggregation, String collectionName, } ReadDocumentCallback readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName); - return execute(collectionName, - collection -> aggregateAndMap(collection, pipeline, options, readCallback)); + return execute(collectionName, collection -> aggregateAndMap(collection, pipeline, options, readCallback)); } private Flux aggregateAndMap(MongoCollection collection, List pipeline, @@ -1995,17 +1990,36 @@ private Document addFieldsForProjection(Document fields, Class domainType, Cl } protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions) { + return convertToCreateCollectionOptions(collectionOptions, Object.class); + } - CreateCollectionOptions result = new CreateCollectionOptions(); + protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions, + Class entityType) { - if (collectionOptions != null) { + CreateCollectionOptions result = new CreateCollectionOptions(); - collectionOptions.getCapped().ifPresent(result::capped); - collectionOptions.getSize().ifPresent(result::sizeInBytes); - collectionOptions.getMaxDocuments().ifPresent(result::maxDocuments); - collectionOptions.getCollation().map(Collation::toMongoCollation).ifPresent(result::collation); + if (collectionOptions == null) { + return result; } + collectionOptions.getCapped().ifPresent(result::capped); + collectionOptions.getSize().ifPresent(result::sizeInBytes); + collectionOptions.getMaxDocuments().ifPresent(result::maxDocuments); + collectionOptions.getCollation().map(Collation::toMongoCollation).ifPresent(result::collation); + + collectionOptions.getValidator().ifPresent(it -> { + + ValidationOptions validationOptions = new ValidationOptions(); + + it.getValidationAction().ifPresent(validationOptions::validationAction); + it.getValidationLevel().ifPresent(validationOptions::validationLevel); + + it.getSchema() + .ifPresent(val -> validationOptions.validator(schemaMapper.mapSchema(val.toDocument(), entityType))); + + result.validationOptions(validationOptions); + }); + return result; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/JsonSchemaMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/JsonSchemaMapper.java new file mode 100644 index 0000000000..1530221d3c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/JsonSchemaMapper.java @@ -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.
+ * 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 new {@link Document} containing the mapped {@literal $jsonSchema} never {@literal null}. + */ + Document mapSchema(Document jsonSchema, Class type); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java new file mode 100644 index 0000000000..2d290e0676 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java @@ -0,0 +1,164 @@ +/* + * 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 + * @author Mark Paluch + * @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, 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))); + } + + @SuppressWarnings("unchecked") + 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 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 mapRequiredProperties(@Nullable PersistentEntity entity, + Collection sourceFields) { + + return sourceFields.stream() /// + .map(fieldName -> getFieldName(entity, fieldName)) // + .collect(Collectors.toList()); + } + + private Document mapProperty(@Nullable PersistentEntity 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 mapEnumValues(Iterable values) { + + List converted = new ArrayList<>(); + for (Object val : values) { + converted.add(converter.convertToMongoType(val)); + } + return converted; + } + + private String getFieldName(@Nullable PersistentEntity entity, String sourceField) { + + if (entity == null) { + return sourceField; + } + + MongoPersistentProperty property = entity.getPersistentProperty(sourceField); + return property != null ? property.getFieldName() : sourceField; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 74bb2e44f1..4fc2f7af28 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -80,6 +80,7 @@ private enum MetaMapping { private final MongoConverter converter; private final MappingContext, 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 QueryMapper(MongoConverter converter) { this.converter = converter; this.mappingContext = converter.getMappingContext(); this.exampleMapper = new MongoExampleMapper(converter); + this.schemaMapper = new MongoJsonSchemaMapper(converter); } public Document getMappedObject(Bson query, Optional> entity) { @@ -272,6 +274,10 @@ protected Document getMappedKeyword(Keyword keyword, @Nullable MongoPersistentEn return exampleMapper.getMappedExample(keyword.> 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 @@ protected boolean isKeyword(String candidate) { * Value object to capture a query keyword representation. * * @author Oliver Gierke + * @author Christoph Strobl */ static class Keyword { @@ -666,6 +673,16 @@ public String getKey() { public 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); + } } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index 650d185025..df83362859 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -24,6 +24,7 @@ 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.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 static Criteria byExample(Example example) { 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 MongoDB Query operator: + * $jsonSchema + */ + 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 Criteria type(int t) { return this; } + /** + * Creates a criterion using the {@literal $type} operator. + * + * @param type must not be {@literal null}. + * @return this + * @since 2.1 + * @see MongoDB Query operator: $type + */ + 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 Criteria alike(Example sample) { return this; } + /** + * Creates a criterion ({@code $jsonSchema}) matching documents against a given structure defined by the + * {@link MongoJsonSchema}.
+ * NOTE: {@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 MongoDB Query operator: + * $jsonSchema + */ + 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 *

diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java new file mode 100644 index 0000000000..07aed09173 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java @@ -0,0 +1,43 @@ +/* + * 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.AllArgsConstructor; +import lombok.NonNull; + +import org.bson.Document; + +/** + * Value object representing a MongoDB-specific JSON schema which is the default {@link MongoJsonSchema} implementation. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + */ +@AllArgsConstructor +class DefaultMongoJsonSchema implements MongoJsonSchema { + + private final @NonNull JsonSchemaObject root; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.MongoJsonSchema#toDocument() + */ + @Override + public Document toDocument() { + return new Document("$jsonSchema", root.toDocument()); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DocumentJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DocumentJsonSchema.java new file mode 100644 index 0000000000..ee1299543e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DocumentJsonSchema.java @@ -0,0 +1,42 @@ +/* + * 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.AllArgsConstructor; +import lombok.NonNull; + +import org.bson.Document; + +/** + * JSON schema backed by a {@link org.bson.Document} object. + * + * @author Mark Paluch + * @since 2.1 + */ +@AllArgsConstructor +class DocumentJsonSchema implements MongoJsonSchema { + + private final @NonNull Document document; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.MongoJsonSchema#toDocument() + */ + @Override + public Document toDocument() { + return new Document("$jsonSchema", new Document(document)); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java new file mode 100644 index 0000000000..0fc95ee098 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java @@ -0,0 +1,930 @@ +/* + * 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 + * @author Mark Paluch + * @since 2.1 + */ +public class IdentifiableJsonSchemaProperty implements JsonSchemaProperty { + + protected final String identifier; + protected final T jsonSchemaObjectDelegate; + + /** + * Creates a new {@link IdentifiableJsonSchemaProperty} for {@code identifier} and {@code jsonSchemaObject}. + * + * @param identifier must not be {@literal null}. + * @param jsonSchemaObject must not be {@literal null}. + */ + 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 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 { + + 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 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 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 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 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 { + + /** + * @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}. + */ + 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 possibleValues) { + return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); + } + + /** + * @param allOf must not be {@literal null}. + * @return new instance of {@link StringJsonSchemaProperty}. + * @see StringJsonSchemaObject#allOf(Collection) + */ + public StringJsonSchemaProperty allOf(Collection 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 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 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 { + + /** + * @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}. + */ + ObjectJsonSchemaProperty(String identifier, ObjectJsonSchemaObject schemaObject) { + super(identifier, schemaObject); + } + + /** + * @param range must not be {@literal null}. + * @return new instance of {@link ObjectJsonSchemaProperty}. + * @see ObjectJsonSchemaObject#propertiesCount + */ + public ObjectJsonSchemaProperty propertiesCount(Range range) { + return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.propertiesCount(range)); + } + + /** + * @param count must not be {@literal null}. + * @return new instance of {@link ObjectJsonSchemaProperty}. + * @see ObjectJsonSchemaObject#minProperties(int) + */ + public ObjectJsonSchemaProperty minProperties(int count) { + return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minProperties(count)); + } + + /** + * @param count must not be {@literal null}. + * @return new instance of {@link ObjectJsonSchemaProperty}. + * @see ObjectJsonSchemaObject#maxProperties(int) + */ + public ObjectJsonSchemaProperty maxProperties(int count) { + return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxProperties(count)); + } + + /** + * @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 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 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 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 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 { + + /** + * @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 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 possibleValues) { + return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); + } + + /** + * @param allOf must not be {@literal null}. + * @return new instance of {@link NumericJsonSchemaProperty}. + * @see NumericJsonSchemaObject#allOf(Collection) + */ + public NumericJsonSchemaProperty allOf(Collection 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 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 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 { + + /** + * @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 range) { + return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.range(range)); + } + + /** + * @param count + * @return new instance of {@link ArrayJsonSchemaProperty}. + * @see ArrayJsonSchemaObject#minItems(int) + */ + public ArrayJsonSchemaProperty minItems(int count) { + return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minItems(count)); + } + + /** + * @param count + * @return new instance of {@link ArrayJsonSchemaProperty}. + * @see ArrayJsonSchemaObject#maxItems(int) + */ + public ArrayJsonSchemaProperty maxItems(int count) { + return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxItems(count)); + } + + /** + * @param items must not be {@literal null}. + * @return new instance of {@link ArrayJsonSchemaProperty}. + * @see ArrayJsonSchemaObject#items(Collection) + */ + public ArrayJsonSchemaProperty items(JsonSchemaObject... items) { + return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(Arrays.asList(items))); + } + + /** + * @param items must not be {@literal null}. + * @return new instance of {@link ArrayJsonSchemaProperty}. + * @see ArrayJsonSchemaObject#items(Collection) + */ + public ArrayJsonSchemaProperty items(Collection items) { + return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(items)); + } + + /** + * @param additionalItemsAllowed + * @return new instance of {@link ArrayJsonSchemaProperty}. + * @see ArrayJsonSchemaObject#additionalItems(boolean) + */ + public ArrayJsonSchemaProperty additionalItems(boolean additionalItemsAllowed) { + return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.additionalItems(additionalItemsAllowed)); + } + + /** + * @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 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 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 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 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 ArrayJsonSchemaObject#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 { + + 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 { + + NullJsonSchemaProperty(String identifier, NullJsonSchemaObject schemaObject) { + super(identifier, schemaObject); + } + + /** + * @param description must not be {@literal null}. + * @return new instance of {@link NullJsonSchemaProperty}. + * @see NullJsonSchemaObject#description(String) + */ + public NullJsonSchemaProperty description(String description) { + return new NullJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); + } + + /** + * @return new instance of {@link NullJsonSchemaProperty}. + * @see NullJsonSchemaObject#generateDescription() + */ + public NullJsonSchemaProperty generatedDescription() { + return new NullJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription()); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java new file mode 100644 index 0000000000..9b223b769b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java @@ -0,0 +1,468 @@ +/* + * 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.BsonTimestamp; +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; + +/** + * Interface that can be implemented by objects that know how to serialize themselves to JSON schema using + * {@link #toDocument()}. + *

+ * This class also declares factory methods for type-specific {@link JsonSchemaObject schema objects} such as + * {@link #string()} or {@link #object()}. For example: + * + *

+ * JsonSchemaProperty.object("address").properties(JsonSchemaProperty.string("city").minLength(3));
+ * 
+ * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + */ +public interface JsonSchemaObject { + + /** + * Get the set of types defined for this schema element.
+ * The {@link Set} is likely to contain only one element in most cases. + * + * @return never {@literal null}. + */ + Set getTypes(); + + /** + * Get the MongoDB specific representation.
+ * 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}. + * + * @param type Java class to create a {@link JsonSchemaObject} for. May be {@literal null} or {@link Void#getClass()} + * to create {@link Type#nullType() null} type. + * @return never {@literal null}. + * @throws IllegalArgumentException if {@code type} is not supported. + */ + static TypedJsonSchemaObject of(@Nullable Class type) { + + if (type == null || type.equals(Void.class)) { + 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(BsonTimestamp.class, type)) { + return of(Type.timestampType()); + } + + 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 + Type OBJECT_ID = bsonTypeOf("objectId"); + Type REGULAR_EXPRESSION = bsonTypeOf("regex"); + Type DOUBLE = bsonTypeOf("double"); + Type BINARY_DATA = bsonTypeOf("binData"); + Type DATE = bsonTypeOf("date"); + Type JAVA_SCRIPT = bsonTypeOf("javascript"); + Type INT_32 = bsonTypeOf("int"); + Type INT_64 = bsonTypeOf("long"); + Type DECIMAL_128 = bsonTypeOf("decimal"); + Type TIMESTAMP = bsonTypeOf("timestamp"); + + Set 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 + Type OBJECT = jsonTypeOf("object"); + Type ARRAY = jsonTypeOf("array"); + Type NUMBER = jsonTypeOf("number"); + Type BOOLEAN = jsonTypeOf("boolean"); + Type STRING = jsonTypeOf("string"); + Type NULL = jsonTypeOf("null"); + + Set 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); + } + + /** + * @return all known JSON types. + */ + static Set jsonTypes() { + return JSON_TYPES; + } + + /** + * @return all known BSON types. + */ + static Set 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 + class JsonType implements Type { + + private final String name; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type#representation() + */ + @Override + public String representation() { + return "type"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type#value() + */ + @Override + public String value() { + return name; + } + } + + /** + * @author Christpoh Strobl + * @since 2.1 + */ + @RequiredArgsConstructor + class BsonType implements Type { + + private final String name; + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type#representation() + */ + @Override + public String representation() { + return "bsonType"; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type#value() + */ + @Override + public String value() { + return name; + } + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java new file mode 100644 index 0000000000..6a9b8ac4a7 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java @@ -0,0 +1,216 @@ +/* + * 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.AccessLevel; +import lombok.RequiredArgsConstructor; + +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 + * @author Mark Paluch + * @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); + } + + /** + * Builder for {@link IdentifiableJsonSchemaProperty}. + */ + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + class JsonSchemaPropertyBuilder { + + private final String identifier; + + /** + * Configure a {@link Type} for the property. + * + * @param type must not be {@literal null}. + * @return + */ + public IdentifiableJsonSchemaProperty ofType(Type type) { + return new IdentifiableJsonSchemaProperty<>(identifier, TypedJsonSchemaObject.of(type)); + } + + /** + * Configure a {@link TypedJsonSchemaObject} for the property. + * + * @param schemaObject must not be {@literal null}. + * @return + */ + public IdentifiableJsonSchemaProperty with(TypedJsonSchemaObject schemaObject) { + return new IdentifiableJsonSchemaProperty<>(identifier, schemaObject); + } + + /** + * @return an untyped {@link IdentifiableJsonSchemaProperty}. + */ + public IdentifiableJsonSchemaProperty withoutType() { + return new IdentifiableJsonSchemaProperty<>(identifier, UntypedJsonSchemaObject.newInstance()); + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java new file mode 100644 index 0000000000..86778bc143 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java @@ -0,0 +1,278 @@ +/* + * 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; + +/** + * Interface defining MongoDB-specific JSON schema object. New objects can be built with {@link #builder()}, for + * example: + * + *
+ * 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 }
+      }
+    }
+  }
+}
+ * 
+ * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + * @see UntypedJsonSchemaObject + * @see TypedJsonSchemaObject + */ +public interface MongoJsonSchema { + + /** + * Create the {@link Document} containing the specified {@code $jsonSchema}.
+ * Property and field names need to be mapped to the domain type ones by running the {@link Document} through a + * {@link org.springframework.data.mongodb.core.convert.JsonSchemaMapper} to apply field name customization. + * + * @return never {@literal null}. + */ + Document toDocument(); + + /** + * Create a new {@link MongoJsonSchema} for a given root object. + * + * @param root must not be {@literal null}. + * @return + */ + static MongoJsonSchema of(JsonSchemaObject root) { + return new DefaultMongoJsonSchema(root); + } + + /** + * Create a new {@link MongoJsonSchema} for a given root {@link Document} containing the schema definition. + * + * @param document must not be {@literal null}. + * @return + */ + static MongoJsonSchema of(Document document) { + return new DocumentJsonSchema(document); + } + + /** + * Obtain a new {@link MongoJsonSchemaBuilder} to fluently define the schema. + * + * @return new instance of {@link MongoJsonSchemaBuilder}. + */ + static MongoJsonSchemaBuilder builder() { + return new MongoJsonSchemaBuilder(); + } + + /** + * {@link MongoJsonSchemaBuilder} provides a fluent API for defining a {@link MongoJsonSchema}. + * + * @author Christoph Strobl + */ + class MongoJsonSchemaBuilder { + + private ObjectJsonSchemaObject root; + + MongoJsonSchemaBuilder() { + root = new ObjectJsonSchemaObject(); + } + + /** + * @param count + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#minProperties(int) + */ + public MongoJsonSchemaBuilder minProperties(int count) { + + root = root.minProperties(count); + return this; + } + + /** + * @param count + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#maxProperties(int) + */ + public MongoJsonSchemaBuilder maxProperties(int count) { + + root = root.maxProperties(count); + return this; + } + + /** + * @param properties + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#required(String...) + */ + public MongoJsonSchemaBuilder required(String... properties) { + + root = root.required(properties); + return this; + } + + /** + * @param additionalPropertiesAllowed + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#additionalProperties(boolean) + */ + public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesAllowed) { + + root = root.additionalProperties(additionalPropertiesAllowed); + return this; + } + + /** + * @param schema + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject) + */ + public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema) { + + root = root.additionalProperties(schema); + return this; + } + + /** + * @param properties + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...) + */ + public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) { + + root = root.properties(properties); + return this; + } + + /** + * @param properties + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#patternProperties(JsonSchemaProperty...) + */ + public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties) { + + root = root.patternProperties(properties); + return this; + } + + /** + * @param property + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#property(JsonSchemaProperty) + */ + public MongoJsonSchemaBuilder property(JsonSchemaProperty property) { + + root = root.property(property); + return this; + } + + /** + * @param possibleValues + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#possibleValues(Collection) + */ + public MongoJsonSchemaBuilder possibleValues(Set possibleValues) { + + root = root.possibleValues(possibleValues); + return this; + } + + /** + * @param allOf + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see UntypedJsonSchemaObject#allOf(Collection) + */ + public MongoJsonSchemaBuilder allOf(Set allOf) { + + root = root.allOf(allOf); + return this; + } + + /** + * @param anyOf + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see UntypedJsonSchemaObject#anyOf(Collection) + */ + public MongoJsonSchemaBuilder anyOf(Set anyOf) { + + root = root.anyOf(anyOf); + return this; + } + + /** + * @param oneOf + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see UntypedJsonSchemaObject#oneOf(Collection) + */ + public MongoJsonSchemaBuilder oneOf(Set oneOf) { + + root = root.oneOf(oneOf); + return this; + } + + /** + * @param notMatch + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see UntypedJsonSchemaObject#notMatch(JsonSchemaObject) + */ + public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) { + + root = root.notMatch(notMatch); + return this; + } + + /** + * @param description + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @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); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java new file mode 100644 index 0000000000..6ca4831f11 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java @@ -0,0 +1,1450 @@ +/* + * 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.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.bson.Document; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Range.Bound; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * A {@link JsonSchemaObject} of a given {@link org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + */ +public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { + + protected final Set types; + + /** + * @param type can be {@literal null}. + * @param description can be {@literal null}. + * @param restrictions can be {@literal null}. + */ + TypedJsonSchemaObject(@Nullable Type type, @Nullable String description, boolean generateDescription, + @Nullable Restrictions restrictions) { + + this(type != null ? Collections.singleton(type) : Collections.emptySet(), description, generateDescription, + restrictions); + } + + /** + * @param types must not be {@literal null}. + * @param description can be {@literal null}. + * @param restrictions can be {@literal null}. Defaults to {@link Restrictions#empty()}. + */ + TypedJsonSchemaObject(Set types, @Nullable String description, boolean generateDescription, + @Nullable Restrictions restrictions) { + + super(restrictions, description, generateDescription); + + Assert.notNull(types, "Types must not be null! Please consider using 'Collections.emptySet()'."); + + this.types = types; + } + + /** + * Creates new {@link TypedJsonSchemaObject} of given types. + * + * @param types must not be {@literal null}. + * @return + */ + public static TypedJsonSchemaObject of(Type... types) { + + Assert.notNull(types, "Types must not be null!"); + Assert.noNullElements(types, "Types must not contain null!"); + + return new TypedJsonSchemaObject(new LinkedHashSet<>(Arrays.asList(types)), null, false, Restrictions.empty()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#getTypes() + */ + @Override + public Set getTypes() { + return types; + } + + /** + * Set the {@literal description}. + * + * @param description must not be {@literal null}. + * @return new instance of {@link TypedJsonSchemaObject}. + */ + @Override + public TypedJsonSchemaObject description(String description) { + return new TypedJsonSchemaObject(types, description, generateDescription, restrictions); + } + + /** + * Auto generate the {@literal description} if not explicitly set. + * + * @param description must not be {@literal null}. + * @return new instance of {@link TypedJsonSchemaObject}. + */ + @Override + public TypedJsonSchemaObject generatedDescription() { + return new TypedJsonSchemaObject(types, description, true, restrictions); + } + + /** + * {@literal enum}erates all possible values of the field. + * + * @param possibleValues must not be {@literal null}. + * @return new instance of {@link TypedJsonSchemaObject}. + */ + @Override + public TypedJsonSchemaObject possibleValues(Collection possibleValues) { + return new TypedJsonSchemaObject(types, description, generateDescription, + restrictions.possibleValues(possibleValues)); + } + + /** + * The field value must match all specified schemas. + * + * @param allOf must not be {@literal null}. + * @return new instance of {@link TypedJsonSchemaObject}. + */ + @Override + public TypedJsonSchemaObject allOf(Collection allOf) { + return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.allOf(allOf)); + } + + /** + * 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}. + */ + @Override + public TypedJsonSchemaObject anyOf(Collection anyOf) { + return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.anyOf(anyOf)); + } + + /** + * The field value must match exactly one of the specified schemas. + * + * @param oneOf must not be {@literal null}. + * @return new instance of {@link TypedJsonSchemaObject}. + */ + @Override + public TypedJsonSchemaObject oneOf(Collection oneOf) { + return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.oneOf(oneOf)); + } + + /** + * The field value must not match the specified schemas. + * + * @param oneOf must not be {@literal null}. + * @return new instance of {@link TypedJsonSchemaObject}. + */ + @Override + public TypedJsonSchemaObject notMatch(JsonSchemaObject notMatch) { + return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.notMatch(notMatch)); + } + + /** + * 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(); + + if (!CollectionUtils.isEmpty(types)) { + + Type theType = types.iterator().next(); + if (types.size() == 1) { + document.append(theType.representation(), theType.value()); + } else { + document.append(theType.representation(), types.stream().map(Type::value).collect(Collectors.toList())); + } + } + + getOrCreateDescription().ifPresent(val -> document.append("description", val)); + document.putAll(restrictions.toDocument()); + + return document; + } + + private Optional 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.
+ * Called by {@link #toDocument()} when no explicit {@link #description} is set. + * + * @return can be {@literal null}. + */ + @Nullable + protected String generateDescription() { + return null; + } + + /** + * {@link JsonSchemaObject} implementation of {@code type : 'object'} schema elements.
+ * Provides programmatic access to schema specifics like {@literal required, properties, patternProperties,...} via a + * fluent API producing immutable {@link JsonSchemaObject schema objects}. + * + * @author Christoph Strobl + * @since 2.1 + */ + public static class ObjectJsonSchemaObject extends TypedJsonSchemaObject { + + private @Nullable Range propertiesCount; + private @Nullable Object additionalProperties; + private List requiredProperties = Collections.emptyList(); + private List properties = Collections.emptyList(); + private List patternProperties = Collections.emptyList(); + + public ObjectJsonSchemaObject() { + this(null, false, null); + } + + /** + * @param description can be {@literal null}. + * @param restrictions can be {@literal null}; + */ + ObjectJsonSchemaObject(@Nullable String description, boolean generateDescription, + @Nullable Restrictions restrictions) { + super(Type.objectType(), description, generateDescription, restrictions); + } + + /** + * Define the {@literal minProperties} and {@literal maxProperties} via the given {@link Range}.
+ * In-/Exclusions via {@link Bound#isInclusive() range bounds} are not taken into account. + * + * @param range must not be {@literal null}. Consider {@link Range#unbounded()} instead. + * @return new instance of {@link ObjectJsonSchemaObject}. + */ + public ObjectJsonSchemaObject propertiesCount(Range range) { + + ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.propertiesCount = range; + return newInstance; + } + + /** + * Define the {@literal minProperties}. + * + * @param count the allowed minimal number of properties. + * @return new instance of {@link ObjectJsonSchemaObject}. + */ + public ObjectJsonSchemaObject minProperties(int count) { + + Bound upper = this.propertiesCount != null ? this.propertiesCount.getUpperBound() : Bound.unbounded(); + return propertiesCount(Range.of(Bound.inclusive(count), upper)); + } + + /** + * Define the {@literal maxProperties}. + * + * @param count the allowed maximum number of properties. + * @return new instance of {@link ObjectJsonSchemaObject}. + */ + public ObjectJsonSchemaObject maxProperties(int count) { + + Bound lower = this.propertiesCount != null ? this.propertiesCount.getLowerBound() : Bound.unbounded(); + return propertiesCount(Range.of(lower, Bound.inclusive(count))); + } + + /** + * Define the Object’s {@literal required} properties. + * + * @param properties the names of required properties. + * @return new instance of {@link ObjectJsonSchemaObject}. + */ + public ObjectJsonSchemaObject required(String... properties) { + + ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.requiredProperties = new ArrayList<>(this.requiredProperties.size() + properties.length); + newInstance.requiredProperties.addAll(this.requiredProperties); + newInstance.requiredProperties.addAll(Arrays.asList(properties)); + + return newInstance; + } + + /** + * If set to {@literal false}, additional fields besides + * {@link #properties(JsonSchemaProperty...)}/{@link #patternProperties(JsonSchemaProperty...)} are not allowed. + * + * @param additionalPropertiesAllowed + * @return new instance of {@link ObjectJsonSchemaObject}. + */ + public ObjectJsonSchemaObject additionalProperties(boolean additionalPropertiesAllowed) { + + ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.additionalProperties = additionalPropertiesAllowed; + + return newInstance; + } + + /** + * If specified, additional fields must validate against the given schema. + * + * @param schema must not be {@literal null}. + * @return new instance of {@link ObjectJsonSchemaObject}. + */ + public ObjectJsonSchemaObject additionalProperties(ObjectJsonSchemaObject schema) { + + ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.additionalProperties = schema; + return newInstance; + } + + /** + * Append the objects properties along with the {@link JsonSchemaObject} validating against. + * + * @param properties must not be {@literal null}. + * @return new instance of {@link ObjectJsonSchemaObject}. + */ + public ObjectJsonSchemaObject properties(JsonSchemaProperty... properties) { + + ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.properties = new ArrayList<>(this.properties.size() + properties.length); + newInstance.properties.addAll(this.properties); + newInstance.properties.addAll(Arrays.asList(properties)); + + return newInstance; + } + + /** + * Append regular expression patterns along with the {@link JsonSchemaObject} matching properties validating + * against. + * + * @param regularExpressions must not be {@literal null}. + * @return new instance of {@link ObjectJsonSchemaObject}. + */ + public ObjectJsonSchemaObject patternProperties(JsonSchemaProperty... regularExpressions) { + + ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.patternProperties = new ArrayList<>(this.patternProperties.size() + regularExpressions.length); + newInstance.patternProperties.addAll(this.patternProperties); + newInstance.patternProperties.addAll(Arrays.asList(regularExpressions)); + + return newInstance; + } + + /** + * Append the objects property along with the {@link JsonSchemaObject} validating against. + * + * @param property must not be {@literal null}. + * @return new instance of {@link ObjectJsonSchemaObject}. + */ + public ObjectJsonSchemaObject property(JsonSchemaProperty property) { + return properties(property); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.UntypedJsonSchemaObject#possibleValues(java.util.Collection) + */ + @Override + public ObjectJsonSchemaObject possibleValues(Collection possibleValues) { + return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.UntypedJsonSchemaObject#allOf(java.util.Collection) + */ + @Override + public ObjectJsonSchemaObject allOf(Collection allOf) { + return newInstance(description, generateDescription, restrictions.allOf(allOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.UntypedJsonSchemaObject#anyOf(java.util.Collection) + */ + @Override + public ObjectJsonSchemaObject anyOf(Collection anyOf) { + return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.UntypedJsonSchemaObject#oneOf(java.util.Collection) + */ + @Override + public ObjectJsonSchemaObject oneOf(Collection oneOf) { + return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.UntypedJsonSchemaObject#notMatch(org.springframework.data.mongodb.core.schema.JsonSchemaObject) + */ + @Override + public ObjectJsonSchemaObject notMatch(JsonSchemaObject notMatch) { + return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.UntypedJsonSchemaObject#description(java.lang.String) + */ + @Override + public ObjectJsonSchemaObject description(String description) { + return newInstance(description, generateDescription, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.UntypedJsonSchemaObject#generatedDescription() + */ + @Override + public ObjectJsonSchemaObject generatedDescription() { + return newInstance(description, true, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument() + */ + @Override + public Document toDocument() { + + Document doc = new Document(super.toDocument()); + if (!CollectionUtils.isEmpty(requiredProperties)) { + doc.append("required", requiredProperties); + } + + if (propertiesCount != null) { + + propertiesCount.getLowerBound().getValue().ifPresent(it -> doc.append("minProperties", it)); + propertiesCount.getUpperBound().getValue().ifPresent(it -> doc.append("maxProperties", it)); + } + + if (!CollectionUtils.isEmpty(properties)) { + doc.append("properties", reduceToDocument(properties)); + } + + if (!CollectionUtils.isEmpty(patternProperties)) { + doc.append("patternProperties", reduceToDocument(patternProperties)); + } + + if (additionalProperties != null) { + + doc.append("additionalProperties", additionalProperties instanceof JsonSchemaObject + ? ((JsonSchemaObject) additionalProperties).toDocument() : additionalProperties); + } + return doc; + } + + private ObjectJsonSchemaObject newInstance(@Nullable String description, boolean generateDescription, + Restrictions restrictions) { + + ObjectJsonSchemaObject newInstance = new ObjectJsonSchemaObject(description, generateDescription, restrictions); + + newInstance.properties = this.properties; + newInstance.requiredProperties = this.requiredProperties; + newInstance.additionalProperties = this.additionalProperties; + newInstance.propertiesCount = this.propertiesCount; + newInstance.patternProperties = this.patternProperties; + + return newInstance; + } + + private Document reduceToDocument(Collection source) { + + return source.stream() // + .map(JsonSchemaProperty::toDocument) // + .collect(Document::new, Document::putAll, (target, propertyDocument) -> {}); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription() + */ + @Override + protected String generateDescription() { + + String description = "Must be an object"; + + if (propertiesCount != null) { + description += String.format(" with %s properties", propertiesCount); + } + + if (!CollectionUtils.isEmpty(requiredProperties)) { + + if (requiredProperties.size() == 1) { + description += String.format(" where %sis mandatory", requiredProperties.iterator().next()); + } else { + description += String.format(" where %s are mandatory", + StringUtils.collectionToDelimitedString(requiredProperties, ", ")); + } + } + if (additionalProperties instanceof Boolean) { + description += (((Boolean) additionalProperties) ? " " : " not ") + "allowing additional properties"; + } + + if (!CollectionUtils.isEmpty(properties)) { + description += String.format(" defining restrictions for %s", StringUtils.collectionToDelimitedString( + properties.stream().map(JsonSchemaProperty::getIdentifier).collect(Collectors.toList()), ", ")); + } + + if (!CollectionUtils.isEmpty(patternProperties)) { + description += String.format(" defining restrictions for patterns %s", StringUtils.collectionToDelimitedString( + patternProperties.stream().map(JsonSchemaProperty::getIdentifier).collect(Collectors.toList()), ", ")); + } + + return description + "."; + } + } + + /** + * {@link JsonSchemaObject} implementation of {@code type : 'number'}, {@code bsonType : 'int'}, + * {@code bsonType : 'long'}, {@code bsonType : 'double'} and {@code bsonType : 'decimal128'} schema elements.
+ * Provides programmatic access to schema specifics like {@literal multipleOf, minimum, maximum,...} via a fluent API + * producing immutable {@link JsonSchemaObject schema objects}. + * + * @author Christoph Strobl + * @since 2.1 + */ + public static class NumericJsonSchemaObject extends TypedJsonSchemaObject { + + private static final Set NUMERIC_TYPES = new HashSet<>( + Arrays.asList(Type.doubleType(), Type.intType(), Type.longType(), Type.numberType(), Type.bigDecimalType())); + + @Nullable Number multipleOf; + @Nullable Range range; + + NumericJsonSchemaObject() { + this(Type.numberType()); + } + + NumericJsonSchemaObject(Type type) { + this(type, null, false); + } + + private NumericJsonSchemaObject(Type type, @Nullable String description, boolean generateDescription) { + this(Collections.singleton(type), description, generateDescription, null); + } + + private NumericJsonSchemaObject(Set types, @Nullable String description, boolean generateDescription, + @Nullable Restrictions restrictions) { + + super(validateTypes(types), description, generateDescription, restrictions); + } + + /** + * Set the value a valid field value must be the multiple of. + * + * @param value must not be {@literal null}. + * @return must not be {@literal null}. + */ + NumericJsonSchemaObject multipleOf(Number value) { + + Assert.notNull(value, "Value must not be null!"); + NumericJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.multipleOf = value; + + return newInstance; + } + + /** + * Set the {@link Range} of valid field values translating to {@literal minimum}, {@literal exclusiveMinimum}, + * {@literal maximum} and {@literal exclusiveMaximum}. + * + * @param range must not be {@literal null}. + * @return new instance of {@link NumericJsonSchemaObject}. + */ + public NumericJsonSchemaObject within(Range range) { + + Assert.notNull(range, "Range must not be null!"); + + NumericJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.range = range; + + return newInstance; + } + + /** + * Set {@literal minimum} to given {@code min} value and {@literal exclusiveMinimum} to {@literal true}. + * + * @param min must not be {@literal null}. + * @return new instance of {@link NumericJsonSchemaObject}. + */ + @SuppressWarnings("unchecked") + public NumericJsonSchemaObject gt(Number min) { + + Assert.notNull(min, "Min must not be null!"); + + Bound upper = this.range != null ? this.range.getUpperBound() : Bound.unbounded(); + return within(Range.of(createBound(min, false), upper)); + } + + /** + * Set {@literal minimum} to given {@code min} value and {@literal exclusiveMinimum} to {@literal false}. + * + * @param min must not be {@literal null}. + * @return new instance of {@link NumericJsonSchemaObject}. + */ + @SuppressWarnings("unchecked") + public NumericJsonSchemaObject gte(Number min) { + + Assert.notNull(min, "Min must not be null!"); + + Bound upper = this.range != null ? this.range.getUpperBound() : Bound.unbounded(); + return within(Range.of(createBound(min, true), upper)); + } + + /** + * Set {@literal maximum} to given {@code max} value and {@literal exclusiveMaximum} to {@literal true}. + * + * @param max must not be {@literal null}. + * @return new instance of {@link NumericJsonSchemaObject}. + */ + @SuppressWarnings("unchecked") + public NumericJsonSchemaObject lt(Number max) { + + Assert.notNull(max, "Max must not be null!"); + + Bound lower = this.range != null ? this.range.getLowerBound() : Bound.unbounded(); + return within(Range.of(lower, createBound(max, false))); + } + + /** + * Set {@literal maximum} to given {@code max} value and {@literal exclusiveMaximum} to {@literal false}. + * + * @param max must not be {@literal null}. + * @return new instance of {@link NumericJsonSchemaObject}. + */ + @SuppressWarnings("unchecked") + NumericJsonSchemaObject lte(Number max) { + + Assert.notNull(max, "Max must not be null!"); + + Bound lower = this.range != null ? this.range.getLowerBound() : Bound.unbounded(); + return within(Range.of(lower, createBound(max, true))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection) + */ + @Override + public NumericJsonSchemaObject possibleValues(Collection possibleValues) { + return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#allOf(java.util.Collection) + */ + @Override + public NumericJsonSchemaObject allOf(Collection allOf) { + return newInstance(description, generateDescription, restrictions.allOf(allOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#anyOf(java.util.Collection) + */ + @Override + public NumericJsonSchemaObject anyOf(Collection anyOf) { + return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#oneOf(java.util.Collection) + */ + @Override + public NumericJsonSchemaObject oneOf(Collection oneOf) { + return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#notMatch(org.springframework.data.mongodb.core.schema.JsonSchemaObject) + */ + @Override + public NumericJsonSchemaObject notMatch(JsonSchemaObject notMatch) { + return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#description(java.lang.String) + */ + @Override + public NumericJsonSchemaObject description(String description) { + return newInstance(description, generateDescription, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generatedDescription() + */ + @Override + public NumericJsonSchemaObject generatedDescription() { + return newInstance(description, true, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument() + */ + @Override + public Document toDocument() { + + Document doc = new Document(super.toDocument()); + + if (multipleOf != null) { + doc.append("multipleOf", multipleOf); + } + + if (range != null) { + + if (range.getLowerBound().isBounded()) { + + range.getLowerBound().getValue().ifPresent(it -> doc.append("minimum", it)); + if (!range.getLowerBound().isInclusive()) { + doc.append("exclusiveMinimum", true); + } + } + + if (range.getUpperBound().isBounded()) { + + range.getUpperBound().getValue().ifPresent(it -> doc.append("maximum", it)); + if (!range.getUpperBound().isInclusive()) { + doc.append("exclusiveMaximum", true); + } + } + } + + return doc; + } + + private NumericJsonSchemaObject newInstance(@Nullable String description, boolean generateDescription, + Restrictions restrictions) { + + NumericJsonSchemaObject newInstance = new NumericJsonSchemaObject(types, description, generateDescription, + restrictions); + + newInstance.multipleOf = this.multipleOf; + newInstance.range = this.range; + + return newInstance; + + } + + private static Bound createBound(Number number, boolean inclusive) { + + if (number instanceof Long) { + return inclusive ? Bound.inclusive((Long) number) : Bound.exclusive((Long) number); + } + if (number instanceof Double) { + return inclusive ? Bound.inclusive((Double) number) : Bound.exclusive((Double) number); + } + if (number instanceof Float) { + return inclusive ? Bound.inclusive((Float) number) : Bound.exclusive((Float) number); + } + if (number instanceof Integer) { + return inclusive ? Bound.inclusive((Integer) number) : Bound.exclusive((Integer) number); + } + if (number instanceof BigDecimal) { + return inclusive ? Bound.inclusive((BigDecimal) number) : Bound.exclusive((BigDecimal) number); + } + + throw new IllegalArgumentException("Unsupported numeric value."); + } + + private static Set validateTypes(Set types) { + + types.forEach(type -> { + Assert.isTrue(NUMERIC_TYPES.contains(type), + () -> String.format("%s is not a valid numeric type. Expected one of %s.", type, NUMERIC_TYPES)); + }); + + return types; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription() + */ + @Override + protected String generateDescription() { + + String description = "Must be a numeric value"; + + if (multipleOf != null) { + description += String.format(" multiple of %s", multipleOf); + } + if (range != null) { + description += String.format(" within range %s", range); + } + + return description + "."; + } + } + + /** + * {@link JsonSchemaObject} implementation of {@code type : 'string'} schema elements.
+ * Provides programmatic access to schema specifics like {@literal minLength, maxLength, pattern,...} via a fluent API + * producing immutable {@link JsonSchemaObject schema objects}. + * + * @author Christoph Strobl + * @since 2.1 + */ + public static class StringJsonSchemaObject extends TypedJsonSchemaObject { + + @Nullable Range length; + @Nullable String pattern; + + StringJsonSchemaObject() { + this(null, false, null); + } + + private StringJsonSchemaObject(@Nullable String description, boolean generateDescription, + @Nullable Restrictions restrictions) { + super(Type.stringType(), description, generateDescription, restrictions); + } + + /** + * Define the valid length range ({@literal minLength} and {@literal maxLength}) for a valid field. + * + * @param range must not be {@literal null}. + * @return new instance of {@link StringJsonSchemaObject}. + */ + public StringJsonSchemaObject length(Range range) { + + Assert.notNull(range, "Range must not be null!"); + + StringJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.length = range; + + return newInstance; + } + + /** + * Define the valid length range ({@literal minLength}) for a valid field. + * + * @param length + * @return new instance of {@link StringJsonSchemaObject}. + */ + public StringJsonSchemaObject minLength(int length) { + + Bound upper = this.length != null ? this.length.getUpperBound() : Bound.unbounded(); + return length(Range.of(Bound.inclusive(length), upper)); + } + + /** + * Define the valid length range ({@literal maxLength}) for a valid field. + * + * @param length + * @return new instance of {@link StringJsonSchemaObject}. + */ + public StringJsonSchemaObject maxLength(int length) { + + Bound lower = this.length != null ? this.length.getLowerBound() : Bound.unbounded(); + return length(Range.of(lower, Bound.inclusive(length))); + } + + /** + * Define the regex pattern to validate field values against. + * + * @param pattern must not be {@literal null}. + * @return new instance of {@link StringJsonSchemaObject}. + */ + public StringJsonSchemaObject matching(String pattern) { + + Assert.notNull(pattern, "Pattern must not be null!"); + + StringJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.pattern = pattern; + + return newInstance; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection) + */ + @Override + public StringJsonSchemaObject possibleValues(Collection possibleValues) { + return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#allOf(java.util.Collection) + */ + @Override + public StringJsonSchemaObject allOf(Collection allOf) { + return newInstance(description, generateDescription, restrictions.allOf(allOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#anyOf(java.util.Collection) + */ + @Override + public StringJsonSchemaObject anyOf(Collection anyOf) { + return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#oneOf(java.util.Collection) + */ + @Override + public StringJsonSchemaObject oneOf(Collection oneOf) { + return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#notMatch(org.springframework.data.mongodb.core.schema.JsonSchemaObject) + */ + @Override + public StringJsonSchemaObject notMatch(JsonSchemaObject notMatch) { + return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#description(java.lang.String) + */ + @Override + public StringJsonSchemaObject description(String description) { + return newInstance(description, generateDescription, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generatedDescription() + */ + @Override + public StringJsonSchemaObject generatedDescription() { + return newInstance(description, true, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#toDocument() + */ + @Override + public Document toDocument() { + + Document doc = new Document(super.toDocument()); + + if (length != null) { + + length.getLowerBound().getValue().ifPresent(it -> doc.append("minLength", it)); + length.getUpperBound().getValue().ifPresent(it -> doc.append("maxLength", it)); + } + + if (!StringUtils.isEmpty(pattern)) { + doc.append("pattern", pattern); + } + + return doc; + } + + private StringJsonSchemaObject newInstance(@Nullable String description, boolean generateDescription, + Restrictions restrictions) { + + StringJsonSchemaObject newInstance = new StringJsonSchemaObject(description, generateDescription, restrictions); + + newInstance.length = this.length; + newInstance.pattern = this.pattern; + + return newInstance; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription() + */ + @Override + protected String generateDescription() { + + String description = "Must be a string"; + + if (length != null) { + description += String.format(" with length %s", length); + } + if (pattern != null) { + description += String.format(" matching %s", pattern); + } + + return description + "."; + } + } + + /** + * {@link JsonSchemaObject} implementation of {@code type : 'array'} schema elements.
+ * Provides programmatic access to schema specifics like {@literal range, minItems, maxItems,...} via a fluent API + * producing immutable {@link JsonSchemaObject schema objects}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + */ + public static class ArrayJsonSchemaObject extends TypedJsonSchemaObject { + + private @Nullable Boolean uniqueItems; + private @Nullable Boolean additionalItems; + private @Nullable Range range; + private Collection items = Collections.emptyList(); + + ArrayJsonSchemaObject() { + this(null, false, null); + } + + private ArrayJsonSchemaObject(@Nullable String description, boolean generateDescription, + @Nullable Restrictions restrictions) { + super(Collections.singleton(Type.arrayType()), description, generateDescription, restrictions); + } + + /** + * Define the whether the array must contain unique items. + * + * @param uniqueItems + * @return new instance of {@link ArrayJsonSchemaObject}. + */ + public ArrayJsonSchemaObject uniqueItems(boolean uniqueItems) { + + ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.uniqueItems = uniqueItems; + + return newInstance; + } + + /** + * Define the {@literal minItems} and {@literal maxItems} via the given {@link Range}.
+ * In-/Exclusions via {@link Bound#isInclusive() range bounds} are not taken into account. + * + * @param range must not be {@literal null}. Consider {@link Range#unbounded()} instead. + * @return new instance of {@link ArrayJsonSchemaObject}. + */ + public ArrayJsonSchemaObject range(Range range) { + + ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.range = range; + + return newInstance; + } + + /** + * Define the {@literal maxItems}. + * + * @param count the allowed minimal number of array items. + * @return new instance of {@link ArrayJsonSchemaObject}. + */ + public ArrayJsonSchemaObject minItems(int count) { + + Bound upper = this.range != null ? this.range.getUpperBound() : Bound.unbounded(); + return range(Range.of(Bound.inclusive(count), upper)); + } + + /** + * Define the {@literal maxItems}. + * + * @param count the allowed maximal number of array items. + * @return new instance of {@link ArrayJsonSchemaObject}. + */ + public ArrayJsonSchemaObject maxItems(int count) { + + Bound lower = this.range != null ? this.range.getLowerBound() : Bound.unbounded(); + return range(Range.of(lower, Bound.inclusive(count))); + } + + /** + * Define the {@code items} allowed in the array. + * + * @param items the allowed items in the array. + * @return new instance of {@link ArrayJsonSchemaObject}. + */ + public ArrayJsonSchemaObject items(Collection items) { + + ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.items = new ArrayList<>(items); + + return newInstance; + } + + /** + * If set to {@literal false}, no additional items besides {@link #items(Collection)} are allowed. + * + * @param additionalItemsAllowed {@literal true} to allow additional items in the array, {@literal false} otherwise. + * @return new instance of {@link ArrayJsonSchemaObject}. + */ + public ArrayJsonSchemaObject additionalItems(boolean additionalItemsAllowed) { + + ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); + newInstance.additionalItems = additionalItemsAllowed; + + return newInstance; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection) + */ + @Override + public ArrayJsonSchemaObject possibleValues(Collection possibleValues) { + return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#allOf(java.util.Collection) + */ + @Override + public ArrayJsonSchemaObject allOf(Collection allOf) { + return newInstance(description, generateDescription, restrictions.allOf(allOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#anyOf(java.util.Collection) + */ + @Override + public ArrayJsonSchemaObject anyOf(Collection anyOf) { + return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#oneOf(java.util.Collection) + */ + @Override + public ArrayJsonSchemaObject oneOf(Collection oneOf) { + return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#notMatch(org.springframework.data.mongodb.core.schema.JsonSchemaObject) + */ + @Override + public ArrayJsonSchemaObject notMatch(JsonSchemaObject notMatch) { + return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#description(java.lang.String) + */ + @Override + public ArrayJsonSchemaObject description(String description) { + return newInstance(description, generateDescription, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generatedDescription() + */ + @Override + public ArrayJsonSchemaObject generatedDescription() { + return newInstance(description, true, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#toDocument() + */ + @Override + public Document toDocument() { + + Document doc = new Document(super.toDocument()); + + if (!CollectionUtils.isEmpty(items)) { + doc.append("items", items.size() == 1 ? items.iterator().next() + : items.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList())); + } + + if (range != null) { + + range.getLowerBound().getValue().ifPresent(it -> doc.append("minItems", it)); + range.getUpperBound().getValue().ifPresent(it -> doc.append("maxItems", it)); + } + + if (ObjectUtils.nullSafeEquals(uniqueItems, Boolean.TRUE)) { + doc.append("uniqueItems", true); + } + + if (additionalItems != null) { + doc.append("additionalItems", additionalItems); + } + + return doc; + } + + private ArrayJsonSchemaObject newInstance(@Nullable String description, boolean generateDescription, + Restrictions restrictions) { + + ArrayJsonSchemaObject newInstance = new ArrayJsonSchemaObject(description, generateDescription, restrictions); + + newInstance.uniqueItems = this.uniqueItems; + newInstance.range = this.range; + newInstance.items = this.items; + newInstance.additionalItems = this.additionalItems; + + return newInstance; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription() + */ + @Override + protected String generateDescription() { + + String description = "Must be an array"; + + if (ObjectUtils.nullSafeEquals(uniqueItems, Boolean.TRUE)) { + description += " of unique values"; + } + + if (ObjectUtils.nullSafeEquals(additionalItems, Boolean.TRUE)) { + description += " with additional items"; + } + + if (ObjectUtils.nullSafeEquals(additionalItems, Boolean.FALSE)) { + description += " with no additional items"; + } + + if (range != null) { + description += String.format(" having size %s", range); + } + + if (!ObjectUtils.isEmpty(items)) { + description += String.format(" with items %s", StringUtils.collectionToDelimitedString( + items.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList()), ", ")); + } + + return description + "."; + } + } + + /** + * {@link JsonSchemaObject} implementation of {@code type : 'boolean'} schema elements.
+ * Provides programmatic access to schema specifics via a fluent API producing immutable {@link JsonSchemaObject + * schema objects}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + */ + public static class BooleanJsonSchemaObject extends TypedJsonSchemaObject { + + BooleanJsonSchemaObject() { + this(null, false, null); + } + + private BooleanJsonSchemaObject(@Nullable String description, boolean generateDescription, + @Nullable Restrictions restrictions) { + super(Type.booleanType(), description, generateDescription, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection) + */ + @Override + public BooleanJsonSchemaObject possibleValues(Collection possibleValues) { + return new BooleanJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#allOf(java.util.Collection) + */ + @Override + public BooleanJsonSchemaObject allOf(Collection allOf) { + return new BooleanJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#anyOf(java.util.Collection) + */ + @Override + public BooleanJsonSchemaObject anyOf(Collection anyOf) { + return new BooleanJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#oneOf(java.util.Collection) + */ + @Override + public BooleanJsonSchemaObject oneOf(Collection oneOf) { + return new BooleanJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#notMatch(org.springframework.data.mongodb.core.schema.JsonSchemaObject) + */ + @Override + public BooleanJsonSchemaObject notMatch(JsonSchemaObject notMatch) { + return new BooleanJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#description(java.lang.String) + */ + @Override + public BooleanJsonSchemaObject description(String description) { + return new BooleanJsonSchemaObject(description, generateDescription, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generatedDescription() + */ + @Override + public BooleanJsonSchemaObject generatedDescription() { + return new BooleanJsonSchemaObject(description, true, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription() + */ + @Override + protected String generateDescription() { + return "Must be a boolean."; + } + } + + /** + * {@link JsonSchemaObject} implementation of {@code type : 'null'} schema elements.
+ * Provides programmatic access to schema specifics via a fluent API producing immutable {@link JsonSchemaObject + * schema objects}. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + */ + static class NullJsonSchemaObject extends TypedJsonSchemaObject { + + NullJsonSchemaObject() { + this(null, false, null); + } + + private NullJsonSchemaObject(@Nullable String description, boolean generateDescription, + @Nullable Restrictions restrictions) { + super(Type.nullType(), description, generateDescription, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection) + */ + @Override + public NullJsonSchemaObject possibleValues(Collection possibleValues) { + return new NullJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#allOf(java.util.Collection) + */ + @Override + public NullJsonSchemaObject allOf(Collection allOf) { + return new NullJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#anyOf(java.util.Collection) + */ + @Override + public NullJsonSchemaObject anyOf(Collection anyOf) { + return new NullJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#oneOf(java.util.Collection) + */ + @Override + public NullJsonSchemaObject oneOf(Collection oneOf) { + return new NullJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#notMatch(org.springframework.data.mongodb.core.schema.JsonSchemaObject) + */ + @Override + public NullJsonSchemaObject notMatch(JsonSchemaObject notMatch) { + return new NullJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#description(java.lang.String) + */ + @Override + public NullJsonSchemaObject description(String description) { + return new NullJsonSchemaObject(description, generateDescription, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generatedDescription() + */ + @Override + public NullJsonSchemaObject generatedDescription() { + return new NullJsonSchemaObject(description, true, restrictions); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription() + */ + @Override + protected String generateDescription() { + return "Must be null."; + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java new file mode 100644 index 0000000000..124bac65fa --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java @@ -0,0 +1,290 @@ +/* + * 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.AccessLevel; +import lombok.RequiredArgsConstructor; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +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. + * Schema objects are immutable. Calling methods to configure properties creates a new object instance. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + */ +public class UntypedJsonSchemaObject implements JsonSchemaObject { + + protected final Restrictions restrictions; + protected final @Nullable String description; + protected final boolean generateDescription; + + UntypedJsonSchemaObject(@Nullable Restrictions restrictions, @Nullable String description, + boolean generateDescription) { + + this.description = description; + this.restrictions = restrictions != null ? restrictions : Restrictions.empty(); + this.generateDescription = generateDescription; + } + + /** + * Create a new instance of {@link UntypedJsonSchemaObject}. + * + * @return the new {@link UntypedJsonSchemaObject}. + */ + public static UntypedJsonSchemaObject newInstance() { + return new UntypedJsonSchemaObject(null, null, false); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#getTypes() + */ + @Override + public Set 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. + * + * @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 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 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 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 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)); + + document.putAll(restrictions.toDocument()); + + return document; + } + + private Optional 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.
+ * Called by {@link #toDocument()} when no explicit {@link #description} is set. + * + * @return can be {@literal null}. + */ + @Nullable + protected String generateDescription() { + return null; + } + + /** + * {@link Restrictions} encapsulates common JSON schema restrictions like {@literal enum}, {@literal allOf}, … that + * are not tied to a specific type. + * + * @author Christoph Strobl + * @since 2.1 + */ + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) + static class Restrictions { + + private final Collection possibleValues; + private final Collection allOf; + private final Collection anyOf; + private final Collection oneOf; + private final @Nullable JsonSchemaObject 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 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 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 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 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", render(allOf)); + } + + if (!CollectionUtils.isEmpty(anyOf)) { + document.append("anyOf", render(anyOf)); + } + + if (!CollectionUtils.isEmpty(oneOf)) { + document.append("oneOf", render(oneOf)); + } + + if (notMatch != null) { + document.append("not", notMatch.toDocument()); + } + + return document; + } + + private static List render(Collection objects) { + return objects.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList()); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java new file mode 100644 index 0000000000..380d92af09 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java @@ -0,0 +1,6 @@ +/** + * MongoDB-specific JSON schema implementation classes. + */ +@org.springframework.lang.NonNullApi +@org.springframework.lang.NonNullFields +package org.springframework.data.mongodb.core.schema; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JsonSchemaQueryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JsonSchemaQueryTests.java new file mode 100644 index 0000000000..b7eef7e29f --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JsonSchemaQueryTests.java @@ -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 + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapperUnitTests.java new file mode 100644 index 0000000000..492d241891 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapperUnitTests.java @@ -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 requiredProperties = Arrays.asList("name", "gradePointAverage"); + List 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 + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java index 50938886c1..f0950b0371 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/CriteriaTests.java @@ -19,12 +19,15 @@ 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 void intersectsShouldWrapGeoJsonTypeInGeometryCorrectly() { 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")))))); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/JsonSchemaObjectUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/JsonSchemaObjectUnitTests.java new file mode 100644 index 0000000000..409a5d5f69 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/JsonSchemaObjectUnitTests.java @@ -0,0 +1,304 @@ +/* + * 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 + * @author Mark Paluch + */ +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().propertiesCount(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)); + } + + @Test // DATAMONGO-1835 + public void arrayObjectShouldRenderAdditionalItemsItemsCorrectly() { + + assertThat(array().additionalItems(true).generatedDescription().toDocument()) + .isEqualTo(new Document("type", "array").append("description", "Must be an array with additional items.") + .append("additionalItems", true)); + assertThat(array().additionalItems(false).generatedDescription().toDocument()) + .isEqualTo(new Document("type", "array").append("description", "Must be an array with no additional items.") + .append("additionalItems", false)); + } + + // ----------------- + // type : 'boolean' + // ----------------- + + @Test // DATAMONGO-1835 + public void booleanShouldRenderCorrectly() { + + assertThat(bool().generatedDescription().toDocument()) + .isEqualTo(new Document("type", "boolean").append("description", "Must be a boolean.")); + } + + // ----------------- + // type : 'null' + // ----------------- + + @Test // DATAMONGO-1835 + public void nullShouldRenderCorrectly() { + + assertThat(nil().generatedDescription().toDocument()) + .isEqualTo(new Document("type", "null").append("description", "Must be null.")); + } + + // ----------------- + // 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"))); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/JsonSchemaPropertyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/JsonSchemaPropertyUnitTests.java new file mode 100644 index 0000000000..a4a5d5df71 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/JsonSchemaPropertyUnitTests.java @@ -0,0 +1,55 @@ +/* + * 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 org.junit.Test; +import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type; + +/** + * Unit tests for {@link JsonSchemaProperty}. + * + * @author Mark Paluch + */ +public class JsonSchemaPropertyUnitTests { + + @Test // DATAMONGO-1835 + public void shouldRenderInt32Correctly() { + assertThat(JsonSchemaProperty.int32("foo").toDocument()).containsEntry("foo.bsonType", "int"); + } + + @Test // DATAMONGO-1835 + public void shouldRenderInt64Correctly() { + assertThat(JsonSchemaProperty.int64("foo").toDocument()).containsEntry("foo.bsonType", "long"); + } + + @Test // DATAMONGO-1835 + public void shouldRenderDecimal128Correctly() { + assertThat(JsonSchemaProperty.decimal128("foo").toDocument()).containsEntry("foo.bsonType", "decimal"); + } + + @Test // DATAMONGO-1835 + public void shouldRenderNullCorrectly() { + assertThat(JsonSchemaProperty.nil("foo").toDocument()).containsEntry("foo.type", "null"); + } + + @Test // DATAMONGO-1835 + public void shouldRenderUntypedCorrectly() { + assertThat(JsonSchemaProperty.named("foo").ofType(Type.binaryType()).toDocument()).containsEntry("foo.bsonType", + "binData"); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java new file mode 100644 index 0000000000..0eabe3bd42 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java @@ -0,0 +1,186 @@ +/* + * 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; + +/** + * Integration tests for {@link MongoJsonSchema}. + * + * @author Christoph Strobl + */ +@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.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; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java new file mode 100644 index 0000000000..054c5d311f --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java @@ -0,0 +1,69 @@ +/* + * 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; + +/** + * Unit tests for {@link MongoJsonSchema}. + * + * @author Christoph Strobl + * @author Mark Paluch + */ +@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 // DATAMONGO-1835 + public void rendersDocumentBasedSchemaCorrectly() { + + Document document = MongoJsonSchema.builder() // + .required("firstname", "lastname") // + .build().toDocument(); + + MongoJsonSchema jsonSchema = MongoJsonSchema.of(document.get("$jsonSchema", Document.class)); + + assertThat(jsonSchema.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((JsonSchemaObject) null); + } + + @Test(expected = IllegalArgumentException.class) // DATAMONGO-1835 + public void throwsExceptionOnNullDocument() { + MongoJsonSchema.of((Document) null); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/ReactiveMongoJsonSchemaTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/ReactiveMongoJsonSchemaTests.java new file mode 100644 index 0000000000..bf2d11a177 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/ReactiveMongoJsonSchemaTests.java @@ -0,0 +1,140 @@ +/* + * 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 reactor.test.StepVerifier; + +import java.time.Duration; +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.AbstractReactiveMongoConfiguration; +import org.springframework.data.mongodb.core.CollectionOptions; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +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.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoClients; + +/** + * Integration tests for {@link MongoJsonSchema} using reactive infrastructure. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class ReactiveMongoJsonSchemaTests { + + public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_6_0 = MongoVersionRule.atLeast(Version.parse("3.6.0")); + + @Configuration + static class Config extends AbstractReactiveMongoConfiguration { + + @Override + public MongoClient reactiveMongoClient() { + return MongoClients.create(); + } + + @Override + protected String getDatabaseName() { + return "json-schema-tests"; + } + } + + @Autowired ReactiveMongoTemplate template; + + @Before + public void setUp() { + StepVerifier.create(template.dropCollection(Person.class)).verifyComplete(); + } + + @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(); + + StepVerifier.create(template.createCollection(Person.class, CollectionOptions.empty().schema(schema))) + .expectNextCount(1).verifyComplete(); + + Document $jsonSchema = new MongoJsonSchemaMapper(template.getConverter()).mapSchema(schema.toDocument(), + Person.class); + + 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))) + .block(Duration.ofSeconds(5)); + + if (collectionInfo == null) { + throw new DataRetrievalFailureException(String.format("Collection %s was not found.", collectionName)); + } + + 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; + } +} diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 00c8abd3c4..526c234786 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -5,6 +5,8 @@ == What's new in Spring Data MongoDB 2.1 * Cursor-based aggregation execution. * <> for imperative and reactive Template API. +* <>. +* <> for queries and collection creation. [[new-features.2-0-0]] == What's new in Spring Data MongoDB 2.0 diff --git a/src/main/asciidoc/reference/mongo-3.adoc b/src/main/asciidoc/reference/mongo-3.adoc index 3ab427b4db..281a63a602 100644 --- a/src/main/asciidoc/reference/mongo-3.adoc +++ b/src/main/asciidoc/reference/mongo-3.adoc @@ -81,6 +81,21 @@ In order to use authentication with XML configuration use the `credentials` attr ---- +[[mongo.mongo-3.validation]] +=== Server-side Validation + +MongoDB supports https://docs.mongodb.com/manual/core/schema-validation/[Schema Validation] as of version 3.2 with query operators +and as of version 3.6 JSON-schema based validation. + +This chapter will point out the specialties for validation in MongoDB and how to apply JSON schema validation. + +[[mongo.mongo-3.validation.json-schema]] +==== JSON Schema Validation + +MongoDB 3.6 allows validation and querying of documents using JSON schema draft 4 including core specification and validation specification, with some differences. `$jsonSchema` can be used in a document validator (when creating a collection), which enforces that inserted or updated documents are valid against the schema. It can also be used to query for documents with the `find` command or `$match` aggregation stage. + +Spring Data MongoDB supports MongoDB's specific JSON schema implementation to define and use schemas. See <> for further details. + [[mongo.mongo-3.misc]] === Other things to be aware of diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index a4b994505a..38f757e13c 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1054,6 +1054,8 @@ As you can see most methods return the `Criteria` object to provide a fluent sty * `Criteria` *regex* `(String re)` Creates a criterion using a `$regex` * `Criteria` *size* `(int s)` Creates a criterion using the `$size` operator * `Criteria` *type* `(int t)` Creates a criterion using the `$type` operator +* `Criteria` *matchingDocumentStructure* `(MongoJsonSchema schema)` Creates a criterion using the `$jsonSchema` operator for <>. `$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. + There are also methods on the Criteria class for geospatial queries. Here is a listing but look at the section on <> to see them in action. @@ -1464,6 +1466,170 @@ AggregationResults 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. + +.Sample JSON schema +==== +[source,json] +---- +{ + "type": "object", <1> + + "required": [ "firstname", "lastname" ], <2> + + "properties": { <3> + + "firstname": { <4> + "type": "string", + "enum": [ "luke", "han" ] + }, + "address": { <5> + "type": "object", + "properties": { + "postCode": { "type": "string", "minLength": 4, "maxLength": 5 } + } + } + } +} +---- +<1> JSON schema documents always describe a whole document from its root. A schema is a schema object itself that can contain +embedded schema objects describing properties and subdocuments. +<2> `required` is a property describing which properties are required in a document. It can be specified optionally along of other +schema constraints. See MongoDB's documentation on https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#available-keywords[available keywords]. +<3> `properties` is related to a schema object describing an `object` type. It contains property-specific schema constraints. +<4> `firstname` specifies constrains for the `firsname` field inside the document. Here it's a string-based properties declaring + possible field values. +<5> `address` is a subdocument defining a schema for values in its `postCode` field. +==== + +You can provide a schema either by specifying a schema document (i.e. using the `Document` API by parsing or building a document object) or by building it with Spring Data's JSON schema utilities in `org.springframework.data.mongodb.core.schema`. `MongoJsonSchema` is the entry point for all JSON schema-related operations. + +.Creating a JSON schema +==== +[source,java] +---- +MongoJsonSchema.builder() <1> + .required("firstname", "lastname") <2> + + .properties( + string("firstname").possibleValues("luke", "han"), <3> + + object("address") + .properties(string("postCode").minLength(4).maxLength(5))) + + .build(); <4> +---- +<1> Obtain a schema builder to configure the schema with a fluent API. +<2> Configure required properties. +<3> Configure the String-typed `firstname` field allowing only `luke` and `han` values. Properties can be typed or untyped. Use a static import of `JsonSchemaProperty` to make the syntax slightly more compact and to get entrypoints like `string(…)`. +<4> Build the schema object. Use the schema to either create a collection or <>. +==== + +`CollectionOptions` provides the entry point to schema support for collections. + +.Create collection with `$jsonSchema` +==== +[source,java] +---- +MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname").build(); + +template.createCollection(Person.class, CollectionOptions.empty().schema(schema)); +---- +==== + +You can use a schema to query any collection for documents that match a given structure defined by a JSON schema. + +.Query for Documents matching a `$jsonSchema` +==== +[source,java] +---- +MongoJsonSchema schema = MongoJsonSchema.builder().required("firstname", "lastname").build(); + +template.find(query(matchingDocumentStructure(schema)), Person.class); +---- +==== + +[cols="3,1,6", options="header"] +.Supported JSON schema types +|=== +| Schema Type +| Java Type +| Schema Properties + +| `untyped` +| - +| `description`, generated `description`, `enum`, `allOf`, `anyOf`, `oneOf`, `not` + +| `object` +| `Object` +| `required`, `additionalProperties`, `properties`, `minProperties`, `maxProperties`, `patternProperties` + +| `array` +| any array except `byte[]` +| `uniqueItems`, `additionalItems`, `items`, `minItems`, `maxItems` + +| `string` +| `String` +| `minLength`, `maxLentgth`, `pattern` + +| `int` +| `int`, `Integer` +| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum` + +| `long` +| `long`, `Long` +| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum` + +| `double` +| `float`, `Float`, `double`, `Double` +| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum` + +| `decimal` +| `BigDecimal` +| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum` + +| `number` +| `Number` +| `multipleOf`, `minimum`, `exclusiveMinimum`, `maximum`, `exclusiveMaximum` + +| `binData` +| `byte[]` +| + +| `boolean` +| `boolean`, `Boolean` +| + +| `null` +| `null` +| + +| `objectId` +| `ObjectId` +| + +| `date` +| `java.util.Date` +| + +| `timestamp` +| `BsonTimestamp` +| + +| `regex` +| `java.util.regex.Pattern` +| + +|=== + +NOTE: `untyped` is a generic type that is inherited by all typed schema types providing all `untyped` schema properties to typed schema types. + +For more information, see https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#op._S_jsonSchema[$jsonSchema]. + [[mongo.query.fluent-template-api]] === Fluent Template API