DATAMONGO-1518 - Add support for collations.

We now support collations for collections, indexes, queries, findAndModify, delete, geo, bulk and aggregation operations in both the imperative and reactive implementations on template level.

Collation collation = Collation.of("fr")
  .strength(ComparisonLevel.secondary()
    .includeCase())
  .numericOrderingEnabled()
  .alternate(Alternate.shifted().punct())
  .forwardDiacriticSort()
  .normalizationEnabled();

template.createCollection(Person.class, CollectionOptions.just(collation));

Query query = new Query(Criteria.where("firstName").is("Amél")).collation(collation);

Original pull request: #459.
This commit is contained in:
Christoph Strobl
2017-04-25 13:29:35 +02:00
committed by Mark Paluch
parent b9d301e525
commit 0b169d5341
25 changed files with 2515 additions and 278 deletions

View File

@@ -0,0 +1,754 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import java.util.Locale;
import java.util.Optional;
import org.bson.Document;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.Assert;
import com.mongodb.client.model.Collation.Builder;
import com.mongodb.client.model.CollationAlternate;
import com.mongodb.client.model.CollationCaseFirst;
import com.mongodb.client.model.CollationMaxVariable;
import com.mongodb.client.model.CollationStrength;
/**
* Central abstraction for MongoDB collation support. <br />
* Allows fluent creation of a collation {@link Document} that can be used for creating collections & indexes as well as
* querying data.
* <p />
* <strong>NOTE:</strong> Please keep in mind that queries will only make use of an index with collation settings if the
* query itself specifies the same collation.
*
* @author Christoph Strobl
* @since 2.0
* @see <a href="https://docs.mongodb.com/manual/reference/collation/">MongoDB Reference - Collation</a>
*/
public class Collation {
private static final Collation DEFAULT = of("simple");
private final ICULocale locale;
private Optional<ICUComparisonLevel> strength = Optional.empty();
private Optional<Boolean> numericOrdering = Optional.empty();
private Optional<Alternate> alternate = Optional.empty();
private Optional<Boolean> backwards = Optional.empty();
private Optional<Boolean> normalization = Optional.empty();
private Optional<String> version = Optional.empty();
private Collation(ICULocale locale) {
Assert.notNull(locale, "ICULocale must not be null!");
this.locale = locale;
}
/**
* Create new {@link Collation} using simple binary comparison.
*
* @return
* @see #binary()
*/
public static Collation simple() {
return binary();
}
/**
* Create new {@link Collation} using simple binary comparison.
*
* @return
*/
public static Collation binary() {
return DEFAULT;
}
/**
* Create new {@link Collation} with locale set to {{@link java.util.Locale#getLanguage()}} and
* {@link java.util.Locale#getVariant()}.
*
* @param locale must not be {@literal null}.
* @return
*/
public static Collation of(Locale locale) {
Assert.notNull(locale, "Locale must not be null!");
return of(ICULocale.of(locale.getLanguage()).variant(locale.getVariant()));
}
/**
* Create new {@link Collation} with locale set to the given ICU language.
*
* @param language must not be {@literal null}.
* @return
*/
public static Collation of(String language) {
return of(ICULocale.of(language));
}
/**
* Create new {@link Collation} with locale set to the given {@link ICULocale}.
*
* @param locale must not be {@literal null}.
* @return
*/
public static Collation of(ICULocale locale) {
return new Collation(locale);
}
/**
* Create new {@link Collation} from values in {@link Document}.
*
* @param source must not be {@literal null}.
* @return
* @see <a href="https://docs.mongodb.com/manual/reference/collation/#collation-document">MongoDB Reference -
* Collation Document</a>
*/
public static Collation from(Document source) {
Assert.notNull(source, "Source must not be null!");
Collation collation = Collation.of(source.getString("locale"));
if (source.containsKey("strength")) {
collation = collation.strength(source.getInteger("strength"));
}
if (source.containsKey("caseLevel")) {
collation = collation.caseLevel(source.getBoolean("caseLevel"));
}
if (source.containsKey("caseFirst")) {
collation = collation.caseFirst(source.getString("caseFirst"));
}
if (source.containsKey("numericOrdering")) {
collation = collation.numericOrdering(source.getBoolean("numericOrdering"));
}
if (source.containsKey("alternate")) {
collation = collation.alternate(source.getString("alternate"));
}
if (source.containsKey("maxVariable")) {
collation = collation.maxVariable(source.getString("maxVariable"));
}
if (source.containsKey("backwards")) {
collation = collation.backwards(source.getBoolean("backwards"));
}
if (source.containsKey("normalization")) {
collation = collation.normalization(source.getBoolean("normalization"));
}
if (source.containsKey("version")) {
collation.version = Optional.of(source.get("version").toString());
}
return collation;
}
/**
* Set the level of comparison to perform.
*
* @param strength must not be {@literal null}.
* @return new {@link Collation}.
*/
public Collation strength(Integer strength) {
ICUComparisonLevel current = this.strength.orElseGet(() -> new ICUComparisonLevel(strength, null, null));
return strength(new ICUComparisonLevel(strength, current.caseFirst.orElse(null), current.caseLevel.orElse(null)));
}
/**
* Set the level of comparison to perform.
*
* @param comparisonLevel must not be {@literal null}.
* @return new {@link Collation}
*/
public Collation strength(ICUComparisonLevel comparisonLevel) {
Collation newInstance = copy();
newInstance.strength = Optional.ofNullable(comparisonLevel);
return newInstance;
}
/**
* Set {@code caseLevel} comarison. <br />
*
* @param caseLevel must not be {@literal null}.
* @return new {@link Collation}.
*/
public Collation caseLevel(Boolean caseLevel) {
ICUComparisonLevel strengthValue = strength.orElseGet(() -> ICUComparisonLevel.primary());
return strength(new ICUComparisonLevel(strengthValue.level, strengthValue.caseFirst.orElse(null), caseLevel));
}
/**
* Set the flag that determines sort order of case differences during tertiary level comparisons.
*
* @param caseFirst must not be {@literal null}.
* @return
*/
public Collation caseFirst(String caseFirst) {
return caseFirst(new ICUCaseFirst(caseFirst));
}
/**
* Set the flag that determines sort order of case differences during tertiary level comparisons.
*
* @param caseFirst must not be {@literal null}.
* @return
*/
public Collation caseFirst(ICUCaseFirst sort) {
ICUComparisonLevel strengthValue = strength.orElseGet(() -> ICUComparisonLevel.tertiary());
return strength(new ICUComparisonLevel(strengthValue.level, sort, strengthValue.caseLevel.orElse(null)));
}
/**
* Treat numeric strings as numbers for comparison.
*
* @return new {@link Collation}.
*/
public Collation numericOrderingEnabled() {
return numericOrdering(true);
}
/**
* Treat numeric strings as string for comparison.
*
* @return new {@link Collation}.
*/
public Collation numericOrderingDisabled() {
return numericOrdering(false);
}
/**
* Set the flag that determines whether to compare numeric strings as numbers or as strings.
*
* @return new {@link Collation}.
*/
public Collation numericOrdering(Boolean flag) {
Collation newInstance = copy();
newInstance.numericOrdering = Optional.ofNullable(flag);
return newInstance;
}
/**
* Set the Field that determines whether collation should consider whitespace and punctuation as base characters for
* purposes of comparison.
*
* @param alternate must not be {@literal null}.
* @return new {@link Collation}.
*/
public Collation alternate(String alternate) {
Alternate instance = this.alternate.orElseGet(() -> new Alternate(alternate, null));
return alternate(new Alternate(alternate, instance.maxVariable.orElse(null)));
}
/**
* Set the Field that determines whether collation should consider whitespace and punctuation as base characters for
* purposes of comparison.
*
* @param alternate must not be {@literal null}.
* @return new {@link Collation}.
*/
public Collation alternate(Alternate alternate) {
Collation newInstance = copy();
newInstance.alternate = Optional.ofNullable(alternate);
return newInstance;
}
/**
* Sort string with diacritics sort from back of the string.
*
* @return new {@link Collation}.
*/
public Collation backwardDiacriticSort() {
return backwards(true);
}
/**
* Do not sort string with diacritics sort from back of the string.
*
* @return new {@link Collation}.
*/
public Collation forwardDiacriticSort() {
return backwards(false);
}
/**
* Set the flag that determines whether strings with diacritics sort from back of the string.
*
* @param backwards must not be {@literal null}.
* @return new {@link Collation}.
*/
public Collation backwards(Boolean backwards) {
Collation newInstance = copy();
newInstance.backwards = Optional.ofNullable(backwards);
return newInstance;
}
/**
* Enable text normalization.
*
* @return new {@link Collation}.
*/
public Collation normalizationEnabled() {
return normalization(true);
}
/**
* Disable text normalization.
*
* @return new {@link Collation}.
*/
public Collation normalizationDisabled() {
return normalization(false);
}
/**
* Set the flag that determines whether to check if text require normalization and to perform normalization.
*
* @param normalization must not be {@literal null}.
* @return new {@link Collation}.
*/
public Collation normalization(Boolean normalization) {
Collation newInstance = copy();
newInstance.normalization = Optional.ofNullable(normalization);
return newInstance;
}
/**
* Set the field that determines up to which characters are considered ignorable when alternate is {@code shifted}.
*
* @param maxVariable must not be {@literal null}.
* @return new {@link Collation}.
*/
public Collation maxVariable(String maxVariable) {
Alternate alternateValue = alternate.orElseGet(() -> Alternate.shifted());
return alternate(new AlternateWithMaxVariable(alternateValue.alternate, maxVariable));
}
/**
* Get the {@link Document} representation of the {@link Collation}.
*
* @return
*/
public Document toDocument() {
return map(toMongoDocumentConverter());
}
/**
* Get the {@link com.mongodb.client.model.Collation} representation of the {@link Collation}.
*
* @return
*/
public com.mongodb.client.model.Collation toMongoCollation() {
return map(toMongoCollationConverter());
}
public <R> R map(Converter<? super Collation, ? extends R> mapper) {
return mapper.convert(this);
}
@Override
public String toString() {
return toDocument().toJson();
}
private Collation copy() {
Collation collation = new Collation(locale);
collation.strength = this.strength;
collation.normalization = this.normalization;
collation.numericOrdering = this.numericOrdering;
collation.alternate = this.alternate;
collation.backwards = this.backwards;
return collation;
}
/**
* Abstraction for the ICU Comparison Levels.
*
* @since 2.0
*/
public static class ICUComparisonLevel {
protected final Integer level;
private final Optional<ICUCaseFirst> caseFirst;
private final Optional<Boolean> caseLevel;
private ICUComparisonLevel(Integer level, ICUCaseFirst caseFirst, Boolean caseLevel) {
this.level = level;
this.caseFirst = Optional.ofNullable(caseFirst);
this.caseLevel = Optional.ofNullable(caseLevel);
}
/**
* Primary level of comparison. Collation performs comparisons of the base characters only, ignoring other
* differences such as diacritics and case. <br />
* The {@code caseLevel} can be set via {@link ComparisonLevelWithCase#caseLevel(Boolean)}.
*
* @return new {@link ComparisonLevelWithCase}.
*/
public static PrimaryICUComparisonLevel primary() {
return new PrimaryICUComparisonLevel(1, null);
}
/**
* Scondary level of comparison. Collation performs comparisons up to secondary differences, such as
* diacritics.<br />
* The {@code caseLevel} can be set via {@link ComparisonLevelWithCase#caseLevel(Boolean)}.
*
* @return new {@link ComparisonLevelWithCase}.
*/
public static SecondaryICUComparisonLevel secondary() {
return new SecondaryICUComparisonLevel(2, null);
}
/**
* Tertiary level of comparison. Collation performs comparisons up to tertiary differences, such as case and letter
* variants. <br />
* The {@code caseLevel} cannot be set for {@link ICUComparisonLevel} above {@code secondary}.
*
* @return new {@link ICUComparisonLevel}.
*/
public static TertiaryICUComparisonLevel tertiary() {
return new TertiaryICUComparisonLevel(3, null);
}
/**
* Quaternary Level. Limited for specific use case to consider punctuation. <br />
* The {@code caseLevel} cannot be set for {@link ICUComparisonLevel} above {@code secondary}.
*
* @return new {@link ICUComparisonLevel}.
*/
public static ICUComparisonLevel quaternary() {
return new ICUComparisonLevel(4, null, null);
}
/**
* Identical Level. Limited for specific use case of tie breaker. <br />
* The {@code caseLevel} cannot be set for {@link ICUComparisonLevel} above {@code secondary}.
*
* @return new {@link ICUComparisonLevel}.
*/
public static ICUComparisonLevel identical() {
return new ICUComparisonLevel(5, null, null);
}
}
public static class TertiaryICUComparisonLevel extends ICUComparisonLevel {
private TertiaryICUComparisonLevel(Integer level, ICUCaseFirst caseFirst) {
super(level, caseFirst, null);
}
/**
* Set the flag that determines sort order of case differences.
*
* @param caseFirstSort must not be {@literal null}.
* @return
*/
public TertiaryICUComparisonLevel caseFirst(ICUCaseFirst caseFirst) {
Assert.notNull(caseFirst, "CaseFirst must not be null!");
return new TertiaryICUComparisonLevel(level, caseFirst);
}
}
public static class PrimaryICUComparisonLevel extends ICUComparisonLevel {
private PrimaryICUComparisonLevel(Integer level, Boolean caseLevel) {
super(level, null, caseLevel);
}
/**
* Include case comparison.
*
* @return new {@link ComparisonLevelWithCase}
*/
public PrimaryICUComparisonLevel includeCase() {
return caseLevel(Boolean.TRUE);
}
/**
* Exclude case comparison.
*
* @return new {@link ComparisonLevelWithCase}
*/
public PrimaryICUComparisonLevel excludeCase() {
return caseLevel(Boolean.FALSE);
}
PrimaryICUComparisonLevel caseLevel(Boolean caseLevel) {
return new PrimaryICUComparisonLevel(level, caseLevel);
}
}
public static class SecondaryICUComparisonLevel extends ICUComparisonLevel {
private SecondaryICUComparisonLevel(Integer level, Boolean caseLevel) {
super(level, null, caseLevel);
}
/**
* Include case comparison.
*
* @return new {@link ComparisonLevelWithCase}
*/
public SecondaryICUComparisonLevel includeCase() {
return caseLevel(Boolean.TRUE);
}
/**
* Exclude case comparison.
*
* @return new {@link ComparisonLevelWithCase}
*/
public SecondaryICUComparisonLevel excludeCase() {
return caseLevel(Boolean.FALSE);
}
SecondaryICUComparisonLevel caseLevel(Boolean caseLevel) {
return new SecondaryICUComparisonLevel(level, caseLevel);
}
}
/**
* @since 2.0
*/
public static class ICUCaseFirst {
private final String state;
private ICUCaseFirst(String state) {
this.state = state;
}
/**
* Sort uppercase before lowercase.
*
* @return new {@link ICUCaseFirst}.
*/
public static ICUCaseFirst upper() {
return new ICUCaseFirst("upper");
}
/**
* Sort lowercase before uppercase.
*
* @return new {@link ICUCaseFirst}.
*/
public static ICUCaseFirst lower() {
return new ICUCaseFirst("lower");
}
/**
* Use the default.
*
* @return new {@link ICUCaseFirst}.
*/
public static ICUCaseFirst off() {
return new ICUCaseFirst("off");
}
}
/**
* @since 2.0
*/
public static class Alternate {
protected final String alternate;
protected Optional<String> maxVariable;
private Alternate(String alternate, String maxVariable) {
this.alternate = alternate;
this.maxVariable = Optional.ofNullable(maxVariable);
}
/**
* Consider Whitespace and punctuation as base characters.
*
* @return new {@link Alternate}.
*/
public static Alternate nonIgnorable() {
return new Alternate("non-ignorable", null);
}
/**
* Whitespace and punctuation are <strong>not</strong> considered base characters and are only distinguished at
* strength. <br />
* <strong>NOTE:</strong> Only works for {@link ICUComparisonLevel} above {@link ICUComparisonLevel#tertiary()}.
*
* @return new {@link AlternateWithMaxVariable}.
*/
public static AlternateWithMaxVariable shifted() {
return new AlternateWithMaxVariable("shifted", null);
}
}
/**
* @since 2.0
*/
public static class AlternateWithMaxVariable extends Alternate {
private AlternateWithMaxVariable(String alternate, String maxVariable) {
super(alternate, maxVariable);
}
/**
* Consider both whitespaces and punctuation as ignorable.
*
* @return new {@link AlternateWithMaxVariable}.
*/
public AlternateWithMaxVariable punct() {
return new AlternateWithMaxVariable(alternate, "punct");
}
/**
* Only consider whitespaces as ignorable.
*
* @return new {@link AlternateWithMaxVariable}.
*/
public AlternateWithMaxVariable space() {
return new AlternateWithMaxVariable(alternate, "space");
}
}
/**
* ICU locale abstraction for usage with MongoDB {@link Collation}.
*
* @since 2.0
* @see <a href="http://site.icu-project.org">ICU - International Components for Unicode</a>
*/
public static class ICULocale {
private final String language;
private final Optional<String> variant;
private ICULocale(String language, String variant) {
this.language = language;
this.variant = Optional.ofNullable(variant);
}
/**
* Create new {@link ICULocale} for given language.
*
* @param language must not be {@literal null}.
* @return
*/
public static ICULocale of(String language) {
Assert.notNull(language, "Code must not be null!");
return new ICULocale(language, null);
}
/**
* Define language variant.
*
* @param variant must not be {@literal null}.
* @return new {@link ICULocale}.
*/
public ICULocale variant(String variant) {
Assert.notNull(variant, "Variant must not be null!");
return new ICULocale(language, variant);
}
/**
* Get the string representation.
*
* @return
*/
public String asString() {
StringBuilder sb = new StringBuilder(language);
variant.ifPresent(val -> {
if (!val.isEmpty()) {
sb.append("@collation=").append(val);
}
});
return sb.toString();
}
}
private static Converter<Collation, Document> toMongoDocumentConverter() {
return source -> {
Document document = new Document();
document.append("locale", source.locale.asString());
source.strength.ifPresent(val -> {
document.append("strength", val.level);
val.caseLevel.ifPresent(cl -> document.append("caseLevel", cl));
val.caseFirst.ifPresent(cl -> document.append("caseFirst", cl.state));
});
source.numericOrdering.ifPresent(val -> document.append("numericOrdering", val));
source.alternate.ifPresent(val -> {
document.append("alternate", val.alternate);
val.maxVariable.ifPresent(maxVariable -> document.append("maxVariable", maxVariable));
});
source.backwards.ifPresent(val -> document.append("backwards", val));
source.normalization.ifPresent(val -> document.append("normalization", val));
source.version.ifPresent(val -> document.append("version", val));
return document;
};
}
private static Converter<Collation, com.mongodb.client.model.Collation> toMongoCollationConverter() {
return source -> {
Builder builder = com.mongodb.client.model.Collation.builder();
builder.locale(source.locale.asString());
source.strength.ifPresent(val -> {
builder.collationStrength(CollationStrength.fromInt(val.level));
val.caseLevel.ifPresent(cl -> builder.caseLevel(cl));
val.caseFirst.ifPresent(cl -> builder.collationCaseFirst(CollationCaseFirst.fromString(cl.state)));
});
source.numericOrdering.ifPresent(val -> builder.numericOrdering(val));
source.alternate.ifPresent(val -> {
builder.collationAlternate(CollationAlternate.fromString(val.alternate));
val.maxVariable
.ifPresent(maxVariable -> builder.collationMaxVariable(CollationMaxVariable.fromString(maxVariable)));
});
source.backwards.ifPresent(val -> builder.backwards(val));
source.normalization.ifPresent(val -> builder.normalization(val));
return builder.build();
};
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2010-2011 the original author or authors.
* Copyright 2010-2017 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.
@@ -15,18 +15,22 @@
*/
package org.springframework.data.mongodb.core;
import java.util.Optional;
import org.springframework.util.Assert;
/**
* Provides a simple wrapper to encapsulate the variety of settings you can use when creating a collection.
*
*
* @author Thomas Risberg
* @author Christoph Strobl
*/
public class CollectionOptions {
private Integer maxDocuments;
private Integer size;
private Boolean capped;
private Collation collation;
/**
* Constructs a new <code>CollectionOptions</code> instance.
@@ -37,12 +41,85 @@ public class CollectionOptions {
* false otherwise.
*/
public CollectionOptions(Integer size, Integer maxDocuments, Boolean capped) {
super();
this.maxDocuments = maxDocuments;
this.size = size;
this.capped = capped;
}
private CollectionOptions() {}
/**
* 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!");
CollectionOptions options = new CollectionOptions();
options.setCollation(collation);
return options;
}
/**
* Create new {@link CollectionOptions} with already given settings and capped set to {@literal true}.
*
* @return new {@link CollectionOptions}.
* @since 2.0
*/
public CollectionOptions capped() {
CollectionOptions options = new CollectionOptions(size, maxDocuments, true);
options.setCollation(collation);
return options;
}
/**
* 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(Integer maxDocuments) {
CollectionOptions options = new CollectionOptions(size, maxDocuments, capped);
options.setCollation(collation);
return options;
}
/**
* 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(Integer size) {
CollectionOptions options = new CollectionOptions(size, maxDocuments, capped);
options.setCollation(collation);
return options;
}
/**
* 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(Collation collation) {
CollectionOptions options = new CollectionOptions(size, maxDocuments, capped);
options.setCollation(collation);
return options;
}
public Integer getMaxDocuments() {
return maxDocuments;
}
@@ -67,4 +144,23 @@ public class CollectionOptions {
this.capped = capped;
}
/**
* Set {@link Collation} options.
*
* @param collation
* @since 2.0
*/
public void setCollation(Collation collation) {
this.collation = collation;
}
/**
* Get the {@link Collation} settings.
*
* @return
* @since 2.0
*/
public Optional<Collation> getCollation() {
return Optional.ofNullable(collation);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015-2016 the original author or authors.
* Copyright 2015-2017 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.
@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.mongodb.client.model.DeleteOptions;
import org.bson.Document;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.PersistenceExceptionTranslator;
@@ -58,12 +59,12 @@ class DefaultBulkOperations implements BulkOperations {
private BulkWriteOptions bulkOptions;
List<WriteModel<Document>> models = new ArrayList<WriteModel<Document>>();
List<WriteModel<Document>> models = new ArrayList<>();
/**
* Creates a new {@link DefaultBulkOperations} for the given {@link MongoOperations}, {@link BulkMode}, collection
* name and {@link WriteConcern}.
*
*
* @param mongoOperations The underlying {@link MongoOperations}, must not be {@literal null}.
* @param bulkMode must not be {@literal null}.
* @param collectionName Name of the collection to work on, must not be {@literal null} or empty.
@@ -88,7 +89,7 @@ class DefaultBulkOperations implements BulkOperations {
/**
* Configures the {@link PersistenceExceptionTranslator} to be used. Defaults to {@link MongoExceptionTranslator}.
*
*
* @param exceptionTranslator can be {@literal null}.
*/
public void setExceptionTranslator(PersistenceExceptionTranslator exceptionTranslator) {
@@ -97,7 +98,7 @@ class DefaultBulkOperations implements BulkOperations {
/**
* Configures the {@link WriteConcernResolver} to be used. Defaults to {@link DefaultWriteConcernResolver}.
*
*
* @param writeConcernResolver can be {@literal null}.
*/
public void setWriteConcernResolver(WriteConcernResolver writeConcernResolver) {
@@ -107,7 +108,7 @@ class DefaultBulkOperations implements BulkOperations {
/**
* Configures the default {@link WriteConcern} to be used. Defaults to {@literal null}.
*
*
* @param defaultWriteConcern can be {@literal null}.
*/
public void setDefaultWriteConcern(WriteConcern defaultWriteConcern) {
@@ -244,7 +245,10 @@ class DefaultBulkOperations implements BulkOperations {
Assert.notNull(query, "Query must not be null!");
models.add(new DeleteManyModel<Document>(query.getQueryObject()));
DeleteOptions deleteOptions = new DeleteOptions();
query.getCollation().map(Collation::toMongoCollation).ifPresent(deleteOptions::collation);
models.add(new DeleteManyModel(query.getQueryObject(), deleteOptions));
return this;
}
@@ -306,11 +310,12 @@ class DefaultBulkOperations implements BulkOperations {
UpdateOptions options = new UpdateOptions();
options.upsert(upsert);
query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation);
if (multi) {
models.add(new UpdateManyModel<Document>(query.getQueryObject(), update.getUpdateObject(), options));
models.add(new UpdateManyModel<>(query.getQueryObject(), update.getUpdateObject(), options));
} else {
models.add(new UpdateOneModel<Document>(query.getQueryObject(), update.getUpdateObject(), options));
models.add(new UpdateOneModel<>(query.getQueryObject(), update.getUpdateObject(), options));
}
return this;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2010-2011 the original author or authors.
* Copyright 2010-2017 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.
@@ -15,14 +15,21 @@
*/
package org.springframework.data.mongodb.core;
import java.util.Optional;
/**
* @author Mark Pollak
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class FindAndModifyOptions {
boolean returnNew;
boolean upsert;
boolean remove;
private Collation collation;
/**
* Static factory method to create a FindAndModifyOptions instance
*
@@ -32,6 +39,27 @@ public class FindAndModifyOptions {
return new FindAndModifyOptions();
}
/**
* @param options
* @return
* @since 2.0
*/
public static FindAndModifyOptions of(FindAndModifyOptions source) {
FindAndModifyOptions options = new FindAndModifyOptions();
if(source == null) {
return options;
}
options.returnNew = source.returnNew;
options.upsert = source.upsert;
options.remove = source.remove;
options.collation = source.collation;
return options;
}
public FindAndModifyOptions returnNew(boolean returnNew) {
this.returnNew = returnNew;
return this;
@@ -47,6 +75,19 @@ public class FindAndModifyOptions {
return this;
}
/**
* Define the {@link Collation} specifying language-specific rules for string comparison.
*
* @param collation
* @return
* @since 2.0
*/
public FindAndModifyOptions collation(Collation collation) {
this.collation = collation;
return this;
}
public boolean isReturnNew() {
return returnNew;
}
@@ -59,4 +100,14 @@ public class FindAndModifyOptions {
return remove;
}
/**
* Get the {@link Collation} specifying language-specific rules for string comparison.
*
* @return
* @since 2.0
*/
public Optional<Collation> getCollation() {
return Optional.ofNullable(collation);
}
}

View File

@@ -16,21 +16,19 @@
package org.springframework.data.mongodb.core;
import static org.springframework.data.domain.Sort.Direction.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.TimeUnit;
import org.bson.Document;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.core.index.IndexDefinition;
import org.springframework.data.mongodb.core.index.IndexField;
import org.springframework.data.mongodb.core.index.IndexInfo;
import org.springframework.util.ObjectUtils;
import com.mongodb.client.model.Collation;
import com.mongodb.client.model.CollationAlternate;
import com.mongodb.client.model.CollationCaseFirst;
import com.mongodb.client.model.CollationMaxVariable;
import com.mongodb.client.model.CollationStrength;
import com.mongodb.client.model.IndexOptions;
/**
@@ -45,10 +43,6 @@ abstract class IndexConverters {
private static final Converter<IndexDefinition, IndexOptions> DEFINITION_TO_MONGO_INDEX_OPTIONS;
private static final Converter<Document, IndexInfo> DOCUMENT_INDEX_INFO;
private static final Double ONE = Double.valueOf(1);
private static final Double MINUS_ONE = Double.valueOf(-1);
private static final Collection<String> TWO_D_IDENTIFIERS = Arrays.asList("2d", "2dsphere");
static {
DEFINITION_TO_MONGO_INDEX_OPTIONS = getIndexDefinitionIndexOptionsConverter();
@@ -117,18 +111,57 @@ abstract class IndexConverters {
}
}
if(indexOptions.containsKey("partialFilterExpression")) {
ops = ops.partialFilterExpression((org.bson.Document)indexOptions.get("partialFilterExpression"));
if (indexOptions.containsKey("partialFilterExpression")) {
ops = ops.partialFilterExpression((org.bson.Document) indexOptions.get("partialFilterExpression"));
}
if (indexOptions.containsKey("collation")) {
ops = ops.collation(fromDocument(indexOptions.get("collation", Document.class)));
}
return ops;
};
}
private static Converter<Document, IndexInfo> getDocumentIndexInfoConverter() {
public static Collation fromDocument(Document source) {
return ix -> {
return IndexInfo.indexInfoOf(ix);
};
if (source == null) {
return null;
}
com.mongodb.client.model.Collation.Builder collationBuilder = Collation.builder();
collationBuilder.locale(source.getString("locale"));
if (source.containsKey("caseLevel")) {
collationBuilder.caseLevel(source.getBoolean("caseLevel"));
}
if (source.containsKey("caseFirst")) {
collationBuilder.collationCaseFirst(CollationCaseFirst.fromString(source.getString("caseFirst")));
}
if (source.containsKey("strength")) {
collationBuilder.collationStrength(CollationStrength.fromInt(source.getInteger("strength")));
}
if (source.containsKey("numericOrdering")) {
collationBuilder.numericOrdering(source.getBoolean("numericOrdering"));
}
if (source.containsKey("alternate")) {
collationBuilder.collationAlternate(CollationAlternate.fromString(source.getString("alternate")));
}
if (source.containsKey("maxVariable")) {
collationBuilder.collationMaxVariable(CollationMaxVariable.fromString(source.getString("maxVariable")));
}
if (source.containsKey("backwards")) {
collationBuilder.backwards(source.getBoolean("backwards"));
}
if (source.containsKey("normalization")) {
collationBuilder.normalization(source.getBoolean("normalization"));
}
return collationBuilder.build();
}
private static Converter<Document, IndexInfo> getDocumentIndexInfoConverter() {
return ix -> IndexInfo.indexInfoOf(ix);
}
}

View File

@@ -20,8 +20,19 @@ import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
import static org.springframework.data.util.Optionals.*;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.bson.Document;
@@ -118,6 +129,7 @@ import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.CreateCollectionOptions;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.Filters;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
@@ -347,12 +359,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Document mappedFields = queryMapper.getMappedFields(query.getFieldsObject(), persistentEntity);
Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), persistentEntity);
FindIterable<Document> cursor = collection.find(mappedQuery).projection(mappedFields);
QueryCursorPreparer cursorPreparer = new QueryCursorPreparer(query, entityType);
FindIterable<Document> cursor = new QueryCursorPreparer(query, entityType)
.prepare(collection.find(mappedQuery).projection(mappedFields));
ReadDocumentCallback<T> readCallback = new ReadDocumentCallback<T>(mongoConverter, entityType, collectionName);
return new CloseableIterableCursorAdapter<T>(cursorPreparer.prepare(cursor), exceptionTranslator, readCallback);
return new CloseableIterableCursorAdapter<T>(cursor, exceptionTranslator,
new ReadDocumentCallback<T>(mongoConverter, entityType, collectionName));
}
});
}
@@ -563,6 +574,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
public <T> T findOne(Query query, Class<T> entityClass, String collectionName) {
if (query.getSortObject() == null) {
return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass);
} else {
@@ -587,7 +599,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), getPersistentEntity(entityClass));
return execute(collectionName, new FindCallback(mappedQuery)).iterator().hasNext();
FindIterable<Document> iterable = execute(collectionName, new FindCallback(mappedQuery));
if (query.getCollation().isPresent()) {
iterable = iterable
.collation(query.getCollation().map(org.springframework.data.mongodb.core.Collation::toMongoCollation).get());
}
return iterable.iterator().hasNext();
}
// Find methods that take a Query to express the query and that return a List of objects.
@@ -698,8 +717,18 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public <T> T findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName) {
FindAndModifyOptions optionsToUse = FindAndModifyOptions.of(options);
Optionals.ifAllPresent(query.getCollation(), optionsToUse.getCollation(), (l, r) -> {
throw new IllegalArgumentException(
"Both Query and FindAndModifyOptions define the collation. Please provide the collation only via one of the two.");
});
query.getCollation().ifPresent(optionsToUse::collation);
return doFindAndModify(collectionName, query.getQueryObject(), query.getFieldsObject(),
getMappedSortObject(query, entityClass), entityClass, update, options);
getMappedSortObject(query, entityClass), entityClass, update, optionsToUse);
}
// Find methods that take a Query to express the query and that return a single object that is also removed from the
@@ -712,7 +741,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
public <T> T findAndRemove(Query query, Class<T> entityClass, String collectionName) {
return doFindAndRemove(collectionName, query.getQueryObject(), query.getFieldsObject(),
getMappedSortObject(query, entityClass), entityClass);
getMappedSortObject(query, entityClass), query.getCollation().orElse(null), entityClass);
}
public long count(Query query, Class<?> entityClass) {
@@ -1151,8 +1180,17 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
increaseVersionForUpdateIfNecessary(entity, update);
Document queryObj = query == null ? new Document()
: queryMapper.getMappedObject(query.getQueryObject(), entity);
UpdateOptions opts = new UpdateOptions();
opts.upsert(upsert);
Document queryObj = new Document();
if (query != null) {
queryObj.putAll(queryMapper.getMappedObject(query.getQueryObject(), entity));
query.getCollation().map(Collation::toMongoCollation).ifPresent(opts::collation);
}
Document updateObj = update == null ? new Document()
: updateMapper.getMappedObject(update.getUpdateObject(), entity);
@@ -1169,9 +1207,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
entityClass, updateObj, queryObj);
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
UpdateOptions opts = new UpdateOptions();
opts.upsert(upsert);
collection = writeConcernToUse != null ? collection.withWriteConcern(writeConcernToUse) : collection;
if (!UpdateMapper.isUpdateObject(updateObj)) {
@@ -1331,6 +1366,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
final Optional<? extends MongoPersistentEntity<?>> entity = getPersistentEntity(entityClass);
return execute(collectionName, new CollectionCallback<DeleteResult>() {
public DeleteResult doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
@@ -1338,8 +1374,12 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Document mappedQuery = queryMapper.getMappedObject(queryObject, entity);
DeleteOptions options = new DeleteOptions();
query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation);
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.REMOVE, collectionName,
entityClass, null, queryObject);
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
DeleteResult dr = null;
@@ -1349,9 +1389,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
if (writeConcernToUse == null) {
dr = collection.deleteMany(mappedQuery);
dr = collection.deleteMany(mappedQuery, options);
} else {
dr = collection.withWriteConcern(writeConcernToUse).deleteMany(mappedQuery);
dr = collection.withWriteConcern(writeConcernToUse).deleteMany(mappedQuery, options);
}
maybeEmitEvent(new AfterDeleteEvent<T>(queryObject, entityClass, collectionName));
@@ -1366,7 +1407,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
public <T> List<T> findAll(Class<T> entityClass, String collectionName) {
return executeFindMultiInternal(new FindCallback(null), null,
return executeFindMultiInternal(new FindCallback(null, null), null,
new ReadDocumentCallback<T>(mongoConverter, entityClass, collectionName), collectionName);
}
@@ -1411,26 +1452,40 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
result = result.filter(queryMapper.getMappedObject(query.getQueryObject(), Optional.empty()));
}
Optional<Collation> collation = query != null ? query.getCollation() : Optional.empty();
if (mapReduceOptions != null) {
Optionals.ifAllPresent(collation, mapReduceOptions.getCollation(), (l, r) -> {
throw new IllegalArgumentException(
"Both Query and MapReduceOptions define the collation. Please provide the collation only via one of the two.");
});
if (mapReduceOptions.getCollation().isPresent()) {
collation = mapReduceOptions.getCollation();
}
if (!CollectionUtils.isEmpty(mapReduceOptions.getScopeVariables())) {
Document vars = new Document();
vars.putAll(mapReduceOptions.getScopeVariables());
result = result.scope(vars);
result = result.scope(new Document(mapReduceOptions.getScopeVariables()));
}
if (mapReduceOptions.getLimit() != null && mapReduceOptions.getLimit().intValue() > 0) {
result = result.limit(mapReduceOptions.getLimit());
}
if (StringUtils.hasText(mapReduceOptions.getFinalizeFunction())) {
result = result.finalizeFunction(mapReduceOptions.getFinalizeFunction());
if (mapReduceOptions.getFinalizeFunction().filter(StringUtils::hasText).isPresent()) {
result = result.finalizeFunction(mapReduceOptions.getFinalizeFunction().get());
}
if (mapReduceOptions.getJavaScriptMode() != null) {
result = result.jsMode(mapReduceOptions.getJavaScriptMode());
}
if (mapReduceOptions.getOutputSharded() != null) {
result = result.sharded(mapReduceOptions.getOutputSharded());
if (mapReduceOptions.getOutputSharded().isPresent()) {
result = result.sharded(mapReduceOptions.getOutputSharded().get());
}
}
if (collation.isPresent()) {
result = result.collation(collation.map(Collation::toMongoCollation).get());
}
List<T> mappedResults = new ArrayList<T>();
DocumentCallback<T> callback = new ReadDocumentCallback<T>(mongoConverter, entityClass, inputCollectionName);
@@ -1709,7 +1764,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Integer cursorBatchSize = options.getCursorBatchSize();
if (cursorBatchSize != null) {
cursor.batchSize(cursorBatchSize);
cursor = cursor.batchSize(cursorBatchSize);
}
if (options.getCollation().isPresent()) {
cursor = cursor.collation(options.getCollation().map(Collation::toMongoCollation).get());
}
return new CloseableIterableCursorAdapter<O>(cursor.iterator(), exceptionTranslator, readCallback);
@@ -1806,9 +1865,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
co.maxDocuments(((Number) collectionOptions.get("max")).longValue());
}
if (collectionOptions.containsKey("collation")) {
co.collation(IndexConverters.fromDocument(collectionOptions.get("collation", Document.class)));
}
db.createCollection(collectionName, co);
MongoCollection<Document> coll = db.getCollection(collectionName, Document.class);
// TODO: Emit a collection created event
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Created collection [{}]", coll.getNamespace().getCollectionName());
@@ -1895,6 +1959,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
}
protected Document convertToDocument(CollectionOptions collectionOptions) {
Document document = new Document();
if (collectionOptions != null) {
if (collectionOptions.getCapped() != null) {
@@ -1906,6 +1971,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
if (collectionOptions.getMaxDocuments() != null) {
document.put("max", collectionOptions.getMaxDocuments().intValue());
}
collectionOptions.getCollation().ifPresent(val -> document.append("collation", val.toDocument()));
}
return document;
}
@@ -1922,7 +1989,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
* @return the List of converted objects.
*/
protected <T> T doFindAndRemove(String collectionName, Document query, Document fields, Document sort,
Class<T> entityClass) {
Collation collation, Class<T> entityClass) {
EntityReader<? super T, Bson> readerToUse = this.mongoConverter;
@@ -1933,7 +2000,8 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
Optional<? extends MongoPersistentEntity<?>> entity = mappingContext.getPersistentEntity(entityClass);
return executeFindOneInternal(new FindAndRemoveCallback(queryMapper.getMappedObject(query, entity), fields, sort),
return executeFindOneInternal(
new FindAndRemoveCallback(queryMapper.getMappedObject(query, entity), fields, sort, collation),
new ReadDocumentCallback<T>(readerToUse, entityClass, collectionName), collectionName);
}
@@ -2210,31 +2278,33 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*
* @author Oliver Gierke
* @author Thomas Risberg
* @author Christoph Strobl
*/
private static class FindOneCallback implements CollectionCallback<Document> {
private final Document query;
private final Document fields;
private final Optional<Document> fields;
public FindOneCallback(Document query, Document fields) {
this.query = query;
this.fields = fields;
this.fields = Optional.ofNullable(fields);
}
public Document doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
if (fields == null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("findOne using query: {} in db.collection: {}", serializeToJsonSafely(query),
collection.getNamespace().getFullName());
}
return collection.find(query).first();
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("findOne using query: {} fields: {} in db.collection: {}", serializeToJsonSafely(query), fields,
collection.getNamespace().getFullName());
}
return collection.find(query).projection(fields).first();
FindIterable<Document> iterable = collection.find(query);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("findOne using query: {} fields: {} in db.collection: {}", serializeToJsonSafely(query),
serializeToJsonSafely(fields.orElseGet(() -> new Document())), collection.getNamespace().getFullName());
}
if (fields.isPresent()) {
iterable = iterable.projection(fields.get());
}
return iterable.first();
}
}
@@ -2244,29 +2314,33 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
*
* @author Oliver Gierke
* @author Thomas Risberg
* @author Christoph Strobl
*/
private static class FindCallback implements CollectionCallback<FindIterable<Document>> {
private final Document query;
private final Document fields;
private final Optional<Document> fields;
public FindCallback(Document query) {
this(query, null);
}
public FindCallback(Document query, Document fields) {
this.query = query == null ? new Document() : query;
this.fields = fields;
this.query = query != null ? query : new Document();
this.fields = Optional.ofNullable(fields);
}
public FindIterable<Document> doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
if (fields == null || fields.isEmpty()) {
return collection.find(query);
} else {
return collection.find(query).projection(fields);
FindIterable<Document> iterable = collection.find(query);
if (fields.filter(val -> !val.isEmpty()).isPresent()) {
iterable = iterable.projection(fields.get());
}
return iterable;
}
}
@@ -2281,18 +2355,20 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
private final Document query;
private final Document fields;
private final Document sort;
private final Optional<Collation> collation;
public FindAndRemoveCallback(Document query, Document fields, Document sort, Collation collation) {
public FindAndRemoveCallback(Document query, Document fields, Document sort) {
this.query = query;
this.fields = fields;
this.sort = sort;
this.collation = Optional.ofNullable(collation);
}
public Document doInCollection(MongoCollection<Document> collection) throws MongoException, DataAccessException {
FindOneAndDeleteOptions opts = new FindOneAndDeleteOptions();
opts.sort(sort);
opts.projection(fields);
FindOneAndDeleteOptions opts = new FindOneAndDeleteOptions().sort(sort).projection(fields);
collation.map(Collation::toMongoCollation).ifPresent(opts::collation);
return collection.findOneAndDelete(query, opts);
}
@@ -2326,9 +2402,11 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
if (options.returnNew) {
opts.returnDocument(ReturnDocument.AFTER);
}
options.getCollation().map(Collation::toMongoCollation).ifPresent(opts::collation);
return collection.findOneAndUpdate(query, update, opts);
}
}
/**
@@ -2435,6 +2513,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
FindIterable<Document> cursorToUse = cursor;
if (query.getCollation().isPresent()) {
cursorToUse = cursorToUse.collation(query.getCollation().map(val -> val.toMongoCollation()).get());
}
try {
if (query.getSkip() > 0) {
cursorToUse = cursorToUse.skip((int) query.getSkip());
@@ -2442,7 +2523,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware,
if (query.getLimit() > 0) {
cursorToUse = cursorToUse.limit(query.getLimit());
}
if (query.getSortObject() != null) {
if (query.getSortObject() != null && !query.getSortObject().isEmpty()) {
Document sort = type != null ? getMappedSortObject(query, type) : query.getSortObject();
cursorToUse = cursorToUse.sort(sort);
}

View File

@@ -18,6 +18,10 @@ package org.springframework.data.mongodb.core;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@@ -88,6 +92,7 @@ import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.mongodb.util.MongoClientVersion;
import org.springframework.data.util.Optionals;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -116,10 +121,6 @@ import com.mongodb.reactivestreams.client.MongoDatabase;
import com.mongodb.reactivestreams.client.Success;
import com.mongodb.util.JSONParseException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.function.Tuple2;
/**
* Primary implementation of {@link ReactiveMongoOperations}. It simplifies the use of Reactive MongoDB usage and helps
* to avoid common errors. It executes core MongoDB workflow, leaving application code to provide {@link Document} and
@@ -336,11 +337,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* @see org.springframework.data.mongodb.core.ReactiveMongoOperations#executeCommand(org.bson.Document)
*/
public Mono<Document> executeCommand(final Document command) {
Assert.notNull(command, "Command must not be null!");
return createFlux(db -> readPreference != null ? db.runCommand(command, readPreference) : db.runCommand(command))
.next();
return executeCommand(command, null);
}
/* (non-Javadoc)
@@ -350,8 +347,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Assert.notNull(command, "Command must not be null!");
return createFlux(db -> readPreference != null ? db.runCommand(command, readPreference) : db.runCommand(command))
.next();
return createFlux(db -> readPreference != null ? db.runCommand(command, readPreference, Document.class)
: db.runCommand(command, Document.class)).next();
}
/* (non-Javadoc)
@@ -541,8 +538,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*/
public <T> Mono<T> findOne(Query query, Class<T> entityClass, String collectionName) {
if (query.getSortObject() == null) {
return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass);
if (ObjectUtils.isEmpty(query.getSortObject())) {
return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass,
query.getCollation().orElse(null));
}
query.limit(1);
@@ -575,7 +573,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return createFlux(collectionName, collection -> {
Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), getPersistentEntity(entityClass));
return collection.find(mappedQuery).limit(1);
FindPublisher findPublisher = collection.find(mappedQuery).projection(new Document("_id", 1));
if (query.getCollation().isPresent()) {
findPublisher = findPublisher.collation(query.getCollation().map(Collation::toMongoCollation).get());
}
return findPublisher.limit(1);
}).hasElements();
}
@@ -612,11 +616,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
public <T> Mono<T> findById(Object id, Class<T> entityClass, String collectionName) {
Optional<? extends MongoPersistentEntity<?>> persistentEntity = mappingContext.getPersistentEntity(entityClass);
MongoPersistentProperty idProperty = persistentEntity.isPresent() ? persistentEntity.get().getIdProperty().orElse(null) : null;
MongoPersistentProperty idProperty = persistentEntity.isPresent()
? persistentEntity.get().getIdProperty().orElse(null) : null;
String idKey = idProperty == null ? ID_FIELD : idProperty.getName();
return doFindOne(collectionName, new Document(idKey, id), null, entityClass);
return doFindOne(collectionName, new Document(idKey, id), null, entityClass, null);
}
/*
@@ -672,12 +677,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return Flux.empty();
}
return Flux.fromIterable(l);
}).skip(near.getSkip() != null ? near.getSkip() : 0).map(new Function<Document, GeoResult<T>>() {
@Override
public GeoResult<T> apply(Document object) {
return callback.doWith(object);
}
});
}).skip(near.getSkip() != null ? near.getSkip() : 0).map(callback::doWith);
});
}
@@ -707,8 +707,18 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*/
public <T> Mono<T> findAndModify(Query query, Update update, FindAndModifyOptions options, Class<T> entityClass,
String collectionName) {
FindAndModifyOptions optionsToUse = FindAndModifyOptions.of(options);
Optionals.ifAllPresent(query.getCollation(), optionsToUse.getCollation(), (l, r) -> {
throw new IllegalArgumentException(
"Both Query and FindAndModifyOptions define the collation. Please provide the collation only via one of the two.");
});
query.getCollation().ifPresent(optionsToUse::collation);
return doFindAndModify(collectionName, query.getQueryObject(), query.getFieldsObject(),
getMappedSortObject(query, entityClass), entityClass, update, options);
getMappedSortObject(query, entityClass), entityClass, update, optionsToUse);
}
/* (non-Javadoc)
@@ -724,7 +734,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
public <T> Mono<T> findAndRemove(Query query, Class<T> entityClass, String collectionName) {
return doFindAndRemove(collectionName, query.getQueryObject(), query.getFieldsObject(),
getMappedSortObject(query, entityClass), entityClass);
getMappedSortObject(query, entityClass), query.getCollation().orElse(null), entityClass);
}
/* (non-Javadoc)
@@ -962,8 +972,11 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
ConvertingPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor(
entity.getPropertyAccessor(objectToSave), mongoConverter.getConversionService());
MongoPersistentProperty idProperty = entity.getIdProperty().orElseThrow(() -> new IllegalArgumentException("No id property present!"));
MongoPersistentProperty versionProperty = entity.getVersionProperty().orElseThrow(() -> new IllegalArgumentException("No version property present!"));;
MongoPersistentProperty idProperty = entity.getIdProperty()
.orElseThrow(() -> new IllegalArgumentException("No id property present!"));
MongoPersistentProperty versionProperty = entity.getVersionProperty()
.orElseThrow(() -> new IllegalArgumentException("No version property present!"));
;
Optional<Object> version = convertingAccessor.getProperty(versionProperty);
Optional<Number> versionNumber = convertingAccessor.getProperty(versionProperty, Number.class);
@@ -977,7 +990,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
// Create query for entity with the id and old version
Optional<Object> id = convertingAccessor.getProperty(idProperty);
Query query = new Query(Criteria.where(idProperty.getName()).is(id.get()).and(versionProperty.getName()).is(version.get()));
Query query = new Query(
Criteria.where(idProperty.getName()).is(id.get()).and(versionProperty.getName()).is(version.get()));
// Bump version number
convertingAccessor.setProperty(versionProperty, Optional.of(versionNumber.orElse(0).longValue() + 1));
@@ -1196,6 +1210,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
MongoCollection<Document> collectionToUse = prepareCollection(collection, writeConcernToUse);
UpdateOptions updateOptions = new UpdateOptions().upsert(upsert);
query.getCollation().map(Collation::toMongoCollation).ifPresent(updateOptions::collation);
if (!UpdateMapper.isUpdateObject(updateObj)) {
return collectionToUse.replaceOne(queryObj, updateObj, updateOptions);
@@ -1349,8 +1364,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private void assertUpdateableIdIfNotSet(Object entity) {
Optional<? extends MongoPersistentEntity<?>> persistentEntity = mappingContext.getPersistentEntity(entity.getClass());
Optional<MongoPersistentProperty> idProperty = persistentEntity.isPresent() ? persistentEntity.get().getIdProperty() : Optional.empty();
Optional<? extends MongoPersistentEntity<?>> persistentEntity = mappingContext
.getPersistentEntity(entity.getClass());
Optional<MongoPersistentProperty> idProperty = persistentEntity.isPresent() ? persistentEntity.get().getIdProperty()
: Optional.empty();
if (!idProperty.isPresent()) {
return;
@@ -1360,8 +1377,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
if (!idValue.isPresent() && !MongoSimpleTypes.AUTOGENERATED_ID_TYPES.contains(idProperty.get().getType())) {
throw new InvalidDataAccessApiUsageException(
String.format("Cannot autogenerate id of type %s for entity of type %s!", idProperty.get().getType().getName(),
entity.getClass().getName()));
String.format("Cannot autogenerate id of type %s for entity of type %s!",
idProperty.get().getType().getName(), entity.getClass().getName()));
}
}
@@ -1414,7 +1431,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
new Object[] { serializeToJsonSafely(dboq), collectionName });
}
query.getCollation().ifPresent(val -> {
// TODO: add collation support as soon as it's there! See https://jira.mongodb.org/browse/JAVARS-27
throw new IllegalArgumentException("DeleteMany does currently not accept collation settings.");
});
return collectionToUse.deleteMany(dboq);
}).doOnNext(deleteResult -> maybeEmitEvent(new AfterDeleteEvent<T>(queryObject, entityClass, collectionName)))
.next();
}
@@ -1530,7 +1554,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
* @param entityClass the parameterized type of the returned list.
* @return the {@link List} of converted objects.
*/
protected <T> Mono<T> doFindOne(String collectionName, Document query, Document fields, Class<T> entityClass) {
protected <T> Mono<T> doFindOne(String collectionName, Document query, Document fields, Class<T> entityClass,
Collation collation) {
Optional<? extends MongoPersistentEntity<?>> entity = mappingContext.getPersistentEntity(entityClass);
Document mappedQuery = queryMapper.getMappedObject(query, entity);
@@ -1541,7 +1566,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
serializeToJsonSafely(query), mappedFields, entityClass, collectionName));
}
return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields),
return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields, collation),
new ReadDocumentCallback<T>(this.mongoConverter, entityClass, collectionName), collectionName);
}
@@ -1624,11 +1649,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
*
* @param collectionName name of the collection to retrieve the objects from
* @param query the query document that specifies the criteria used to find a record
* @param collation collation
* @param entityClass the parameterized type of the returned list.
* @return the List of converted objects.
*/
protected <T> Mono<T> doFindAndRemove(String collectionName, Document query, Document fields, Document sort,
Class<T> entityClass) {
Collation collation, Class<T> entityClass) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("findAndRemove using query: %s fields: %s sort: %s for class: %s in collection: %s",
@@ -1637,19 +1663,15 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
Optional<? extends MongoPersistentEntity<?>> entity = mappingContext.getPersistentEntity(entityClass);
return executeFindOneInternal(new FindAndRemoveCallback(queryMapper.getMappedObject(query, entity), fields, sort),
return executeFindOneInternal(
new FindAndRemoveCallback(queryMapper.getMappedObject(query, entity), fields, sort, collation),
new ReadDocumentCallback<T>(this.mongoConverter, entityClass, collectionName), collectionName);
}
protected <T> Mono<T> doFindAndModify(String collectionName, Document query, Document fields, Document sort,
Class<T> entityClass, Update update, FindAndModifyOptions options) {
FindAndModifyOptions optionsToUse;
if (options == null) {
optionsToUse = new FindAndModifyOptions();
} else {
optionsToUse = options;
}
FindAndModifyOptions optionsToUse = options != null ? options : new FindAndModifyOptions();
Optional<? extends MongoPersistentEntity<?>> entity = mappingContext.getPersistentEntity(entityClass);
@@ -1976,34 +1998,36 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private static class FindOneCallback implements ReactiveCollectionCallback<Document> {
private final Document query;
private final Document fields;
private final Optional<Document> fields;
private final Optional<Collation> collation;
FindOneCallback(Document query, Document fields) {
FindOneCallback(Document query, Document fields, Collation collation) {
this.query = query;
this.fields = fields;
this.fields = Optional.ofNullable(fields);
this.collation = Optional.ofNullable(collation);
}
@Override
public Publisher<Document> doInCollection(MongoCollection<Document> collection)
throws MongoException, DataAccessException {
if (fields == null) {
FindPublisher publisher = collection.find(query);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("findOne using query: %s in db.collection: %s", serializeToJsonSafely(query),
collection.getNamespace().getFullName()));
}
if (LOGGER.isDebugEnabled()) {
return collection.find(query).limit(1).first();
} else {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug(String.format("findOne using query: %s fields: %s in db.collection: %s",
serializeToJsonSafely(query), fields, collection.getNamespace().getFullName()));
}
return collection.find(query).projection(fields).limit(1);
LOGGER.debug("findOne using query: {} fields: {} in db.collection: {}", serializeToJsonSafely(query),
serializeToJsonSafely(fields.orElseGet(() -> new Document())), collection.getNamespace().getFullName());
}
if (fields.isPresent()) {
publisher = publisher.projection(fields.get());
}
if (collation.isPresent()) {
publisher = publisher.collation(collation.map(Collation::toMongoCollation).get());
}
return publisher.limit(1).first();
}
}
@@ -2056,12 +2080,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
private final Document query;
private final Document fields;
private final Document sort;
private final Optional<Collation> collation;
FindAndRemoveCallback(Document query, Document fields, Document sort) {
FindAndRemoveCallback(Document query, Document fields, Document sort, Collation collation) {
this.query = query;
this.fields = fields;
this.sort = sort;
this.collation = Optional.ofNullable(collation);
}
@Override
@@ -2069,6 +2095,8 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
throws MongoException, DataAccessException {
FindOneAndDeleteOptions findOneAndDeleteOptions = convertToFindOneAndDeleteOptions(fields, sort);
collation.map(Collation::toMongoCollation).ifPresent(findOneAndDeleteOptions::collation);
return collection.findOneAndDelete(query, findOneAndDeleteOptions);
}
}
@@ -2100,6 +2128,12 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
if (options.isRemove()) {
FindOneAndDeleteOptions findOneAndDeleteOptions = convertToFindOneAndDeleteOptions(fields, sort);
if (options.getCollation().isPresent()) {
findOneAndDeleteOptions = findOneAndDeleteOptions
.collation(options.getCollation().map(Collation::toMongoCollation).get());
}
return collection.findOneAndDelete(query, findOneAndDeleteOptions);
}
@@ -2120,6 +2154,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
result = result.returnDocument(ReturnDocument.BEFORE);
}
if (options.getCollation().isPresent()) {
result = result.collation(options.getCollation().map(Collation::toMongoCollation).get());
}
return result;
}
}
@@ -2255,21 +2293,25 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
return findPublisher;
}
if (query.getSkip() <= 0 && query.getLimit() <= 0 && query.getSortObject() == null
&& !StringUtils.hasText(query.getHint()) && !query.getMeta().hasValues()) {
return findPublisher;
FindPublisher<T> findPublisherToUse = findPublisher;
if (query.getCollation().isPresent()) {
findPublisherToUse = findPublisherToUse.collation(query.getCollation().map(Collation::toMongoCollation).get());
}
FindPublisher<T> findPublisherToUse = findPublisher;
if (query.getSkip() <= 0 && query.getLimit() <= 0 && query.getSortObject() == null
&& !StringUtils.hasText(query.getHint()) && !query.getMeta().hasValues()) {
return findPublisherToUse;
}
try {
if (query.getSkip() > 0) {
findPublisherToUse = findPublisherToUse.skip((int)query.getSkip());
findPublisherToUse = findPublisherToUse.skip((int) query.getSkip());
}
if (query.getLimit() > 0) {
findPublisherToUse = findPublisherToUse.limit(query.getLimit());
}
if (query.getSortObject() != null) {
if (!ObjectUtils.isEmpty(query.getSortObject())) {
Document sort = type != null ? getMappedSortObject(query, type) : query.getSortObject();
findPublisherToUse = findPublisherToUse.sort(sort);
}

View File

@@ -15,7 +15,10 @@
*/
package org.springframework.data.mongodb.core.aggregation;
import java.util.Optional;
import org.bson.Document;
import org.springframework.data.mongodb.core.Collation;
import org.springframework.util.Assert;
import com.mongodb.DBObject;
@@ -39,10 +42,12 @@ public class AggregationOptions {
private static final String CURSOR = "cursor";
private static final String EXPLAIN = "explain";
private static final String ALLOW_DISK_USE = "allowDiskUse";
private static final String COLLATION = "collation";
private final boolean allowDiskUse;
private final boolean explain;
private final Document cursor;
private final Optional<Document> cursor;
private final Optional<Collation> collation;
/**
* Creates a new {@link AggregationOptions}.
@@ -52,10 +57,25 @@ public class AggregationOptions {
* @param cursor can be {@literal null}, used to pass additional options to the aggregation.
*/
public AggregationOptions(boolean allowDiskUse, boolean explain, Document cursor) {
this(allowDiskUse, explain, cursor, null);
}
/**
* Creates a new {@link AggregationOptions}.
*
* @param allowDiskUse whether to off-load intensive sort-operations to disk.
* @param explain whether to get the execution plan for the aggregation instead of the actual results.
* @param cursor can be {@literal null}, used to pass additional options (such as {@code batchSize}) to the
* aggregation.
* @param collation collation for string comparison. Can be {@literal null}.
* @since 2.0
*/
public AggregationOptions(boolean allowDiskUse, boolean explain, Document cursor, Collation collation) {
this.allowDiskUse = allowDiskUse;
this.explain = explain;
this.cursor = cursor;
this.cursor = Optional.ofNullable(cursor);
this.collation = Optional.ofNullable(collation);
}
/**
@@ -67,7 +87,7 @@ public class AggregationOptions {
* @since 2.0
*/
public AggregationOptions(boolean allowDiskUse, boolean explain, int cursorBatchSize) {
this(allowDiskUse, explain, createCursor(cursorBatchSize));
this(allowDiskUse, explain, createCursor(cursorBatchSize), null);
}
/**
@@ -81,23 +101,13 @@ public class AggregationOptions {
Assert.notNull(document, "Document must not be null!");
boolean allowDiskUse = false;
boolean explain = false;
Document cursor = null;
boolean allowDiskUse = document.getBoolean(ALLOW_DISK_USE, false);
boolean explain = document.getBoolean(EXPLAIN, false);
Document cursor = document.get(CURSOR, Document.class);
Collation collation = document.containsKey(COLLATION) ? Collation.from(document.get(COLLATION, Document.class))
: null;
if (document.containsKey(ALLOW_DISK_USE)) {
allowDiskUse = document.get(ALLOW_DISK_USE, Boolean.class);
}
if (document.containsKey(EXPLAIN)) {
explain = (Boolean) document.get(EXPLAIN);
}
if (document.containsKey(CURSOR)) {
cursor = document.get(CURSOR, Document.class);
}
return new AggregationOptions(allowDiskUse, explain, cursor);
return new AggregationOptions(allowDiskUse, explain, cursor, collation);
}
/**
@@ -127,8 +137,8 @@ public class AggregationOptions {
*/
public Integer getCursorBatchSize() {
if (cursor != null && cursor.containsKey("batchSize")) {
return cursor.get("batchSize", Integer.class);
if (cursor.filter(val -> val.containsKey(BATCH_SIZE)).isPresent()) {
return cursor.get().get(BATCH_SIZE, Integer.class);
}
return null;
@@ -139,10 +149,20 @@ public class AggregationOptions {
*
* @return
*/
public Document getCursor() {
public Optional<Document> getCursor() {
return cursor;
}
/**
* Get collation settings for string comparison.
*
* @return
* @since 2.0
*/
public Optional<Collation> getCollation() {
return collation;
}
/**
* Returns a new potentially adjusted copy for the given {@code aggregationCommandObject} with the configuration
* applied.
@@ -162,8 +182,12 @@ public class AggregationOptions {
result.put(EXPLAIN, explain);
}
if (cursor != null && !result.containsKey(CURSOR)) {
result.put(CURSOR, cursor);
if (!result.containsKey(CURSOR)) {
cursor.ifPresent(val -> result.put(CURSOR, val));
}
if (!result.containsKey(COLLATION)) {
collation.map(Collation::toDocument).ifPresent(val -> result.append(COLLATION, val));
}
return result;
@@ -179,7 +203,9 @@ public class AggregationOptions {
Document document = new Document();
document.put(ALLOW_DISK_USE, allowDiskUse);
document.put(EXPLAIN, explain);
document.put(CURSOR, cursor);
cursor.ifPresent(val -> document.put(CURSOR, val));
collation.ifPresent(val -> document.append(COLLATION, val.toDocument()));
return document;
}
@@ -207,6 +233,7 @@ public class AggregationOptions {
private boolean allowDiskUse;
private boolean explain;
private Document cursor;
private Collation collation;
/**
* Defines whether to off-load intensive sort-operations to disk.
@@ -257,13 +284,25 @@ public class AggregationOptions {
return this;
}
/**
* Define collation settings for string comparison.
*
* @param collation can be {@literal null}.
* @return
*/
public Builder collation(Collation collation) {
this.collation = collation;
return this;
}
/**
* Returns a new {@link AggregationOptions} instance with the given configuration.
*
* @return
*/
public AggregationOptions build() {
return new AggregationOptions(allowDiskUse, explain, cursor);
return new AggregationOptions(allowDiskUse, explain, cursor, collation);
}
}
}

View File

@@ -22,7 +22,7 @@ import org.springframework.data.domain.Range;
import org.springframework.util.Assert;
/**
* Utility methods for aggregation ooperation implementations.
* Utility methods for aggregation operation implementations.
*
* @author Oliver Gierke
*/

View File

@@ -15,7 +15,10 @@
*/
package org.springframework.data.mongodb.core.index;
import java.util.Optional;
import org.bson.Document;
import org.springframework.data.mongodb.core.Collation;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -37,7 +40,8 @@ public class GeospatialIndex implements IndexDefinition {
private GeoSpatialIndexType type = GeoSpatialIndexType.GEO_2D;
private Double bucketSize = 1.0;
private String additionalField;
private IndexFilter filter;
private Optional<IndexFilter> filter = Optional.empty();
private Optional<Collation> collation = Optional.empty();
/**
* Creates a new {@link GeospatialIndex} for the given field.
@@ -129,7 +133,23 @@ public class GeospatialIndex implements IndexDefinition {
*/
public GeospatialIndex partial(IndexFilter filter) {
this.filter = filter;
this.filter = Optional.ofNullable(filter);
return this;
}
/**
* Set the {@link Collation} to specify language-specific rules for string comparison, such as rules for lettercase
* and accent marks.<br />
* <strong>NOTE:</strong> Only queries using the same {@link Collation} as the {@link Index} actually make use of the
* index.
*
* @param collation can be {@literal null}.
* @return
* @since 2.0
*/
public GeospatialIndex collation(Collation collation) {
this.collation = Optional.ofNullable(collation);
return this;
}
@@ -168,9 +188,9 @@ public class GeospatialIndex implements IndexDefinition {
return null;
}
Document dbo = new Document();
Document document = new Document();
if (StringUtils.hasText(name)) {
dbo.put("name", name);
document.put("name", name);
}
switch (type) {
@@ -178,13 +198,13 @@ public class GeospatialIndex implements IndexDefinition {
case GEO_2D:
if (min != null) {
dbo.put("min", min);
document.put("min", min);
}
if (max != null) {
dbo.put("max", max);
document.put("max", max);
}
if (bits != null) {
dbo.put("bits", bits);
document.put("bits", bits);
}
break;
@@ -195,16 +215,15 @@ public class GeospatialIndex implements IndexDefinition {
case GEO_HAYSTACK:
if (bucketSize != null) {
dbo.put("bucketSize", bucketSize);
document.put("bucketSize", bucketSize);
}
break;
}
if (filter != null) {
dbo.put("partialFilterExpression", filter.getFilterObject());
}
filter.ifPresent(val -> document.put("partialFilterExpression", val.getFilterObject()));
collation.ifPresent(val -> document.append("collation", val.toDocument()));
return dbo;
return document;
}
/*

View File

@@ -18,10 +18,12 @@ package org.springframework.data.mongodb.core.index;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import org.bson.Document;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.Collation;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -50,7 +52,9 @@ public class Index implements IndexDefinition {
private long expire = -1;
private IndexFilter filter;
private Optional<IndexFilter> filter = Optional.empty();
private Optional<Collation> collation = Optional.empty();
public Index() {}
@@ -72,7 +76,8 @@ public class Index implements IndexDefinition {
* Reject all documents that contain a duplicate value for the indexed field.
*
* @return
* @see <a href="https://docs.mongodb.org/manual/core/index-unique/">https://docs.mongodb.org/manual/core/index-unique/</a>
* @see <a href=
* "https://docs.mongodb.org/manual/core/index-unique/">https://docs.mongodb.org/manual/core/index-unique/</a>
*/
public Index unique() {
this.unique = true;
@@ -83,7 +88,8 @@ public class Index implements IndexDefinition {
* Skip over any document that is missing the indexed field.
*
* @return
* @see <a href="https://docs.mongodb.org/manual/core/index-sparse/">https://docs.mongodb.org/manual/core/index-sparse/</a>
* @see <a href=
* "https://docs.mongodb.org/manual/core/index-sparse/">https://docs.mongodb.org/manual/core/index-sparse/</a>
*/
public Index sparse() {
this.sparse = true;
@@ -139,7 +145,23 @@ public class Index implements IndexDefinition {
*/
public Index partial(IndexFilter filter) {
this.filter = filter;
this.filter = Optional.ofNullable(filter);
return this;
}
/**
* Set the {@link Collation} to specify language-specific rules for string comparison, such as rules for lettercase
* and accent marks.<br />
* <strong>NOTE:</strong> Only queries using the same {@link Collation} as the {@link Index} actually make use of the
* index.
*
* @param collation can be {@literal null}.
* @return
* @since 2.0
*/
public Index collation(Collation collation) {
this.collation = Optional.ofNullable(collation);
return this;
}
@@ -180,9 +202,9 @@ public class Index implements IndexDefinition {
document.put("expireAfterSeconds", expire);
}
if (filter != null) {
document.put("partialFilterExpression", filter.getFilterObject());
}
filter.ifPresent(val -> document.put("partialFilterExpression", val.getFilterObject()));
collation.ifPresent(val -> document.append("collation", val.toDocument()));
return document;
}

View File

@@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.bson.Document;
import org.springframework.util.Assert;
@@ -46,9 +47,9 @@ public class IndexInfo {
private final boolean sparse;
private final String language;
private String partialFilterExpression;
private Document collation;
public IndexInfo(List<IndexField> indexFields, String name, boolean unique, boolean sparse,
String language) {
public IndexInfo(List<IndexField> indexFields, String name, boolean unique, boolean sparse, String language) {
this.indexFields = Collections.unmodifiableList(indexFields);
this.name = name;
@@ -106,10 +107,11 @@ public class IndexInfo {
String language = sourceDocument.containsKey("default_language") ? (String) sourceDocument.get("default_language")
: "";
String partialFilter = sourceDocument.containsKey("partialFilterExpression")
? ((Document)sourceDocument.get("partialFilterExpression")).toJson() : "";
? ((Document) sourceDocument.get("partialFilterExpression")).toJson() : "";
IndexInfo info = new IndexInfo(indexFields, name, unique, sparse, language);
info.partialFilterExpression = partialFilter;
info.collation = sourceDocument.get("collation", Document.class);
return info;
}
@@ -169,10 +171,21 @@ public class IndexInfo {
return partialFilterExpression;
}
/**
* Get collation information.
*
* @return
* @since 2.0
*/
public Optional<Document> getCollation() {
return Optional.ofNullable(collation);
}
@Override
public String toString() {
return "IndexInfo [indexFields=" + indexFields + ", name=" + name + ", unique=" + unique + ", sparse=" + sparse + ", language=" + language + ", partialFilterExpression="
+ partialFilterExpression + "]";
return "IndexInfo [indexFields=" + indexFields + ", name=" + name + ", unique=" + unique + ", sparse=" + sparse
+ ", language=" + language + ", partialFilterExpression=" + partialFilterExpression + ", collation=" + collation
+ "]";
}
@Override
@@ -186,6 +199,7 @@ public class IndexInfo {
result = prime * result + (unique ? 1231 : 1237);
result = prime * result + ObjectUtils.nullSafeHashCode(language);
result = prime * result + ObjectUtils.nullSafeHashCode(partialFilterExpression);
result = prime * result + ObjectUtils.nullSafeHashCode(collation);
return result;
}
@@ -227,6 +241,10 @@ public class IndexInfo {
if (!ObjectUtils.nullSafeEquals(partialFilterExpression, other.partialFilterExpression)) {
return false;
}
if (!ObjectUtils.nullSafeEquals(collation, collation)) {
return false;
}
return true;
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2010-2016 the original author or authors.
* Copyright 2010-2017 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.
@@ -15,7 +15,10 @@
*/
package org.springframework.data.mongodb.core.mapreduce;
import java.util.Optional;
import org.bson.Document;
import org.springframework.data.mongodb.core.Collation;
/**
* Collects the parameters required to perform a group operation on a collection. The query condition and the input
@@ -27,80 +30,138 @@ import org.bson.Document;
*/
public class GroupBy {
private Document dboKeys;
private String keyFunction;
private String initial;
private Document initialDocument;
private String reduce;
private String finalize;
private Optional<Document> dboKeys = Optional.empty();
private Optional<String> keyFunction = Optional.empty();
private Optional<String> initial = Optional.empty();
private Optional<String> finalize = Optional.empty();
private Optional<Collation> collation = Optional.empty();
public GroupBy(String... keys) {
Document document = new Document();
for (String key : keys) {
document.put(key, 1);
}
dboKeys = document;
dboKeys = Optional.of(document);
}
// NOTE GroupByCommand does not handle keyfunction.
public GroupBy(String key, boolean isKeyFunction) {
Document document = new Document();
if (isKeyFunction) {
keyFunction = key;
keyFunction = Optional.ofNullable(key);
} else {
document.put(key, 1);
dboKeys = document;
dboKeys = Optional.of(document);
}
}
/**
* Create new {@link GroupBy} with the field to group.
*
* @param key
* @return
*/
public static GroupBy keyFunction(String key) {
return new GroupBy(key, true);
}
/**
* Create new {@link GroupBy} with the fields to group.
*
* @param keys
* @return
*/
public static GroupBy key(String... keys) {
return new GroupBy(keys);
}
/**
* Define the aggregation result document.
*
* @param initialDocument can be {@literal null}.
* @return
*/
public GroupBy initialDocument(String initialDocument) {
initial = initialDocument;
initial = Optional.ofNullable(initialDocument);
return this;
}
/**
* Define the aggregation result document.
*
* @param initialDocument can be {@literal null}.
* @return
*/
public GroupBy initialDocument(Document initialDocument) {
this.initialDocument = initialDocument;
return this;
}
/**
* Define the aggregation function that operates on the documents during the grouping operation
*
* @param reduceFunction
* @return
*/
public GroupBy reduceFunction(String reduceFunction) {
reduce = reduceFunction;
return this;
}
/**
* Define the function that runs each item in the result set before db.collection.group() returns the final value.
*
* @param finalizeFunction
* @return
*/
public GroupBy finalizeFunction(String finalizeFunction) {
finalize = finalizeFunction;
finalize = Optional.ofNullable(finalizeFunction);
return this;
}
/**
* Define the Collation specifying language-specific rules for string comparison.
*
* @param collation can be {@literal null}.
* @return
* @since 2.0
*/
public GroupBy collation(Collation collation) {
this.collation = Optional.ofNullable(collation);
return this;
}
/**
* Get the {@link Document} representation of the {@link GroupBy}.
*
* @return
*/
public Document getGroupByObject() {
// return new GroupCommand(dbCollection, dboKeys, condition, initial, reduce, finalize);
Document document = new Document();
if (dboKeys != null) {
document.put("key", dboKeys);
}
if (keyFunction != null) {
document.put("$keyf", keyFunction);
}
dboKeys.ifPresent(val -> document.append("key", val));
keyFunction.ifPresent(val -> document.append("$keyf", val));
document.put("$reduce", reduce);
document.put("initial", initialDocument);
if (initial != null) {
document.put("initial", initial);
}
if (finalize != null) {
document.put("finalize", finalize);
}
initial.ifPresent(val -> document.append("initial", val));
finalize.ifPresent(val -> document.append("finalize", val));
collation.ifPresent(val -> document.append("collation", val.toDocument()));
return document;
}

View File

@@ -17,8 +17,10 @@ package org.springframework.data.mongodb.core.mapreduce;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.bson.Document;
import org.springframework.data.mongodb.core.Collation;
import com.mongodb.MapReduceCommand;
@@ -31,23 +33,17 @@ public class MapReduceOptions {
private String outputCollection;
private String outputDatabase;
private Boolean outputSharded;
private Optional<String> outputDatabase = Optional.empty();
private MapReduceCommand.OutputType outputType = MapReduceCommand.OutputType.REPLACE;
private String finalizeFunction;
private Map<String, Object> scopeVariables = new HashMap<String, Object>();
private Map<String, Object> extraOptions = new HashMap<String, Object>();
private Boolean jsMode;
private Boolean verbose = true;
private Boolean verbose = Boolean.TRUE;
private Integer limit;
private Map<String, Object> extraOptions = new HashMap<String, Object>();
private Optional<Boolean> outputSharded = Optional.empty();
private Optional<String> finalizeFunction = Optional.empty();
private Optional<Collation> collation = Optional.empty();
/**
* Static factory method to create a MapReduceOptions instance
@@ -79,6 +75,7 @@ public class MapReduceOptions {
* @return MapReduceOptions so that methods can be chained in a fluent API style
*/
public MapReduceOptions outputCollection(String collectionName) {
this.outputCollection = collectionName;
return this;
}
@@ -91,7 +88,8 @@ public class MapReduceOptions {
* @return MapReduceOptions so that methods can be chained in a fluent API style
*/
public MapReduceOptions outputDatabase(String outputDatabase) {
this.outputDatabase = outputDatabase;
this.outputDatabase = Optional.ofNullable(outputDatabase);
return this;
}
@@ -103,6 +101,7 @@ public class MapReduceOptions {
* @return MapReduceOptions so that methods can be chained in a fluent API style
*/
public MapReduceOptions outputTypeInline() {
this.outputType = MapReduceCommand.OutputType.INLINE;
return this;
}
@@ -114,6 +113,7 @@ public class MapReduceOptions {
* @return MapReduceOptions so that methods can be chained in a fluent API style
*/
public MapReduceOptions outputTypeMerge() {
this.outputType = MapReduceCommand.OutputType.MERGE;
return this;
}
@@ -137,6 +137,7 @@ public class MapReduceOptions {
* @return MapReduceOptions so that methods can be chained in a fluent API style
*/
public MapReduceOptions outputTypeReplace() {
this.outputType = MapReduceCommand.OutputType.REPLACE;
return this;
}
@@ -149,7 +150,8 @@ public class MapReduceOptions {
* @return MapReduceOptions so that methods can be chained in a fluent API style
*/
public MapReduceOptions outputSharded(boolean outputShared) {
this.outputSharded = outputShared;
this.outputSharded = Optional.of(outputShared);
return this;
}
@@ -160,7 +162,8 @@ public class MapReduceOptions {
* @return MapReduceOptions so that methods can be chained in a fluent API style
*/
public MapReduceOptions finalizeFunction(String finalizeFunction) {
this.finalizeFunction = finalizeFunction;
this.finalizeFunction = Optional.ofNullable(finalizeFunction);
return this;
}
@@ -172,6 +175,7 @@ public class MapReduceOptions {
* @return MapReduceOptions so that methods can be chained in a fluent API style
*/
public MapReduceOptions scopeVariables(Map<String, Object> scopeVariables) {
this.scopeVariables = scopeVariables;
return this;
}
@@ -184,6 +188,7 @@ public class MapReduceOptions {
* @return MapReduceOptions so that methods can be chained in a fluent API style
*/
public MapReduceOptions javaScriptMode(boolean javaScriptMode) {
this.jsMode = javaScriptMode;
return this;
}
@@ -194,6 +199,7 @@ public class MapReduceOptions {
* @return MapReduceOptions so that methods can be chained in a fluent API style
*/
public MapReduceOptions verbose(boolean verbose) {
this.verbose = verbose;
return this;
}
@@ -210,10 +216,24 @@ public class MapReduceOptions {
*/
@Deprecated
public MapReduceOptions extraOption(String key, Object value) {
extraOptions.put(key, value);
return this;
}
/**
* Define the Collation specifying language-specific rules for string comparison.
*
* @param collation can be {@literal null}.
* @return
* @since 2.0
*/
public MapReduceOptions collation(Collation collation) {
this.collation = Optional.ofNullable(collation);
return this;
}
/**
* @return
* @deprecated since 1.7
@@ -223,7 +243,7 @@ public class MapReduceOptions {
return extraOptions;
}
public String getFinalizeFunction() {
public Optional<String> getFinalizeFunction() {
return this.finalizeFunction;
}
@@ -235,11 +255,11 @@ public class MapReduceOptions {
return this.outputCollection;
}
public String getOutputDatabase() {
public Optional<String> getOutputDatabase() {
return this.outputDatabase;
}
public Boolean getOutputSharded() {
public Optional<Boolean> getOutputSharded() {
return this.outputSharded;
}
@@ -260,7 +280,18 @@ public class MapReduceOptions {
return limit;
}
/**
* Get the Collation specifying language-specific rules for string comparison.
*
* @return
* @since 2.0
*/
public Optional<Collation> getCollation() {
return collation;
}
public Document getOptionsObject() {
Document cmd = new Document();
if (verbose != null) {
@@ -269,9 +300,7 @@ public class MapReduceOptions {
cmd.put("out", createOutObject());
if (finalizeFunction != null) {
cmd.put("finalize", finalizeFunction);
}
finalizeFunction.ifPresent(val -> cmd.append("finalize", val));
if (scopeVariables != null) {
cmd.put("scope", scopeVariables);
@@ -285,10 +314,13 @@ public class MapReduceOptions {
cmd.putAll(extraOptions);
}
getCollation().ifPresent(val -> cmd.append("collation", val.toDocument()));
return cmd;
}
protected Document createOutObject() {
Document out = new Document();
switch (outputType) {
@@ -306,13 +338,8 @@ public class MapReduceOptions {
break;
}
if (outputDatabase != null) {
out.put("db", outputDatabase);
}
if (outputSharded != null) {
out.put("sharded", outputSharded);
}
outputDatabase.ifPresent(val -> out.append("db", val));
outputSharded.ifPresent(val -> out.append("sharded", val));
return out;
}

View File

@@ -404,7 +404,9 @@ public final class NearQuery {
Document document = new Document();
if (query != null) {
document.put("query", query.getQueryObject());
query.getCollation().ifPresent(collation -> document.append("collation", collation.toDocument()));
}
if (maxDistance != null) {

View File

@@ -24,6 +24,7 @@ import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -32,6 +33,7 @@ import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.mongodb.InvalidMongoDbApiUsageException;
import org.springframework.data.mongodb.core.Collation;
import org.springframework.util.Assert;
/**
@@ -45,8 +47,8 @@ public class Query {
private static final String RESTRICTED_TYPES_KEY = "_$RESTRICTED_TYPES";
private final Set<Class<?>> restrictedTypes = new HashSet<Class<?>>();
private final Map<String, CriteriaDefinition> criteria = new LinkedHashMap<String, CriteriaDefinition>();
private final Set<Class<?>> restrictedTypes = new HashSet<>();
private final Map<String, CriteriaDefinition> criteria = new LinkedHashMap<>();
private Field fieldSpec = null;
private Sort sort = Sort.unsorted();
private long skip;
@@ -55,6 +57,8 @@ public class Query {
private Meta meta = new Meta();
private Optional<Collation> collation = Optional.empty();
/**
* Static factory method to create a {@link Query} using the provided {@link CriteriaDefinition}.
*
@@ -192,7 +196,7 @@ public class Query {
* @return the restrictedTypes
*/
public Set<Class<?>> getRestrictedTypes() {
return restrictedTypes == null ? Collections.<Class<?>> emptySet() : restrictedTypes;
return restrictedTypes == null ? Collections.emptySet() : restrictedTypes;
}
/**
@@ -395,8 +399,31 @@ public class Query {
this.meta = meta;
}
/**
* Set the {@link Collation} applying language-specific rules for string comparison.
*
* @param collation can be {@literal null}.
* @return
* @since 2.0
*/
public Query collation(Collation collation) {
this.collation = Optional.ofNullable(collation);
return this;
}
/**
* Get the {@link Collation} defining language-specific rules for string comparison.
*
* @return
* @since 2.0
*/
public Optional<Collation> getCollation() {
return collation;
}
protected List<CriteriaDefinition> getCriteria() {
return new ArrayList<CriteriaDefinition>(this.criteria.values());
return new ArrayList<>(this.criteria.values());
}
/*
@@ -442,8 +469,10 @@ public class Query {
boolean skipEqual = this.skip == that.skip;
boolean limitEqual = this.limit == that.limit;
boolean metaEqual = nullSafeEquals(this.meta, that.meta);
boolean collationEqual = nullSafeEquals(this.collation.orElse(null), that.collation.orElse(null));
return criteriaEqual && fieldsEqual && sortEqual && hintEqual && skipEqual && limitEqual && metaEqual;
return criteriaEqual && fieldsEqual && sortEqual && hintEqual && skipEqual && limitEqual && metaEqual
&& collationEqual;
}
/*
@@ -462,6 +491,7 @@ public class Query {
result += 31 * skip;
result += 31 * limit;
result += 31 * nullSafeHashCode(meta);
result += 31 * nullSafeHashCode(collation.orElse(null));
return result;
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import org.bson.Document;
import org.junit.Test;
import org.springframework.data.mongodb.core.Collation.Alternate;
import org.springframework.data.mongodb.core.Collation.ICUCaseFirst;
import org.springframework.data.mongodb.core.Collation.ICUComparisonLevel;
import org.springframework.data.mongodb.core.Collation.ICULocale;
/**
* @author Christoph Strobl
*/
public class CollationUnitTests {
static final Document BINARY_COMPARISON = new Document().append("locale", "simple");
static final Document JUST_LOCALE = new Document().append("locale", "en_US");
static final Document LOCALE_WITH_VARIANT = new Document().append("locale", "de_AT@collation=phonebook");
static final Document WITH_STRENGTH_PRIMARY = new Document(JUST_LOCALE).append("strength", 1);
static final Document WITH_STRENGTH_PRIMARY_INCLUDE_CASE = new Document(WITH_STRENGTH_PRIMARY).append("caseLevel",
true);
static final Document WITH_NORMALIZATION = new Document(JUST_LOCALE).append("normalization", true);
static final Document WITH_BACKWARDS = new Document(JUST_LOCALE).append("backwards", true);
static final Document WITH_NUMERIC_ORDERING = new Document(JUST_LOCALE).append("numericOrdering", true);
static final Document WITH_CASE_FIRST_UPPER = new Document(JUST_LOCALE).append("strength", 3).append("caseFirst",
"upper");
static final Document WITH_ALTERNATE_SHIFTED = new Document(JUST_LOCALE).append("alternate", "shifted");
static final Document WITH_ALTERNATE_SHIFTED_MAX_VARIABLE_PUNCT = new Document(WITH_ALTERNATE_SHIFTED)
.append("maxVariable", "punct");
static final Document ALL_THE_THINGS = new Document(LOCALE_WITH_VARIANT).append("strength", 1)
.append("caseLevel", true).append("backwards", true).append("numericOrdering", true)
.append("alternate", "shifted").append("maxVariable", "punct").append("normalization", true);
@Test // DATAMONGO-1518
public void justLocale() {
assertThat(Collation.of("en_US").toDocument()).isEqualTo(JUST_LOCALE);
}
@Test // DATAMONGO-1518
public void justLocaleFromDocument() {
assertThat(Collation.from(JUST_LOCALE).toDocument()).isEqualTo(JUST_LOCALE);
}
@Test // DATAMONGO-1518
public void localeWithVariant() {
assertThat(Collation.of(ICULocale.of("de_AT").variant("phonebook")).toDocument()).isEqualTo(LOCALE_WITH_VARIANT);
}
@Test // DATAMONGO-1518
public void localeWithVariantFromDocument() {
assertThat(Collation.from(LOCALE_WITH_VARIANT).toDocument()).isEqualTo(LOCALE_WITH_VARIANT);
}
@Test // DATAMONGO-1518
public void lcaleFromJavaUtilLocale() {
assertThat(Collation.of(java.util.Locale.US).toDocument()).isEqualTo(new Document().append("locale", "en"));
}
@Test // DATAMONGO-1518
public void withStrenghPrimary() {
assertThat(Collation.of("en_US").strength(ICUComparisonLevel.primary()).toDocument())
.isEqualTo(WITH_STRENGTH_PRIMARY);
}
@Test // DATAMONGO-1518
public void withStrenghPrimaryFromDocument() {
assertThat(Collation.from(WITH_STRENGTH_PRIMARY).toDocument()).isEqualTo(WITH_STRENGTH_PRIMARY);
}
@Test // DATAMONGO-1518
public void withStrenghPrimaryAndIncludeCase() {
assertThat(Collation.of("en_US").strength(ICUComparisonLevel.primary().includeCase()).toDocument())
.isEqualTo(WITH_STRENGTH_PRIMARY_INCLUDE_CASE);
}
@Test // DATAMONGO-1518
public void withStrenghPrimaryAndIncludeCaseFromDocument() {
assertThat(Collation.from(WITH_STRENGTH_PRIMARY_INCLUDE_CASE).toDocument())
.isEqualTo(WITH_STRENGTH_PRIMARY_INCLUDE_CASE);
}
@Test // DATAMONGO-1518
public void withNormalization() {
assertThat(Collation.of("en_US").normalization(true).toDocument()).isEqualTo(WITH_NORMALIZATION);
}
@Test // DATAMONGO-1518
public void withNormalizationFromDocument() {
assertThat(Collation.from(WITH_NORMALIZATION).toDocument()).isEqualTo(WITH_NORMALIZATION);
}
@Test // DATAMONGO-1518
public void withBackwards() {
assertThat(Collation.of("en_US").backwards(true).toDocument()).isEqualTo(WITH_BACKWARDS);
}
@Test // DATAMONGO-1518
public void withBackwardsFromDocument() {
assertThat(Collation.from(WITH_BACKWARDS).toDocument()).isEqualTo(WITH_BACKWARDS);
}
@Test // DATAMONGO-1518
public void withNumericOrdering() {
assertThat(Collation.of("en_US").numericOrdering(true).toDocument()).isEqualTo(WITH_NUMERIC_ORDERING);
}
@Test // DATAMONGO-1518
public void withNumericOrderingFromDocument() {
assertThat(Collation.from(WITH_NUMERIC_ORDERING).toDocument()).isEqualTo(WITH_NUMERIC_ORDERING);
}
@Test // DATAMONGO-1518
public void withCaseFirst() {
assertThat(Collation.of("en_US").caseFirst(ICUCaseFirst.upper()).toDocument()).isEqualTo(WITH_CASE_FIRST_UPPER);
}
@Test // DATAMONGO-1518
public void withCaseFirstFromDocument() {
assertThat(Collation.from(WITH_CASE_FIRST_UPPER).toDocument()).isEqualTo(WITH_CASE_FIRST_UPPER);
}
@Test // DATAMONGO-1518
public void withAlternate() {
assertThat(Collation.of("en_US").alternate(Alternate.shifted()).toDocument()).isEqualTo(WITH_ALTERNATE_SHIFTED);
}
@Test // DATAMONGO-1518
public void withAlternateFromDocument() {
assertThat(Collation.from(WITH_ALTERNATE_SHIFTED).toDocument()).isEqualTo(WITH_ALTERNATE_SHIFTED);
}
@Test // DATAMONGO-1518
public void withAlternateAndMaxVariable() {
assertThat(Collation.of("en_US").alternate(Alternate.shifted().punct()).toDocument())
.isEqualTo(WITH_ALTERNATE_SHIFTED_MAX_VARIABLE_PUNCT);
}
@Test // DATAMONGO-1518
public void withAlternateAndMaxVariableFromDocument() {
assertThat(Collation.from(WITH_ALTERNATE_SHIFTED_MAX_VARIABLE_PUNCT).toDocument())
.isEqualTo(WITH_ALTERNATE_SHIFTED_MAX_VARIABLE_PUNCT);
}
@Test // DATAMONGO-1518
public void allTheThings() {
assertThat(Collation.of(ICULocale.of("de_AT").variant("phonebook"))
.strength(ICUComparisonLevel.primary().includeCase()).normalizationEnabled().backwardDiacriticSort()
.numericOrderingEnabled().alternate(Alternate.shifted().punct()).toDocument()).isEqualTo(ALL_THE_THINGS);
}
@Test // DATAMONGO-1518
public void allTheThingsFromDocument() {
assertThat(Collation.from(ALL_THE_THINGS).toDocument()).isEqualTo(ALL_THE_THINGS);
}
@Test // DATAMONGO-1518
public void justTheDefault() {
assertThat(Collation.binary().toDocument()).isEqualTo(BINARY_COMPARISON);
}
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.any;
import java.util.List;
import org.bson.Document;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Update;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.model.DeleteManyModel;
import com.mongodb.client.model.UpdateManyModel;
import com.mongodb.client.model.UpdateOneModel;
import com.mongodb.client.model.WriteModel;
/**
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
public class DefaultBulkOperationsUnitTests {
@Mock MongoTemplate template;
@Mock MongoCollection collection;
DefaultBulkOperations ops;
@Before
public void setUp() {
when(template.getCollection(anyString())).thenReturn(collection);
ops = new DefaultBulkOperations(template, BulkMode.ORDERED, "collection-1", SomeDomainType.class);
}
@Test // DATAMONGO-1518
public void updateOneShouldUseCollationWhenPresent() {
ops.updateOne(new BasicQuery("{}").collation(Collation.of("de")), new Update().set("lastName", "targaryen"))
.execute();
ArgumentCaptor<List<WriteModel<Document>>> captor = ArgumentCaptor.forClass(List.class);
verify(collection).bulkWrite(captor.capture(), any());
assertThat(captor.getValue().get(0)).isInstanceOf(UpdateOneModel.class);
assertThat(((UpdateOneModel) captor.getValue().get(0)).getOptions().getCollation())
.isEqualTo(com.mongodb.client.model.Collation.builder().locale("de").build());
}
@Test // DATAMONGO-1518
public void updateMayShouldUseCollationWhenPresent() {
ops.updateMulti(new BasicQuery("{}").collation(Collation.of("de")), new Update().set("lastName", "targaryen"))
.execute();
ArgumentCaptor<List<WriteModel<Document>>> captor = ArgumentCaptor.forClass(List.class);
verify(collection).bulkWrite(captor.capture(), any());
assertThat(captor.getValue().get(0)).isInstanceOf(UpdateManyModel.class);
assertThat(((UpdateManyModel) captor.getValue().get(0)).getOptions().getCollation())
.isEqualTo(com.mongodb.client.model.Collation.builder().locale("de").build());
}
@Test // DATAMONGO-1518
public void removeShouldUseCollationWhenPresent() {
ops.remove(new BasicQuery("{}").collation(Collation.of("de"))).execute();
ArgumentCaptor<List<WriteModel<Document>>> captor = ArgumentCaptor.forClass(List.class);
verify(collection).bulkWrite(captor.capture(), any());
assertThat(captor.getValue().get(0)).isInstanceOf(DeleteManyModel.class);
assertThat(((DeleteManyModel) captor.getValue().get(0)).getOptions().getCollation())
.isEqualTo(com.mongodb.client.model.Collation.builder().locale("de").build());
}
class SomeDomainType {
@Id String id;
Gender gender;
@Field("first_name") String firstName;
@Field String lastName;
}
enum Gender {
M, F
}
}

View File

@@ -15,8 +15,8 @@
*/
package org.springframework.data.mongodb.core;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.assertj.core.api.Assertions.*;
import static org.hamcrest.core.Is.*;
import static org.junit.Assume.*;
import static org.springframework.data.mongodb.core.index.PartialIndexFilter.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
@@ -27,6 +27,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.mongodb.core.Collation.ICUCaseFirst;
import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexDefinition;
@@ -50,6 +51,7 @@ import com.mongodb.client.MongoCollection;
public class DefaultIndexOperationsIntegrationTests {
private static final Version THREE_DOT_TWO = new Version(3, 2);
private static final Version THREE_DOT_FOUR = new Version(3, 4);
private static Version mongoVersion;
static final org.bson.Document GEO_SPHERE_2D = new org.bson.Document("loaction", "2dsphere");
@@ -83,7 +85,7 @@ public class DefaultIndexOperationsIntegrationTests {
collection.createIndex(GEO_SPHERE_2D);
IndexInfo info = findAndReturnIndexInfo(GEO_SPHERE_2D);
assertThat(info.getIndexFields().get(0).isGeo(), is(true));
assertThat(info.getIndexFields().get(0).isGeo()).isEqualTo(true);
}
@Test // DATAMONGO-1467
@@ -97,7 +99,7 @@ public class DefaultIndexOperationsIntegrationTests {
indexOps.ensureIndex(id);
IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-criteria");
assertThat(info.getPartialFilterExpression(), is(equalTo("{ \"q-t-y\" : { \"$gte\" : 10 } }")));
assertThat(info.getPartialFilterExpression()).isEqualTo("{ \"q-t-y\" : { \"$gte\" : 10 } }");
}
@Test // DATAMONGO-1467
@@ -111,7 +113,7 @@ public class DefaultIndexOperationsIntegrationTests {
indexOps.ensureIndex(id);
IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-mapped-criteria");
assertThat(info.getPartialFilterExpression(), is(equalTo("{ \"qty\" : { \"$gte\" : 10 } }")));
assertThat(info.getPartialFilterExpression()).isEqualTo("{ \"qty\" : { \"$gte\" : 10 } }");
}
@Test // DATAMONGO-1467
@@ -125,7 +127,7 @@ public class DefaultIndexOperationsIntegrationTests {
indexOps.ensureIndex(id);
IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-dbo");
assertThat(info.getPartialFilterExpression(), is(equalTo("{ \"qty\" : { \"$gte\" : 10 } }")));
assertThat(info.getPartialFilterExpression()).isEqualTo("{ \"qty\" : { \"$gte\" : 10 } }");
}
@Test // DATAMONGO-1467
@@ -143,7 +145,42 @@ public class DefaultIndexOperationsIntegrationTests {
indexOps.ensureIndex(id);
IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-inheritance");
assertThat(info.getPartialFilterExpression(), is(equalTo("{ \"a_g_e\" : { \"$gte\" : 10 } }")));
assertThat(info.getPartialFilterExpression()).isEqualTo("{ \"a_g_e\" : { \"$gte\" : 10 } }");
}
@Test // DATAMONGO-1518
public void shouldCreateIndexWithCollationCorrectly() {
assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_FOUR), is(true));
IndexDefinition id = new Index().named("with-collation").on("xyz", Direction.ASC)
.collation(Collation.of("de_AT").caseFirst(ICUCaseFirst.off()));
new DefaultIndexOperations(template.getMongoDbFactory(),
this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class),
new QueryMapper(template.getConverter()), MappingToSameCollection.class);
indexOps.ensureIndex(id);
Document expected = new Document("locale", "de_AT") //
.append("caseLevel", false) //
.append("caseFirst", "off") //
.append("strength", 3) //
.append("numericOrdering", false) //
.append("alternate", "non-ignorable") //
.append("maxVariable", "punct") //
.append("normalization", false) //
.append("backwards", false);
IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "with-collation");
assertThat(info.getCollation()).isPresent();
// version is set by MongoDB server - we remove it to avoid errors when upgrading server version.
Document result = info.getCollation().get();
result.remove("version");
assertThat(result).isEqualTo(expected);
}
private IndexInfo findAndReturnIndexInfo(org.bson.Document keys) {

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import static org.hamcrest.core.Is.*;
import static org.junit.Assume.*;
import reactor.test.StepVerifier;
import org.bson.Document;
import org.junit.Before;
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.data.domain.Sort.Direction;
import org.springframework.data.mongodb.config.AbstractReactiveMongoConfiguration;
import org.springframework.data.mongodb.core.Collation.ICUCaseFirst;
import org.springframework.data.mongodb.core.DefaultIndexOperationsIntegrationTests.DefaultIndexOperationsIntegrationTestsSample;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.index.IndexDefinition;
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;
import com.mongodb.reactivestreams.client.MongoCollection;
/**
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class DefaultReactiveIndexOperationsTests {
@Configuration
static class Config extends AbstractReactiveMongoConfiguration {
@Override
public MongoClient mongoClient() {
return MongoClients.create();
}
@Override
protected String getDatabaseName() {
return "index-ops-tests";
}
}
private static final Version THREE_DOT_FOUR = new Version(3, 4);
private static Version mongoVersion;
@Autowired ReactiveMongoTemplate template;
MongoCollection<Document> collection;
DefaultReactiveIndexOperations indexOps;
@Before
public void setUp() {
queryMongoVersionIfNecessary();
String collectionName = this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class);
this.collection = this.template.getMongoDatabase().getCollection(collectionName, Document.class);
this.collection.dropIndexes();
this.indexOps = new DefaultReactiveIndexOperations(template, collectionName);
}
private void queryMongoVersionIfNecessary() {
if (mongoVersion == null) {
Document result = template.executeCommand("{ buildInfo: 1 }").block();
mongoVersion = Version.parse(result.get("version").toString());
}
}
@Test // DATAMONGO-1518
public void shouldCreateIndexWithCollationCorrectly() {
assumeThat(mongoVersion.isGreaterThanOrEqualTo(THREE_DOT_FOUR), is(true));
IndexDefinition id = new Index().named("with-collation").on("xyz", Direction.ASC)
.collation(Collation.of("de_AT").caseFirst(ICUCaseFirst.off()));
indexOps.ensureIndex(id).subscribe();
Document expected = new Document("locale", "de_AT") //
.append("caseLevel", false) //
.append("caseFirst", "off") //
.append("strength", 3) //
.append("numericOrdering", false) //
.append("alternate", "non-ignorable") //
.append("maxVariable", "punct") //
.append("normalization", false) //
.append("backwards", false);
StepVerifier.create(indexOps.getIndexInfo().filter(val -> val.getName().equals("with-collation")))
.consumeNextWith(indexInfo -> {
assertThat(indexInfo.getCollation()).isPresent();
// version is set by MongoDB server - we remove it to avoid errors when upgrading server version.
Document result = indexInfo.getCollation().get();
result.remove("version");
assertThat(result).isEqualTo(expected);
}) //
.verifyComplete();
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import static org.assertj.core.api.Assertions.*;
import 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.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.Collation.Alternate;
import org.springframework.data.mongodb.core.Collation.ICUComparisonLevel;
import org.springframework.data.mongodb.core.Collation.ICULocale;
import org.springframework.data.mongodb.test.util.MongoVersionRule;
import org.springframework.data.util.Version;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.mongodb.MongoClient;
/**
* @author Christoph Strobl
*/
@RunWith(SpringJUnit4ClassRunner.class)
public class MongoTemplateCollationTests {
public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_4_0 = MongoVersionRule.atLeast(Version.parse("3.4.0"));
public static final String COLLECTION_NAME = "collation-1";
@Configuration
static class Config extends AbstractMongoConfiguration {
@Override
public MongoClient mongoClient() {
return new MongoClient();
}
@Override
protected String getDatabaseName() {
return "collation-tests";
}
}
@Autowired MongoTemplate template;
@Before
public void setUp() {
template.dropCollection(COLLECTION_NAME);
}
@Test // DATAMONGO-1518
public void createCollectionWithCollation() {
template.createCollection(COLLECTION_NAME, CollectionOptions.just(Collation.of("en_US")));
Document collation = getCollationInfo(COLLECTION_NAME);
assertThat(collation.get("locale")).isEqualTo("en_US");
}
@Test // DATAMONGO-1518
public void createCollectionWithCollationHavingLocaleVariant() {
template.createCollection(COLLECTION_NAME,
CollectionOptions.just(Collation.of(ICULocale.of("de_AT").variant("phonebook"))));
Document collation = getCollationInfo(COLLECTION_NAME);
assertThat(collation.get("locale")).isEqualTo("de_AT@collation=phonebook");
}
@Test // DATAMONGO-1518
public void createCollectionWithCollationHavingStrength() {
template.createCollection(COLLECTION_NAME,
CollectionOptions.just(Collation.of("en_US").strength(ICUComparisonLevel.primary().includeCase())));
Document collation = getCollationInfo(COLLECTION_NAME);
assertThat(collation.get("strength")).isEqualTo(1);
assertThat(collation.get("caseLevel")).isEqualTo(true);
}
@Test // DATAMONGO-1518
public void createCollectionWithCollationHavingBackwardsAndNumericOrdering() {
template.createCollection(COLLECTION_NAME,
CollectionOptions.just(Collation.of("en_US").backwardDiacriticSort().numericOrderingEnabled()));
Document collation = getCollationInfo(COLLECTION_NAME);
assertThat(collation.get("backwards")).isEqualTo(true);
assertThat(collation.get("numericOrdering")).isEqualTo(true);
}
@Test // DATAMONGO-1518
public void createCollationWithCollationHavingAlternate() {
template.createCollection(COLLECTION_NAME,
CollectionOptions.just(Collation.of("en_US").alternate(Alternate.shifted().punct())));
Document collation = getCollationInfo(COLLECTION_NAME);
assertThat(collation.get("alternate")).isEqualTo("shifted");
assertThat(collation.get("maxVariable")).isEqualTo("punct");
}
private Document getCollationInfo(String collectionName) {
return getCollectionInfo(collectionName).get("options", Document.class).get("collation", Document.class);
}
private Document getCollectionInfo(String collectionName) {
return template.execute(db -> {
Document result = db.runCommand(
new Document().append("listCollections", 1).append("filter", new Document("name", collectionName)));
return (Document) result.get("cursor", Document.class).get("firstBatch", List.class).get(0);
});
}
}

View File

@@ -19,6 +19,7 @@ import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.any;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.test.util.IsBsonObject.*;
import java.math.BigInteger;
@@ -62,6 +63,7 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCre
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.AbstractMongoEventListener;
import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent;
import org.springframework.data.mongodb.core.mapreduce.GroupBy;
import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Criteria;
@@ -79,6 +81,8 @@ import com.mongodb.client.MapReduceIterable;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoCursor;
import com.mongodb.client.MongoDatabase;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.result.UpdateResult;
@@ -101,6 +105,9 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
@Mock MongoCollection<Document> collection;
@Mock MongoCursor<Document> cursor;
@Mock FindIterable<Document> findIterable;
@Mock MapReduceIterable mapReduceIterable;
Document commandResultDocument = new Document();
MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();
MappingMongoConverter converter;
@@ -113,9 +120,16 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
when(factory.getDb()).thenReturn(db);
when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator);
when(db.getCollection(Mockito.any(String.class), eq(Document.class))).thenReturn(collection);
when(db.runCommand(Mockito.any(), Mockito.any(Class.class))).thenReturn(commandResultDocument);
when(collection.find(Mockito.any(org.bson.Document.class))).thenReturn(findIterable);
when(collection.mapReduce(Mockito.any(), Mockito.any())).thenReturn(mapReduceIterable);
when(findIterable.projection(Mockito.any())).thenReturn(findIterable);
when(findIterable.sort(Mockito.any(org.bson.Document.class))).thenReturn(findIterable);
when(findIterable.modifiers(Mockito.any(org.bson.Document.class))).thenReturn(findIterable);
when(findIterable.collation(Mockito.any())).thenReturn(findIterable);
when(findIterable.limit(anyInt())).thenReturn(findIterable);
when(mapReduceIterable.collation(Mockito.any())).thenReturn(mapReduceIterable);
when(mapReduceIterable.iterator()).thenReturn(cursor);
this.mappingContext = new MongoMappingContext();
this.converter = new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext);
@@ -306,7 +320,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
BasicQuery query = new BasicQuery("{'foo':'bar'}");
template.findAllAndRemove(query, VersionedEntity.class);
verify(collection, times(1)).deleteMany(queryCaptor.capture());
verify(collection, times(1)).deleteMany(queryCaptor.capture(), Mockito.any());
Document idField = DocumentTestUtils.getAsDocument(queryCaptor.getValue(), "_id");
assertThat((List<Object>) idField.get("$in"),
@@ -345,7 +359,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
.thenReturn(mock(Document.class));
template.setReadPreference(ReadPreference.secondary());
template.aggregate(Aggregation.newAggregation(Aggregation.unwind("foo")), "collection-1", Wrapper.class);
template.aggregate(newAggregation(Aggregation.unwind("foo")), "collection-1", Wrapper.class);
verify(this.db, times(1)).runCommand(Mockito.any(org.bson.Document.class), eq(ReadPreference.secondary()),
eq(Document.class));
@@ -357,7 +371,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
when(db.runCommand(Mockito.any(org.bson.Document.class), eq(org.bson.Document.class)))
.thenReturn(mock(Document.class));
template.aggregate(Aggregation.newAggregation(Aggregation.unwind("foo")), "collection-1", Wrapper.class);
template.aggregate(newAggregation(Aggregation.unwind("foo")), "collection-1", Wrapper.class);
verify(this.db, times(1)).runCommand(Mockito.any(org.bson.Document.class), eq(org.bson.Document.class));
}
@@ -615,6 +629,160 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests {
assertThat(updateCaptor.getValue(), isBsonObject().containing("$set.jon", "snow").notContaining("$isolated"));
}
@Test // DATAMONGO-1518
public void executeQueryShouldUseCollationWhenPresent() {
template.executeQuery(new BasicQuery("{}").collation(Collation.of("fr")), "collection-1", val -> {});
verify(findIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
public void streamQueryShouldUseCollationWhenPresent() {
template.stream(new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class).next();
verify(findIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
public void findShouldUseCollationWhenPresent() {
template.find(new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class);
verify(findIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
public void findOneShouldUseCollationWhenPresent() {
template.findOne(new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class);
verify(findIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
public void existsShouldUseCollationWhenPresent() {
template.exists(new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class);
verify(findIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
public void findAndModfiyShoudUseCollationWhenPresent() {
template.findAndModify(new BasicQuery("{}").collation(Collation.of("fr")), new Update(), AutogenerateableId.class);
ArgumentCaptor<FindOneAndUpdateOptions> options = ArgumentCaptor.forClass(FindOneAndUpdateOptions.class);
verify(collection).findOneAndUpdate(Mockito.any(), Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Test // DATAMONGO-1518
public void findAndRemoveShouldUseCollationWhenPresent() {
template.findAndRemove(new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class);
ArgumentCaptor<FindOneAndDeleteOptions> options = ArgumentCaptor.forClass(FindOneAndDeleteOptions.class);
verify(collection).findOneAndDelete(Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Test // DATAMONGO-1518
public void findAndRemoveManyShouldUseCollationWhenPresent() {
template.doRemove("collection-1", new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class);
ArgumentCaptor<DeleteOptions> options = ArgumentCaptor.forClass(DeleteOptions.class);
verify(collection).deleteMany(Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Test // DATAMONGO-1518
public void updateOneShouldUseCollationWhenPresent() {
template.updateFirst(new BasicQuery("{}").collation(Collation.of("fr")), new Update().set("foo", "bar"),
AutogenerateableId.class);
ArgumentCaptor<UpdateOptions> options = ArgumentCaptor.forClass(UpdateOptions.class);
verify(collection).updateOne(Mockito.any(), Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Test // DATAMONGO-1518
public void updateManyShouldUseCollationWhenPresent() {
template.updateMulti(new BasicQuery("{}").collation(Collation.of("fr")), new Update().set("foo", "bar"),
AutogenerateableId.class);
ArgumentCaptor<UpdateOptions> options = ArgumentCaptor.forClass(UpdateOptions.class);
verify(collection).updateMany(Mockito.any(), Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Test // DATAMONGO-1518
public void replaceOneShouldUseCollationWhenPresent() {
template.updateFirst(new BasicQuery("{}").collation(Collation.of("fr")), new Update(), AutogenerateableId.class);
ArgumentCaptor<UpdateOptions> options = ArgumentCaptor.forClass(UpdateOptions.class);
verify(collection).replaceOne(Mockito.any(), Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Test // DATAMONGO-1518
public void aggregateShouldUseCollationWhenPresent() {
Aggregation aggregation = newAggregation(project("id"))
.withOptions(newAggregationOptions().collation(Collation.of("fr")).build());
template.aggregate(aggregation, AutogenerateableId.class, Document.class);
ArgumentCaptor<Document> cmd = ArgumentCaptor.forClass(Document.class);
verify(db).runCommand(cmd.capture(), Mockito.any(Class.class));
assertThat(cmd.getValue().get("collation", Document.class), equalTo(new Document("locale", "fr")));
}
@Test // DATAMONGO-1518
public void mapReduceShouldUseCollationWhenPresent() {
template.mapReduce("", "", "", MapReduceOptions.options().collation(Collation.of("fr")), AutogenerateableId.class);
verify(mapReduceIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
public void geoNearShouldUseCollationWhenPresent() {
NearQuery query = NearQuery.near(0D, 0D).query(new BasicQuery("{}").collation(Collation.of("fr")));
template.geoNear(query, AutogenerateableId.class);
ArgumentCaptor<Document> cmd = ArgumentCaptor.forClass(Document.class);
verify(db).runCommand(cmd.capture(), Mockito.any(Class.class));
assertThat(cmd.getValue().get("collation", Document.class), equalTo(new Document("locale", "fr")));
}
@Test // DATAMONGO-1518
public void groupShouldUseCollationWhenPresent() {
commandResultDocument.append("retval", Collections.emptySet());
template.group("collection-1", GroupBy.key("id").reduceFunction("bar").collation(Collation.of("fr")), AutogenerateableId.class);
ArgumentCaptor<Document> cmd = ArgumentCaptor.forClass(Document.class);
verify(db).runCommand(cmd.capture(), Mockito.any(Class.class));
assertThat(cmd.getValue().get("group", Document.class).get("collation", Document.class), equalTo(new Document("locale", "fr")));
}
class AutogenerateableId {
@Id BigInteger id;

View File

@@ -32,6 +32,7 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.MongoTemplate.QueryCursorPreparer;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.Meta;
import org.springframework.data.mongodb.core.query.Query;
@@ -59,6 +60,7 @@ public class QueryCursorPreparerUnitTests {
when(factory.getExceptionTranslator()).thenReturn(exceptionTranslatorMock);
when(cursor.modifiers(any(Document.class))).thenReturn(cursor);
when(cursor.noCursorTimeout(anyBoolean())).thenReturn(cursor);
when(cursor.collation(any())).thenReturn(cursor);
}
@Test // DATAMONGO-185
@@ -132,7 +134,6 @@ public class QueryCursorPreparerUnitTests {
assertThat(captor.getValue(), equalTo(new Document("$snapshot", true)));
}
@Test // DATAMONGO-1480
public void appliesNoCursorTimeoutCorrectly() {
@@ -143,6 +144,14 @@ public class QueryCursorPreparerUnitTests {
verify(cursor).noCursorTimeout(eq(true));
}
@Test // DATAMONGO-1518
public void appliesCollationCorrectly() {
prepare(new BasicQuery("{}").collation(Collation.of("fr")));
verify(cursor).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
private FindIterable<Document> prepare(Query query) {
CursorPreparer preparer = new MongoTemplate(factory).new QueryCursorPreparer(query, null);

View File

@@ -13,36 +13,60 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.mongodb.core;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.mockito.Mockito.any;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import org.bson.Document;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.reactivestreams.Publisher;
import org.springframework.data.mongodb.core.MongoTemplateUnitTests.AutogenerateableId;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate.NoOpDbRefResolver;
import org.springframework.data.mongodb.core.aggregation.Aggregation;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.BasicQuery;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.test.util.ReflectionTestUtils;
import com.mongodb.client.model.DeleteOptions;
import com.mongodb.client.model.FindOneAndDeleteOptions;
import com.mongodb.client.model.FindOneAndUpdateOptions;
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.reactivestreams.client.FindPublisher;
import com.mongodb.reactivestreams.client.MongoClient;
import com.mongodb.reactivestreams.client.MongoCollection;
import com.mongodb.reactivestreams.client.MongoDatabase;
/**
* Unit tests for {@link ReactiveMongoTemplate}.
*
* @author Mark Paluch
* @author Christoph Strobl
*/
@RunWith(MockitoJUnitRunner.class)
@RunWith(MockitoJUnitRunner.Silent.class)
public class ReactiveMongoTemplateUnitTests {
ReactiveMongoTemplate template;
@Mock SimpleReactiveMongoDatabaseFactory factory;
@Mock MongoClient mongoClient;
@Mock MongoDatabase db;
@Mock MongoCollection collection;
@Mock FindPublisher findPublisher;
@Mock Publisher runCommandPublisher;
MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator();
MappingMongoConverter converter;
@@ -52,10 +76,21 @@ public class ReactiveMongoTemplateUnitTests {
public void setUp() {
when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator);
when(factory.getMongoDatabase()).thenReturn(db);
when(db.getCollection(any())).thenReturn(collection);
when(db.getCollection(any(), any())).thenReturn(collection);
when(db.runCommand(any(), any(Class.class))).thenReturn(runCommandPublisher);
when(collection.find()).thenReturn(findPublisher);
when(collection.find(Mockito.any(Document.class))).thenReturn(findPublisher);
when(findPublisher.projection(any())).thenReturn(findPublisher);
when(findPublisher.limit(anyInt())).thenReturn(findPublisher);
when(findPublisher.collation(any())).thenReturn(findPublisher);
when(findPublisher.first()).thenReturn(findPublisher);
this.mappingContext = new MongoMappingContext();
this.converter = new MappingMongoConverter(new NoOpDbRefResolver(), mappingContext);
this.template = new ReactiveMongoTemplate(factory, converter);
}
@Test(expected = IllegalArgumentException.class) // DATAMONGO-1444
@@ -74,4 +109,153 @@ public class ReactiveMongoTemplateUnitTests {
assertTrue(ReflectionTestUtils.getField(template, "mongoConverter") instanceof MappingMongoConverter);
}
@Test // DATAMONGO-1518
public void findShouldUseCollationWhenPresent() {
template.find(new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class).subscribe();
verify(findPublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
//
@Test // DATAMONGO-1518
public void findOneShouldUseCollationWhenPresent() {
template.findOne(new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class).subscribe();
verify(findPublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
public void existsShouldUseCollationWhenPresent() {
template.exists(new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class).subscribe();
verify(findPublisher).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
public void findAndModfiyShoudUseCollationWhenPresent() {
template.findAndModify(new BasicQuery("{}").collation(Collation.of("fr")), new Update(), AutogenerateableId.class)
.subscribe();
ArgumentCaptor<FindOneAndUpdateOptions> options = ArgumentCaptor.forClass(FindOneAndUpdateOptions.class);
verify(collection).findOneAndUpdate(Mockito.any(), Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Test // DATAMONGO-1518
public void findAndRemoveShouldUseCollationWhenPresent() {
template.findAndRemove(new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class).subscribe();
ArgumentCaptor<FindOneAndDeleteOptions> options = ArgumentCaptor.forClass(FindOneAndDeleteOptions.class);
verify(collection).findOneAndDelete(Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Ignore("see https://jira.mongodb.org/browse/JAVARS-27")
@Test // DATAMONGO-1518
public void findAndRemoveManyShouldUseCollationWhenPresent() {
template.doRemove("collection-1", new BasicQuery("{}").collation(Collation.of("fr")), AutogenerateableId.class)
.subscribe();
ArgumentCaptor<DeleteOptions> options = ArgumentCaptor.forClass(DeleteOptions.class);
// the current mongodb-driver-reactivestreams:1.4.0 driver does not offer deleteMany with options.
// verify(collection).deleteMany(Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Test // DATAMONGO-1518
public void updateOneShouldUseCollationWhenPresent() {
template.updateFirst(new BasicQuery("{}").collation(Collation.of("fr")), new Update().set("foo", "bar"),
AutogenerateableId.class).subscribe();
ArgumentCaptor<UpdateOptions> options = ArgumentCaptor.forClass(UpdateOptions.class);
verify(collection).updateOne(Mockito.any(), Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Test // DATAMONGO-1518
public void updateManyShouldUseCollationWhenPresent() {
template.updateMulti(new BasicQuery("{}").collation(Collation.of("fr")), new Update().set("foo", "bar"),
AutogenerateableId.class).subscribe();
ArgumentCaptor<UpdateOptions> options = ArgumentCaptor.forClass(UpdateOptions.class);
verify(collection).updateMany(Mockito.any(), Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Test // DATAMONGO-1518
public void replaceOneShouldUseCollationWhenPresent() {
template.updateFirst(new BasicQuery("{}").collation(Collation.of("fr")), new Update(), AutogenerateableId.class)
.subscribe();
ArgumentCaptor<UpdateOptions> options = ArgumentCaptor.forClass(UpdateOptions.class);
verify(collection).replaceOne(Mockito.any(), Mockito.any(), options.capture());
assertThat(options.getValue().getCollation().getLocale(), is("fr"));
}
@Ignore("currently no aggregation")
@Test // DATAMONGO-1518
public void aggregateShouldUseCollationWhenPresent() {
Aggregation aggregation = newAggregation(project("id"))
.withOptions(newAggregationOptions().collation(Collation.of("fr")).build());
// template.aggregate(aggregation, AutogenerateableId.class, Document.class).subscribe();
ArgumentCaptor<Document> cmd = ArgumentCaptor.forClass(Document.class);
verify(db).runCommand(cmd.capture(), Mockito.any(Class.class));
assertThat(cmd.getValue().get("collation", Document.class), equalTo(new Document("locale", "fr")));
}
@Ignore("currently no mapReduce")
@Test // DATAMONGO-1518
public void mapReduceShouldUseCollationWhenPresent() {
// template.mapReduce("", "", "", MapReduceOptions.options().collation(Collation.of("fr")),
// AutogenerateableId.class).subscribe();
//
// verify(mapReduceIterable).collation(eq(com.mongodb.client.model.Collation.builder().locale("fr").build()));
}
@Test // DATAMONGO-1518
public void geoNearShouldUseCollationWhenPresent() {
NearQuery query = NearQuery.near(0D, 0D).query(new BasicQuery("{}").collation(Collation.of("fr")));
template.geoNear(query, AutogenerateableId.class).subscribe();
ArgumentCaptor<Document> cmd = ArgumentCaptor.forClass(Document.class);
verify(db).runCommand(cmd.capture(), Mockito.any(Class.class));
assertThat(cmd.getValue().get("collation", Document.class), equalTo(new Document("locale", "fr")));
}
@Ignore("currently no groupBy")
@Test // DATAMONGO-1518
public void groupShouldUseCollationWhenPresent() {
// template.group("collection-1", GroupBy.key("id").reduceFunction("bar").collation(Collation.of("fr")),
// AutogenerateableId.class).subscribe();
//
// ArgumentCaptor<Document> cmd = ArgumentCaptor.forClass(Document.class);
// verify(db).runCommand(cmd.capture(), Mockito.any(Class.class));
//
// assertThat(cmd.getValue().get("group", Document.class).get("collation", Document.class),
// equalTo(new Document("locale", "fr")));
}
}

View File

@@ -19,8 +19,6 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import org.bson.Document;
import org.junit.Before;
import org.junit.Test;
@@ -30,6 +28,7 @@ import org.junit.Test;
*
* @author Thomas Darimont
* @author Mark Paluch
* @author Christoph Strobl
* @since 1.6
*/
public class AggregationOptionsTests {
@@ -49,7 +48,7 @@ public class AggregationOptionsTests {
assertThat(aggregationOptions.isAllowDiskUse(), is(true));
assertThat(aggregationOptions.isExplain(), is(true));
assertThat(aggregationOptions.getCursor(), is(new Document("batchSize", 1)));
assertThat(aggregationOptions.getCursor().get(), is(new Document("batchSize", 1)));
}
@Test // DATAMONGO-1637
@@ -64,7 +63,7 @@ public class AggregationOptionsTests {
assertThat(aggregationOptions.isAllowDiskUse(), is(true));
assertThat(aggregationOptions.isExplain(), is(true));
assertThat(aggregationOptions.getCursor(), is(new Document("batchSize", 1)));
assertThat(aggregationOptions.getCursor().get(), is(new Document("batchSize", 1)));
assertThat(aggregationOptions.getCursorBatchSize(), is(1));
}