CollectionOptions.java
/*
* Copyright 2010-present 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
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Function;
import org.bson.BsonBinary;
import org.bson.BsonBinarySubType;
import org.bson.BsonNull;
import org.bson.Document;
import org.jspecify.annotations.Nullable;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.Collation;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
import org.springframework.data.mongodb.core.schema.QueryCharacteristic;
import org.springframework.data.mongodb.core.timeseries.Granularity;
import org.springframework.data.mongodb.core.timeseries.GranularityDefinition;
import org.springframework.data.mongodb.core.timeseries.Span;
import org.springframework.data.mongodb.core.validation.Validator;
import org.springframework.data.util.Optionals;
import org.springframework.lang.CheckReturnValue;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import com.mongodb.client.model.ValidationAction;
import com.mongodb.client.model.ValidationLevel;
/**
* Provides a simple wrapper to encapsulate the variety of settings you can use when creating a collection.
*
* @author Thomas Risberg
* @author Christoph Strobl
* @author Mark Paluch
* @author Andreas Zink
* @author Ben Foster
* @author Ross Lawley
*/
public class CollectionOptions {
private @Nullable Long maxDocuments;
private @Nullable Long size;
private @Nullable Boolean capped;
private @Nullable Collation collation;
private ValidationOptions validationOptions;
private @Nullable TimeSeriesOptions timeSeriesOptions;
private @Nullable CollectionChangeStreamOptions changeStreamOptions;
private @Nullable EncryptedFieldsOptions encryptedFieldsOptions;
private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped,
@Nullable Collation collation, ValidationOptions validationOptions, @Nullable TimeSeriesOptions timeSeriesOptions,
@Nullable CollectionChangeStreamOptions changeStreamOptions,
@Nullable EncryptedFieldsOptions encryptedFieldsOptions) {
this.maxDocuments = maxDocuments;
this.size = size;
this.capped = capped;
this.collation = collation;
this.validationOptions = validationOptions;
this.timeSeriesOptions = timeSeriesOptions;
this.changeStreamOptions = changeStreamOptions;
this.encryptedFieldsOptions = encryptedFieldsOptions;
}
/**
* Create new {@link CollectionOptions} by just providing the {@link Collation} to use.
*
* @param collation must not be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.0
*/
public static CollectionOptions just(Collation collation) {
Assert.notNull(collation, "Collation must not be null");
return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null, null);
}
/**
* Create new empty {@link CollectionOptions}.
*
* @return new {@link CollectionOptions}.
* @since 2.0
*/
public static CollectionOptions empty() {
return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null, null);
}
/**
* Quick way to set up {@link CollectionOptions} for a Time Series collection. For more advanced settings use
* {@link #timeSeries(String, Function)}.
*
* @param timeField The name of the property which contains the date in each time series document. Must not be
* {@literal null}.
* @return new instance of {@link CollectionOptions}.
* @see #timeSeries(TimeSeriesOptions)
* @since 3.3
*/
public static CollectionOptions timeSeries(String timeField) {
return timeSeries(timeField, it -> it);
}
/**
* Set up {@link CollectionOptions} for a Time Series collection.
*
* @param timeField the name of the field that contains the date in each time series document.
* @param options a function to apply additional settings to {@link TimeSeriesOptions}.
* @return new instance of {@link CollectionOptions}.
* @since 4.4
*/
public static CollectionOptions timeSeries(String timeField, Function<TimeSeriesOptions, TimeSeriesOptions> options) {
return empty().timeSeries(options.apply(TimeSeriesOptions.timeSeries(timeField)));
}
/**
* Quick way to set up {@link CollectionOptions} for emitting (pre and post) change events.
*
* @return new instance of {@link CollectionOptions}.
* @see #changeStream(CollectionChangeStreamOptions)
* @see CollectionChangeStreamOptions#preAndPostImages(boolean)
* @since 4.0
*/
public static CollectionOptions emitChangedRevisions() {
return empty().changeStream(CollectionChangeStreamOptions.preAndPostImages(true));
}
/**
* Create new {@link CollectionOptions} with the given {@code encryptedFields}.
*
* @param encryptedFieldsOptions can be null
* @return new instance of {@link CollectionOptions}.
* @since 4.5.0
*/
@Contract("_ -> new")
@CheckReturnValue
public static CollectionOptions encryptedCollection(@Nullable EncryptedFieldsOptions encryptedFieldsOptions) {
return new CollectionOptions(null, null, null, null, ValidationOptions.NONE, null, null, encryptedFieldsOptions);
}
/**
* Create new {@link CollectionOptions} reading encryption options from the given {@link MongoJsonSchema}.
*
* @param schema must not be {@literal null}.
* @return new instance of {@link CollectionOptions}.
* @since 4.5.0
*/
@Contract("_ -> new")
@CheckReturnValue
public static CollectionOptions encryptedCollection(MongoJsonSchema schema) {
return encryptedCollection(EncryptedFieldsOptions.fromSchema(schema));
}
/**
* Create new {@link CollectionOptions} building encryption options in a fluent style.
*
* @param optionsFunction must not be {@literal null}.
* @return new instance of {@link CollectionOptions}.
* @since 4.5.0
*/
@Contract("_ -> new")
@CheckReturnValue
public static CollectionOptions encryptedCollection(
Function<EncryptedFieldsOptions, EncryptedFieldsOptions> optionsFunction) {
return encryptedCollection(optionsFunction.apply(new EncryptedFieldsOptions()));
}
/**
* Create new {@link CollectionOptions} with already given settings and capped set to {@literal true}. <br />
* <strong>NOTE:</strong> Using capped collections requires defining {@link #size(long)}.
*
* @return new {@link CollectionOptions}.
* @since 2.0
*/
public CollectionOptions capped() {
return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, timeSeriesOptions,
changeStreamOptions, encryptedFieldsOptions);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code maxDocuments} set to given value.
*
* @param maxDocuments can be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.0
*/
public CollectionOptions maxDocuments(long maxDocuments) {
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions, encryptedFieldsOptions);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code size} set to given value.
*
* @param size can be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.0
*/
public CollectionOptions size(long size) {
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions, encryptedFieldsOptions);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code collation} set to given value.
*
* @param collation can be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.0
*/
public CollectionOptions collation(@Nullable Collation collation) {
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions, encryptedFieldsOptions);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given
* {@link MongoJsonSchema}.
*
* @param schema must not be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions schema(MongoJsonSchema schema) {
return validator(schema != null ? Validator.schema(schema) : null);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given
* {@link Validator}.
*
* @param validator can be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions validator(@Nullable Validator validator) {
return validation(validationOptions.validator(validator));
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to
* {@link ValidationLevel#OFF}.
*
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions disableValidation() {
return schemaValidationLevel(ValidationLevel.OFF);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to
* {@link ValidationLevel#STRICT}.
*
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions strictValidation() {
return schemaValidationLevel(ValidationLevel.STRICT);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to
* {@link ValidationLevel#MODERATE}.
*
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions moderateValidation() {
return schemaValidationLevel(ValidationLevel.MODERATE);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationAction} set to
* {@link ValidationAction#WARN}.
*
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions warnOnValidationError() {
return schemaValidationAction(ValidationAction.WARN);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationAction} set to
* {@link ValidationAction#ERROR}.
*
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions failOnValidationError() {
return schemaValidationAction(ValidationAction.ERROR);
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set given
* {@link ValidationLevel}.
*
* @param validationLevel must not be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions schemaValidationLevel(ValidationLevel validationLevel) {
Assert.notNull(validationLevel, "ValidationLevel must not be null");
return validation(validationOptions.validationLevel(validationLevel));
}
/**
* Create new {@link CollectionOptions} with already given settings and {@code validationAction} set given
* {@link ValidationAction}.
*
* @param validationAction must not be {@literal null}.
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions schemaValidationAction(ValidationAction validationAction) {
Assert.notNull(validationAction, "ValidationAction must not be null");
return validation(validationOptions.validationAction(validationAction));
}
/**
* Create new {@link CollectionOptions} with the given {@link ValidationOptions}.
*
* @param validationOptions must not be {@literal null}. Use {@link ValidationOptions#none()} to remove validation.
* @return new {@link CollectionOptions}.
* @since 2.1
*/
public CollectionOptions validation(ValidationOptions validationOptions) {
Assert.notNull(validationOptions, "ValidationOptions must not be null");
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions, encryptedFieldsOptions);
}
/**
* Create new {@link CollectionOptions} with the given {@link TimeSeriesOptions}.
*
* @param timeSeriesOptions must not be {@literal null}.
* @return new instance of {@link CollectionOptions}.
* @since 3.3
*/
public CollectionOptions timeSeries(TimeSeriesOptions timeSeriesOptions) {
Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null");
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions, encryptedFieldsOptions);
}
/**
* Create new {@link CollectionOptions} with the given {@link TimeSeriesOptions}.
*
* @param changeStreamOptions must not be {@literal null}.
* @return new instance of {@link CollectionOptions}.
* @since 3.3
*/
public CollectionOptions changeStream(CollectionChangeStreamOptions changeStreamOptions) {
Assert.notNull(changeStreamOptions, "ChangeStreamOptions must not be null");
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions, encryptedFieldsOptions);
}
/**
* Set the {@link EncryptedFieldsOptions} for collections using queryable encryption.
*
* @param encryptedFieldsOptions must not be {@literal null}.
* @return new instance of {@link CollectionOptions}.
* @since 4.5
*/
@Contract("_ -> new")
@CheckReturnValue
public CollectionOptions encrypted(EncryptedFieldsOptions encryptedFieldsOptions) {
Assert.notNull(encryptedFieldsOptions, "EncryptedCollectionOptions must not be null");
return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
changeStreamOptions, encryptedFieldsOptions);
}
/**
* Get the max number of documents the collection should be limited to.
*
* @return {@link Optional#empty()} if not set.
*/
public Optional<Long> getMaxDocuments() {
return Optional.ofNullable(maxDocuments);
}
/**
* Get the {@literal size} in bytes the collection should be limited to.
*
* @return {@link Optional#empty()} if not set.
*/
public Optional<Long> getSize() {
return Optional.ofNullable(size);
}
/**
* Get if the collection should be capped.
*
* @return {@link Optional#empty()} if not set.
* @since 2.0
*/
public Optional<Boolean> getCapped() {
return Optional.ofNullable(capped);
}
/**
* Get the {@link Collation} settings.
*
* @return {@link Optional#empty()} if not set.
* @since 2.0
*/
public Optional<Collation> getCollation() {
return Optional.ofNullable(collation);
}
/**
* Get the {@link MongoJsonSchema} for the collection.
*
* @return {@link Optional#empty()} if not set.
* @since 2.1
*/
public Optional<ValidationOptions> getValidationOptions() {
return validationOptions.isEmpty() ? Optional.empty() : Optional.of(validationOptions);
}
/**
* Get the {@link TimeSeriesOptions} if available.
*
* @return {@link Optional#empty()} if not specified.
* @since 3.3
*/
public Optional<TimeSeriesOptions> getTimeSeriesOptions() {
return Optional.ofNullable(timeSeriesOptions);
}
/**
* Get the {@link CollectionChangeStreamOptions} if available.
*
* @return {@link Optional#empty()} if not specified.
* @since 4.0
*/
public Optional<CollectionChangeStreamOptions> getChangeStreamOptions() {
return Optional.ofNullable(changeStreamOptions);
}
/**
* Get the {@code encryptedFields} if available.
*
* @return {@link Optional#empty()} if not specified.
* @since 4.5
*/
public Optional<EncryptedFieldsOptions> getEncryptedFieldsOptions() {
return Optional.ofNullable(encryptedFieldsOptions);
}
@Override
public String toString() {
return "CollectionOptions{" + "maxDocuments=" + maxDocuments + ", size=" + size + ", capped=" + capped
+ ", collation=" + collation + ", validationOptions=" + validationOptions + ", timeSeriesOptions="
+ timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", encryptedCollectionOptions="
+ encryptedFieldsOptions + ", disableValidation=" + disableValidation() + ", strictValidation="
+ strictValidation() + ", moderateValidation=" + moderateValidation() + ", warnOnValidationError="
+ warnOnValidationError() + ", failOnValidationError=" + failOnValidationError() + '}';
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CollectionOptions that = (CollectionOptions) o;
if (!ObjectUtils.nullSafeEquals(maxDocuments, that.maxDocuments)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(size, that.size)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(capped, that.capped)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(collation, that.collation)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(validationOptions, that.validationOptions)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(timeSeriesOptions, that.timeSeriesOptions)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions)) {
return false;
}
return ObjectUtils.nullSafeEquals(encryptedFieldsOptions, that.encryptedFieldsOptions);
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(maxDocuments);
result = 31 * result + ObjectUtils.nullSafeHashCode(size);
result = 31 * result + ObjectUtils.nullSafeHashCode(capped);
result = 31 * result + ObjectUtils.nullSafeHashCode(collation);
result = 31 * result + ObjectUtils.nullSafeHashCode(validationOptions);
result = 31 * result + ObjectUtils.nullSafeHashCode(timeSeriesOptions);
result = 31 * result + ObjectUtils.nullSafeHashCode(changeStreamOptions);
result = 31 * result + ObjectUtils.nullSafeHashCode(encryptedFieldsOptions);
return result;
}
/**
* Encapsulation of ValidationOptions options.
*
* @author Christoph Strobl
* @author Andreas Zink
* @since 2.1
*/
public static class ValidationOptions {
private static final ValidationOptions NONE = new ValidationOptions(null, null, null);
private final @Nullable Validator validator;
private final @Nullable ValidationLevel validationLevel;
private final @Nullable ValidationAction validationAction;
public ValidationOptions(@Nullable Validator validator, @Nullable ValidationLevel validationLevel,
@Nullable ValidationAction validationAction) {
this.validator = validator;
this.validationLevel = validationLevel;
this.validationAction = validationAction;
}
/**
* Create an empty {@link ValidationOptions}.
*
* @return never {@literal null}.
*/
public static ValidationOptions none() {
return NONE;
}
/**
* Define the {@link Validator} to be used for document validation.
*
* @param validator can be {@literal null}.
* @return new instance of {@link ValidationOptions}.
*/
@Contract("_ -> new")
public ValidationOptions validator(@Nullable Validator validator) {
return new ValidationOptions(validator, validationLevel, validationAction);
}
/**
* Define the validation level to apply.
*
* @param validationLevel can be {@literal null}.
* @return new instance of {@link ValidationOptions}.
*/
@Contract("_ -> new")
public ValidationOptions validationLevel(ValidationLevel validationLevel) {
return new ValidationOptions(validator, validationLevel, validationAction);
}
/**
* Define the validation action to take.
*
* @param validationAction can be {@literal null}.
* @return new instance of {@link ValidationOptions}.
*/
@Contract("_ -> new")
public ValidationOptions validationAction(ValidationAction validationAction) {
return new ValidationOptions(validator, validationLevel, validationAction);
}
/**
* Get the {@link Validator} to use.
*
* @return never {@literal null}.
*/
public Optional<Validator> getValidator() {
return Optional.ofNullable(validator);
}
/**
* Get the {@code validationLevel} to apply.
*
* @return {@link Optional#empty()} if not set.
*/
public Optional<ValidationLevel> getValidationLevel() {
return Optional.ofNullable(validationLevel);
}
/**
* Get the {@code validationAction} to perform.
*
* @return {@link Optional#empty()} if not set.
*/
public Optional<ValidationAction> getValidationAction() {
return Optional.ofNullable(validationAction);
}
/**
* @return {@literal true} if no arguments set.
*/
boolean isEmpty() {
return !Optionals.isAnyPresent(getValidator(), getValidationAction(), getValidationLevel());
}
@Override
public String toString() {
return "ValidationOptions{" + "validator=" + validator + ", validationLevel=" + validationLevel
+ ", validationAction=" + validationAction + '}';
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ValidationOptions that = (ValidationOptions) o;
if (!ObjectUtils.nullSafeEquals(validator, that.validator)) {
return false;
}
if (validationLevel != that.validationLevel)
return false;
return validationAction == that.validationAction;
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(validator);
result = 31 * result + ObjectUtils.nullSafeHashCode(validationLevel);
result = 31 * result + ObjectUtils.nullSafeHashCode(validationAction);
return result;
}
}
/**
* Encapsulation of Encryption options for collections.
*
* @author Christoph Strobl
* @since 4.5
*/
public static class EncryptedFieldsOptions {
private static final EncryptedFieldsOptions NONE = new EncryptedFieldsOptions();
private final @Nullable MongoJsonSchema schema;
private final List<JsonSchemaProperty> properties;
EncryptedFieldsOptions() {
this(null, List.of());
}
private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema, List<JsonSchemaProperty> queryableProperties) {
this.schema = schema;
this.properties = queryableProperties;
}
/**
* @return {@link EncryptedFieldsOptions#NONE}
*/
public static EncryptedFieldsOptions none() {
return NONE;
}
/**
* @return new instance of {@link EncryptedFieldsOptions}.
*/
public static EncryptedFieldsOptions fromSchema(MongoJsonSchema schema) {
return new EncryptedFieldsOptions(schema, List.of());
}
/**
* @return new instance of {@link EncryptedFieldsOptions}.
*/
public static EncryptedFieldsOptions fromProperties(List<? extends JsonSchemaProperty> properties) {
return new EncryptedFieldsOptions(null, List.copyOf(properties));
}
/**
* Add a new {@link QueryableJsonSchemaProperty queryable property} for the given source property.
* <p>
* Please note that, a given {@link JsonSchemaProperty} may override options from a given {@link MongoJsonSchema} if
* set.
*
* @param property the queryable source - typically
* {@link org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty
* encrypted}.
* @param characteristics the query options to set.
* @return new instance of {@link EncryptedFieldsOptions}.
*/
@Contract("_, _ -> new")
@CheckReturnValue
public EncryptedFieldsOptions queryable(JsonSchemaProperty property, QueryCharacteristic... characteristics) {
List<JsonSchemaProperty> targetPropertyList = new ArrayList<>(properties.size() + 1);
targetPropertyList.addAll(properties);
targetPropertyList.add(JsonSchemaProperty.queryable(property, List.of(characteristics)));
return new EncryptedFieldsOptions(schema, targetPropertyList);
}
/**
* Add an {@link EncryptedJsonSchemaProperty encrypted property} that should not be queryable.
*
* @param property must not be {@literal null}.
* @return new instance of {@link EncryptedFieldsOptions}.
* @since 4.5.1
*/
@Contract("_ -> new")
@CheckReturnValue
public EncryptedFieldsOptions with(EncryptedJsonSchemaProperty property) {
return encrypted(property, null);
}
/**
* Add a {@link JsonSchemaProperty property} that should be encrypted but not queryable.
*
* @param property must not be {@literal null}.
* @param keyId the key identifier to be used, can be {@literal null}.
* @return new instance of {@link EncryptedFieldsOptions}.
* @since 4.5.1
*/
@Contract("_, _ -> new")
@CheckReturnValue
public EncryptedFieldsOptions encrypted(JsonSchemaProperty property, @Nullable Object keyId) {
List<JsonSchemaProperty> targetPropertyList = new ArrayList<>(properties.size() + 1);
targetPropertyList.addAll(properties);
if (property instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty) {
targetPropertyList.add(property);
} else {
EncryptedJsonSchemaProperty encryptedJsonSchemaProperty = new EncryptedJsonSchemaProperty(property);
if (keyId != null) {
targetPropertyList.add(encryptedJsonSchemaProperty.keyId(keyId));
}
}
return new EncryptedFieldsOptions(schema, targetPropertyList);
}
public Document toDocument() {
return new Document("fields", selectPaths());
}
private List<Document> selectPaths() {
Map<String, Document> fields = new LinkedHashMap<>();
for (Document field : fromSchema()) {
fields.put(field.get("path", String.class), field);
}
for (Document field : fromProperties()) {
fields.put(field.get("path", String.class), field);
}
return List.copyOf(fields.values());
}
private List<Document> fromProperties() {
if (properties.isEmpty()) {
return List.of();
}
List<Document> converted = new ArrayList<>(properties.size());
for (JsonSchemaProperty property : properties) {
converted.add(getEncryptedField(property));
}
return converted;
}
private Document getEncryptedField(JsonSchemaProperty property) {
Document field = new Document("path", property.getIdentifier());
if (!property.getTypes().isEmpty()) {
field.append("bsonType", property.getTypes().iterator().next().toBsonType().value());
}
if (property instanceof QueryableJsonSchemaProperty qp
&& qp.getTargetProperty() instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted
&& encrypted.getKeyId() != null) {
if (encrypted.getKeyId() instanceof String stringKey) {
field.append("keyId",
new BsonBinary(BsonBinarySubType.UUID_STANDARD, stringKey.getBytes(StandardCharsets.UTF_8)));
} else {
field.append("keyId", encrypted.getKeyId());
}
} else if (property instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted
&& encrypted.getKeyId() != null) {
if (encrypted.getKeyId() instanceof String stringKey) {
field.append("keyId",
new BsonBinary(BsonBinarySubType.UUID_STANDARD, stringKey.getBytes(StandardCharsets.UTF_8)));
} else {
field.append("keyId", encrypted.getKeyId());
}
}
if (property instanceof QueryableJsonSchemaProperty qp) {
field.append("queries", qp.getCharacteristics().map(QueryCharacteristic::toDocument).toList());
}
if (!field.containsKey("keyId")) {
field.append("keyId", BsonNull.VALUE);
}
return field;
}
private List<Document> fromSchema() {
if (schema == null) {
return List.of();
}
Document root = schema.schemaDocument();
Map<String, Document> paths = new LinkedHashMap<>();
collectPaths(root, null, paths);
List<Document> fields = new ArrayList<>();
if (paths.isEmpty()) {
return fields;
}
for (Entry<String, Document> entry : paths.entrySet()) {
Document field = new Document("path", entry.getKey());
field.append("keyId", entry.getValue().getOrDefault("keyId", BsonNull.VALUE));
if (entry.getValue().containsKey("bsonType")) {
field.append("bsonType", entry.getValue().get("bsonType"));
}
if (entry.getValue().containsKey("queries")) {
field.put("queries", entry.getValue().get("queries"));
}
fields.add(field);
}
return fields;
}
}
private static void collectPaths(Document document, @Nullable String currentPath, Map<String, Document> paths) {
if (document.containsKey("type") && document.get("type").equals("object")) {
Object o = document.get("properties");
if (!(o instanceof Document properties)) {
return;
}
for (Entry<String, Object> entry : properties.entrySet()) {
if (entry.getValue() instanceof Document nested) {
String path = currentPath == null ? entry.getKey() : (currentPath + "." + entry.getKey());
if (nested.containsKey("encrypt")) {
Document target = new Document(nested.get("encrypt", Document.class));
if (nested.containsKey("queries")) {
List<?> queries = nested.get("queries", List.class);
if (!queries.isEmpty() && queries.iterator().next() instanceof Document qd) {
target.putAll(qd);
}
}
paths.put(path, target);
} else {
collectPaths(nested, path, paths);
}
}
}
}
}
/**
* Encapsulation of options applied to define collections change stream behaviour.
*
* @author Christoph Strobl
* @since 4.0
*/
public static class CollectionChangeStreamOptions {
private final boolean preAndPostImages;
private CollectionChangeStreamOptions(boolean emitChangedRevisions) {
this.preAndPostImages = emitChangedRevisions;
}
/**
* Output the version of a document before and after changes (the document pre- and post-images).
*
* @return new instance of {@link CollectionChangeStreamOptions}.
*/
public static CollectionChangeStreamOptions preAndPostImages(boolean emitChangedRevisions) {
return new CollectionChangeStreamOptions(true);
}
public boolean getPreAndPostImages() {
return preAndPostImages;
}
@Override
public String toString() {
return "CollectionChangeStreamOptions{" + "preAndPostImages=" + preAndPostImages + '}';
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CollectionChangeStreamOptions that = (CollectionChangeStreamOptions) o;
return preAndPostImages == that.preAndPostImages;
}
@Override
public int hashCode() {
return (preAndPostImages ? 1 : 0);
}
}
/**
* Options applicable to Time Series collections.
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/core/timeseries-collections">https://docs.mongodb.com/manual/core/timeseries-collections</a>
* @since 3.3
*/
public static class TimeSeriesOptions {
private final String timeField;
private @Nullable final String metaField;
private final GranularityDefinition granularity;
private final @Nullable Span span;
private final Duration expireAfter;
private TimeSeriesOptions(String timeField, @Nullable String metaField, GranularityDefinition granularity,
@Nullable Span span, Duration expireAfter) {
Assert.hasText(timeField, "Time field must not be empty or null");
if (!Granularity.DEFAULT.equals(granularity) && span != null) {
throw new IllegalArgumentException(
"Cannot use granularity [%s] in conjunction with span".formatted(granularity.name()));
}
this.timeField = timeField;
this.metaField = metaField;
this.granularity = granularity;
this.span = span;
this.expireAfter = expireAfter;
}
/**
* Create a new instance of {@link TimeSeriesOptions} using the given field as its {@literal timeField}. The one,
* that contains the date in each time series document. <br />
* {@link Field#name() Annotated fieldnames} will be considered during the mapping process.
*
* @param timeField must not be {@literal null}.
* @return new instance of {@link TimeSeriesOptions}.
*/
public static TimeSeriesOptions timeSeries(String timeField) {
return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT, null, Duration.ofSeconds(-1));
}
/**
* Set the name of the field which contains metadata in each time series document. Should not be the {@literal id}
* nor {@link TimeSeriesOptions#timeSeries(String)} timeField} nor point to an {@literal array} or
* {@link java.util.Collection}. <br />
* {@link Field#name() Annotated fieldnames} will be considered during the mapping process.
*
* @param metaField use {@literal null} to unset.
* @return new instance of {@link TimeSeriesOptions}.
*/
@Contract("_ -> new")
public TimeSeriesOptions metaField(@Nullable String metaField) {
return new TimeSeriesOptions(timeField, metaField, granularity, span, expireAfter);
}
/**
* Select the {@link GranularityDefinition} parameter to define how data in the time series collection is organized.
* Select one that is closest to the time span between incoming measurements.
*
* @return new instance of {@link TimeSeriesOptions}.
* @see Granularity
*/
@Contract("_ -> new")
public TimeSeriesOptions granularity(GranularityDefinition granularity) {
return new TimeSeriesOptions(timeField, metaField, granularity, span, expireAfter);
}
/**
* Select the time between timestamps in the same bucket to define how data in the time series collection is
* organized. Cannot be used in conjunction with {@link #granularity(GranularityDefinition)}.
*
* @param span use {@literal null} to unset.
* @return new instance of {@link TimeSeriesOptions}.
* @see Span
* @since 5.0
*/
@Contract("_ -> new")
public TimeSeriesOptions span(@Nullable Span span) {
return new TimeSeriesOptions(timeField, metaField, granularity, span, expireAfter);
}
/**
* Set the {@link Duration} for automatic removal of documents older than a specified value.
*
* @param ttl must not be {@literal null}.
* @return new instance of {@link TimeSeriesOptions}.
* @see com.mongodb.client.model.CreateCollectionOptions#expireAfter(long, java.util.concurrent.TimeUnit)
* @since 4.4
*/
@Contract("_ -> new")
public TimeSeriesOptions expireAfter(Duration ttl) {
return new TimeSeriesOptions(timeField, metaField, granularity, span, ttl);
}
/**
* @return never {@literal null}.
*/
public String getTimeField() {
return timeField;
}
/**
* @return can be {@literal null}. Might be an {@literal empty} {@link String} as well, so maybe check via
* {@link org.springframework.util.StringUtils#hasText(String)}.
*/
@Nullable
public String getMetaField() {
return metaField;
}
/**
* @return never {@literal null}.
*/
public GranularityDefinition getGranularity() {
return granularity;
}
/**
* Get the {@link Duration} for automatic removal of documents.
*
* @return a {@link Duration#isNegative() negative} value if not specified.
* @since 4.4
*/
public Duration getExpireAfter() {
return expireAfter;
}
/**
* Get the span that defines a bucket.
*
* @return {@literal null} if not specified.
* @since 5.0
*/
public @Nullable Span getSpan() {
return span;
}
@Override
public String toString() {
return "TimeSeriesOptions{" + "timeField='" + timeField + '\'' + ", metaField='" + metaField + '\''
+ ", granularity=" + granularity + ", span=" + span + ", expireAfter=" + expireAfter + '}';
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TimeSeriesOptions that = (TimeSeriesOptions) o;
if (!ObjectUtils.nullSafeEquals(timeField, that.timeField)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(metaField, that.metaField)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(span, that.span)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(expireAfter, that.expireAfter)) {
return false;
}
return ObjectUtils.nullSafeEquals(granularity, that.granularity);
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(timeField);
result = 31 * result + ObjectUtils.nullSafeHashCode(metaField);
result = 31 * result + ObjectUtils.nullSafeHashCode(granularity);
result = 31 * result + ObjectUtils.nullSafeHashCode(span);
result = 31 * result + ObjectUtils.nullSafeHashCode(expireAfter);
return result;
}
}
}