Browse Source

DATAMONGO-1835 - Polishing.

Add JSON schema to collection creation using reactive API. Refactor MongoJsonSchema to interface with default implementations for JsonSchemaObject and Document-based schemas. Make fields final and methods static where possible.

Add minItems/maxItems/additionalItems properties to ArrayJsonSchemaProperty. Add missing overrides to NullJsonSchemaProperty.
Slightly rename methods for item/property counts. Add generics, Javadoc, minor tweaks.

Original pull request: #524.
pull/522/merge
Mark Paluch 8 years ago
parent
commit
f2bb46724c
  1. 36
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
  2. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  3. 56
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  4. 6
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java
  5. 43
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java
  6. 42
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DocumentJsonSchema.java
  7. 102
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java
  8. 87
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java
  9. 37
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java
  10. 103
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java
  11. 406
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java
  12. 63
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java
  13. 16
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java
  14. 37
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/JsonSchemaObjectUnitTests.java
  15. 55
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/JsonSchemaPropertyUnitTests.java
  16. 8
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaTests.java
  17. 23
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java
  18. 140
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/ReactiveMongoJsonSchemaTests.java

36
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java

@ -15,6 +15,8 @@
*/ */
package org.springframework.data.mongodb.core; package org.springframework.data.mongodb.core;
import lombok.RequiredArgsConstructor;
import java.util.Optional; import java.util.Optional;
import org.springframework.data.mongodb.core.query.Collation; 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}. * @return new {@link CollectionOptions}.
* @since 2.1 * @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}. * @return new {@link CollectionOptions}.
* @since 2.1 * @since 2.1
@ -167,7 +171,7 @@ public class CollectionOptions {
/** /**
* Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to * Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to
* {@code moderate}. * {@link ValidationLevel#MODERATE}.
* *
* @return new {@link CollectionOptions}. * @return new {@link CollectionOptions}.
* @since 2.1 * @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}. * @return new {@link CollectionOptions}.
* @since 2.1 * @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}. * @return new {@link CollectionOptions}.
* @since 2.1 * @since 2.1
@ -291,25 +297,18 @@ public class CollectionOptions {
* @author Christoph Strobl * @author Christoph Strobl
* @since 2.1 * @since 2.1
*/ */
@RequiredArgsConstructor
public static class Validator { public static class Validator {
private static final Validator NONE = new Validator(null, null, null); private static final Validator NONE = new Validator(null, null, null);
private @Nullable MongoJsonSchema schema; private final @Nullable MongoJsonSchema schema;
private @Nullable ValidationLevel validationLevel; private final @Nullable ValidationLevel validationLevel;
private @Nullable ValidationAction validationAction; private final @Nullable ValidationAction validationAction;
private Validator(@Nullable MongoJsonSchema schema, @Nullable ValidationLevel validationLevel,
@Nullable ValidationAction validationAction) {
this.schema = schema;
this.validationLevel = validationLevel;
this.validationAction = validationAction;
}
/** /**
* Create an empty {@link Validator}. * Create an empty {@link Validator}.
* *
* @return never {@literal null}. * @return never {@literal null}.
*/ */
public static Validator none() { public static Validator none() {
@ -321,7 +320,6 @@ public class CollectionOptions {
* *
* @return {@link Optional#empty()} if not set. * @return {@link Optional#empty()} if not set.
*/ */
@Nullable
public Optional<MongoJsonSchema> getSchema() { public Optional<MongoJsonSchema> getSchema() {
return Optional.ofNullable(schema); return Optional.ofNullable(schema);
} }
@ -331,7 +329,6 @@ public class CollectionOptions {
* *
* @return {@link Optional#empty()} if not set. * @return {@link Optional#empty()} if not set.
*/ */
@Nullable
public Optional<ValidationLevel> getValidationLevel() { public Optional<ValidationLevel> getValidationLevel() {
return Optional.ofNullable(validationLevel); return Optional.ofNullable(validationLevel);
} }
@ -341,7 +338,6 @@ public class CollectionOptions {
* *
* @return @return {@link Optional#empty()} if not set. * @return @return {@link Optional#empty()} if not set.
*/ */
@Nullable
public Optional<ValidationAction> getValidationAction() { public Optional<ValidationAction> getValidationAction() {
return Optional.ofNullable(validationAction); return Optional.ofNullable(validationAction);
} }

5
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.ResourceUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.Mongo;
import com.mongodb.MongoClient; import com.mongodb.MongoClient;
import com.mongodb.MongoException; import com.mongodb.MongoException;
import com.mongodb.ReadPreference; import com.mongodb.ReadPreference;
@ -2370,7 +2373,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Document doc = convertToDocument(collectionOptions); Document doc = convertToDocument(collectionOptions);
if (collectionOptions.getValidator().isPresent()) { if (collectionOptions != null && collectionOptions.getValidator().isPresent()) {
Validator v = collectionOptions.getValidator().get(); Validator v = collectionOptions.getValidator().get();
v.getSchema().ifPresent(val -> doc.put("validator", schemaMapper.mapSchema(val.toDocument(), targetType))); v.getSchema().ifPresent(val -> doc.put("validator", schemaMapper.mapSchema(val.toDocument(), targetType)));
} }

56
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.AggregationOptions;
import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
import org.springframework.data.mongodb.core.convert.DbRefProxyHandler; import org.springframework.data.mongodb.core.convert.*;
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.index.IndexOperationsAdapter; import org.springframework.data.mongodb.core.index.IndexOperationsAdapter;
import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher; import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator; 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.FindOneAndUpdateOptions;
import com.mongodb.client.model.ReturnDocument; import com.mongodb.client.model.ReturnDocument;
import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.model.ValidationOptions;
import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult; import com.mongodb.client.result.UpdateResult;
import com.mongodb.reactivestreams.client.AggregatePublisher; import com.mongodb.reactivestreams.client.AggregatePublisher;
@ -176,6 +169,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final PersistenceExceptionTranslator exceptionTranslator; private final PersistenceExceptionTranslator exceptionTranslator;
private final QueryMapper queryMapper; private final QueryMapper queryMapper;
private final UpdateMapper updateMapper; private final UpdateMapper updateMapper;
private final JsonSchemaMapper schemaMapper;
private final SpelAwareProxyProjectionFactory projectionFactory; private final SpelAwareProxyProjectionFactory projectionFactory;
private @Nullable WriteConcern writeConcern; private @Nullable WriteConcern writeConcern;
@ -220,6 +214,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter() : mongoConverter; this.mongoConverter = mongoConverter == null ? getDefaultMongoConverter() : mongoConverter;
this.queryMapper = new QueryMapper(this.mongoConverter); this.queryMapper = new QueryMapper(this.mongoConverter);
this.updateMapper = new UpdateMapper(this.mongoConverter); this.updateMapper = new UpdateMapper(this.mongoConverter);
this.schemaMapper = new MongoJsonSchemaMapper(this.mongoConverter);
this.projectionFactory = new SpelAwareProxyProjectionFactory(); this.projectionFactory = new SpelAwareProxyProjectionFactory();
// We always have a mapping context in the converter, whether it's a simple one or not // 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 <T> Mono<MongoCollection<Document>> createCollection(Class<T> entityClass, public <T> Mono<MongoCollection<Document>> createCollection(Class<T> entityClass,
@Nullable CollectionOptions collectionOptions) { @Nullable CollectionOptions collectionOptions) {
return createCollection(determineCollectionName(entityClass), collectionOptions); return doCreateCollection(determineCollectionName(entityClass),
convertToCreateCollectionOptions(collectionOptions, entityClass));
} }
/* /*
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#createCollection(java.lang.String) * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#createCollection(java.lang.String)
*/ */
public Mono<MongoCollection<Document>> createCollection(final String collectionName) { public Mono<MongoCollection<Document>> createCollection(String collectionName) {
return doCreateCollection(collectionName, new CreateCollectionOptions()); return doCreateCollection(collectionName, new CreateCollectionOptions());
} }
@ -501,8 +497,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* (non-Javadoc) * (non-Javadoc)
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#createCollection(java.lang.String, org.springframework.data.mongodb.core.CollectionOptions) * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#createCollection(java.lang.String, org.springframework.data.mongodb.core.CollectionOptions)
*/ */
public Mono<MongoCollection<Document>> createCollection(final String collectionName, public Mono<MongoCollection<Document>> createCollection(String collectionName,
final CollectionOptions collectionOptions) { @Nullable CollectionOptions collectionOptions) {
return doCreateCollection(collectionName, convertToCreateCollectionOptions(collectionOptions)); return doCreateCollection(collectionName, convertToCreateCollectionOptions(collectionOptions));
} }
@ -814,8 +810,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
} }
ReadDocumentCallback<O> readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName); ReadDocumentCallback<O> readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName);
return execute(collectionName, return execute(collectionName, collection -> aggregateAndMap(collection, pipeline, options, readCallback));
collection -> aggregateAndMap(collection, pipeline, options, readCallback));
} }
private <O> Flux<O> aggregateAndMap(MongoCollection<Document> collection, List<Document> pipeline, private <O> Flux<O> aggregateAndMap(MongoCollection<Document> collection, List<Document> pipeline,
@ -1995,17 +1990,36 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
} }
protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions) { 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); if (collectionOptions == null) {
collectionOptions.getSize().ifPresent(result::sizeInBytes); return result;
collectionOptions.getMaxDocuments().ifPresent(result::maxDocuments);
collectionOptions.getCollation().map(Collation::toMongoCollation).ifPresent(result::collation);
} }
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; return result;
} }

6
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. * provided domain type.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
* @since 2.1 * @since 2.1
*/ */
public class MongoJsonSchemaMapper implements JsonSchemaMapper { 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) * @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) { 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))); 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<?, MongoPersistentProperty> entity, Document source) {
Document sink = new Document(source); Document sink = new Document(source);

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

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

102
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. * {@link JsonSchemaProperty} implementation.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
* @since 2.1 * @since 2.1
*/ */
public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implements JsonSchemaProperty { public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implements JsonSchemaProperty {
protected String identifier; protected final String identifier;
protected T jsonSchemaObjectDelegate; 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(identifier, "Identifier must not be null!");
Assert.notNull(jsonSchemaObject, "JsonSchemaObject must not be null!"); Assert.notNull(jsonSchemaObject, "JsonSchemaObject must not be null!");
@ -85,7 +92,7 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
*/ */
public static class UntypedJsonSchemaProperty extends IdentifiableJsonSchemaProperty<UntypedJsonSchemaObject> { public static class UntypedJsonSchemaProperty extends IdentifiableJsonSchemaProperty<UntypedJsonSchemaObject> {
public UntypedJsonSchemaProperty(String identifier, UntypedJsonSchemaObject jsonSchemaObject) { UntypedJsonSchemaProperty(String identifier, UntypedJsonSchemaObject jsonSchemaObject) {
super(identifier, jsonSchemaObject); super(identifier, jsonSchemaObject);
} }
@ -201,7 +208,7 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
* {@literal null} nor {@literal empty}. * {@literal null} nor {@literal empty}.
* @param schemaObject must not be {@literal null}. * @param schemaObject must not be {@literal null}.
*/ */
public StringJsonSchemaProperty(String identifier, StringJsonSchemaObject schemaObject) { StringJsonSchemaProperty(String identifier, StringJsonSchemaObject schemaObject) {
super(identifier, schemaObject); super(identifier, schemaObject);
} }
@ -274,8 +281,7 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
* @see StringJsonSchemaObject#possibleValues(Collection) * @see StringJsonSchemaObject#possibleValues(Collection)
*/ */
public StringJsonSchemaProperty possibleValues(Collection<String> possibleValues) { public StringJsonSchemaProperty possibleValues(Collection<String> possibleValues) {
return new StringJsonSchemaProperty(identifier, return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
jsonSchemaObjectDelegate.possibleValues((Collection) possibleValues));
} }
/** /**
@ -345,35 +351,35 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
* {@literal null} nor {@literal empty}. * {@literal null} nor {@literal empty}.
* @param schemaObject must not be {@literal null}. * @param schemaObject must not be {@literal null}.
*/ */
public ObjectJsonSchemaProperty(String identifier, ObjectJsonSchemaObject schemaObject) { ObjectJsonSchemaProperty(String identifier, ObjectJsonSchemaObject schemaObject) {
super(identifier, schemaObject); super(identifier, schemaObject);
} }
/** /**
* @param range must not be {@literal null}. * @param range must not be {@literal null}.
* @return new instance of {@link ObjectJsonSchemaProperty}. * @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#nrProperties * @see ObjectJsonSchemaObject#propertiesCount
*/ */
public ObjectJsonSchemaProperty nrProperties(Range<Integer> range) { public ObjectJsonSchemaProperty propertiesCount(Range<Integer> range) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.nrProperties(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}. * @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#minNrProperties(int) * @see ObjectJsonSchemaObject#minProperties(int)
*/ */
public ObjectJsonSchemaProperty minNrProperties(int nrProperties) { public ObjectJsonSchemaProperty minProperties(int count) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minNrProperties(nrProperties)); 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}. * @return new instance of {@link ObjectJsonSchemaProperty}.
* @see ObjectJsonSchemaObject#maxNrProperties(int) * @see ObjectJsonSchemaObject#maxProperties(int)
*/ */
public ObjectJsonSchemaProperty maxNrProperties(int nrProperties) { public ObjectJsonSchemaProperty maxProperties(int count) {
return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxNrProperties(nrProperties)); return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxProperties(count));
} }
/** /**
@ -626,8 +632,7 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
* @see NumericJsonSchemaObject#possibleValues(Collection) * @see NumericJsonSchemaObject#possibleValues(Collection)
*/ */
public NumericJsonSchemaProperty possibleValues(Collection<Number> possibleValues) { public NumericJsonSchemaProperty possibleValues(Collection<Number> possibleValues) {
return new NumericJsonSchemaProperty(identifier, return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues));
jsonSchemaObjectDelegate.possibleValues((Collection) possibleValues));
} }
/** /**
@ -719,6 +724,33 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.range(range)); return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.range(range));
} }
/**
* @param count
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#minItems(int)
*/
public ArrayJsonSchemaProperty minItems(int count) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minItems(count));
}
/**
* @param count
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#maxItems(int)
*/
public ArrayJsonSchemaProperty maxItems(int count) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxItems(count));
}
/**
* @param items must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#items(Collection)
*/
public ArrayJsonSchemaProperty items(JsonSchemaObject... items) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(Arrays.asList(items)));
}
/** /**
* @param items must not be {@literal null}. * @param items must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}. * @return new instance of {@link ArrayJsonSchemaProperty}.
@ -728,6 +760,15 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(items)); return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(items));
} }
/**
* @param additionalItemsAllowed
* @return new instance of {@link ArrayJsonSchemaProperty}.
* @see ArrayJsonSchemaObject#additionalItems(boolean)
*/
public ArrayJsonSchemaProperty additionalItems(boolean additionalItemsAllowed) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.additionalItems(additionalItemsAllowed));
}
/** /**
* @param possibleValues must not be {@literal null}. * @param possibleValues must not be {@literal null}.
* @return new instance of {@link ArrayJsonSchemaProperty}. * @return new instance of {@link ArrayJsonSchemaProperty}.
@ -812,7 +853,7 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
/** /**
* @param description must not be {@literal null}. * @param description must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}. * @return new instance of {@link NumericJsonSchemaProperty}.
* @see ArrayJsonSchemaObjecty#description(String) * @see ArrayJsonSchemaObject#description(String)
*/ */
public ArrayJsonSchemaProperty description(String description) { public ArrayJsonSchemaProperty description(String description) {
return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description));
@ -835,7 +876,7 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
*/ */
public static class BooleanJsonSchemaProperty extends IdentifiableJsonSchemaProperty<BooleanJsonSchemaObject> { public static class BooleanJsonSchemaProperty extends IdentifiableJsonSchemaProperty<BooleanJsonSchemaObject> {
public BooleanJsonSchemaProperty(String identifier, BooleanJsonSchemaObject schemaObject) { BooleanJsonSchemaProperty(String identifier, BooleanJsonSchemaObject schemaObject) {
super(identifier, schemaObject); super(identifier, schemaObject);
} }
@ -865,18 +906,25 @@ public class IdentifiableJsonSchemaProperty<T extends JsonSchemaObject> implemen
*/ */
public static class NullJsonSchemaProperty extends IdentifiableJsonSchemaProperty<NullJsonSchemaObject> { public static class NullJsonSchemaProperty extends IdentifiableJsonSchemaProperty<NullJsonSchemaObject> {
public NullJsonSchemaProperty(String identifier, NullJsonSchemaObject schemaObject) { NullJsonSchemaProperty(String identifier, NullJsonSchemaObject schemaObject) {
super(identifier, schemaObject); super(identifier, schemaObject);
} }
/** /**
* @param description must not be {@literal null}. * @param description must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaProperty}. * @return new instance of {@link NullJsonSchemaProperty}.
* @see NullJsonSchemaObject#description(String) * @see NullJsonSchemaObject#description(String)
*/ */
public NullJsonSchemaProperty description(String description) { public NullJsonSchemaProperty description(String description) {
return new NullJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(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());
}
}
} }

87
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.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.bson.BsonTimestamp;
import org.bson.Document; import org.bson.Document;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject;
@ -36,7 +37,18 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
/** /**
* Interface that can be implemented by objects that know how to serialize themselves to JSON schema using
* {@link #toDocument()}.
* <p/>
* This class also declares factory methods for type-specific {@link JsonSchemaObject schema objects} such as
* {@link #string()} or {@link #object()}. For example:
*
* <pre class="code">
* JsonSchemaProperty.object("address").properties(JsonSchemaProperty.string("city").minLength(3));
* </pre>
*
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
* @since 2.1 * @since 2.1
*/ */
public interface JsonSchemaObject { public interface JsonSchemaObject {
@ -134,7 +146,10 @@ public interface JsonSchemaObject {
/** /**
* Create a new {@link JsonSchemaObject} matching the given {@code type}. * 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}. * @return never {@literal null}.
* @throws IllegalArgumentException if {@code type} is not supported.
*/ */
static TypedJsonSchemaObject of(@Nullable Class<?> type) { static TypedJsonSchemaObject of(@Nullable Class<?> type) {
@ -167,6 +182,10 @@ public interface JsonSchemaObject {
return of(Type.dateType()); return of(Type.dateType());
} }
if (ClassUtils.isAssignable(BsonTimestamp.class, type)) {
return of(Type.timestampType());
}
if (ClassUtils.isAssignable(Pattern.class, type)) { if (ClassUtils.isAssignable(Pattern.class, type)) {
return of(Type.regexType()); return of(Type.regexType());
} }
@ -200,11 +219,11 @@ public interface JsonSchemaObject {
return of(Type.numberType()); 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 * @author Christoph Strobl
* @since 2.1 * @since 2.1
@ -212,29 +231,29 @@ public interface JsonSchemaObject {
interface Type { interface Type {
// BSON TYPES // BSON TYPES
final Type OBJECT_ID = bsonTypeOf("objectId"); Type OBJECT_ID = bsonTypeOf("objectId");
final Type REGULAR_EXPRESSION = bsonTypeOf("regex"); Type REGULAR_EXPRESSION = bsonTypeOf("regex");
final Type DOUBLE = bsonTypeOf("double"); Type DOUBLE = bsonTypeOf("double");
final Type BINARY_DATA = bsonTypeOf("binData"); Type BINARY_DATA = bsonTypeOf("binData");
final Type DATE = bsonTypeOf("date"); Type DATE = bsonTypeOf("date");
final Type JAVA_SCRIPT = bsonTypeOf("javascript"); Type JAVA_SCRIPT = bsonTypeOf("javascript");
final Type INT_32 = bsonTypeOf("int"); Type INT_32 = bsonTypeOf("int");
final Type INT_64 = bsonTypeOf("long"); Type INT_64 = bsonTypeOf("long");
final Type DECIMAL_128 = bsonTypeOf("decimal"); Type DECIMAL_128 = bsonTypeOf("decimal");
final Type TIMESTAMP = bsonTypeOf("timestamp"); Type TIMESTAMP = bsonTypeOf("timestamp");
final Set<Type> BSON_TYPES = new HashSet<>(Arrays.asList(OBJECT_ID, REGULAR_EXPRESSION, DOUBLE, BINARY_DATA, DATE, Set<Type> BSON_TYPES = new HashSet<>(Arrays.asList(OBJECT_ID, REGULAR_EXPRESSION, DOUBLE, BINARY_DATA, DATE,
JAVA_SCRIPT, INT_32, INT_64, DECIMAL_128, TIMESTAMP)); JAVA_SCRIPT, INT_32, INT_64, DECIMAL_128, TIMESTAMP));
// JSON SCHEMA TYPES // JSON SCHEMA TYPES
final Type OBJECT = jsonTypeOf("object"); Type OBJECT = jsonTypeOf("object");
final Type ARRAY = jsonTypeOf("array"); Type ARRAY = jsonTypeOf("array");
final Type NUMBER = jsonTypeOf("number"); Type NUMBER = jsonTypeOf("number");
final Type BOOLEAN = jsonTypeOf("boolean"); Type BOOLEAN = jsonTypeOf("boolean");
final Type STRING = jsonTypeOf("string"); Type STRING = jsonTypeOf("string");
final Type NULL = jsonTypeOf("null"); Type NULL = jsonTypeOf("null");
final Set<Type> JSON_TYPES = new HashSet<>(Arrays.asList(OBJECT, ARRAY, NUMBER, BOOLEAN, STRING, NULL)); Set<Type> JSON_TYPES = new HashSet<>(Arrays.asList(OBJECT, ARRAY, NUMBER, BOOLEAN, STRING, NULL));
/** /**
* @return a constant {@link Type} representing {@code bsonType : 'objectId' }. * @return a constant {@link Type} representing {@code bsonType : 'objectId' }.
@ -362,10 +381,16 @@ public interface JsonSchemaObject {
return new JsonType(name); return new JsonType(name);
} }
/**
* @return all known JSON types.
*/
static Set<Type> jsonTypes() { static Set<Type> jsonTypes() {
return JSON_TYPES; return JSON_TYPES;
} }
/**
* @return all known BSON types.
*/
static Set<Type> bsonTypes() { static Set<Type> bsonTypes() {
return BSON_TYPES; return BSON_TYPES;
} }
@ -389,15 +414,23 @@ public interface JsonSchemaObject {
* @since 2.1 * @since 2.1
*/ */
@RequiredArgsConstructor @RequiredArgsConstructor
static class JsonType implements Type { class JsonType implements Type {
private final String name; private final String name;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type#representation()
*/
@Override @Override
public String representation() { public String representation() {
return "type"; return "type";
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type#value()
*/
@Override @Override
public String value() { public String value() {
return name; return name;
@ -409,15 +442,23 @@ public interface JsonSchemaObject {
* @since 2.1 * @since 2.1
*/ */
@RequiredArgsConstructor @RequiredArgsConstructor
static class BsonType implements Type { class BsonType implements Type {
private final String name; private final String name;
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type#representation()
*/
@Override @Override
public String representation() { public String representation() {
return "bsonType"; return "bsonType";
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type#value()
*/
@Override @Override
public String value() { public String value() {
return name; return name;

37
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; 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.ArrayJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.BooleanJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.BooleanJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NullJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.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'}. * A {@literal property} or {@literal patternProperty} within a {@link JsonSchemaObject} of {@code type : 'object'}.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
* @since 2.1 * @since 2.1
*/ */
public interface JsonSchemaProperty extends JsonSchemaObject { public interface JsonSchemaProperty extends JsonSchemaObject {
@ -165,7 +169,7 @@ public interface JsonSchemaProperty extends JsonSchemaObject {
/** /**
* Obtain a builder to create a {@link JsonSchemaProperty}. * Obtain a builder to create a {@link JsonSchemaProperty}.
* *
* @param identifier * @param identifier
* @return * @return
*/ */
@ -173,24 +177,39 @@ public interface JsonSchemaProperty extends JsonSchemaObject {
return new JsonSchemaPropertyBuilder(identifier); return new JsonSchemaPropertyBuilder(identifier);
} }
/**
* Builder for {@link IdentifiableJsonSchemaProperty}.
*/
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
class JsonSchemaPropertyBuilder { class JsonSchemaPropertyBuilder {
private String identifier; private final String identifier;
public JsonSchemaPropertyBuilder(String identifier) {
this.identifier = identifier;
}
/**
* Configure a {@link Type} for the property.
*
* @param type must not be {@literal null}.
* @return
*/
public IdentifiableJsonSchemaProperty<TypedJsonSchemaObject> ofType(Type type) { public IdentifiableJsonSchemaProperty<TypedJsonSchemaObject> 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<TypedJsonSchemaObject> with(TypedJsonSchemaObject schemaObject) { public IdentifiableJsonSchemaProperty<TypedJsonSchemaObject> with(TypedJsonSchemaObject schemaObject) {
return new IdentifiableJsonSchemaProperty(identifier, schemaObject); return new IdentifiableJsonSchemaProperty<>(identifier, schemaObject);
} }
/**
* @return an untyped {@link IdentifiableJsonSchemaProperty}.
*/
public IdentifiableJsonSchemaProperty<UntypedJsonSchemaObject> withoutType() { public IdentifiableJsonSchemaProperty<UntypedJsonSchemaObject> withoutType() {
return new IdentifiableJsonSchemaProperty(identifier, UntypedJsonSchemaObject.newInstance()); return new IdentifiableJsonSchemaProperty<>(identifier, UntypedJsonSchemaObject.newInstance());
} }
} }

103
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.bson.Document;
import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; 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 * 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;
* </pre> * </pre>
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
* @since 2.1 * @since 2.1
**/ * @see UntypedJsonSchemaObject
public class MongoJsonSchema { * @see TypedJsonSchemaObject
*/
private final JsonSchemaObject root; public interface MongoJsonSchema {
private MongoJsonSchema(JsonSchemaObject root) {
Assert.notNull(root, "Root must not be null!");
this.root = root;
}
/** /**
* Create the {@link Document} containing the specified {@code $jsonSchema}. <br /> * Create the {@link Document} containing the specified {@code $jsonSchema}. <br />
* Property and field names still need to be mapped to the domain type ones by running the {@link Document} through a * 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}. * {@link org.springframework.data.mongodb.core.convert.JsonSchemaMapper} to apply field name customization.
* *
* @return never {@literal null}. * @return never {@literal null}.
*/ */
public Document toDocument() { Document toDocument();
return new Document("$jsonSchema", root.toDocument());
}
/** /**
* Create a new {@link MongoJsonSchema} for a given root object. * Create a new {@link MongoJsonSchema} for a given root object.
@ -84,8 +76,18 @@ public class MongoJsonSchema {
* @param root must not be {@literal null}. * @param root must not be {@literal null}.
* @return * @return
*/ */
public static MongoJsonSchema of(JsonSchemaObject root) { static MongoJsonSchema of(JsonSchemaObject root) {
return new MongoJsonSchema(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}. * @return new instance of {@link MongoJsonSchemaBuilder}.
*/ */
public static MongoJsonSchemaBuilder builder() { static MongoJsonSchemaBuilder builder() {
return new MongoJsonSchemaBuilder(); return new MongoJsonSchemaBuilder();
} }
/** /**
* {@link MongoJsonSchemaBuilder} provides a fluent API for defining a {@link MongoJsonSchema}. * {@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; private ObjectJsonSchemaObject root;
@ -111,141 +113,155 @@ public class MongoJsonSchema {
} }
/** /**
* @param nrProperties * @param count
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see ObjectJsonSchemaObject#minNrProperties(int) * @see ObjectJsonSchemaObject#minProperties(int)
*/ */
public MongoJsonSchemaBuilder minNrProperties(int nrProperties) { public MongoJsonSchemaBuilder minProperties(int count) {
root = root.minNrProperties(nrProperties);
root = root.minProperties(count);
return this; return this;
} }
/** /**
* @param nrProperties * @param count
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see ObjectJsonSchemaObject#maxNrProperties(int) * @see ObjectJsonSchemaObject#maxProperties(int)
*/ */
public MongoJsonSchemaBuilder maxNrProperties(int nrProperties) { public MongoJsonSchemaBuilder maxProperties(int count) {
root = root.maxNrProperties(nrProperties);
root = root.maxProperties(count);
return this; return this;
} }
/** /**
* @param properties * @param properties
* @return * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see ObjectJsonSchemaObject#required(String...) * @see ObjectJsonSchemaObject#required(String...)
*/ */
public MongoJsonSchemaBuilder required(String... properties) { public MongoJsonSchemaBuilder required(String... properties) {
root = root.required(properties); root = root.required(properties);
return this; return this;
} }
/** /**
* @param additionalPropertiesAllowed * @param additionalPropertiesAllowed
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see ObjectJsonSchemaObject#additionalProperties(boolean) * @see ObjectJsonSchemaObject#additionalProperties(boolean)
*/ */
public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesAllowed) { public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesAllowed) {
root = root.additionalProperties(additionalPropertiesAllowed); root = root.additionalProperties(additionalPropertiesAllowed);
return this; return this;
} }
/** /**
* @param schema * @param schema
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject) * @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject)
*/ */
public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema) { public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema) {
root = root.additionalProperties(schema); root = root.additionalProperties(schema);
return this; return this;
} }
/** /**
* @param properties * @param properties
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...) * @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...)
*/ */
public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) { public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) {
root = root.properties(properties); root = root.properties(properties);
return this; return this;
} }
/** /**
* @param properties * @param properties
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see ObjectJsonSchemaObject#patternProperties(JsonSchemaProperty...) * @see ObjectJsonSchemaObject#patternProperties(JsonSchemaProperty...)
*/ */
public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties) { public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties) {
root = root.patternProperties(properties); root = root.patternProperties(properties);
return this; return this;
} }
/** /**
* @param property * @param property
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see ObjectJsonSchemaObject#property(JsonSchemaProperty) * @see ObjectJsonSchemaObject#property(JsonSchemaProperty)
*/ */
public MongoJsonSchemaBuilder property(JsonSchemaProperty property) { public MongoJsonSchemaBuilder property(JsonSchemaProperty property) {
root = root.property(property); root = root.property(property);
return this; return this;
} }
/** /**
* @param possibleValues * @param possibleValues
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see ObjectJsonSchemaObject#possibleValues(Collection) * @see ObjectJsonSchemaObject#possibleValues(Collection)
*/ */
public MongoJsonSchemaBuilder possibleValues(Set<Object> possibleValues) { public MongoJsonSchemaBuilder possibleValues(Set<Object> possibleValues) {
root = root.possibleValues(possibleValues); root = root.possibleValues(possibleValues);
return this; return this;
} }
/** /**
* @param allOf * @param allOf
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see UntypedJsonSchemaObject#allOf(Collection) * @see UntypedJsonSchemaObject#allOf(Collection)
*/ */
public MongoJsonSchemaBuilder allOf(Set<JsonSchemaObject> allOf) { public MongoJsonSchemaBuilder allOf(Set<JsonSchemaObject> allOf) {
root = root.allOf(allOf); root = root.allOf(allOf);
return this; return this;
} }
/** /**
* @param anyOf * @param anyOf
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see UntypedJsonSchemaObject#anyOf(Collection) * @see UntypedJsonSchemaObject#anyOf(Collection)
*/ */
public MongoJsonSchemaBuilder anyOf(Set<JsonSchemaObject> anyOf) { public MongoJsonSchemaBuilder anyOf(Set<JsonSchemaObject> anyOf) {
root = root.anyOf(anyOf); root = root.anyOf(anyOf);
return this; return this;
} }
/** /**
* @param oneOf * @param oneOf
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see UntypedJsonSchemaObject#oneOf(Collection) * @see UntypedJsonSchemaObject#oneOf(Collection)
*/ */
public MongoJsonSchemaBuilder oneOf(Set<JsonSchemaObject> oneOf) { public MongoJsonSchemaBuilder oneOf(Set<JsonSchemaObject> oneOf) {
root = root.oneOf(oneOf); root = root.oneOf(oneOf);
return this; return this;
} }
/** /**
* @param notMatch * @param notMatch
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see UntypedJsonSchemaObject#notMatch(JsonSchemaObject) * @see UntypedJsonSchemaObject#notMatch(JsonSchemaObject)
*/ */
public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) { public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) {
root = root.notMatch(notMatch); root = root.notMatch(notMatch);
return this; return this;
} }
/** /**
* @param description * @param description
* @return this * @return {@code this} {@link MongoJsonSchemaBuilder}.
* @see UntypedJsonSchemaObject#description(String) * @see UntypedJsonSchemaObject#description(String)
*/ */
public MongoJsonSchemaBuilder description(String description) { public MongoJsonSchemaBuilder description(String description) {
root = root.description(description); root = root.description(description);
return this; return this;
} }
@ -259,5 +275,4 @@ public class MongoJsonSchema {
return MongoJsonSchema.of(root); return MongoJsonSchema.of(root);
} }
} }
} }

406
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}. * A {@link JsonSchemaObject} of a given {@link org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type}.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
* @since 2.1 * @since 2.1
*/ */
public class TypedJsonSchemaObject extends UntypedJsonSchemaObject { public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
@ -51,8 +52,9 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @param description can be {@literal null}. * @param description can be {@literal null}.
* @param restrictions 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) { @Nullable Restrictions restrictions) {
this(type != null ? Collections.singleton(type) : Collections.emptySet(), description, generateDescription, this(type != null ? Collections.singleton(type) : Collections.emptySet(), description, generateDescription,
restrictions); restrictions);
} }
@ -62,10 +64,11 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @param description can be {@literal null}. * @param description can be {@literal null}.
* @param restrictions can be {@literal null}. Defaults to {@link Restrictions#empty()}. * @param restrictions can be {@literal null}. Defaults to {@link Restrictions#empty()}.
*/ */
protected TypedJsonSchemaObject(Set<Type> types, @Nullable String description, boolean generateDescription, TypedJsonSchemaObject(Set<Type> types, @Nullable String description, boolean generateDescription,
@Nullable Restrictions restrictions) { @Nullable Restrictions restrictions) {
super(restrictions, description, generateDescription); super(restrictions, description, generateDescription);
Assert.notNull(types, "Types must not be null! Please consider using 'Collections.emptySet()'."); Assert.notNull(types, "Types must not be null! Please consider using 'Collections.emptySet()'.");
this.types = types; this.types = types;
@ -73,16 +76,27 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
/** /**
* Creates new {@link TypedJsonSchemaObject} of given types. * Creates new {@link TypedJsonSchemaObject} of given types.
* *
* @param types must not be {@literal null}. * @param types must not be {@literal null}.
* @return * @return
*/ */
public static TypedJsonSchemaObject of(Type... types) { public static TypedJsonSchemaObject of(Type... types) {
Assert.notNull(types, "Types must not be null!");
Assert.noNullElements(types, "Types must not contain null!"); Assert.noNullElements(types, "Types must not contain null!");
return new TypedJsonSchemaObject(new LinkedHashSet<>(Arrays.asList(types)), null, false, Restrictions.empty()); 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<Type> getTypes() {
return types;
}
/** /**
* Set the {@literal description}. * Set the {@literal description}.
* *
@ -112,7 +126,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @return new instance of {@link TypedJsonSchemaObject}. * @return new instance of {@link TypedJsonSchemaObject}.
*/ */
@Override @Override
public TypedJsonSchemaObject possibleValues(Collection<Object> possibleValues) { public TypedJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
return new TypedJsonSchemaObject(types, description, generateDescription, return new TypedJsonSchemaObject(types, description, generateDescription,
restrictions.possibleValues(possibleValues)); restrictions.possibleValues(possibleValues));
} }
@ -161,17 +175,8 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.notMatch(notMatch)); return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.notMatch(notMatch));
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#getTypes()
*/
@Override
public Set<Type> 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. * {@literal description} and the fields of {@link Restrictions#toDocument()} if set.
*/ */
@Override @Override
@ -190,10 +195,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
} }
getOrCreateDescription().ifPresent(val -> document.append("description", val)); getOrCreateDescription().ifPresent(val -> document.append("description", val));
document.putAll(restrictions.toDocument());
if (restrictions != null) {
document.putAll(restrictions.toDocument());
}
return document; return document;
} }
@ -226,9 +228,9 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @author Christoph Strobl * @author Christoph Strobl
* @since 2.1 * @since 2.1
*/ */
static class ObjectJsonSchemaObject extends TypedJsonSchemaObject { public static class ObjectJsonSchemaObject extends TypedJsonSchemaObject {
private @Nullable Range<Integer> nrProperties; private @Nullable Range<Integer> propertiesCount;
private @Nullable Object additionalProperties; private @Nullable Object additionalProperties;
private List<String> requiredProperties = Collections.emptyList(); private List<String> requiredProperties = Collections.emptyList();
private List<JsonSchemaProperty> properties = Collections.emptyList(); private List<JsonSchemaProperty> properties = Collections.emptyList();
@ -254,35 +256,35 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @param range must not be {@literal null}. Consider {@link Range#unbounded()} instead. * @param range must not be {@literal null}. Consider {@link Range#unbounded()} instead.
* @return new instance of {@link ObjectJsonSchemaObject}. * @return new instance of {@link ObjectJsonSchemaObject}.
*/ */
public ObjectJsonSchemaObject nrProperties(Range<Integer> range) { public ObjectJsonSchemaObject propertiesCount(Range<Integer> range) {
ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
newInstance.nrProperties = range; newInstance.propertiesCount = range;
return newInstance; return newInstance;
} }
/** /**
* Define the {@literal minProperties}. * 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}. * @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(); Bound<Integer> upper = this.propertiesCount != null ? this.propertiesCount.getUpperBound() : Bound.unbounded();
return nrProperties(Range.of(Bound.inclusive(nrProperties), upper)); return propertiesCount(Range.of(Bound.inclusive(count), upper));
} }
/** /**
* Define the {@literal maxProperties}. * 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}. * @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(); Bound<Integer> lower = this.propertiesCount != null ? this.propertiesCount.getLowerBound() : Bound.unbounded();
return nrProperties(Range.of(lower, Bound.inclusive(nrProperties))); 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 = new ArrayList<>(this.requiredProperties.size() + properties.length);
newInstance.requiredProperties.addAll(this.requiredProperties); newInstance.requiredProperties.addAll(this.requiredProperties);
newInstance.requiredProperties.addAll(Arrays.asList(properties)); newInstance.requiredProperties.addAll(Arrays.asList(properties));
return newInstance; 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 * @param additionalPropertiesAllowed
* @return new instance of {@link ObjectJsonSchemaObject}. * @return new instance of {@link ObjectJsonSchemaObject}.
@ -310,6 +314,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
newInstance.additionalProperties = additionalPropertiesAllowed; newInstance.additionalProperties = additionalPropertiesAllowed;
return newInstance; return newInstance;
} }
@ -338,6 +343,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
newInstance.properties = new ArrayList<>(this.properties.size() + properties.length); newInstance.properties = new ArrayList<>(this.properties.size() + properties.length);
newInstance.properties.addAll(this.properties); newInstance.properties.addAll(this.properties);
newInstance.properties.addAll(Arrays.asList(properties)); newInstance.properties.addAll(Arrays.asList(properties));
return newInstance; return newInstance;
} }
@ -354,13 +360,14 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
newInstance.patternProperties = new ArrayList<>(this.patternProperties.size() + regularExpressions.length); newInstance.patternProperties = new ArrayList<>(this.patternProperties.size() + regularExpressions.length);
newInstance.patternProperties.addAll(this.patternProperties); newInstance.patternProperties.addAll(this.patternProperties);
newInstance.patternProperties.addAll(Arrays.asList(regularExpressions)); newInstance.patternProperties.addAll(Arrays.asList(regularExpressions));
return newInstance; return newInstance;
} }
/** /**
* Append the objects property along with the {@link JsonSchemaObject} validating against. * 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}. * @return new instance of {@link ObjectJsonSchemaObject}.
*/ */
public ObjectJsonSchemaObject property(JsonSchemaProperty property) { 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) * @see org.springframework.data.mongodb.core.schema.UntypedJsonSchemaObject#possibleValues(java.util.Collection)
*/ */
@Override @Override
public ObjectJsonSchemaObject possibleValues(Collection<Object> possibleValues) { public ObjectJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues));
} }
@ -442,17 +449,12 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
doc.append("required", requiredProperties); doc.append("required", requiredProperties);
} }
if (nrProperties != null) { if (propertiesCount != null) {
if (nrProperties.getLowerBound().isBounded()) {
doc.append("minProperties", nrProperties.getLowerBound().getValue().get());
}
if (nrProperties.getUpperBound().isBounded()) { propertiesCount.getLowerBound().getValue().ifPresent(it -> doc.append("minProperties", it));
doc.append("maxProperties", nrProperties.getUpperBound().getValue().get()); propertiesCount.getUpperBound().getValue().ifPresent(it -> doc.append("maxProperties", it));
}
} }
if (!CollectionUtils.isEmpty(properties)) { if (!CollectionUtils.isEmpty(properties)) {
doc.append("properties", reduceToDocument(properties)); doc.append("properties", reduceToDocument(properties));
} }
@ -469,15 +471,17 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
return doc; return doc;
} }
private ObjectJsonSchemaObject newInstance(String description, boolean generateDescription, private ObjectJsonSchemaObject newInstance(@Nullable String description, boolean generateDescription,
Restrictions restrictions) { Restrictions restrictions) {
ObjectJsonSchemaObject newInstance = new ObjectJsonSchemaObject(description, generateDescription, restrictions); ObjectJsonSchemaObject newInstance = new ObjectJsonSchemaObject(description, generateDescription, restrictions);
newInstance.properties = this.properties; newInstance.properties = this.properties;
newInstance.requiredProperties = this.requiredProperties; newInstance.requiredProperties = this.requiredProperties;
newInstance.additionalProperties = this.additionalProperties; newInstance.additionalProperties = this.additionalProperties;
newInstance.nrProperties = this.nrProperties; newInstance.propertiesCount = this.propertiesCount;
newInstance.patternProperties = this.patternProperties; newInstance.patternProperties = this.patternProperties;
return newInstance; return newInstance;
} }
@ -485,43 +489,46 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
return source.stream() // return source.stream() //
.map(JsonSchemaProperty::toDocument) // .map(JsonSchemaProperty::toDocument) //
.collect(Document::new, (target, propertyDocument) -> target.putAll(propertyDocument), .collect(Document::new, Document::putAll, (target, propertyDocument) -> {});
(target, propertyDocument) -> {});
} }
@Nullable /*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription()
*/
@Override @Override
protected String generateDescription() { protected String generateDescription() {
String errorMsg = "Must be an object"; String description = "Must be an object";
if (nrProperties != null) { if (propertiesCount != null) {
errorMsg += " with " + nrProperties + " properties"; description += String.format(" with %s properties", propertiesCount);
} }
if (!CollectionUtils.isEmpty(requiredProperties)) { if (!CollectionUtils.isEmpty(requiredProperties)) {
if (requiredProperties.size() == 1) { if (requiredProperties.size() == 1) {
errorMsg += " where " + requiredProperties.iterator().next() + "is mandatory"; description += String.format(" where %sis mandatory", requiredProperties.iterator().next());
} else { } else {
errorMsg += " where " + StringUtils.collectionToDelimitedString(requiredProperties, ", ") + " are mandatory"; description += String.format(" where %s are mandatory",
StringUtils.collectionToDelimitedString(requiredProperties, ", "));
} }
} }
if (additionalProperties instanceof Boolean) { if (additionalProperties instanceof Boolean) {
errorMsg += (((Boolean) additionalProperties) ? " " : " not ") + "allowing additional properties"; description += (((Boolean) additionalProperties) ? " " : " not ") + "allowing additional properties";
} }
if (!CollectionUtils.isEmpty(properties)) { if (!CollectionUtils.isEmpty(properties)) {
errorMsg += " defining restrictions for " + StringUtils.collectionToDelimitedString( description += String.format(" defining restrictions for %s", StringUtils.collectionToDelimitedString(
properties.stream().map(val -> val.getIdentifier()).collect(Collectors.toList()), ", "); properties.stream().map(JsonSchemaProperty::getIdentifier).collect(Collectors.toList()), ", "));
} }
if (!CollectionUtils.isEmpty(patternProperties)) { if (!CollectionUtils.isEmpty(patternProperties)) {
errorMsg += " defining restrictions for patterns " + StringUtils.collectionToDelimitedString( description += String.format(" defining restrictions for patterns %s", StringUtils.collectionToDelimitedString(
patternProperties.stream().map(val -> val.getIdentifier()).collect(Collectors.toList()), ", "); patternProperties.stream().map(JsonSchemaProperty::getIdentifier).collect(Collectors.toList()), ", "));
} }
return errorMsg + "."; return description + ".";
} }
} }
@ -534,7 +541,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @author Christoph Strobl * @author Christoph Strobl
* @since 2.1 * @since 2.1
*/ */
static class NumericJsonSchemaObject extends TypedJsonSchemaObject { public static class NumericJsonSchemaObject extends TypedJsonSchemaObject {
private static final Set<Type> NUMERIC_TYPES = new HashSet<>( private static final Set<Type> NUMERIC_TYPES = new HashSet<>(
Arrays.asList(Type.doubleType(), Type.intType(), Type.longType(), Type.numberType(), Type.bigDecimalType())); 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 Number multipleOf;
@Nullable Range<? extends Number> range; @Nullable Range<? extends Number> range;
public NumericJsonSchemaObject() { NumericJsonSchemaObject() {
this(Type.numberType()); this(Type.numberType());
} }
public NumericJsonSchemaObject(Type type) { NumericJsonSchemaObject(Type type) {
this(type, null, false); this(type, null, false);
} }
@ -571,6 +578,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
Assert.notNull(value, "Value must not be null!"); Assert.notNull(value, "Value must not be null!");
NumericJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); NumericJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
newInstance.multipleOf = value; newInstance.multipleOf = value;
return newInstance; return newInstance;
} }
@ -587,6 +595,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
NumericJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); NumericJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
newInstance.range = range; newInstance.range = range;
return newInstance; return newInstance;
} }
@ -596,6 +605,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @param min must not be {@literal null}. * @param min must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaObject}. * @return new instance of {@link NumericJsonSchemaObject}.
*/ */
@SuppressWarnings("unchecked")
public NumericJsonSchemaObject gt(Number min) { public NumericJsonSchemaObject gt(Number min) {
Assert.notNull(min, "Min must not be null!"); Assert.notNull(min, "Min must not be null!");
@ -610,6 +620,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @param min must not be {@literal null}. * @param min must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaObject}. * @return new instance of {@link NumericJsonSchemaObject}.
*/ */
@SuppressWarnings("unchecked")
public NumericJsonSchemaObject gte(Number min) { public NumericJsonSchemaObject gte(Number min) {
Assert.notNull(min, "Min must not be null!"); Assert.notNull(min, "Min must not be null!");
@ -624,6 +635,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @param max must not be {@literal null}. * @param max must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaObject}. * @return new instance of {@link NumericJsonSchemaObject}.
*/ */
@SuppressWarnings("unchecked")
public NumericJsonSchemaObject lt(Number max) { public NumericJsonSchemaObject lt(Number max) {
Assert.notNull(max, "Max must not be null!"); Assert.notNull(max, "Max must not be null!");
@ -638,6 +650,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @param max must not be {@literal null}. * @param max must not be {@literal null}.
* @return new instance of {@link NumericJsonSchemaObject}. * @return new instance of {@link NumericJsonSchemaObject}.
*/ */
@SuppressWarnings("unchecked")
NumericJsonSchemaObject lte(Number max) { NumericJsonSchemaObject lte(Number max) {
Assert.notNull(max, "Max must not be null!"); 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) * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection)
*/ */
@Override @Override
public NumericJsonSchemaObject possibleValues(Collection<Object> possibleValues) { public NumericJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues));
} }
@ -706,7 +719,6 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
*/ */
@Override @Override
public NumericJsonSchemaObject generatedDescription() { public NumericJsonSchemaObject generatedDescription() {
return newInstance(description, true, restrictions); return newInstance(description, true, restrictions);
} }
@ -726,14 +738,16 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
if (range != null) { if (range != null) {
if (range.getLowerBound().isBounded()) { if (range.getLowerBound().isBounded()) {
doc.append("minimum", range.getLowerBound().getValue().get());
range.getLowerBound().getValue().ifPresent(it -> doc.append("minimum", it));
if (!range.getLowerBound().isInclusive()) { if (!range.getLowerBound().isInclusive()) {
doc.append("exclusiveMinimum", true); doc.append("exclusiveMinimum", true);
} }
} }
if (range.getUpperBound().isBounded()) { if (range.getUpperBound().isBounded()) {
doc.append("maximum", range.getUpperBound().getValue().get());
range.getUpperBound().getValue().ifPresent(it -> doc.append("maximum", it));
if (!range.getUpperBound().isInclusive()) { if (!range.getUpperBound().isInclusive()) {
doc.append("exclusiveMaximum", true); doc.append("exclusiveMaximum", true);
} }
@ -743,7 +757,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
return doc; return doc;
} }
private NumericJsonSchemaObject newInstance(String description, boolean generateDescription, private NumericJsonSchemaObject newInstance(@Nullable String description, boolean generateDescription,
Restrictions restrictions) { Restrictions restrictions) {
NumericJsonSchemaObject newInstance = new NumericJsonSchemaObject(types, description, generateDescription, NumericJsonSchemaObject newInstance = new NumericJsonSchemaObject(types, description, generateDescription,
@ -751,11 +765,12 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
newInstance.multipleOf = this.multipleOf; newInstance.multipleOf = this.multipleOf;
newInstance.range = this.range; newInstance.range = this.range;
return newInstance; return newInstance;
} }
private Bound createBound(Number number, boolean inclusive) { private static Bound<?> createBound(Number number, boolean inclusive) {
if (number instanceof Long) { if (number instanceof Long) {
return inclusive ? Bound.inclusive((Long) number) : Bound.exclusive((Long) number); return inclusive ? Bound.inclusive((Long) number) : Bound.exclusive((Long) number);
@ -786,20 +801,23 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
return types; return types;
} }
@Nullable /*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription()
*/
@Override @Override
protected String generateDescription() { protected String generateDescription() {
String errorMsg = "Must be a numeric value"; String description = "Must be a numeric value";
if (multipleOf != null) { if (multipleOf != null) {
errorMsg += " multiple of " + multipleOf; description += String.format(" multiple of %s", multipleOf);
} }
if (range != null) { 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 * @author Christoph Strobl
* @since 2.1 * @since 2.1
*/ */
static class StringJsonSchemaObject extends TypedJsonSchemaObject { public static class StringJsonSchemaObject extends TypedJsonSchemaObject {
@Nullable Range<Integer> length; @Nullable Range<Integer> length;
@Nullable String pattern; @Nullable String pattern;
public StringJsonSchemaObject() { StringJsonSchemaObject() {
this(null, false, null); this(null, false, null);
} }
@ -837,30 +855,31 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
StringJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); StringJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
newInstance.length = range; newInstance.length = range;
return newInstance; 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 * @param length
* @return new instance of {@link StringJsonSchemaObject}. * @return new instance of {@link StringJsonSchemaObject}.
*/ */
public StringJsonSchemaObject minLength(int length) { public StringJsonSchemaObject minLength(int length) {
Bound upper = this.length != null ? this.length.getUpperBound() : Bound.unbounded(); Bound<Integer> upper = this.length != null ? this.length.getUpperBound() : Bound.unbounded();
return length(Range.of(Bound.inclusive(length), upper)); 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 * @param length
* @return new instance of {@link StringJsonSchemaObject}. * @return new instance of {@link StringJsonSchemaObject}.
*/ */
public StringJsonSchemaObject maxLength(int length) { public StringJsonSchemaObject maxLength(int length) {
Bound lower = this.length != null ? this.length.getLowerBound() : Bound.unbounded(); Bound<Integer> lower = this.length != null ? this.length.getLowerBound() : Bound.unbounded();
return length(Range.of(lower, Bound.inclusive(length))); return length(Range.of(lower, Bound.inclusive(length)));
} }
@ -876,6 +895,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
StringJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); StringJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
newInstance.pattern = pattern; newInstance.pattern = pattern;
return newInstance; return newInstance;
} }
@ -884,7 +904,7 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection) * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection)
*/ */
@Override @Override
public StringJsonSchemaObject possibleValues(Collection<Object> possibleValues) { public StringJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues));
} }
@ -953,13 +973,8 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
if (length != null) { if (length != null) {
if (length.getLowerBound().isBounded()) { length.getLowerBound().getValue().ifPresent(it -> doc.append("minLength", it));
doc.append("minLength", length.getLowerBound().getValue().get()); length.getUpperBound().getValue().ifPresent(it -> doc.append("maxLength", it));
}
if (length.getUpperBound().isBounded()) {
doc.append("maxLength", length.getUpperBound().getValue().get());
}
} }
if (!StringUtils.isEmpty(pattern)) { if (!StringUtils.isEmpty(pattern)) {
@ -969,40 +984,54 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
return doc; return doc;
} }
private StringJsonSchemaObject newInstance(String description, boolean generateDescription, private StringJsonSchemaObject newInstance(@Nullable String description, boolean generateDescription,
Restrictions restrictions) { Restrictions restrictions) {
StringJsonSchemaObject newInstance = new StringJsonSchemaObject(description, generateDescription, restrictions); StringJsonSchemaObject newInstance = new StringJsonSchemaObject(description, generateDescription, restrictions);
newInstance.length = this.length; newInstance.length = this.length;
newInstance.pattern = this.pattern; newInstance.pattern = this.pattern;
return newInstance; return newInstance;
} }
@Nullable /*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription()
*/
@Override @Override
protected String generateDescription() { protected String generateDescription() {
String errorMsg = "Must be a string"; String description = "Must be a string";
if (length != null) { if (length != null) {
errorMsg += " with length " + length; description += String.format(" with length %s", length);
} }
if (pattern != null) { 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.<br />
* 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 uniqueItems;
private @Nullable Boolean additionalItems;
private @Nullable Range<Integer> range; private @Nullable Range<Integer> range;
private Collection<JsonSchemaObject> items = Collections.emptyList(); private Collection<JsonSchemaObject> items = Collections.emptyList();
public ArrayJsonSchemaObject() { ArrayJsonSchemaObject() {
this(null, false, null); this(null, false, null);
} }
@ -1011,64 +1040,136 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
super(Collections.singleton(Type.arrayType()), description, generateDescription, restrictions); super(Collections.singleton(Type.arrayType()), description, generateDescription, restrictions);
} }
/**
* Define the whether the array must contain unique items.
*
* @param uniqueItems
* @return new instance of {@link ArrayJsonSchemaObject}.
*/
public ArrayJsonSchemaObject uniqueItems(boolean uniqueItems) { public ArrayJsonSchemaObject uniqueItems(boolean uniqueItems) {
ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
newInstance.uniqueItems = uniqueItems; newInstance.uniqueItems = uniqueItems;
return newInstance; return newInstance;
} }
/**
* Define the {@literal minItems} and {@literal maxItems} via the given {@link Range}.<br />
* 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<Integer> range) { public ArrayJsonSchemaObject range(Range<Integer> range) {
ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
newInstance.range = range; newInstance.range = range;
return newInstance; 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(); Bound<Integer> upper = this.range != null ? this.range.getUpperBound() : Bound.unbounded();
return range(Range.of(lower, Bound.inclusive(nrItems))); 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(); Bound<Integer> lower = this.range != null ? this.range.getLowerBound() : Bound.unbounded();
return range(Range.of(Bound.inclusive(nrItems), upper)); 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<JsonSchemaObject> items) { public ArrayJsonSchemaObject items(Collection<JsonSchemaObject> items) {
ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions);
newInstance.items = new ArrayList<>(items); 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; return newInstance;
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection)
*/
@Override @Override
public ArrayJsonSchemaObject possibleValues(Collection<Object> possibleValues) { public ArrayJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues));
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#allOf(java.util.Collection)
*/
@Override @Override
public ArrayJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) { public ArrayJsonSchemaObject allOf(Collection<JsonSchemaObject> allOf) {
return newInstance(description, generateDescription, restrictions.allOf(allOf)); return newInstance(description, generateDescription, restrictions.allOf(allOf));
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#anyOf(java.util.Collection)
*/
@Override @Override
public ArrayJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) { public ArrayJsonSchemaObject anyOf(Collection<JsonSchemaObject> anyOf) {
return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); return newInstance(description, generateDescription, restrictions.anyOf(anyOf));
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#oneOf(java.util.Collection)
*/
@Override @Override
public ArrayJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) { public ArrayJsonSchemaObject oneOf(Collection<JsonSchemaObject> oneOf) {
return newInstance(description, generateDescription, restrictions.oneOf(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 @Override
public ArrayJsonSchemaObject notMatch(JsonSchemaObject notMatch) { public ArrayJsonSchemaObject notMatch(JsonSchemaObject notMatch) {
return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); return newInstance(description, generateDescription, restrictions.notMatch(notMatch));
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#description(java.lang.String)
*/
@Override @Override
public ArrayJsonSchemaObject description(String description) { public ArrayJsonSchemaObject description(String description) {
return newInstance(description, generateDescription, restrictions); return newInstance(description, generateDescription, restrictions);
@ -1083,8 +1184,13 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
return newInstance(description, true, restrictions); return newInstance(description, true, restrictions);
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#toDocument()
*/
@Override @Override
public Document toDocument() { public Document toDocument() {
Document doc = new Document(super.toDocument()); Document doc = new Document(super.toDocument());
if (!CollectionUtils.isEmpty(items)) { if (!CollectionUtils.isEmpty(items)) {
@ -1094,59 +1200,80 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
if (range != null) { if (range != null) {
if (range.getLowerBound().isBounded()) { range.getLowerBound().getValue().ifPresent(it -> doc.append("minItems", it));
doc.append("minItems", range.getLowerBound().getValue().get()); range.getUpperBound().getValue().ifPresent(it -> doc.append("maxItems", it));
}
if (range.getUpperBound().isBounded()) {
doc.append("maxItems", range.getUpperBound().getValue().get());
}
} }
if (ObjectUtils.nullSafeEquals(uniqueItems, Boolean.TRUE)) { if (ObjectUtils.nullSafeEquals(uniqueItems, Boolean.TRUE)) {
doc.append("uniqueItems", true); doc.append("uniqueItems", true);
} }
if (additionalItems != null) {
doc.append("additionalItems", additionalItems);
}
return doc; return doc;
} }
private ArrayJsonSchemaObject newInstance(String description, boolean generateDescription, private ArrayJsonSchemaObject newInstance(@Nullable String description, boolean generateDescription,
Restrictions restrictions) { Restrictions restrictions) {
ArrayJsonSchemaObject newInstance = new ArrayJsonSchemaObject(description, generateDescription, restrictions); ArrayJsonSchemaObject newInstance = new ArrayJsonSchemaObject(description, generateDescription, restrictions);
newInstance.uniqueItems = this.uniqueItems; newInstance.uniqueItems = this.uniqueItems;
newInstance.range = this.range; newInstance.range = this.range;
newInstance.items = this.items; newInstance.items = this.items;
newInstance.additionalItems = this.additionalItems;
return newInstance; return newInstance;
} }
@Nullable /*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#generateDescription()
*/
@Override @Override
protected String generateDescription() { protected String generateDescription() {
String errorMsg = "Must be an array"; String description = "Must be an array";
if (ObjectUtils.nullSafeEquals(uniqueItems, Boolean.TRUE)) { 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) { if (range != null) {
errorMsg += " having size " + range; description += String.format(" having size %s", range);
} }
if (!ObjectUtils.isEmpty(items)) { if (!ObjectUtils.isEmpty(items)) {
errorMsg += " with items " + StringUtils.collectionToDelimitedString( description += String.format(" with items %s", StringUtils.collectionToDelimitedString(
items.stream().map(val -> val.toDocument()).collect(Collectors.toList()), ", "); 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.<br />
* 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); 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) * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection)
*/ */
@Override @Override
public BooleanJsonSchemaObject possibleValues(Collection<Object> possibleValues) { public BooleanJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
return new BooleanJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); return new BooleanJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues));
} }
@ -1218,11 +1345,28 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
return new BooleanJsonSchemaObject(description, true, restrictions); 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.<br />
* 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 { static class NullJsonSchemaObject extends TypedJsonSchemaObject {
public NullJsonSchemaObject() { NullJsonSchemaObject() {
this(null, false, null); 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) * @see org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject#possibleValues(java.util.Collection)
*/ */
@Override @Override
public NullJsonSchemaObject possibleValues(Collection<Object> possibleValues) { public NullJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
return new NullJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); return new NullJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues));
} }
@ -1284,5 +1428,23 @@ public class TypedJsonSchemaObject extends UntypedJsonSchemaObject {
public NullJsonSchemaObject description(String description) { public NullJsonSchemaObject description(String description) {
return new NullJsonSchemaObject(description, generateDescription, restrictions); 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.";
}
} }
} }

63
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; package org.springframework.data.mongodb.core.schema;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; 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. * 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 Christoph Strobl
* @author Mark Paluch
* @since 2.1 * @since 2.1
*/ */
public class UntypedJsonSchemaObject implements JsonSchemaObject { public class UntypedJsonSchemaObject implements JsonSchemaObject {
protected final @Nullable String description;
protected final Restrictions restrictions; protected final Restrictions restrictions;
protected final @Nullable String description;
protected final boolean generateDescription; 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.description = description;
this.restrictions = restrictions != null ? restrictions : Restrictions.empty(); this.restrictions = restrictions != null ? restrictions : Restrictions.empty();
this.generateDescription = generateDescription; this.generateDescription = generateDescription;
} }
/**
* Create a new instance of {@link UntypedJsonSchemaObject}.
*
* @return the new {@link UntypedJsonSchemaObject}.
*/
public static UntypedJsonSchemaObject newInstance() { public static UntypedJsonSchemaObject newInstance() {
return new UntypedJsonSchemaObject(null, null, false); return new UntypedJsonSchemaObject(null, null, false);
} }
/*
* (non-Javadoc)
* @see org.springframework.data.mongodb.core.schema.JsonSchemaObject#getTypes()
*/
@Override @Override
public Set<Type> getTypes() { public Set<Type> getTypes() {
return Collections.emptySet(); return Collections.emptySet();
@ -67,7 +83,6 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject {
/** /**
* Auto generate the {@literal description} if not explicitly set. * Auto generate the {@literal description} if not explicitly set.
* *
* @param description must not be {@literal null}.
* @return new instance of {@link TypedJsonSchemaObject}. * @return new instance of {@link TypedJsonSchemaObject}.
*/ */
public UntypedJsonSchemaObject generatedDescription() { public UntypedJsonSchemaObject generatedDescription() {
@ -80,7 +95,7 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject {
* @param possibleValues must not be {@literal null}. * @param possibleValues must not be {@literal null}.
* @return new instance of {@link TypedJsonSchemaObject}. * @return new instance of {@link TypedJsonSchemaObject}.
*/ */
public UntypedJsonSchemaObject possibleValues(Collection<Object> possibleValues) { public UntypedJsonSchemaObject possibleValues(Collection<? extends Object> possibleValues) {
return new UntypedJsonSchemaObject(restrictions.possibleValues(possibleValues), description, generateDescription); 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. * {@literal description} and the fields of {@link Restrictions#toDocument()} if set.
*/ */
@Override @Override
@ -135,9 +150,7 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject {
getOrCreateDescription().ifPresent(val -> document.append("description", val)); getOrCreateDescription().ifPresent(val -> document.append("description", val));
if (restrictions != null) { document.putAll(restrictions.toDocument());
document.putAll(restrictions.toDocument());
}
return document; 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 * @author Christoph Strobl
* @since 2.1 * @since 2.1
*/ */
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
static class Restrictions { static class Restrictions {
private final Collection<Object> possibleValues; private final Collection<? extends Object> possibleValues;
private final Collection<JsonSchemaObject> allOf; private final Collection<JsonSchemaObject> allOf;
private final Collection<JsonSchemaObject> anyOf; private final Collection<JsonSchemaObject> anyOf;
private final Collection<JsonSchemaObject> oneOf; private final Collection<JsonSchemaObject> oneOf;
private final @Nullable JsonSchemaObject notMatch; private final @Nullable JsonSchemaObject notMatch;
Restrictions(Collection<Object> possibleValues, Collection<JsonSchemaObject> allOf,
Collection<JsonSchemaObject> anyOf, Collection<JsonSchemaObject> oneOf, @Nullable JsonSchemaObject notMatch) {
this.possibleValues = possibleValues;
this.allOf = allOf;
this.anyOf = anyOf;
this.oneOf = oneOf;
this.notMatch = notMatch;
}
/** /**
* @return new empty {@link Restrictions}. * @return new empty {@link Restrictions}.
*/ */
@ -199,7 +204,7 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject {
* @param possibleValues must not be {@literal null}. * @param possibleValues must not be {@literal null}.
* @return * @return
*/ */
Restrictions possibleValues(Collection<Object> possibleValues) { Restrictions possibleValues(Collection<? extends Object> possibleValues) {
Assert.notNull(possibleValues, "PossibleValues must not be null!"); Assert.notNull(possibleValues, "PossibleValues must not be null!");
return new Restrictions(possibleValues, allOf, anyOf, oneOf, notMatch); 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. * {@literal allOf}, {@literal anyOf}, {@literal oneOf}, {@literal notMatch} if set.
* *
* @return never {@literal null} * @return never {@literal null}
@ -258,20 +263,28 @@ public class UntypedJsonSchemaObject implements JsonSchemaObject {
if (!CollectionUtils.isEmpty(possibleValues)) { if (!CollectionUtils.isEmpty(possibleValues)) {
document.append("enum", possibleValues); document.append("enum", possibleValues);
} }
if (!CollectionUtils.isEmpty(allOf)) { if (!CollectionUtils.isEmpty(allOf)) {
document.append("allOf", allOf.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList())); document.append("allOf", render(allOf));
} }
if (!CollectionUtils.isEmpty(anyOf)) { if (!CollectionUtils.isEmpty(anyOf)) {
document.append("anyOf", anyOf.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList())); document.append("anyOf", render(anyOf));
} }
if (!CollectionUtils.isEmpty(oneOf)) { if (!CollectionUtils.isEmpty(oneOf)) {
document.append("oneOf", oneOf.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList())); document.append("oneOf", render(oneOf));
} }
if (notMatch != null) { if (notMatch != null) {
document.append("not", notMatch.toDocument()); document.append("not", notMatch.toDocument());
} }
return document; return document;
} }
private static List<Document> render(Collection<JsonSchemaObject> objects) {
return objects.stream().map(JsonSchemaObject::toDocument).collect(Collectors.toList());
}
} }
} }

16
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. * MongoDB-specific JSON schema implementation classes.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
@org.springframework.lang.NonNullApi @org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields @org.springframework.lang.NonNullFields

37
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. * Tests verifying {@link org.bson.Document} representation of {@link JsonSchemaObject}s.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
*/ */
public class JsonSchemaObjectUnitTests { public class JsonSchemaObjectUnitTests {
@ -49,7 +50,7 @@ public class JsonSchemaObjectUnitTests {
@Test // DATAMONGO-1835 @Test // DATAMONGO-1835
public void objectObjectShouldRenderNrPropertiesCorrectly() { 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.") .isEqualTo(new Document("type", "object").append("description", "Must be an object with [10-20] properties.")
.append("minProperties", 10).append("maxProperties", 20)); .append("minProperties", 10).append("maxProperties", 20));
} }
@ -100,6 +101,7 @@ public class JsonSchemaObjectUnitTests {
.append("properties", new Document("city", new Document("type", "string") .append("properties", new Document("city", new Document("type", "string")
.append("description", "Must be a string with length [3-unbounded.").append("minLength", 3))))); .append("description", "Must be a string with length [3-unbounded.").append("minLength", 3)))));
assertThat(object() assertThat(object()
.properties(JsonSchemaProperty.object("address") .properties(JsonSchemaProperty.object("address")
.properties(JsonSchemaProperty.string("city").minLength(3).generatedDescription()).generatedDescription()) .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)); .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' // type : 'any'
// ----------------- // -----------------

55
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");
}
}

8
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; import com.mongodb.client.model.ValidationOptions;
/** /**
* Integration tests for {@link MongoJsonSchema}.
*
* @author Christoph Strobl * @author Christoph Strobl
* @since 2017/12
*/ */
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration @ContextConfiguration
@ -153,10 +154,6 @@ public class MongoJsonSchemaTests {
Document collectionInfo = template Document collectionInfo = template
.executeCommand(new Document("listCollections", 1).append("filter", new Document("name", collectionName))); .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")) { if (collectionInfo.containsKey("cursor")) {
collectionInfo = (Document) collectionInfo.get("cursor", Document.class).get("firstBatch", List.class).iterator() collectionInfo = (Document) collectionInfo.get("cursor", Document.class).get("firstBatch", List.class).iterator()
.next(); .next();
@ -186,5 +183,4 @@ public class MongoJsonSchemaTests {
@Field("post_code") String postCode; @Field("post_code") String postCode;
} }
} }

23
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; import org.mockito.junit.MockitoJUnitRunner;
/** /**
* Unit tests for {@link MongoJsonSchema}.
*
* @author Christoph Strobl * @author Christoph Strobl
* @author Mark Paluch
*/ */
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class MongoJsonSchemaUnitTests { public class MongoJsonSchemaUnitTests {
@ -41,8 +44,26 @@ public class MongoJsonSchemaUnitTests {
new Document("type", "object").append("required", Arrays.asList("firstname", "lastname")))); 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 @Test(expected = IllegalArgumentException.class) // DATAMONGO-1835
public void throwsExceptionOnNullRoot() { public void throwsExceptionOnNullRoot() {
MongoJsonSchema.of(null); MongoJsonSchema.of((JsonSchemaObject) null);
}
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1835
public void throwsExceptionOnNullDocument() {
MongoJsonSchema.of((Document) null);
} }
} }

140
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;
}
}
Loading…
Cancel
Save