From 0b169d53414aa3597e523f305890d3112da7e41d Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 25 Apr 2017 13:29:35 +0200 Subject: [PATCH] DATAMONGO-1518 - Add support for collations. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../data/mongodb/core/Collation.java | 754 ++++++++++++++++++ .../data/mongodb/core/CollectionOptions.java | 106 ++- .../mongodb/core/DefaultBulkOperations.java | 23 +- .../mongodb/core/FindAndModifyOptions.java | 57 +- .../data/mongodb/core/IndexConverters.java | 67 +- .../data/mongodb/core/MongoTemplate.java | 189 +++-- .../mongodb/core/ReactiveMongoTemplate.java | 166 ++-- .../core/aggregation/AggregationOptions.java | 91 ++- .../core/aggregation/AggregationUtils.java | 2 +- .../mongodb/core/index/GeospatialIndex.java | 43 +- .../data/mongodb/core/index/Index.java | 36 +- .../data/mongodb/core/index/IndexInfo.java | 28 +- .../data/mongodb/core/mapreduce/GroupBy.java | 109 ++- .../core/mapreduce/MapReduceOptions.java | 83 +- .../data/mongodb/core/query/NearQuery.java | 2 + .../data/mongodb/core/query/Query.java | 40 +- .../data/mongodb/core/CollationUnitTests.java | 182 +++++ .../core/DefaultBulkOperationsUnitTests.java | 117 +++ ...efaultIndexOperationsIntegrationTests.java | 51 +- .../DefaultReactiveIndexOperationsTests.java | 126 +++ .../core/MongoTemplateCollationTests.java | 135 ++++ .../mongodb/core/MongoTemplateUnitTests.java | 174 +++- .../core/QueryCursorPreparerUnitTests.java | 11 +- .../core/ReactiveMongoTemplateUnitTests.java | 188 ++++- .../aggregation/AggregationOptionsTests.java | 7 +- 25 files changed, 2512 insertions(+), 275 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/Collation.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollationUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/Collation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/Collation.java new file mode 100644 index 0000000000..3dae36ee8f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/Collation.java @@ -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.
+ * Allows fluent creation of a collation {@link Document} that can be used for creating collections & indexes as well as + * querying data. + *

+ * NOTE: 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 MongoDB Reference - Collation + */ +public class Collation { + + private static final Collation DEFAULT = of("simple"); + + private final ICULocale locale; + + private Optional strength = Optional.empty(); + private Optional numericOrdering = Optional.empty(); + private Optional alternate = Optional.empty(); + private Optional backwards = Optional.empty(); + private Optional normalization = Optional.empty(); + private Optional 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 MongoDB Reference - + * Collation Document + */ + 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.
+ * + * @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 map(Converter 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 caseFirst; + private final Optional 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.
+ * 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.
+ * 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.
+ * 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.
+ * 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.
+ * 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 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 not considered base characters and are only distinguished at + * strength.
+ * NOTE: 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 ICU - International Components for Unicode + */ + public static class ICULocale { + + private final String language; + private final Optional 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 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 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(); + }; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index 756e2863e4..e694110f0a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -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 CollectionOptions 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 void setCapped(Boolean capped) { 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 getCollation() { + return Optional.ofNullable(collation); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java index d06a4bff2f..fb4411b47e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java @@ -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.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> models = new ArrayList>(); + List> 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 @@ public void setExceptionTranslator(PersistenceExceptionTranslator exceptionTrans /** * 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 @@ public void setWriteConcernResolver(WriteConcernResolver writeConcernResolver) { /** * 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 @@ public BulkOperations remove(Query query) { Assert.notNull(query, "Query must not be null!"); - models.add(new DeleteManyModel(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 @@ private BulkOperations update(Query query, Update update, boolean upsert, boolea UpdateOptions options = new UpdateOptions(); options.upsert(upsert); + query.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation); if (multi) { - models.add(new UpdateManyModel(query.getQueryObject(), update.getUpdateObject(), options)); + models.add(new UpdateManyModel<>(query.getQueryObject(), update.getUpdateObject(), options)); } else { - models.add(new UpdateOneModel(query.getQueryObject(), update.getUpdateObject(), options)); + models.add(new UpdateOneModel<>(query.getQueryObject(), update.getUpdateObject(), options)); } return this; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java index c5e88c7fd9..1142f9491b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java @@ -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 static FindAndModifyOptions options() { 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 FindAndModifyOptions remove(boolean remove) { 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 boolean isRemove() { return remove; } + /** + * Get the {@link Collation} specifying language-specific rules for string comparison. + * + * @return + * @since 2.0 + */ + public Optional getCollation() { + return Optional.ofNullable(collation); + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index 4885464040..2dbaa3fc62 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java @@ -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 DEFINITION_TO_MONGO_INDEX_OPTIONS; private static final Converter DOCUMENT_INDEX_INFO; - private static final Double ONE = Double.valueOf(1); - private static final Double MINUS_ONE = Double.valueOf(-1); - private static final Collection TWO_D_IDENTIFIERS = Arrays.asList("2d", "2dsphere"); - static { DEFINITION_TO_MONGO_INDEX_OPTIONS = getIndexDefinitionIndexOptionsConverter(); @@ -117,18 +111,57 @@ private static Converter getIndexDefinitionIndexO } } - 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 getDocumentIndexInfoConverter() { + public static Collation fromDocument(Document source) { + + 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(); + } - return ix -> { - return IndexInfo.indexInfoOf(ix); - }; + private static Converter getDocumentIndexInfoConverter() { + return ix -> IndexInfo.indexInfoOf(ix); } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 5e36f86581..d039a3286c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -20,8 +20,19 @@ 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.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 CloseableIterator doInCollection(MongoCollection collection) Document mappedFields = queryMapper.getMappedFields(query.getFieldsObject(), persistentEntity); Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), persistentEntity); - FindIterable cursor = collection.find(mappedQuery).projection(mappedFields); - QueryCursorPreparer cursorPreparer = new QueryCursorPreparer(query, entityType); + FindIterable cursor = new QueryCursorPreparer(query, entityType) + .prepare(collection.find(mappedQuery).projection(mappedFields)); - ReadDocumentCallback readCallback = new ReadDocumentCallback(mongoConverter, entityType, collectionName); - - return new CloseableIterableCursorAdapter(cursorPreparer.prepare(cursor), exceptionTranslator, readCallback); + return new CloseableIterableCursorAdapter(cursor, exceptionTranslator, + new ReadDocumentCallback(mongoConverter, entityType, collectionName)); } }); } @@ -563,6 +574,7 @@ public T findOne(Query query, Class entityClass) { } public T findOne(Query query, Class entityClass, String collectionName) { + if (query.getSortObject() == null) { return doFindOne(collectionName, query.getQueryObject(), query.getFieldsObject(), entityClass); } else { @@ -587,7 +599,14 @@ public boolean exists(Query query, Class entityClass, String collectionName) } Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), getPersistentEntity(entityClass)); - return execute(collectionName, new FindCallback(mappedQuery)).iterator().hasNext(); + FindIterable 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 T findAndModify(Query query, Update update, FindAndModifyOptions opti public T findAndModify(Query query, Update update, FindAndModifyOptions options, Class 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 T findAndRemove(Query query, Class entityClass) { public T findAndRemove(Query query, Class 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 UpdateResult doInCollection(MongoCollection collection) 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 UpdateResult doInCollection(MongoCollection collection) 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 @@ protected DeleteResult doRemove(final String collectionName, final Query que final Optional> entity = getPersistentEntity(entityClass); return execute(collectionName, new CollectionCallback() { + public DeleteResult doInCollection(MongoCollection collection) throws MongoException, DataAccessException { @@ -1338,8 +1374,12 @@ public DeleteResult doInCollection(MongoCollection collection) 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 DeleteResult doInCollection(MongoCollection collection) } 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(queryObject, entityClass, collectionName)); @@ -1366,7 +1407,7 @@ public List findAll(Class entityClass) { } public List findAll(Class entityClass, String collectionName) { - return executeFindMultiInternal(new FindCallback(null), null, + return executeFindMultiInternal(new FindCallback(null, null), null, new ReadDocumentCallback(mongoConverter, entityClass, collectionName), collectionName); } @@ -1411,26 +1452,40 @@ public MapReduceResults mapReduce(Query query, String inputCollectionName result = result.filter(queryMapper.getMappedObject(query.getQueryObject(), Optional.empty())); } + Optional 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 mappedResults = new ArrayList(); DocumentCallback callback = new ReadDocumentCallback(mongoConverter, entityClass, inputCollectionName); @@ -1709,7 +1764,11 @@ public CloseableIterator doInCollection(MongoCollection collection) 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(cursor.iterator(), exceptionTranslator, readCallback); @@ -1806,9 +1865,14 @@ public MongoCollection doInDB(MongoDatabase db) throws MongoException, 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 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 @@ protected List doFind(String collectionName, Document query, Document } protected Document convertToDocument(CollectionOptions collectionOptions) { + Document document = new Document(); if (collectionOptions != null) { if (collectionOptions.getCapped() != null) { @@ -1906,6 +1971,8 @@ protected Document convertToDocument(CollectionOptions collectionOptions) { 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 @@ protected Document convertToDocument(CollectionOptions collectionOptions) { * @return the List of converted objects. */ protected T doFindAndRemove(String collectionName, Document query, Document fields, Document sort, - Class entityClass) { + Collation collation, Class entityClass) { EntityReader readerToUse = this.mongoConverter; @@ -1933,7 +2000,8 @@ protected T doFindAndRemove(String collectionName, Document query, Document Optional> 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(readerToUse, entityClass, collectionName), collectionName); } @@ -2210,31 +2278,33 @@ private static List consolidateIdentifiers(List ids, List { private final Document query; - private final Document fields; + private final Optional fields; public FindOneCallback(Document query, Document fields) { this.query = query; - this.fields = fields; + this.fields = Optional.ofNullable(fields); } public Document doInCollection(MongoCollection 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 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 Document doInCollection(MongoCollection collection) throws Mong * * @author Oliver Gierke * @author Thomas Risberg + * @author Christoph Strobl */ private static class FindCallback implements CollectionCallback> { private final Document query; - private final Document fields; + private final Optional 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 doInCollection(MongoCollection collection) throws MongoException, DataAccessException { - if (fields == null || fields.isEmpty()) { - return collection.find(query); - } else { - return collection.find(query).projection(fields); + FindIterable iterable = collection.find(query); + + if (fields.filter(val -> !val.isEmpty()).isPresent()) { + iterable = iterable.projection(fields.get()); } + + return iterable; } } @@ -2281,18 +2355,20 @@ private static class FindAndRemoveCallback implements CollectionCallback 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 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 Document doInCollection(MongoCollection collection) throws Mong 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 FindIterable prepare(FindIterable cursor) { FindIterable 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 FindIterable prepare(FindIterable cursor) { 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); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 4aaec24b69..0e21292c5b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -18,6 +18,10 @@ 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.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.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 Mono executeCommand(String jsonCommand) { * @see org.springframework.data.mongodb.core.ReactiveMongoOperations#executeCommand(org.bson.Document) */ public Mono 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 Mono executeCommand(final Document command, final ReadPreferenc 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 Mono findOne(Query query, Class entityClass) { */ public Mono findOne(Query query, Class 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 Mono exists(final Query query, final Class entityClass, Strin 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 Mono findById(Object id, Class entityClass) { public Mono findById(Object id, Class entityClass, String collectionName) { Optional> 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 Flux> geoNear(NearQuery near, Class entityClass, Stri return Flux.empty(); } return Flux.fromIterable(l); - }).skip(near.getSkip() != null ? near.getSkip() : 0).map(new Function>() { - @Override - public GeoResult apply(Document object) { - return callback.doWith(object); - } - }); + }).skip(near.getSkip() != null ? near.getSkip() : 0).map(callback::doWith); }); } @@ -707,8 +707,18 @@ public Mono findAndModify(Query query, Update update, FindAndModifyOption */ public Mono findAndModify(Query query, Update update, FindAndModifyOptions options, Class 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 Mono findAndRemove(Query query, Class entityClass) { public Mono findAndRemove(Query query, Class 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 @@ private Mono doSaveVersioned(T objectToSave, MongoPersistentEntity ent 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 version = convertingAccessor.getProperty(versionProperty); Optional versionNumber = convertingAccessor.getProperty(versionProperty, Number.class); @@ -977,7 +990,8 @@ private Mono doSaveVersioned(T objectToSave, MongoPersistentEntity ent // Create query for entity with the id and old version Optional 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 @@ protected Mono doUpdate(final String collectionName, final Query q MongoCollection 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 @@ private Query getIdInQueryFor(Collection objects) { private void assertUpdateableIdIfNotSet(Object entity) { - Optional> persistentEntity = mappingContext.getPersistentEntity(entity.getClass()); - Optional idProperty = persistentEntity.isPresent() ? persistentEntity.get().getIdProperty() : Optional.empty(); + Optional> persistentEntity = mappingContext + .getPersistentEntity(entity.getClass()); + Optional idProperty = persistentEntity.isPresent() ? persistentEntity.get().getIdProperty() + : Optional.empty(); if (!idProperty.isPresent()) { return; @@ -1360,8 +1377,8 @@ private void assertUpdateableIdIfNotSet(Object entity) { 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 @@ protected Mono doRemove(final String collectionName, final Que 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(queryObject, entityClass, collectionName))) .next(); } @@ -1530,7 +1554,8 @@ protected Mono> doCreateCollection(final String collec * @param entityClass the parameterized type of the returned list. * @return the {@link List} of converted objects. */ - protected Mono doFindOne(String collectionName, Document query, Document fields, Class entityClass) { + protected Mono doFindOne(String collectionName, Document query, Document fields, Class entityClass, + Collation collation) { Optional> entity = mappingContext.getPersistentEntity(entityClass); Document mappedQuery = queryMapper.getMappedObject(query, entity); @@ -1541,7 +1566,7 @@ protected Mono doFindOne(String collectionName, Document query, Document serializeToJsonSafely(query), mappedFields, entityClass, collectionName)); } - return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields), + return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields, collation), new ReadDocumentCallback(this.mongoConverter, entityClass, collectionName), collectionName); } @@ -1624,11 +1649,12 @@ protected CreateCollectionOptions convertToCreateCollectionOptions(CollectionOpt * * @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 Mono doFindAndRemove(String collectionName, Document query, Document fields, Document sort, - Class entityClass) { + Collation collation, Class 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 @@ protected Mono doFindAndRemove(String collectionName, Document query, Doc Optional> 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(this.mongoConverter, entityClass, collectionName), collectionName); } protected Mono doFindAndModify(String collectionName, Document query, Document fields, Document sort, Class entityClass, Update update, FindAndModifyOptions options) { - FindAndModifyOptions optionsToUse; - if (options == null) { - optionsToUse = new FindAndModifyOptions(); - } else { - optionsToUse = options; - } + FindAndModifyOptions optionsToUse = options != null ? options : new FindAndModifyOptions(); Optional> entity = mappingContext.getPersistentEntity(entityClass); @@ -1976,34 +1998,36 @@ private void initializeVersionProperty(Object entity) { private static class FindOneCallback implements ReactiveCollectionCallback { private final Document query; - private final Document fields; + private final Optional fields; + private final Optional 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 doInCollection(MongoCollection 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 { + LOGGER.debug("findOne using query: {} fields: {} in db.collection: {}", serializeToJsonSafely(query), + serializeToJsonSafely(fields.orElseGet(() -> new Document())), collection.getNamespace().getFullName()); + } - if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("findOne using query: %s fields: %s in db.collection: %s", - serializeToJsonSafely(query), fields, collection.getNamespace().getFullName())); - } + if (fields.isPresent()) { + publisher = publisher.projection(fields.get()); + } - return collection.find(query).projection(fields).limit(1); + if (collation.isPresent()) { + publisher = publisher.collation(collation.map(Collation::toMongoCollation).get()); } + + return publisher.limit(1).first(); } } @@ -2056,12 +2080,14 @@ private static class FindAndRemoveCallback implements ReactiveCollectionCallback private final Document query; private final Document fields; private final Document sort; + private final Optional 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 Publisher doInCollection(MongoCollection collection) 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 Publisher doInCollection(MongoCollection collection) 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 @@ private FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOp 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 FindPublisher prepare(FindPublisher findPublisher) { return findPublisher; } + FindPublisher findPublisherToUse = findPublisher; + + if (query.getCollation().isPresent()) { + findPublisherToUse = findPublisherToUse.collation(query.getCollation().map(Collation::toMongoCollation).get()); + } + if (query.getSkip() <= 0 && query.getLimit() <= 0 && query.getSortObject() == null && !StringUtils.hasText(query.getHint()) && !query.getMeta().hasValues()) { - return findPublisher; + return findPublisherToUse; } - FindPublisher findPublisherToUse = findPublisher; - 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); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java index 954d526451..d146a38cb3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java @@ -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 cursor; + private final Optional 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 AggregationOptions(boolean allowDiskUse, boolean explain, Document cursor * @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 static AggregationOptions fromDocument(Document document) { Assert.notNull(document, "Document must not be null!"); - boolean allowDiskUse = false; - boolean explain = false; - Document cursor = 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); - } + 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; - return new AggregationOptions(allowDiskUse, explain, cursor); + return new AggregationOptions(allowDiskUse, explain, cursor, collation); } /** @@ -127,8 +137,8 @@ public boolean isExplain() { */ 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 Integer getCursorBatchSize() { * * @return */ - public Document getCursor() { + public Optional getCursor() { return cursor; } + /** + * Get collation settings for string comparison. + * + * @return + * @since 2.0 + */ + public Optional getCollation() { + return collation; + } + /** * Returns a new potentially adjusted copy for the given {@code aggregationCommandObject} with the configuration * applied. @@ -162,8 +182,12 @@ Document applyAndReturnPotentiallyChangedCommand(Document command) { 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 Document toDocument() { 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 static class Builder { 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 Builder cursorBatchSize(int batchSize) { 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); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUtils.java index 2962993c42..ebe2021427 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUtils.java @@ -22,7 +22,7 @@ import org.springframework.util.Assert; /** - * Utility methods for aggregation ooperation implementations. + * Utility methods for aggregation operation implementations. * * @author Oliver Gierke */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java index 89851f229c..4c34fae33b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java @@ -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 filter = Optional.empty(); + private Optional collation = Optional.empty(); /** * Creates a new {@link GeospatialIndex} for the given field. @@ -129,7 +133,23 @@ public GeospatialIndex withAdditionalField(String fieldName) { */ 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.
+ * NOTE: 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 Document getIndexOptions() { 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 Document getIndexOptions() { 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 Document getIndexOptions() { 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; } /* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java index e321f00b61..5f7e985ccf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java @@ -18,10 +18,12 @@ 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 enum Duplicates { private long expire = -1; - private IndexFilter filter; + private Optional filter = Optional.empty(); + + private Optional collation = Optional.empty(); public Index() {} @@ -72,7 +76,8 @@ public Index named(String name) { * Reject all documents that contain a duplicate value for the indexed field. * * @return - * @see https://docs.mongodb.org/manual/core/index-unique/ + * @see https://docs.mongodb.org/manual/core/index-unique/ */ public Index unique() { this.unique = true; @@ -83,7 +88,8 @@ public Index unique() { * Skip over any document that is missing the indexed field. * * @return - * @see https://docs.mongodb.org/manual/core/index-sparse/ + * @see https://docs.mongodb.org/manual/core/index-sparse/ */ public Index sparse() { this.sparse = true; @@ -139,7 +145,23 @@ public Index expire(long value, TimeUnit unit) { */ 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.
+ * NOTE: 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 Document getIndexOptions() { 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; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java index f74c694394..6766a14b44 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java @@ -22,6 +22,7 @@ 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 indexFields, String name, boolean unique, boolean sparse, - String language) { + public IndexInfo(List indexFields, String name, boolean unique, boolean sparse, String language) { this.indexFields = Collections.unmodifiableList(indexFields); this.name = name; @@ -106,10 +107,11 @@ public static IndexInfo indexInfoOf(Document sourceDocument) { 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 String getPartialFilterExpression() { return partialFilterExpression; } + /** + * Get collation information. + * + * @return + * @since 2.0 + */ + public Optional 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 int hashCode() { 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 boolean equals(Object obj) { if (!ObjectUtils.nullSafeEquals(partialFilterExpression, other.partialFilterExpression)) { return false; } + + if (!ObjectUtils.nullSafeEquals(collation, collation)) { + return false; + } return true; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java index c6017de6bf..63a0457c63 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/GroupBy.java @@ -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 @@ */ public class GroupBy { - private Document dboKeys; - private String keyFunction; - private String initial; private Document initialDocument; private String reduce; - private String finalize; + + private Optional dboKeys = Optional.empty(); + private Optional keyFunction = Optional.empty(); + private Optional initial = Optional.empty(); + private Optional finalize = Optional.empty(); + private Optional 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); - } - document.put("$reduce", reduce); + 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; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java index 808c0b6b6b..2bc76ccb40 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java @@ -17,8 +17,10 @@ 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 outputDatabase = Optional.empty(); private MapReduceCommand.OutputType outputType = MapReduceCommand.OutputType.REPLACE; - - private String finalizeFunction; - private Map scopeVariables = new HashMap(); - + private Map extraOptions = new HashMap(); private Boolean jsMode; - - private Boolean verbose = true; - + private Boolean verbose = Boolean.TRUE; private Integer limit; - private Map extraOptions = new HashMap(); + private Optional outputSharded = Optional.empty(); + private Optional finalizeFunction = Optional.empty(); + private Optional collation = Optional.empty(); /** * Static factory method to create a MapReduceOptions instance @@ -79,6 +75,7 @@ public MapReduceOptions limit(int limit) { * @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 MapReduceOptions outputCollection(String collectionName) { * @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 MapReduceOptions outputDatabase(String outputDatabase) { * @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 MapReduceOptions outputTypeInline() { * @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 MapReduceOptions outputTypeReduce() { * @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 MapReduceOptions outputTypeReplace() { * @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 MapReduceOptions outputSharded(boolean outputShared) { * @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 MapReduceOptions finalizeFunction(String finalizeFunction) { * @return MapReduceOptions so that methods can be chained in a fluent API style */ public MapReduceOptions scopeVariables(Map scopeVariables) { + this.scopeVariables = scopeVariables; return this; } @@ -184,6 +188,7 @@ public MapReduceOptions scopeVariables(Map scopeVariables) { * @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 MapReduceOptions javaScriptMode(boolean javaScriptMode) { * @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 MapReduceOptions verbose(boolean verbose) { */ @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 Map getExtraOptions() { return extraOptions; } - public String getFinalizeFunction() { + public Optional getFinalizeFunction() { return this.finalizeFunction; } @@ -235,11 +255,11 @@ public String getOutputCollection() { return this.outputCollection; } - public String getOutputDatabase() { + public Optional getOutputDatabase() { return this.outputDatabase; } - public Boolean getOutputSharded() { + public Optional getOutputSharded() { return this.outputSharded; } @@ -260,7 +280,18 @@ public Integer getLimit() { return limit; } + /** + * Get the Collation specifying language-specific rules for string comparison. + * + * @return + * @since 2.0 + */ + public Optional getCollation() { + return collation; + } + public Document getOptionsObject() { + Document cmd = new Document(); if (verbose != null) { @@ -269,9 +300,7 @@ public Document getOptionsObject() { 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 Document getOptionsObject() { 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 @@ protected Document createOutObject() { 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; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java index fae298b584..31184ea3dc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java @@ -404,7 +404,9 @@ public Document toDocument() { Document document = new Document(); if (query != null) { + document.put("query", query.getQueryObject()); + query.getCollation().ifPresent(collation -> document.append("collation", collation.toDocument())); } if (maxDistance != null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java index bf6447a727..87f2427f0d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java @@ -24,6 +24,7 @@ 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.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> restrictedTypes = new HashSet>(); - private final Map criteria = new LinkedHashMap(); + private final Set> restrictedTypes = new HashSet<>(); + private final Map 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 = Optional.empty(); + /** * Static factory method to create a {@link Query} using the provided {@link CriteriaDefinition}. * @@ -192,7 +196,7 @@ public Query with(Sort sort) { * @return the restrictedTypes */ public Set> getRestrictedTypes() { - return restrictedTypes == null ? Collections.> emptySet() : restrictedTypes; + return restrictedTypes == null ? Collections.emptySet() : restrictedTypes; } /** @@ -395,8 +399,31 @@ public void setMeta(Meta meta) { 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 getCollation() { + return collation; + } + protected List getCriteria() { - return new ArrayList(this.criteria.values()); + return new ArrayList<>(this.criteria.values()); } /* @@ -442,8 +469,10 @@ protected boolean querySettingsEquals(Query that) { 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 int hashCode() { result += 31 * skip; result += 31 * limit; result += 31 * nullSafeHashCode(meta); + result += 31 * nullSafeHashCode(collation.orElse(null)); return result; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollationUnitTests.java new file mode 100644 index 0000000000..bf3c63061a --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollationUnitTests.java @@ -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); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java new file mode 100644 index 0000000000..f0d3ea1d1d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java @@ -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>> 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>> 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>> 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 + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java index 001db507bd..a13a0eecba 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java @@ -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.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 @@ 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 void getIndexInfoShouldBeAbleToRead2dsphereIndex() { 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 void shouldApplyPartialFilterCorrectly() { 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 void shouldApplyPartialFilterWithMappedPropertyCorrectly() { 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 void shouldApplyPartialDBOFilterCorrectly() { 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 void shouldFavorExplicitMappingHintViaClass() { 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) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java new file mode 100644 index 0000000000..b2e0c90039 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperationsTests.java @@ -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 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(); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java new file mode 100644 index 0000000000..04d47b9838 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateCollationTests.java @@ -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); + }); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 41266089f7..3600e097c2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -19,6 +19,7 @@ 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.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.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 collection; @Mock MongoCursor cursor; @Mock FindIterable findIterable; + @Mock MapReduceIterable mapReduceIterable; + + Document commandResultDocument = new Document(); MongoExceptionTranslator exceptionTranslator = new MongoExceptionTranslator(); MappingMongoConverter converter; @@ -113,9 +120,16 @@ public void setUp() { 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 void findAllAndRemoveShouldRemoveDocumentsReturedByFindQuery() { 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) idField.get("$in"), @@ -345,7 +359,7 @@ public void aggregateShouldHonorReadPreferenceWhenSet() { .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 void aggregateShouldIgnoreReadPreferenceWhenNotSet() { 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 void onBeforeConvert(BeforeConvertEvent event) { 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 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 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 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 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 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 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 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 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 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; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryCursorPreparerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryCursorPreparerUnitTests.java index 58a3852044..a6138f256f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryCursorPreparerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryCursorPreparerUnitTests.java @@ -32,6 +32,7 @@ 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 void setUp() { 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 void appliesSnapshotCorrectly() { assertThat(captor.getValue(), equalTo(new Document("$snapshot", true))); } - @Test // DATAMONGO-1480 public void appliesNoCursorTimeoutCorrectly() { @@ -143,6 +144,14 @@ public void appliesNoCursorTimeoutCorrectly() { 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 prepare(Query query) { CursorPreparer preparer = new MongoTemplate(factory).new QueryCursorPreparer(query, null); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index 1e14cc6fb5..f226df4d91 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -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 void defaultsConverterToMappingMongoConverter() throws Exception { 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 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 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 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 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 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 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 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 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 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"))); + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationOptionsTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationOptionsTests.java index 4074bab847..6abe96f94b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationOptionsTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationOptionsTests.java @@ -19,8 +19,6 @@ 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 @@ * * @author Thomas Darimont * @author Mark Paluch + * @author Christoph Strobl * @since 1.6 */ public class AggregationOptionsTests { @@ -49,7 +48,7 @@ public void aggregationOptionsBuilderShouldSetOptionsAccordingly() { 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 void shouldInitializeFromDocument() { 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)); }