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 383ac1fae..b1a7d8214 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,6 +15,8 @@ */ package org.springframework.data.mongodb.core; +import lombok.RequiredArgsConstructor; + import java.util.Optional; import org.springframework.data.mongodb.core.query.Collation; @@ -146,7 +148,8 @@ public class CollectionOptions { } /** - * Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to {@code off}. + * Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to + * {@link ValidationLevel#OFF}. * * @return new {@link CollectionOptions}. * @since 2.1 @@ -156,7 +159,8 @@ public class CollectionOptions { } /** - * Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to {@code strict}. + * Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to + * {@link ValidationLevel#STRICT}. * * @return new {@link CollectionOptions}. * @since 2.1 @@ -167,7 +171,7 @@ public class CollectionOptions { /** * Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to - * {@code moderate}. + * {@link ValidationLevel#MODERATE}. * * @return new {@link CollectionOptions}. * @since 2.1 @@ -177,7 +181,8 @@ public class CollectionOptions { } /** - * Create new {@link CollectionOptions} with already given settings and {@code validationAction} set to {@code warn}. + * Create new {@link CollectionOptions} with already given settings and {@code validationAction} set to + * {@link ValidationAction#WARN}. * * @return new {@link CollectionOptions}. * @since 2.1 @@ -187,7 +192,8 @@ public class CollectionOptions { } /** - * Create new {@link CollectionOptions} with already given settings and {@code validationAction} set to {@code error}. + * Create new {@link CollectionOptions} with already given settings and {@code validationAction} set to + * {@link ValidationAction#ERROR}. * * @return new {@link CollectionOptions}. * @since 2.1 @@ -291,25 +297,18 @@ public class CollectionOptions { * @author Christoph Strobl * @since 2.1 */ + @RequiredArgsConstructor public static class Validator { private static final Validator NONE = new Validator(null, null, null); - private @Nullable MongoJsonSchema schema; - private @Nullable ValidationLevel validationLevel; - private @Nullable ValidationAction validationAction; - - private Validator(@Nullable MongoJsonSchema schema, @Nullable ValidationLevel validationLevel, - @Nullable ValidationAction validationAction) { - - this.schema = schema; - this.validationLevel = validationLevel; - this.validationAction = validationAction; - } + 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() { @@ -321,7 +320,6 @@ public class CollectionOptions { * * @return {@link Optional#empty()} if not set. */ - @Nullable public Optional getSchema() { return Optional.ofNullable(schema); } @@ -331,7 +329,6 @@ public class CollectionOptions { * * @return {@link Optional#empty()} if not set. */ - @Nullable public Optional getValidationLevel() { return Optional.ofNullable(validationLevel); } @@ -341,7 +338,6 @@ public class CollectionOptions { * * @return @return {@link Optional#empty()} if not set. */ - @Nullable public Optional getValidationAction() { return Optional.ofNullable(validationAction); } 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 4878fe0ad..6b0612d55 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 @@ -124,6 +124,9 @@ import org.springframework.util.ObjectUtils; 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; @@ -2370,7 +2373,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Document doc = convertToDocument(collectionOptions); - if (collectionOptions.getValidator().isPresent()) { + if (collectionOptions != null && collectionOptions.getValidator().isPresent()) { Validator v = collectionOptions.getValidator().get(); v.getSchema().ifPresent(val -> doc.put("validator", schemaMapper.mapSchema(val.toDocument(), targetType))); } 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 46e6ea434..71fffaa92 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.AggregationOperationCon 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.FindOneAndDeleteOptions; 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 class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati 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 class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati */ 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 class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati * (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 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } 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 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } 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/MongoJsonSchemaMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java index 8b23b4258..2d290e067 100644 --- 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 @@ -34,6 +34,7 @@ import org.springframework.util.Assert; * provided domain type. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.1 */ public class MongoJsonSchemaMapper implements JsonSchemaMapper { @@ -60,7 +61,7 @@ public class MongoJsonSchemaMapper implements JsonSchemaMapper { } /* - * (non-Javadoc) + * (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) { @@ -78,7 +79,8 @@ public class MongoJsonSchemaMapper implements JsonSchemaMapper { mapSchemaObject(mappingContext.getPersistentEntity(type), jsonSchema.get($JSON_SCHEMA, Document.class))); } - private Document mapSchemaObject(@Nullable PersistentEntity entity, Document source) { + @SuppressWarnings("unchecked") + private Document mapSchemaObject(@Nullable PersistentEntity entity, Document source) { Document sink = new Document(source); 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 000000000..07aed0917 --- /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 000000000..ee1299543 --- /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 index 50b5804f3..0fc95ee09 100644 --- 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 @@ -32,16 +32,23 @@ import org.springframework.util.Assert; /** * {@link JsonSchemaProperty} implementation. - * + * * @author Christoph Strobl + * @author Mark Paluch * @since 2.1 */ public class IdentifiableJsonSchemaProperty implements JsonSchemaProperty { - protected String identifier; - protected T jsonSchemaObjectDelegate; + protected final String identifier; + protected final T jsonSchemaObjectDelegate; - public IdentifiableJsonSchemaProperty(String identifier, T jsonSchemaObject) { + /** + * 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!"); @@ -85,7 +92,7 @@ public class IdentifiableJsonSchemaProperty implemen */ public static class UntypedJsonSchemaProperty extends IdentifiableJsonSchemaProperty { - public UntypedJsonSchemaProperty(String identifier, UntypedJsonSchemaObject jsonSchemaObject) { + UntypedJsonSchemaProperty(String identifier, UntypedJsonSchemaObject jsonSchemaObject) { super(identifier, jsonSchemaObject); } @@ -201,7 +208,7 @@ public class IdentifiableJsonSchemaProperty implemen * {@literal null} nor {@literal empty}. * @param schemaObject must not be {@literal null}. */ - public StringJsonSchemaProperty(String identifier, StringJsonSchemaObject schemaObject) { + StringJsonSchemaProperty(String identifier, StringJsonSchemaObject schemaObject) { super(identifier, schemaObject); } @@ -274,8 +281,7 @@ public class IdentifiableJsonSchemaProperty implemen * @see StringJsonSchemaObject#possibleValues(Collection) */ public StringJsonSchemaProperty possibleValues(Collection possibleValues) { - return new StringJsonSchemaProperty(identifier, - jsonSchemaObjectDelegate.possibleValues((Collection) possibleValues)); + return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } /** @@ -345,35 +351,35 @@ public class IdentifiableJsonSchemaProperty implemen * {@literal null} nor {@literal empty}. * @param schemaObject must not be {@literal null}. */ - public ObjectJsonSchemaProperty(String identifier, ObjectJsonSchemaObject schemaObject) { + ObjectJsonSchemaProperty(String identifier, ObjectJsonSchemaObject schemaObject) { super(identifier, schemaObject); } /** * @param range must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaProperty}. - * @see ObjectJsonSchemaObject#nrProperties + * @see ObjectJsonSchemaObject#propertiesCount */ - public ObjectJsonSchemaProperty nrProperties(Range range) { - return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.nrProperties(range)); + public ObjectJsonSchemaProperty propertiesCount(Range range) { + return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.propertiesCount(range)); } /** - * @param nrProperties must not be {@literal null}. + * @param count must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaProperty}. - * @see ObjectJsonSchemaObject#minNrProperties(int) + * @see ObjectJsonSchemaObject#minProperties(int) */ - public ObjectJsonSchemaProperty minNrProperties(int nrProperties) { - return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minNrProperties(nrProperties)); + public ObjectJsonSchemaProperty minProperties(int count) { + return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minProperties(count)); } /** - * @param nrProperties must not be {@literal null}. + * @param count must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaProperty}. - * @see ObjectJsonSchemaObject#maxNrProperties(int) + * @see ObjectJsonSchemaObject#maxProperties(int) */ - public ObjectJsonSchemaProperty maxNrProperties(int nrProperties) { - return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxNrProperties(nrProperties)); + public ObjectJsonSchemaProperty maxProperties(int count) { + return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxProperties(count)); } /** @@ -626,8 +632,7 @@ public class IdentifiableJsonSchemaProperty implemen * @see NumericJsonSchemaObject#possibleValues(Collection) */ public NumericJsonSchemaProperty possibleValues(Collection possibleValues) { - return new NumericJsonSchemaProperty(identifier, - jsonSchemaObjectDelegate.possibleValues((Collection) possibleValues)); + return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } /** @@ -719,6 +724,33 @@ public class IdentifiableJsonSchemaProperty implemen 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}. @@ -728,6 +760,15 @@ public class IdentifiableJsonSchemaProperty implemen 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}. @@ -812,7 +853,7 @@ public class IdentifiableJsonSchemaProperty implemen /** * @param description must not be {@literal null}. * @return new instance of {@link NumericJsonSchemaProperty}. - * @see ArrayJsonSchemaObjecty#description(String) + * @see ArrayJsonSchemaObject#description(String) */ public ArrayJsonSchemaProperty description(String description) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); @@ -835,7 +876,7 @@ public class IdentifiableJsonSchemaProperty implemen */ public static class BooleanJsonSchemaProperty extends IdentifiableJsonSchemaProperty { - public BooleanJsonSchemaProperty(String identifier, BooleanJsonSchemaObject schemaObject) { + BooleanJsonSchemaProperty(String identifier, BooleanJsonSchemaObject schemaObject) { super(identifier, schemaObject); } @@ -865,18 +906,25 @@ public class IdentifiableJsonSchemaProperty implemen */ public static class NullJsonSchemaProperty extends IdentifiableJsonSchemaProperty { - public NullJsonSchemaProperty(String identifier, NullJsonSchemaObject schemaObject) { + NullJsonSchemaProperty(String identifier, NullJsonSchemaObject schemaObject) { super(identifier, schemaObject); } /** * @param description must not be {@literal null}. - * @return new instance of {@link NumericJsonSchemaProperty}. + * @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 index 3aaa7826f..650e1711a 100644 --- 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 @@ -24,6 +24,7 @@ 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; @@ -36,7 +37,18 @@ 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 { @@ -134,7 +146,10 @@ public interface JsonSchemaObject { /** * Create a new {@link JsonSchemaObject} matching the given {@code type}. * + * @param type Java class to create a {@link JsonSchemaObject} for. May be {@literal null} 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) { @@ -167,6 +182,10 @@ public interface JsonSchemaObject { return of(Type.dateType()); } + if (ClassUtils.isAssignable(BsonTimestamp.class, type)) { + return of(Type.timestampType()); + } + if (ClassUtils.isAssignable(Pattern.class, type)) { return of(Type.regexType()); } @@ -200,11 +219,11 @@ public interface JsonSchemaObject { return of(Type.numberType()); } - throw new IllegalArgumentException(String.format("No json schema type found for %s.", type)); + 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}. + * Type represents either a JSON schema {@literal type} or a MongoDB specific {@literal bsonType}. * * @author Christoph Strobl * @since 2.1 @@ -212,29 +231,29 @@ public interface JsonSchemaObject { interface Type { // BSON TYPES - final Type OBJECT_ID = bsonTypeOf("objectId"); - final Type REGULAR_EXPRESSION = bsonTypeOf("regex"); - final Type DOUBLE = bsonTypeOf("double"); - final Type BINARY_DATA = bsonTypeOf("binData"); - final Type DATE = bsonTypeOf("date"); - final Type JAVA_SCRIPT = bsonTypeOf("javascript"); - final Type INT_32 = bsonTypeOf("int"); - final Type INT_64 = bsonTypeOf("long"); - final Type DECIMAL_128 = bsonTypeOf("decimal"); - final Type TIMESTAMP = bsonTypeOf("timestamp"); - - final Set BSON_TYPES = new HashSet<>(Arrays.asList(OBJECT_ID, REGULAR_EXPRESSION, DOUBLE, BINARY_DATA, DATE, + 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 - final Type OBJECT = jsonTypeOf("object"); - final Type ARRAY = jsonTypeOf("array"); - final Type NUMBER = jsonTypeOf("number"); - final Type BOOLEAN = jsonTypeOf("boolean"); - final Type STRING = jsonTypeOf("string"); - final Type NULL = jsonTypeOf("null"); + Type OBJECT = jsonTypeOf("object"); + Type ARRAY = jsonTypeOf("array"); + Type NUMBER = jsonTypeOf("number"); + Type BOOLEAN = jsonTypeOf("boolean"); + Type STRING = jsonTypeOf("string"); + Type NULL = jsonTypeOf("null"); - final Set JSON_TYPES = new HashSet<>(Arrays.asList(OBJECT, ARRAY, NUMBER, BOOLEAN, STRING, NULL)); + Set JSON_TYPES = new HashSet<>(Arrays.asList(OBJECT, ARRAY, NUMBER, BOOLEAN, STRING, NULL)); /** * @return a constant {@link Type} representing {@code bsonType : 'objectId' }. @@ -362,10 +381,16 @@ public interface JsonSchemaObject { 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; } @@ -389,15 +414,23 @@ public interface JsonSchemaObject { * @since 2.1 */ @RequiredArgsConstructor - static class JsonType implements Type { + 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; @@ -409,15 +442,23 @@ public interface JsonSchemaObject { * @since 2.1 */ @RequiredArgsConstructor - static class BsonType implements Type { + 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 index 02e4190af..6a9b8ac4a 100644 --- 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 @@ -15,6 +15,9 @@ */ 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; @@ -29,6 +32,7 @@ import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.Object * 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 { @@ -165,7 +169,7 @@ public interface JsonSchemaProperty extends JsonSchemaObject { /** * Obtain a builder to create a {@link JsonSchemaProperty}. - * + * * @param identifier * @return */ @@ -173,24 +177,39 @@ public interface JsonSchemaProperty extends JsonSchemaObject { return new JsonSchemaPropertyBuilder(identifier); } + /** + * Builder for {@link IdentifiableJsonSchemaProperty}. + */ + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) class JsonSchemaPropertyBuilder { - private String identifier; - - public JsonSchemaPropertyBuilder(String identifier) { - this.identifier = identifier; - } + 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)); + 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 new IdentifiableJsonSchemaProperty<>(identifier, schemaObject); } + /** + * @return an untyped {@link IdentifiableJsonSchemaProperty}. + */ public IdentifiableJsonSchemaProperty withoutType() { - return new IdentifiableJsonSchemaProperty(identifier, UntypedJsonSchemaObject.newInstance()); + 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 index f837c0bfb..86778bc14 100644 --- 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 @@ -20,7 +20,6 @@ import java.util.Set; import org.bson.Document; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; -import org.springframework.util.Assert; /** * Interface defining MongoDB-specific JSON schema object. New objects can be built with {@link #builder()}, for @@ -55,28 +54,21 @@ import org.springframework.util.Assert; * * * @author Christoph Strobl + * @author Mark Paluch * @since 2.1 - **/ -public class MongoJsonSchema { - - private final JsonSchemaObject root; - - private MongoJsonSchema(JsonSchemaObject root) { - - Assert.notNull(root, "Root must not be null!"); - this.root = root; - } + * @see UntypedJsonSchemaObject + * @see TypedJsonSchemaObject + */ +public interface MongoJsonSchema { /** * Create the {@link Document} containing the specified {@code $jsonSchema}.
- * Property and field names still need to be mapped to the domain type ones by running the {@link Document} through a - * {@link org.springframework.data.mongodb.core.convert.JsonSchemaMapper}. + * 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}. */ - public Document toDocument() { - return new Document("$jsonSchema", root.toDocument()); - } + Document toDocument(); /** * Create a new {@link MongoJsonSchema} for a given root object. @@ -84,8 +76,18 @@ public class MongoJsonSchema { * @param root must not be {@literal null}. * @return */ - public static MongoJsonSchema of(JsonSchemaObject root) { - return new MongoJsonSchema(root); + 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); } /** @@ -93,16 +95,16 @@ public class MongoJsonSchema { * * @return new instance of {@link MongoJsonSchemaBuilder}. */ - public static MongoJsonSchemaBuilder builder() { + static MongoJsonSchemaBuilder builder() { return new MongoJsonSchemaBuilder(); } /** * {@link MongoJsonSchemaBuilder} provides a fluent API for defining a {@link MongoJsonSchema}. * - * @since 2.1 + * @author Christoph Strobl */ - public static class MongoJsonSchemaBuilder { + class MongoJsonSchemaBuilder { private ObjectJsonSchemaObject root; @@ -111,141 +113,155 @@ public class MongoJsonSchema { } /** - * @param nrProperties - * @return this - * @see ObjectJsonSchemaObject#minNrProperties(int) + * @param count + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#minProperties(int) */ - public MongoJsonSchemaBuilder minNrProperties(int nrProperties) { - root = root.minNrProperties(nrProperties); + public MongoJsonSchemaBuilder minProperties(int count) { + + root = root.minProperties(count); return this; } /** - * @param nrProperties - * @return this - * @see ObjectJsonSchemaObject#maxNrProperties(int) + * @param count + * @return {@code this} {@link MongoJsonSchemaBuilder}. + * @see ObjectJsonSchemaObject#maxProperties(int) */ - public MongoJsonSchemaBuilder maxNrProperties(int nrProperties) { - root = root.maxNrProperties(nrProperties); + public MongoJsonSchemaBuilder maxProperties(int count) { + + root = root.maxProperties(count); return this; } /** * @param properties - * @return + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#required(String...) */ public MongoJsonSchemaBuilder required(String... properties) { + root = root.required(properties); return this; } /** * @param additionalPropertiesAllowed - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#additionalProperties(boolean) */ public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesAllowed) { + root = root.additionalProperties(additionalPropertiesAllowed); return this; } /** * @param schema - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject) */ public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema) { + root = root.additionalProperties(schema); return this; } /** * @param properties - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...) */ public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) { + root = root.properties(properties); return this; } /** * @param properties - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#patternProperties(JsonSchemaProperty...) */ public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties) { + root = root.patternProperties(properties); return this; } /** * @param property - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#property(JsonSchemaProperty) */ public MongoJsonSchemaBuilder property(JsonSchemaProperty property) { + root = root.property(property); return this; } /** * @param possibleValues - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#possibleValues(Collection) */ public MongoJsonSchemaBuilder possibleValues(Set possibleValues) { + root = root.possibleValues(possibleValues); return this; } /** * @param allOf - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#allOf(Collection) */ public MongoJsonSchemaBuilder allOf(Set allOf) { + root = root.allOf(allOf); return this; } /** * @param anyOf - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#anyOf(Collection) */ public MongoJsonSchemaBuilder anyOf(Set anyOf) { + root = root.anyOf(anyOf); return this; } /** * @param oneOf - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#oneOf(Collection) */ public MongoJsonSchemaBuilder oneOf(Set oneOf) { + root = root.oneOf(oneOf); return this; } /** * @param notMatch - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#notMatch(JsonSchemaObject) */ public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) { + root = root.notMatch(notMatch); return this; } /** * @param description - * @return this + * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#description(String) */ public MongoJsonSchemaBuilder description(String description) { + root = root.description(description); return this; } @@ -259,5 +275,4 @@ public class MongoJsonSchema { 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 index 6dcb16cce..6ca4831f1 100644 --- 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 @@ -40,6 +40,7 @@ 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 { @@ -51,8 +52,9 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @param description can be {@literal null}. * @param restrictions can be {@literal null}. */ - protected TypedJsonSchemaObject(@Nullable Type type, @Nullable String description, boolean generateDescription, + TypedJsonSchemaObject(@Nullable Type type, @Nullable String description, boolean generateDescription, @Nullable Restrictions restrictions) { + this(type != null ? Collections.singleton(type) : Collections.emptySet(), description, generateDescription, restrictions); } @@ -62,10 +64,11 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @param description can be {@literal null}. * @param restrictions can be {@literal null}. Defaults to {@link Restrictions#empty()}. */ - protected TypedJsonSchemaObject(Set types, @Nullable String description, boolean generateDescription, + 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; @@ -73,16 +76,27 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { /** * 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}. * @@ -112,7 +126,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override - public TypedJsonSchemaObject possibleValues(Collection possibleValues) { + public TypedJsonSchemaObject possibleValues(Collection possibleValues) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.possibleValues(possibleValues)); } @@ -161,17 +175,8 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.notMatch(notMatch)); } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#getTypes() - */ - @Override - public Set getTypes() { - return types; - } - /** - * Create the json schema complying {@link Document} representation. This includes {@literal type}, + * Create the JSON schema complying {@link Document} representation. This includes {@literal type}, * {@literal description} and the fields of {@link Restrictions#toDocument()} if set. */ @Override @@ -190,10 +195,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { } getOrCreateDescription().ifPresent(val -> document.append("description", val)); - - if (restrictions != null) { - document.putAll(restrictions.toDocument()); - } + document.putAll(restrictions.toDocument()); return document; } @@ -226,9 +228,9 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @author Christoph Strobl * @since 2.1 */ - static class ObjectJsonSchemaObject extends TypedJsonSchemaObject { + public static class ObjectJsonSchemaObject extends TypedJsonSchemaObject { - private @Nullable Range nrProperties; + private @Nullable Range propertiesCount; private @Nullable Object additionalProperties; private List requiredProperties = Collections.emptyList(); private List properties = Collections.emptyList(); @@ -254,35 +256,35 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @param range must not be {@literal null}. Consider {@link Range#unbounded()} instead. * @return new instance of {@link ObjectJsonSchemaObject}. */ - public ObjectJsonSchemaObject nrProperties(Range range) { + public ObjectJsonSchemaObject propertiesCount(Range range) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); - newInstance.nrProperties = range; + newInstance.propertiesCount = range; return newInstance; } /** * Define the {@literal minProperties}. * - * @param nrProperties the allowed minimal number of properties. + * @param count the allowed minimal number of properties. * @return new instance of {@link ObjectJsonSchemaObject}. */ - public ObjectJsonSchemaObject minNrProperties(int nrProperties) { + public ObjectJsonSchemaObject minProperties(int count) { - Bound upper = this.nrProperties != null ? this.nrProperties.getUpperBound() : Bound.unbounded(); - return nrProperties(Range.of(Bound.inclusive(nrProperties), upper)); + Bound upper = this.propertiesCount != null ? this.propertiesCount.getUpperBound() : Bound.unbounded(); + return propertiesCount(Range.of(Bound.inclusive(count), upper)); } /** * Define the {@literal maxProperties}. * - * @param nrProperties the allowed maximum number of properties. + * @param count the allowed maximum number of properties. * @return new instance of {@link ObjectJsonSchemaObject}. */ - public ObjectJsonSchemaObject maxNrProperties(int nrProperties) { + public ObjectJsonSchemaObject maxProperties(int count) { - Bound lower = this.nrProperties != null ? this.nrProperties.getLowerBound() : Bound.unbounded(); - return nrProperties(Range.of(lower, Bound.inclusive(nrProperties))); + Bound lower = this.propertiesCount != null ? this.propertiesCount.getLowerBound() : Bound.unbounded(); + return propertiesCount(Range.of(lower, Bound.inclusive(count))); } /** @@ -297,11 +299,13 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { 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}, no additional fields are allowed. + * 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}. @@ -310,6 +314,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); newInstance.additionalProperties = additionalPropertiesAllowed; + return newInstance; } @@ -338,6 +343,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { newInstance.properties = new ArrayList<>(this.properties.size() + properties.length); newInstance.properties.addAll(this.properties); newInstance.properties.addAll(Arrays.asList(properties)); + return newInstance; } @@ -354,13 +360,14 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { 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 properties must not be {@literal null}. + * @param property must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaObject}. */ public ObjectJsonSchemaObject property(JsonSchemaProperty property) { @@ -372,7 +379,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @see org.springframework.data.mongodb.core.schema.UntypedJsonSchemaObject#possibleValues(java.util.Collection) */ @Override - public ObjectJsonSchemaObject possibleValues(Collection possibleValues) { + public ObjectJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @@ -442,17 +449,12 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { doc.append("required", requiredProperties); } - if (nrProperties != null) { - - if (nrProperties.getLowerBound().isBounded()) { - doc.append("minProperties", nrProperties.getLowerBound().getValue().get()); - - } + if (propertiesCount != null) { - if (nrProperties.getUpperBound().isBounded()) { - doc.append("maxProperties", nrProperties.getUpperBound().getValue().get()); - } + 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)); } @@ -469,15 +471,17 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { return doc; } - private ObjectJsonSchemaObject newInstance(String description, boolean generateDescription, + 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.nrProperties = this.nrProperties; + newInstance.propertiesCount = this.propertiesCount; newInstance.patternProperties = this.patternProperties; + return newInstance; } @@ -485,43 +489,46 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { return source.stream() // .map(JsonSchemaProperty::toDocument) // - .collect(Document::new, (target, propertyDocument) -> target.putAll(propertyDocument), - (target, propertyDocument) -> {}); + .collect(Document::new, Document::putAll, (target, propertyDocument) -> {}); } - @Nullable + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription() + */ @Override protected String generateDescription() { - String errorMsg = "Must be an object"; + String description = "Must be an object"; - if (nrProperties != null) { - errorMsg += " with " + nrProperties + " properties"; + if (propertiesCount != null) { + description += String.format(" with %s properties", propertiesCount); } if (!CollectionUtils.isEmpty(requiredProperties)) { if (requiredProperties.size() == 1) { - errorMsg += " where " + requiredProperties.iterator().next() + "is mandatory"; + description += String.format(" where %sis mandatory", requiredProperties.iterator().next()); } else { - errorMsg += " where " + StringUtils.collectionToDelimitedString(requiredProperties, ", ") + " are mandatory"; + description += String.format(" where %s are mandatory", + StringUtils.collectionToDelimitedString(requiredProperties, ", ")); } } if (additionalProperties instanceof Boolean) { - errorMsg += (((Boolean) additionalProperties) ? " " : " not ") + "allowing additional properties"; + description += (((Boolean) additionalProperties) ? " " : " not ") + "allowing additional properties"; } if (!CollectionUtils.isEmpty(properties)) { - errorMsg += " defining restrictions for " + StringUtils.collectionToDelimitedString( - properties.stream().map(val -> val.getIdentifier()).collect(Collectors.toList()), ", "); + description += String.format(" defining restrictions for %s", StringUtils.collectionToDelimitedString( + properties.stream().map(JsonSchemaProperty::getIdentifier).collect(Collectors.toList()), ", ")); } if (!CollectionUtils.isEmpty(patternProperties)) { - errorMsg += " defining restrictions for patterns " + StringUtils.collectionToDelimitedString( - patternProperties.stream().map(val -> val.getIdentifier()).collect(Collectors.toList()), ", "); + description += String.format(" defining restrictions for patterns %s", StringUtils.collectionToDelimitedString( + patternProperties.stream().map(JsonSchemaProperty::getIdentifier).collect(Collectors.toList()), ", ")); } - return errorMsg + "."; + return description + "."; } } @@ -534,7 +541,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @author Christoph Strobl * @since 2.1 */ - static class NumericJsonSchemaObject extends TypedJsonSchemaObject { + 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())); @@ -542,11 +549,11 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { @Nullable Number multipleOf; @Nullable Range range; - public NumericJsonSchemaObject() { + NumericJsonSchemaObject() { this(Type.numberType()); } - public NumericJsonSchemaObject(Type type) { + NumericJsonSchemaObject(Type type) { this(type, null, false); } @@ -571,6 +578,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { Assert.notNull(value, "Value must not be null!"); NumericJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); newInstance.multipleOf = value; + return newInstance; } @@ -587,6 +595,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { NumericJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); newInstance.range = range; + return newInstance; } @@ -596,6 +605,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @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!"); @@ -610,6 +620,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @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!"); @@ -624,6 +635,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @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!"); @@ -638,6 +650,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @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!"); @@ -651,7 +664,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection) */ @Override - public NumericJsonSchemaObject possibleValues(Collection possibleValues) { + public NumericJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @@ -706,7 +719,6 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { */ @Override public NumericJsonSchemaObject generatedDescription() { - return newInstance(description, true, restrictions); } @@ -726,14 +738,16 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { if (range != null) { if (range.getLowerBound().isBounded()) { - doc.append("minimum", range.getLowerBound().getValue().get()); + + range.getLowerBound().getValue().ifPresent(it -> doc.append("minimum", it)); if (!range.getLowerBound().isInclusive()) { doc.append("exclusiveMinimum", true); } } if (range.getUpperBound().isBounded()) { - doc.append("maximum", range.getUpperBound().getValue().get()); + + range.getUpperBound().getValue().ifPresent(it -> doc.append("maximum", it)); if (!range.getUpperBound().isInclusive()) { doc.append("exclusiveMaximum", true); } @@ -743,7 +757,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { return doc; } - private NumericJsonSchemaObject newInstance(String description, boolean generateDescription, + private NumericJsonSchemaObject newInstance(@Nullable String description, boolean generateDescription, Restrictions restrictions) { NumericJsonSchemaObject newInstance = new NumericJsonSchemaObject(types, description, generateDescription, @@ -751,11 +765,12 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { newInstance.multipleOf = this.multipleOf; newInstance.range = this.range; + return newInstance; } - private Bound createBound(Number number, boolean inclusive) { + private static Bound createBound(Number number, boolean inclusive) { if (number instanceof Long) { return inclusive ? Bound.inclusive((Long) number) : Bound.exclusive((Long) number); @@ -786,20 +801,23 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { return types; } - @Nullable + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription() + */ @Override protected String generateDescription() { - String errorMsg = "Must be a numeric value"; + String description = "Must be a numeric value"; if (multipleOf != null) { - errorMsg += " multiple of " + multipleOf; + description += String.format(" multiple of %s", multipleOf); } if (range != null) { - errorMsg += " within range " + range; + description += String.format(" within range %s", range); } - return errorMsg + "."; + return description + "."; } } @@ -811,12 +829,12 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @author Christoph Strobl * @since 2.1 */ - static class StringJsonSchemaObject extends TypedJsonSchemaObject { + public static class StringJsonSchemaObject extends TypedJsonSchemaObject { @Nullable Range length; @Nullable String pattern; - public StringJsonSchemaObject() { + StringJsonSchemaObject() { this(null, false, null); } @@ -837,30 +855,31 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { StringJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); newInstance.length = range; + return newInstance; } /** - * Define the valid length range ({@literal minLength} for a valid field. + * 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(); + 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. + * 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(); + Bound lower = this.length != null ? this.length.getLowerBound() : Bound.unbounded(); return length(Range.of(lower, Bound.inclusive(length))); } @@ -876,6 +895,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { StringJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); newInstance.pattern = pattern; + return newInstance; } @@ -884,7 +904,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection) */ @Override - public StringJsonSchemaObject possibleValues(Collection possibleValues) { + public StringJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @@ -953,13 +973,8 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { if (length != null) { - if (length.getLowerBound().isBounded()) { - doc.append("minLength", length.getLowerBound().getValue().get()); - } - - if (length.getUpperBound().isBounded()) { - doc.append("maxLength", length.getUpperBound().getValue().get()); - } + length.getLowerBound().getValue().ifPresent(it -> doc.append("minLength", it)); + length.getUpperBound().getValue().ifPresent(it -> doc.append("maxLength", it)); } if (!StringUtils.isEmpty(pattern)) { @@ -969,40 +984,54 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { return doc; } - private StringJsonSchemaObject newInstance(String description, boolean generateDescription, + 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; } - @Nullable + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription() + */ @Override protected String generateDescription() { - String errorMsg = "Must be a string"; + String description = "Must be a string"; if (length != null) { - errorMsg += " with length " + length; + description += String.format(" with length %s", length); } if (pattern != null) { - errorMsg += " matching " + pattern; + description += String.format(" matching %s", pattern); } - return errorMsg + "."; + return description + "."; } } - static class ArrayJsonSchemaObject extends TypedJsonSchemaObject { + /** + * {@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(); - public ArrayJsonSchemaObject() { + ArrayJsonSchemaObject() { this(null, false, null); } @@ -1011,64 +1040,136 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { 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; } - public ArrayJsonSchemaObject maxItems(int nrItems) { + /** + * 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 lower = this.range != null ? this.range.getLowerBound() : Bound.unbounded(); - return range(Range.of(lower, Bound.inclusive(nrItems))); + Bound upper = this.range != null ? this.range.getUpperBound() : Bound.unbounded(); + return range(Range.of(Bound.inclusive(count), upper)); } - public ArrayJsonSchemaObject minItems(int nrItems) { + /** + * 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 upper = this.range != null ? this.range.getUpperBound() : Bound.unbounded(); - return range(Range.of(Bound.inclusive(nrItems), upper)); + 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) { + 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); @@ -1083,8 +1184,13 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { 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)) { @@ -1094,59 +1200,80 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { if (range != null) { - if (range.getLowerBound().isBounded()) { - doc.append("minItems", range.getLowerBound().getValue().get()); - } - - if (range.getUpperBound().isBounded()) { - doc.append("maxItems", range.getUpperBound().getValue().get()); - } + 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(String description, boolean generateDescription, + 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; } - @Nullable + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription() + */ @Override protected String generateDescription() { - String errorMsg = "Must be an array"; + String description = "Must be an array"; if (ObjectUtils.nullSafeEquals(uniqueItems, Boolean.TRUE)) { - errorMsg += " of unique values"; + 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) { - errorMsg += " having size " + range; + description += String.format(" having size %s", range); } if (!ObjectUtils.isEmpty(items)) { - errorMsg += " with items " + StringUtils.collectionToDelimitedString( - items.stream().map(val -> val.toDocument()).collect(Collectors.toList()), ", "); + description += String.format(" with items %s", StringUtils.collectionToDelimitedString( + items.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList()), ", ")); } - return errorMsg + "."; - + return description + "."; } } - static class BooleanJsonSchemaObject extends TypedJsonSchemaObject { + /** + * {@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 { - public BooleanJsonSchemaObject() { + BooleanJsonSchemaObject() { this(null, false, null); } @@ -1160,7 +1287,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection) */ @Override - public BooleanJsonSchemaObject possibleValues(Collection possibleValues) { + public BooleanJsonSchemaObject possibleValues(Collection possibleValues) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); } @@ -1218,11 +1345,28 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { 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 { - public NullJsonSchemaObject() { + NullJsonSchemaObject() { this(null, false, null); } @@ -1236,7 +1380,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection) */ @Override - public NullJsonSchemaObject possibleValues(Collection possibleValues) { + public NullJsonSchemaObject possibleValues(Collection possibleValues) { return new NullJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); } @@ -1284,5 +1428,23 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { 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 index dcf8407af..124bac65f 100644 --- 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 @@ -15,8 +15,12 @@ */ 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; @@ -28,27 +32,39 @@ 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 @Nullable String description; protected final Restrictions restrictions; + protected final @Nullable String description; protected final boolean generateDescription; - protected UntypedJsonSchemaObject(Restrictions restrictions, @Nullable String description, 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(); @@ -67,7 +83,6 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject { /** * Auto generate the {@literal description} if not explicitly set. * - * @param description must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ public UntypedJsonSchemaObject generatedDescription() { @@ -80,7 +95,7 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject { * @param possibleValues must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ - public UntypedJsonSchemaObject possibleValues(Collection possibleValues) { + public UntypedJsonSchemaObject possibleValues(Collection possibleValues) { return new UntypedJsonSchemaObject(restrictions.possibleValues(possibleValues), description, generateDescription); } @@ -125,7 +140,7 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject { } /** - * Create the json schema complying {@link Document} representation. This includes {@literal type}, + * Create the JSON schema complying {@link Document} representation. This includes {@literal type}, * {@literal description} and the fields of {@link Restrictions#toDocument()} if set. */ @Override @@ -135,9 +150,7 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject { getOrCreateDescription().ifPresent(val -> document.append("description", val)); - if (restrictions != null) { - document.putAll(restrictions.toDocument()); - } + document.putAll(restrictions.toDocument()); return document; } @@ -163,29 +176,21 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject { } /** - * {@link Restrictions} encapsulate common json schema restrictions like {@literal enum}, {@literal allOf}, ... + * {@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 possibleValues; private final Collection allOf; private final Collection anyOf; private final Collection oneOf; private final @Nullable JsonSchemaObject notMatch; - Restrictions(Collection possibleValues, Collection allOf, - Collection anyOf, Collection oneOf, @Nullable JsonSchemaObject notMatch) { - - this.possibleValues = possibleValues; - this.allOf = allOf; - this.anyOf = anyOf; - this.oneOf = oneOf; - this.notMatch = notMatch; - } - /** * @return new empty {@link Restrictions}. */ @@ -199,7 +204,7 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject { * @param possibleValues must not be {@literal null}. * @return */ - Restrictions possibleValues(Collection possibleValues) { + Restrictions possibleValues(Collection possibleValues) { Assert.notNull(possibleValues, "PossibleValues must not be null!"); return new Restrictions(possibleValues, allOf, anyOf, oneOf, notMatch); @@ -246,7 +251,7 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject { } /** - * Create the json schema complying {@link Document} representation. This includes {@literal enum}, + * 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} @@ -258,20 +263,28 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject { if (!CollectionUtils.isEmpty(possibleValues)) { document.append("enum", possibleValues); } + if (!CollectionUtils.isEmpty(allOf)) { - document.append("allOf", allOf.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList())); + document.append("allOf", render(allOf)); } + if (!CollectionUtils.isEmpty(anyOf)) { - document.append("anyOf", anyOf.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList())); + document.append("anyOf", render(anyOf)); } + if (!CollectionUtils.isEmpty(oneOf)) { - document.append("oneOf", oneOf.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList())); + 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 index 51e1aa2f5..380d92af0 100644 --- 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 @@ -1,17 +1,5 @@ -/* - * 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. +/** + * MongoDB-specific JSON schema implementation classes. */ @org.springframework.lang.NonNullApi @org.springframework.lang.NonNullFields 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 index 491f14ce9..409a5d5f6 100644 --- 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 @@ -32,6 +32,7 @@ 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 { @@ -49,7 +50,7 @@ public class JsonSchemaObjectUnitTests { @Test // DATAMONGO-1835 public void objectObjectShouldRenderNrPropertiesCorrectly() { - assertThat(object().nrProperties(from(inclusive(10)).to(inclusive(20))).generatedDescription().toDocument()) + 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)); } @@ -100,6 +101,7 @@ public class JsonSchemaObjectUnitTests { .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()) @@ -228,6 +230,39 @@ public class JsonSchemaObjectUnitTests { .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' // ----------------- 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 000000000..a4a5d5df7 --- /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 index 44246f4fd..0eabe3bd4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java @@ -47,8 +47,9 @@ import com.mongodb.client.model.ValidationLevel; import com.mongodb.client.model.ValidationOptions; /** + * Integration tests for {@link MongoJsonSchema}. + * * @author Christoph Strobl - * @since 2017/12 */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @@ -153,10 +154,6 @@ public class MongoJsonSchemaTests { Document collectionInfo = template .executeCommand(new Document("listCollections", 1).append("filter", new Document("name", collectionName))); - if (collectionInfo == null) { - throw new DataRetrievalFailureException(String.format("Collection %s was not found.")); - } - if (collectionInfo.containsKey("cursor")) { collectionInfo = (Document) collectionInfo.get("cursor", Document.class).get("firstBatch", List.class).iterator() .next(); @@ -186,5 +183,4 @@ public class MongoJsonSchemaTests { @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 index 2d3bf03a6..054c5d311 100644 --- 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 @@ -25,7 +25,10 @@ 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 { @@ -41,8 +44,26 @@ public class MongoJsonSchemaUnitTests { 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(null); + 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 000000000..bf2d11a17 --- /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; + } +}