From 16a0e0461788802ee014c31a68fe604872ee9c0c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 24 Apr 2017 10:31:38 +0200 Subject: [PATCH 1/4] DATAMONGO-1518 - Add support for collations. Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-cross-store/pom.xml | 4 ++-- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb-log4j/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pom.xml b/pom.xml index eea61f492c..fb54e3fb96 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1518-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index 4a49168713..a56830737e 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1518-SNAPSHOT ../pom.xml @@ -48,7 +48,7 @@ org.springframework.data spring-data-mongodb - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1518-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 750ed23aa8..47a73cef5f 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1518-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml index 50d0a6454a..fb85f2c5f7 100644 --- a/spring-data-mongodb-log4j/pom.xml +++ b/spring-data-mongodb-log4j/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1518-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 886bcca7b6..049523bcd6 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATAMONGO-1518-SNAPSHOT ../pom.xml From e010f9856a2948920d7de449e5a08f845703065b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 25 Apr 2017 13:29:35 +0200 Subject: [PATCH 2/4] DATAMONGO-1518 - Add support for Collation. 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. Things we still need to think about collation support via annotations for the repository layer and QueryByExampleExecuter. --- .../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)); } From 0b555f3c00312897b32783e69b52ac4f26f7c055 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 3 May 2017 11:57:55 +0200 Subject: [PATCH 3/4] DATAMONGO-1518 - Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rename ICULocale to CollationLocale. Introduce interface for ComparisonLevel construction and let ICUComparisonLevel types implement that interface. Make value types immutable where possible. Provide static instances for default comparison level instances. Replace collation conversion IndexConverters with Collation.from(…).toMongoCollation() converter. Introduce missing generic types. Replace Optional.get() with Optional.map(…).orElse(…). --- .../data/mongodb/core/Collation.java | 419 +++++++++++------- .../data/mongodb/core/CollectionOptions.java | 51 ++- .../mongodb/core/FindAndModifyOptions.java | 11 +- .../data/mongodb/core/IndexConverters.java | 36 +- .../data/mongodb/core/MongoTemplate.java | 63 +-- .../mongodb/core/ReactiveMongoTemplate.java | 64 ++- .../data/mongodb/core/index/Index.java | 15 +- .../data/mongodb/core/mapreduce/GroupBy.java | 10 +- .../data/mongodb/core/CollationUnitTests.java | 31 +- ...efaultIndexOperationsIntegrationTests.java | 5 +- .../DefaultReactiveIndexOperationsTests.java | 5 +- .../core/MongoTemplateCollationTests.java | 9 +- .../core/ReactiveMongoTemplateUnitTests.java | 14 - 13 files changed, 376 insertions(+), 357 deletions(-) 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 index 3dae36ee8f..a0b7d9e51d 100644 --- 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 @@ -15,12 +15,18 @@ */ package org.springframework.data.mongodb.core; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + 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 org.springframework.util.StringUtils; import com.mongodb.client.model.Collation.Builder; import com.mongodb.client.model.CollationAlternate; @@ -37,45 +43,36 @@ * query itself specifies the same collation. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.0 * @see MongoDB Reference - Collation */ public class Collation { - private static final Collation DEFAULT = of("simple"); + private static final Collation SIMPLE = of("simple"); - private final ICULocale locale; + private final CollationLocale locale; - private Optional strength = Optional.empty(); + 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) { + private Collation(CollationLocale locale) { Assert.notNull(locale, "ICULocale must not be null!"); this.locale = locale; } /** - * Create new {@link Collation} using simple binary comparison. + * Create a {@link Collation} using {@literal simple} binary comparison. * - * @return - * @see #binary() + * @return a {@link Collation} for {@literal simple} binary comparison. */ public static Collation simple() { - return binary(); - } - - /** - * Create new {@link Collation} using simple binary comparison. - * - * @return - */ - public static Collation binary() { - return DEFAULT; + return SIMPLE; } /** @@ -88,7 +85,16 @@ public static Collation binary() { public static Collation of(Locale locale) { Assert.notNull(locale, "Locale must not be null!"); - return of(ICULocale.of(locale.getLanguage()).variant(locale.getVariant())); + + String format; + + if (StringUtils.hasText(locale.getCountry())) { + format = String.format("%s_%s", locale.getLanguage(), locale.getCountry()); + } else { + format = locale.getLanguage(); + } + + return of(CollationLocale.of(format).variant(locale.getVariant())); } /** @@ -98,16 +104,16 @@ public static Collation of(Locale locale) { * @return */ public static Collation of(String language) { - return of(ICULocale.of(language)); + return of(CollationLocale.of(language)); } /** - * Create new {@link Collation} with locale set to the given {@link ICULocale}. + * Create new {@link Collation} with locale set to the given {@link CollationLocale}. * * @param locale must not be {@literal null}. * @return */ - public static Collation of(ICULocale locale) { + public static Collation of(CollationLocale locale) { return new Collation(locale); } @@ -157,13 +163,13 @@ public static Collation from(Document source) { /** * Set the level of comparison to perform. * - * @param strength must not be {@literal null}. + * @param strength * @return new {@link Collation}. */ - public Collation strength(Integer strength) { + public Collation strength(int strength) { - ICUComparisonLevel current = this.strength.orElseGet(() -> new ICUComparisonLevel(strength, null, null)); - return strength(new ICUComparisonLevel(strength, current.caseFirst.orElse(null), current.caseLevel.orElse(null))); + ComparisonLevel current = this.strength.orElseGet(() -> new ICUComparisonLevel(strength)); + return strength(new ICUComparisonLevel(strength, current.getCaseFirst(), current.getCaseLevel())); } /** @@ -172,23 +178,24 @@ public Collation strength(Integer strength) { * @param comparisonLevel must not be {@literal null}. * @return new {@link Collation} */ - public Collation strength(ICUComparisonLevel comparisonLevel) { + public Collation strength(ComparisonLevel comparisonLevel) { Collation newInstance = copy(); - newInstance.strength = Optional.ofNullable(comparisonLevel); + newInstance.strength = Optional.of(comparisonLevel); return newInstance; } /** - * Set {@code caseLevel} comarison.
+ * Set whether to include {@code caseLevel} comparison.
* - * @param caseLevel must not be {@literal null}. + * @param caseLevel * @return new {@link Collation}. */ - public Collation caseLevel(Boolean caseLevel) { + public Collation caseLevel(boolean caseLevel) { - ICUComparisonLevel strengthValue = strength.orElseGet(() -> ICUComparisonLevel.primary()); - return strength(new ICUComparisonLevel(strengthValue.level, strengthValue.caseFirst.orElse(null), caseLevel)); + ComparisonLevel strengthValue = strength.orElseGet(ComparisonLevel::primary); + return strength( + new ICUComparisonLevel(strengthValue.getLevel(), strengthValue.getCaseFirst(), Optional.of(caseLevel))); } /** @@ -198,7 +205,7 @@ public Collation caseLevel(Boolean caseLevel) { * @return */ public Collation caseFirst(String caseFirst) { - return caseFirst(new ICUCaseFirst(caseFirst)); + return caseFirst(new CaseFirst(caseFirst)); } /** @@ -207,10 +214,10 @@ public Collation caseFirst(String caseFirst) { * @param caseFirst must not be {@literal null}. * @return */ - public Collation caseFirst(ICUCaseFirst sort) { + public Collation caseFirst(CaseFirst sort) { - ICUComparisonLevel strengthValue = strength.orElseGet(() -> ICUComparisonLevel.tertiary()); - return strength(new ICUComparisonLevel(strengthValue.level, sort, strengthValue.caseLevel.orElse(null))); + ComparisonLevel strengthValue = strength.orElseGet(ComparisonLevel::tertiary); + return strength(new ICUComparisonLevel(strengthValue.getLevel(), Optional.of(sort), strengthValue.getCaseLevel())); } /** @@ -236,10 +243,10 @@ public Collation numericOrderingDisabled() { * * @return new {@link Collation}. */ - public Collation numericOrdering(Boolean flag) { + public Collation numericOrdering(boolean flag) { Collation newInstance = copy(); - newInstance.numericOrdering = Optional.ofNullable(flag); + newInstance.numericOrdering = Optional.of(flag); return newInstance; } @@ -252,8 +259,8 @@ public Collation numericOrdering(Boolean flag) { */ public Collation alternate(String alternate) { - Alternate instance = this.alternate.orElseGet(() -> new Alternate(alternate, null)); - return alternate(new Alternate(alternate, instance.maxVariable.orElse(null))); + Alternate instance = this.alternate.orElseGet(() -> new Alternate(alternate, Optional.empty())); + return alternate(new Alternate(alternate, instance.maxVariable)); } /** @@ -340,7 +347,7 @@ public Collation normalization(Boolean normalization) { */ public Collation maxVariable(String maxVariable) { - Alternate alternateValue = alternate.orElseGet(() -> Alternate.shifted()); + Alternate alternateValue = alternate.orElseGet(Alternate::shifted); return alternate(new AlternateWithMaxVariable(alternateValue.alternate, maxVariable)); } @@ -362,6 +369,13 @@ public com.mongodb.client.model.Collation toMongoCollation() { return map(toMongoCollationConverter()); } + /** + * Transform {@code this} {@link Collation} by applying a {@link Converter}. + * + * @param mapper + * @param + * @return + */ public R map(Converter mapper) { return mapper.convert(this); } @@ -387,39 +401,30 @@ private Collation copy() { * * @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); - } + public interface ComparisonLevel { /** * 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)}. + * The {@code caseLevel} can be set via {@link PrimaryICUComparisonLevel#includeCase()} and + * {@link PrimaryICUComparisonLevel#excludeCase()}. * - * @return new {@link ComparisonLevelWithCase}. + * @return new {@link SecondaryICUComparisonLevel}. */ - public static PrimaryICUComparisonLevel primary() { - return new PrimaryICUComparisonLevel(1, null); + static PrimaryICUComparisonLevel primary() { + return PrimaryICUComparisonLevel.DEFAULT; } /** - * Scondary level of comparison. Collation performs comparisons up to secondary differences, such as + * Secondary level of comparison. Collation performs comparisons up to secondary differences, such as * diacritics.
- * The {@code caseLevel} can be set via {@link ComparisonLevelWithCase#caseLevel(Boolean)}. + * The {@code caseLevel} can be set via {@link SecondaryICUComparisonLevel#includeCase()} and + * {@link SecondaryICUComparisonLevel#excludeCase()}. * - * @return new {@link ComparisonLevelWithCase}. + * @return new {@link SecondaryICUComparisonLevel}. */ - public static SecondaryICUComparisonLevel secondary() { - return new SecondaryICUComparisonLevel(2, null); + static SecondaryICUComparisonLevel secondary() { + return SecondaryICUComparisonLevel.DEFAULT; } /** @@ -429,159 +434,231 @@ public static SecondaryICUComparisonLevel secondary() { * * @return new {@link ICUComparisonLevel}. */ - public static TertiaryICUComparisonLevel tertiary() { - return new TertiaryICUComparisonLevel(3, null); + static TertiaryICUComparisonLevel tertiary() { + return TertiaryICUComparisonLevel.DEFAULT; } /** * 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}. + * @return new {@link ComparisonLevel}. */ - public static ICUComparisonLevel quaternary() { - return new ICUComparisonLevel(4, null, null); + static ComparisonLevel quaternary() { + return ComparisonLevels.QUATERNARY; } /** * 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}. + * @return new {@link ComparisonLevel}. */ - public static ICUComparisonLevel identical() { - return new ICUComparisonLevel(5, null, null); + static ComparisonLevel identical() { + return ComparisonLevels.IDENTICAL; + } + + /** + * @return collation strength, {@literal 1} for primary, {@literal 2} for secondary and so on. + */ + int getLevel(); + + default Optional getCaseFirst() { + return Optional.empty(); + } + + default Optional getCaseLevel() { + return Optional.empty(); } } - public static class TertiaryICUComparisonLevel extends ICUComparisonLevel { + /** + * Abstraction for the ICU Comparison Levels. + * + * @since 2.0 + */ + @AllArgsConstructor(access = AccessLevel.PACKAGE) + @Getter + static class ICUComparisonLevel implements ComparisonLevel { + + private final int level; + private final Optional caseFirst; + private final Optional caseLevel; - private TertiaryICUComparisonLevel(Integer level, ICUCaseFirst caseFirst) { - super(level, caseFirst, null); + ICUComparisonLevel(int level) { + this(level, Optional.empty(), Optional.empty()); } + } - /** - * Set the flag that determines sort order of case differences. - * - * @param caseFirstSort must not be {@literal null}. - * @return - */ - public TertiaryICUComparisonLevel caseFirst(ICUCaseFirst caseFirst) { + /** + * Simple comparison levels. + */ + enum ComparisonLevels implements ComparisonLevel { - Assert.notNull(caseFirst, "CaseFirst must not be null!"); - return new TertiaryICUComparisonLevel(level, caseFirst); + QUATERNARY(4), IDENTICAL(5); + + private final int level; + + ComparisonLevels(int level) { + this.level = level; + } + + @Override + public int getLevel() { + return level; } } + /** + * Primary-strength {@link ICUComparisonLevel}. + */ public static class PrimaryICUComparisonLevel extends ICUComparisonLevel { - private PrimaryICUComparisonLevel(Integer level, Boolean caseLevel) { - super(level, null, caseLevel); + static final PrimaryICUComparisonLevel DEFAULT = new PrimaryICUComparisonLevel(); + static final PrimaryICUComparisonLevel WITH_CASE_LEVEL = new PrimaryICUComparisonLevel(true); + static final PrimaryICUComparisonLevel WITHOUT_CASE_LEVEL = new PrimaryICUComparisonLevel(false); + + private PrimaryICUComparisonLevel() { + super(1); + } + + private PrimaryICUComparisonLevel(boolean caseLevel) { + super(1, Optional.empty(), Optional.of(caseLevel)); } /** * Include case comparison. * - * @return new {@link ComparisonLevelWithCase} + * @return new {@link ICUComparisonLevel} */ - public PrimaryICUComparisonLevel includeCase() { - return caseLevel(Boolean.TRUE); + public ComparisonLevel includeCase() { + return WITH_CASE_LEVEL; } /** * Exclude case comparison. * - * @return new {@link ComparisonLevelWithCase} + * @return new {@link ICUComparisonLevel} */ - public PrimaryICUComparisonLevel excludeCase() { - return caseLevel(Boolean.FALSE); - } - - PrimaryICUComparisonLevel caseLevel(Boolean caseLevel) { - return new PrimaryICUComparisonLevel(level, caseLevel); + public ComparisonLevel excludeCase() { + return WITHOUT_CASE_LEVEL; } } + /** + * Secondary-strength {@link ICUComparisonLevel}. + */ public static class SecondaryICUComparisonLevel extends ICUComparisonLevel { - private SecondaryICUComparisonLevel(Integer level, Boolean caseLevel) { - super(level, null, caseLevel); + static final SecondaryICUComparisonLevel DEFAULT = new SecondaryICUComparisonLevel(); + static final SecondaryICUComparisonLevel WITH_CASE_LEVEL = new SecondaryICUComparisonLevel(true); + static final SecondaryICUComparisonLevel WITHOUT_CASE_LEVEL = new SecondaryICUComparisonLevel(false); + + private SecondaryICUComparisonLevel() { + super(2); + } + + private SecondaryICUComparisonLevel(boolean caseLevel) { + super(2, Optional.empty(), Optional.of(caseLevel)); } /** * Include case comparison. * - * @return new {@link ComparisonLevelWithCase} + * @return new {@link SecondaryICUComparisonLevel} */ - public SecondaryICUComparisonLevel includeCase() { - return caseLevel(Boolean.TRUE); + public ComparisonLevel includeCase() { + return WITH_CASE_LEVEL; } /** * Exclude case comparison. * - * @return new {@link ComparisonLevelWithCase} + * @return new {@link SecondaryICUComparisonLevel} */ - public SecondaryICUComparisonLevel excludeCase() { - return caseLevel(Boolean.FALSE); + public ComparisonLevel excludeCase() { + return WITHOUT_CASE_LEVEL; + } + } + + /** + * Tertiary-strength {@link ICUComparisonLevel}. + */ + public static class TertiaryICUComparisonLevel extends ICUComparisonLevel { + + static final TertiaryICUComparisonLevel DEFAULT = new TertiaryICUComparisonLevel(); + + private TertiaryICUComparisonLevel() { + super(3); } - SecondaryICUComparisonLevel caseLevel(Boolean caseLevel) { - return new SecondaryICUComparisonLevel(level, caseLevel); + private TertiaryICUComparisonLevel(CaseFirst caseFirst) { + super(3, Optional.of(caseFirst), Optional.empty()); + } + + /** + * Set the flag that determines sort order of case differences. + * + * @param caseFirst must not be {@literal null}. + * @return new {@link ICUComparisonLevel} + */ + public ComparisonLevel caseFirst(CaseFirst caseFirst) { + + Assert.notNull(caseFirst, "CaseFirst must not be null!"); + return new TertiaryICUComparisonLevel(caseFirst); } } /** * @since 2.0 */ - public static class ICUCaseFirst { + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static class CaseFirst { - private final String state; + private static final CaseFirst UPPER = new CaseFirst("upper"); + private static final CaseFirst LOWER = new CaseFirst("lower"); + private static final CaseFirst OFF = new CaseFirst("off"); - private ICUCaseFirst(String state) { - this.state = state; - } + private final String state; /** * Sort uppercase before lowercase. * - * @return new {@link ICUCaseFirst}. + * @return new {@link CaseFirst}. */ - public static ICUCaseFirst upper() { - return new ICUCaseFirst("upper"); + public static CaseFirst upper() { + return UPPER; } /** * Sort lowercase before uppercase. * - * @return new {@link ICUCaseFirst}. + * @return new {@link CaseFirst}. */ - public static ICUCaseFirst lower() { - return new ICUCaseFirst("lower"); + public static CaseFirst lower() { + return LOWER; } /** * Use the default. * - * @return new {@link ICUCaseFirst}. + * @return new {@link CaseFirst}. */ - public static ICUCaseFirst off() { - return new ICUCaseFirst("off"); + public static CaseFirst off() { + return OFF; } } /** * @since 2.0 */ + @RequiredArgsConstructor(access = AccessLevel.PACKAGE) public static class Alternate { - protected final String alternate; - protected Optional maxVariable; + private static final Alternate NON_IGNORABLE = new Alternate("non-ignorable", Optional.empty()); - private Alternate(String alternate, String maxVariable) { - this.alternate = alternate; - this.maxVariable = Optional.ofNullable(maxVariable); - } + final String alternate; + final Optional maxVariable; /** * Consider Whitespace and punctuation as base characters. @@ -589,18 +666,18 @@ private Alternate(String alternate, String maxVariable) { * @return new {@link Alternate}. */ public static Alternate nonIgnorable() { - return new Alternate("non-ignorable", null); + return NON_IGNORABLE; } /** * Whitespace and punctuation are not considered base characters and are only distinguished at * strength.
- * NOTE: Only works for {@link ICUComparisonLevel} above {@link ICUComparisonLevel#tertiary()}. + * NOTE: Only works for {@link ICUComparisonLevel} above {@link ComparisonLevel#tertiary()}. * * @return new {@link AlternateWithMaxVariable}. */ public static AlternateWithMaxVariable shifted() { - return new AlternateWithMaxVariable("shifted", null); + return AlternateWithMaxVariable.DEFAULT; } } @@ -609,8 +686,16 @@ public static AlternateWithMaxVariable shifted() { */ public static class AlternateWithMaxVariable extends Alternate { + static final AlternateWithMaxVariable DEFAULT = new AlternateWithMaxVariable("shifted"); + static final Alternate SHIFTED_PUNCT = new AlternateWithMaxVariable("shifted", "punct"); + static final Alternate SHIFTED_SPACE = new AlternateWithMaxVariable("shifted", "space"); + + private AlternateWithMaxVariable(String alternate) { + super(alternate, Optional.empty()); + } + private AlternateWithMaxVariable(String alternate, String maxVariable) { - super(alternate, maxVariable); + super(alternate, Optional.of(maxVariable)); } /** @@ -618,8 +703,8 @@ private AlternateWithMaxVariable(String alternate, String maxVariable) { * * @return new {@link AlternateWithMaxVariable}. */ - public AlternateWithMaxVariable punct() { - return new AlternateWithMaxVariable(alternate, "punct"); + public Alternate punct() { + return SHIFTED_PUNCT; } /** @@ -627,10 +712,9 @@ public AlternateWithMaxVariable punct() { * * @return new {@link AlternateWithMaxVariable}. */ - public AlternateWithMaxVariable space() { - return new AlternateWithMaxVariable(alternate, "space"); + public Alternate space() { + return SHIFTED_SPACE; } - } /** @@ -639,38 +723,34 @@ public AlternateWithMaxVariable space() { * @since 2.0 * @see ICU - International Components for Unicode */ - public static class ICULocale { + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static class CollationLocale { 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. + * Create new {@link CollationLocale} for given language. * * @param language must not be {@literal null}. * @return */ - public static ICULocale of(String language) { + public static CollationLocale of(String language) { Assert.notNull(language, "Code must not be null!"); - return new ICULocale(language, null); + return new CollationLocale(language, Optional.empty()); } /** * Define language variant. * * @param variant must not be {@literal null}. - * @return new {@link ICULocale}. + * @return new {@link CollationLocale}. */ - public ICULocale variant(String variant) { + public CollationLocale variant(String variant) { Assert.notNull(variant, "Variant must not be null!"); - return new ICULocale(language, variant); + return new CollationLocale(language, Optional.of(variant)); } /** @@ -681,12 +761,13 @@ public ICULocale variant(String variant) { public String asString() { StringBuilder sb = new StringBuilder(language); - variant.ifPresent(val -> { - if (!val.isEmpty()) { - sb.append("@collation=").append(val); - } + variant.filter(it -> !it.isEmpty()).ifPresent(val -> { + + // Mongo requires variant rendered as ICU keyword (@key=value;key=value…) + sb.append("@collation=").append(val); }); + return sb.toString(); } } @@ -698,24 +779,24 @@ private static Converter toMongoDocumentConverter() { Document document = new Document(); document.append("locale", source.locale.asString()); - source.strength.ifPresent(val -> { + source.strength.ifPresent(strength -> { - document.append("strength", val.level); + document.append("strength", strength.getLevel()); - val.caseLevel.ifPresent(cl -> document.append("caseLevel", cl)); - val.caseFirst.ifPresent(cl -> document.append("caseFirst", cl.state)); + strength.getCaseLevel().ifPresent(it -> document.append("caseLevel", it)); + strength.getCaseFirst().ifPresent(it -> document.append("caseFirst", it.state)); }); source.numericOrdering.ifPresent(val -> document.append("numericOrdering", val)); - source.alternate.ifPresent(val -> { + source.alternate.ifPresent(it -> { - document.append("alternate", val.alternate); - val.maxVariable.ifPresent(maxVariable -> document.append("maxVariable", maxVariable)); + document.append("alternate", it.alternate); + it.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)); + source.backwards.ifPresent(it -> document.append("backwards", it)); + source.normalization.ifPresent(it -> document.append("normalization", it)); + source.version.ifPresent(it -> document.append("version", it)); return document; }; @@ -729,24 +810,24 @@ private static Converter toMongoC builder.locale(source.locale.asString()); - source.strength.ifPresent(val -> { + source.strength.ifPresent(strength -> { - builder.collationStrength(CollationStrength.fromInt(val.level)); + builder.collationStrength(CollationStrength.fromInt(strength.getLevel())); - val.caseLevel.ifPresent(cl -> builder.caseLevel(cl)); - val.caseFirst.ifPresent(cl -> builder.collationCaseFirst(CollationCaseFirst.fromString(cl.state))); + strength.getCaseLevel().ifPresent(builder::caseLevel); + strength.getCaseFirst().ifPresent(it -> builder.collationCaseFirst(CollationCaseFirst.fromString(it.state))); }); - source.numericOrdering.ifPresent(val -> builder.numericOrdering(val)); - source.alternate.ifPresent(val -> { + source.numericOrdering.ifPresent(builder::numericOrdering); + source.alternate.ifPresent(it -> { - builder.collationAlternate(CollationAlternate.fromString(val.alternate)); - val.maxVariable + builder.collationAlternate(CollationAlternate.fromString(it.alternate)); + it.maxVariable .ifPresent(maxVariable -> builder.collationMaxVariable(CollationMaxVariable.fromString(maxVariable))); }); - source.backwards.ifPresent(val -> builder.backwards(val)); - source.normalization.ifPresent(val -> builder.normalization(val)); + source.backwards.ifPresent(builder::backwards); + source.normalization.ifPresent(builder::normalization); 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 e694110f0a..469fc1c947 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 @@ -24,27 +24,33 @@ * * @author Thomas Risberg * @author Christoph Strobl + * @author Mark Paluch */ public class CollectionOptions { private Integer maxDocuments; private Integer size; private Boolean capped; - private Collation collation; + private Optional collation; /** * Constructs a new CollectionOptions instance. - * - * @param size the collection size in bytes, this data space is preallocated + * + * @param size the collection size in bytes, this data space is preallocated. * @param maxDocuments the maximum number of documents in the collection. * @param capped true to created a "capped" collection (fixed size with auto-FIFO behavior based on insertion order), * false otherwise. */ public CollectionOptions(Integer size, Integer maxDocuments, Boolean capped) { + this(size, maxDocuments, capped, Optional.empty()); + } + + private CollectionOptions(Integer size, Integer maxDocuments, Boolean capped, Optional collation) { this.maxDocuments = maxDocuments; this.size = size; this.capped = capped; + this.collation = collation; } private CollectionOptions() {} @@ -66,16 +72,24 @@ public static CollectionOptions just(Collation collation) { } /** - * Create new {@link CollectionOptions} with already given settings and capped set to {@literal true}. + * Create new empty {@link CollectionOptions}. * * @return new {@link CollectionOptions}. * @since 2.0 */ - public CollectionOptions capped() { + public static CollectionOptions empty() { + return new CollectionOptions(); + } - CollectionOptions options = new CollectionOptions(size, maxDocuments, true); - options.setCollation(collation); - return options; + /** + * Create new {@link CollectionOptions} with already given settings and capped set to {@literal true}. + * + * @param size the collection size in bytes, this data space is preallocated. + * @return new {@link CollectionOptions}. + * @since 2.0 + */ + public CollectionOptions capped(int size) { + return new CollectionOptions(size, maxDocuments, true, collation); } /** @@ -86,10 +100,7 @@ public CollectionOptions capped() { * @since 2.0 */ public CollectionOptions maxDocuments(Integer maxDocuments) { - - CollectionOptions options = new CollectionOptions(size, maxDocuments, capped); - options.setCollation(collation); - return options; + return new CollectionOptions(size, maxDocuments, capped, collation); } /** @@ -99,11 +110,8 @@ public CollectionOptions maxDocuments(Integer maxDocuments) { * @return new {@link CollectionOptions}. * @since 2.0 */ - public CollectionOptions size(Integer size) { - - CollectionOptions options = new CollectionOptions(size, maxDocuments, capped); - options.setCollation(collation); - return options; + public CollectionOptions size(int size) { + return new CollectionOptions(size, maxDocuments, capped, collation); } /** @@ -114,10 +122,7 @@ public CollectionOptions size(Integer size) { * @since 2.0 */ public CollectionOptions collation(Collation collation) { - - CollectionOptions options = new CollectionOptions(size, maxDocuments, capped); - options.setCollation(collation); - return options; + return new CollectionOptions(size, maxDocuments, capped, Optional.ofNullable(collation)); } public Integer getMaxDocuments() { @@ -151,7 +156,7 @@ public void setCapped(Boolean capped) { * @since 2.0 */ public void setCollation(Collation collation) { - this.collation = collation; + this.collation = Optional.ofNullable(collation); } /** @@ -161,6 +166,6 @@ public void setCollation(Collation collation) { * @since 2.0 */ public Optional getCollation() { - return Optional.ofNullable(collation); + return collation; } } 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 1142f9491b..6a53f18fda 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 @@ -24,15 +24,15 @@ */ public class FindAndModifyOptions { - boolean returnNew; - boolean upsert; - boolean remove; + private boolean returnNew; + private boolean upsert; + private boolean remove; private Collation collation; /** * Static factory method to create a FindAndModifyOptions instance - * + * * @return a new instance */ public static FindAndModifyOptions options() { @@ -46,9 +46,8 @@ public static FindAndModifyOptions options() { */ public static FindAndModifyOptions of(FindAndModifyOptions source) { - FindAndModifyOptions options = new FindAndModifyOptions(); - if(source == null) { + if (source == null) { return options; } 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 2dbaa3fc62..66ddfdb0e5 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 @@ -25,10 +25,6 @@ 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; /** @@ -129,39 +125,11 @@ public static Collation fromDocument(Document source) { 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 org.springframework.data.mongodb.core.Collation.from(source).toMongoCollation(); } private static Converter getDocumentIndexInfoConverter() { - return ix -> IndexInfo.indexInfoOf(ix); + return IndexInfo::indexInfoOf; } } 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 d039a3286c..7b418aafaf 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,19 +20,8 @@ import static org.springframework.data.util.Optionals.*; import java.io.IOException; -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.*; 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; @@ -60,6 +49,7 @@ import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metric; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -722,7 +712,7 @@ public T findAndModify(Query query, Update update, FindAndModifyOptions opti 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."); + "Both Query and FindAndModifyOptions define a collation. Please provide the collation only via one of the two."); }); query.getCollation().ifPresent(optionsToUse::collation); @@ -885,7 +875,7 @@ private void initializeVersionProperty(Object entity) { Optional> persistentEntity = getPersistentEntity(entity.getClass()); - ifAllPresent(persistentEntity, persistentEntity.flatMap(it -> it.getVersionProperty()), (l, r) -> { + ifAllPresent(persistentEntity, persistentEntity.flatMap(PersistentEntity::getVersionProperty), (l, r) -> { ConvertingPropertyAccessor accessor = new ConvertingPropertyAccessor(l.getPropertyAccessor(entity), mongoConverter.getConversionService()); accessor.setProperty(r, Optional.of(0)); @@ -972,7 +962,7 @@ public void save(Object objectToSave, String collectionName) { Assert.hasText(collectionName, "Collection name must not be null or empty!"); Optional> entity = getPersistentEntity(objectToSave.getClass()); - Optional versionProperty = entity.flatMap(it -> it.getVersionProperty()); + Optional versionProperty = entity.flatMap(PersistentEntity::getVersionProperty); mapIfAllPresent(entity, versionProperty, // (l, r) -> doSaveVersioned(objectToSave, l, collectionName))// @@ -1225,18 +1215,19 @@ public UpdateResult doInCollection(MongoCollection collection) private void increaseVersionForUpdateIfNecessary(Optional> persistentEntity, Update update) { - ifAllPresent(persistentEntity, persistentEntity.flatMap(it -> it.getVersionProperty()), (entity, property) -> { - String versionFieldName = property.getFieldName(); - if (!update.modifies(versionFieldName)) { - update.inc(versionFieldName, 1L); - } - }); + ifAllPresent(persistentEntity, persistentEntity.flatMap(PersistentEntity::getVersionProperty), + (entity, property) -> { + String versionFieldName = property.getFieldName(); + if (!update.modifies(versionFieldName)) { + update.inc(versionFieldName, 1L); + } + }); } private boolean documentContainsVersionProperty(Document document, Optional> persistentEntity) { - return mapIfAllPresent(persistentEntity, persistentEntity.flatMap(it -> it.getVersionProperty()), // + return mapIfAllPresent(persistentEntity, persistentEntity.flatMap(PersistentEntity::getVersionProperty), // (entity, property) -> document.containsKey(property.getFieldName()))// .orElse(false); } @@ -1458,7 +1449,7 @@ public MapReduceResults mapReduce(Query query, String inputCollectionName 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."); + "Both Query and MapReduceOptions define a collation. Please provide the collation only via one of the two."); }); if (mapReduceOptions.getCollation().isPresent()) { @@ -1482,9 +1473,7 @@ public MapReduceResults mapReduce(Query query, String inputCollectionName } } - if (collation.isPresent()) { - result = result.collation(collation.map(Collation::toMongoCollation).get()); - } + result = collation.map(Collation::toMongoCollation).map(result::collation).orElse(result); List mappedResults = new ArrayList(); DocumentCallback callback = new ReadDocumentCallback(mongoConverter, entityClass, inputCollectionName); @@ -2297,7 +2286,7 @@ public Document doInCollection(MongoCollection collection) throws Mong if (LOGGER.isDebugEnabled()) { LOGGER.debug("findOne using query: {} fields: {} in db.collection: {}", serializeToJsonSafely(query), - serializeToJsonSafely(fields.orElseGet(() -> new Document())), collection.getNamespace().getFullName()); + serializeToJsonSafely(fields.orElseGet(Document::new)), collection.getNamespace().getFullName()); } if (fields.isPresent()) { @@ -2336,11 +2325,7 @@ public FindIterable doInCollection(MongoCollection collectio FindIterable iterable = collection.find(query); - if (fields.filter(val -> !val.isEmpty()).isPresent()) { - iterable = iterable.projection(fields.get()); - } - - return iterable; + return fields.filter(val -> !val.isEmpty()).map(iterable::projection).orElse(iterable); } } @@ -2399,7 +2384,7 @@ public Document doInCollection(MongoCollection collection) throws Mong opts.upsert(true); } opts.projection(fields); - if (options.returnNew) { + if (options.isReturnNew()) { opts.returnDocument(ReturnDocument.AFTER); } @@ -2506,16 +2491,16 @@ public FindIterable prepare(FindIterable cursor) { return cursor; } - if (query.getSkip() <= 0 && query.getLimit() <= 0 && query.getSortObject() == null - && !StringUtils.hasText(query.getHint()) && !query.getMeta().hasValues()) { + if (query.getSkip() <= 0 && query.getLimit() <= 0 + && (query.getSortObject() == null || query.getSortObject().isEmpty()) && !StringUtils.hasText(query.getHint()) + && !query.getMeta().hasValues() && !query.getCollation().isPresent()) { return cursor; } - FindIterable cursorToUse = cursor; + FindIterable cursorToUse; + + cursorToUse = query.getCollation().map(Collation::toMongoCollation).map(cursor::collation).orElse(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()); 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 0e21292c5b..c658e428c0 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 @@ -59,6 +59,7 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.Metric; +import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -573,11 +574,10 @@ public Mono exists(final Query query, final Class entityClass, Strin return createFlux(collectionName, collection -> { Document mappedQuery = queryMapper.getMappedObject(query.getQueryObject(), getPersistentEntity(entityClass)); - FindPublisher findPublisher = collection.find(mappedQuery).projection(new Document("_id", 1)); + FindPublisher findPublisher = collection.find(mappedQuery).projection(new Document("_id", 1)); - if (query.getCollation().isPresent()) { - findPublisher = findPublisher.collation(query.getCollation().map(Collation::toMongoCollation).get()); - } + findPublisher = query.getCollation().map(Collation::toMongoCollation).map(findPublisher::collation) + .orElse(findPublisher); return findPublisher.limit(1); }).hasElements(); @@ -616,8 +616,7 @@ 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.flatMap(PersistentEntity::getIdProperty).orElse(null); String idKey = idProperty == null ? ID_FIELD : idProperty.getName(); @@ -712,7 +711,7 @@ public Mono findAndModify(Query query, Update update, FindAndModifyOption 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."); + "Both Query and FindAndModifyOptions define a collation. Please provide the collation only via one of the two."); }); query.getCollation().ifPresent(optionsToUse::collation); @@ -1091,34 +1090,35 @@ private MongoCollection prepareCollection(MongoCollection co return collectionToUse; } - protected Mono saveDocument(final String collectionName, final Document dbDoc, final Class entityClass) { + protected Mono saveDocument(final String collectionName, final Document document, + final Class entityClass) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Saving Document containing fields: " + dbDoc.keySet()); + LOGGER.debug("Saving Document containing fields: " + document.keySet()); } return createMono(collectionName, collection -> { MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.SAVE, collectionName, entityClass, - dbDoc, null); + document, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); Publisher publisher; - if (!dbDoc.containsKey(ID_FIELD)) { + if (!document.containsKey(ID_FIELD)) { if (writeConcernToUse == null) { - publisher = collection.insertOne(dbDoc); + publisher = collection.insertOne(document); } else { - publisher = collection.withWriteConcern(writeConcernToUse).insertOne(dbDoc); + publisher = collection.withWriteConcern(writeConcernToUse).insertOne(document); } } else if (writeConcernToUse == null) { - publisher = collection.replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), dbDoc, + publisher = collection.replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document, new UpdateOptions().upsert(true)); } else { - publisher = collection.withWriteConcern(writeConcernToUse).replaceOne(Filters.eq(ID_FIELD, dbDoc.get(ID_FIELD)), - dbDoc, new UpdateOptions().upsert(true)); + publisher = collection.withWriteConcern(writeConcernToUse) + .replaceOne(Filters.eq(ID_FIELD, document.get(ID_FIELD)), document, new UpdateOptions().upsert(true)); } - return Mono.from(publisher).map(o -> dbDoc.get(ID_FIELD)); + return Mono.from(publisher).map(o -> document.get(ID_FIELD)); }); } @@ -1317,7 +1317,7 @@ private Entry extractIdPropertyAndValue(Object object) { } Optional> entity = mappingContext.getPersistentEntity(objectType); - MongoPersistentProperty idProp = entity.isPresent() ? entity.get().getIdProperty().orElse(null) : null; + MongoPersistentProperty idProp = entity.flatMap(PersistentEntity::getIdProperty).orElse(null); if (idProp == null) { throw new MappingException("No id property found for object of type " + objectType); @@ -1907,8 +1907,9 @@ private MongoPersistentEntity getPersistentEntity(Class type) { } private MongoPersistentProperty getIdPropertyFor(Class type) { + Optional> persistentEntity = mappingContext.getPersistentEntity(type); - return persistentEntity.isPresent() ? persistentEntity.get().getIdProperty().orElse(null) : null; + return persistentEntity.flatMap(PersistentEntity::getIdProperty).orElse(null); } private String determineEntityCollectionName(T obj) { @@ -2011,21 +2012,19 @@ private static class FindOneCallback implements ReactiveCollectionCallback doInCollection(MongoCollection collection) throws MongoException, DataAccessException { - FindPublisher publisher = collection.find(query); + FindPublisher publisher = 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()); + serializeToJsonSafely(fields.orElseGet(Document::new)), collection.getNamespace().getFullName()); } if (fields.isPresent()) { publisher = publisher.projection(fields.get()); } - if (collation.isPresent()) { - publisher = publisher.collation(collation.map(Collation::toMongoCollation).get()); - } + publisher = collation.map(Collation::toMongoCollation).map(publisher::collation).orElse(publisher); return publisher.limit(1).first(); } @@ -2129,10 +2128,8 @@ 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()); - } + findOneAndDeleteOptions = options.getCollation().map(Collation::toMongoCollation) + .map(findOneAndDeleteOptions::collation).orElse(findOneAndDeleteOptions); return collection.findOneAndDelete(query, findOneAndDeleteOptions); } @@ -2154,9 +2151,7 @@ private FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOp result = result.returnDocument(ReturnDocument.BEFORE); } - if (options.getCollation().isPresent()) { - result = result.collation(options.getCollation().map(Collation::toMongoCollation).get()); - } + result = options.getCollation().map(Collation::toMongoCollation).map(result::collation).orElse(result); return result; } @@ -2293,11 +2288,10 @@ public FindPublisher prepare(FindPublisher findPublisher) { return findPublisher; } - FindPublisher findPublisherToUse = findPublisher; + FindPublisher findPublisherToUse; - if (query.getCollation().isPresent()) { - findPublisherToUse = findPublisherToUse.collation(query.getCollation().map(Collation::toMongoCollation).get()); - } + findPublisherToUse = query.getCollation().map(Collation::toMongoCollation).map(findPublisher::collation) + .orElse(findPublisher); if (query.getSkip() <= 0 && query.getLimit() <= 0 && query.getSortObject() == null && !StringUtils.hasText(query.getHint()) && !query.getMeta().hasValues()) { 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 5f7e985ccf..7805c0f18d 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 @@ -30,6 +30,7 @@ /** * @author Oliver Gierke * @author Christoph Strobl + * @author Mark Paluch */ @SuppressWarnings("deprecation") public class Index implements IndexDefinition { @@ -39,21 +40,13 @@ public enum Duplicates { } private final Map fieldSpec = new LinkedHashMap(); - private String name; - private boolean unique = false; - private boolean dropDuplicates = false; - private boolean sparse = false; - private boolean background = false; - private long expire = -1; - private Optional filter = Optional.empty(); - private Optional collation = Optional.empty(); public Index() {} @@ -98,7 +91,7 @@ public Index sparse() { /** * Build the index in background (non blocking). - * + * * @return * @since 1.5 */ @@ -110,7 +103,7 @@ public Index background() { /** * Specifies TTL in seconds. - * + * * @param value * @return * @since 1.5 @@ -121,7 +114,7 @@ public Index expire(long value) { /** * Specifies TTL with given {@link TimeUnit}. - * + * * @param value * @param unit * @return 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 63a0457c63..3bc139f9f8 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 @@ -24,7 +24,7 @@ * Collects the parameters required to perform a group operation on a collection. The query condition and the input * collection are specified on the group method as method arguments to be consistent with other operations, e.g. * map-reduce. - * + * * @author Mark Pollack * @author Christoph Strobl */ @@ -33,7 +33,7 @@ public class GroupBy { private Document initialDocument; private String reduce; - private Optional dboKeys = Optional.empty(); + private Optional keys = Optional.empty(); private Optional keyFunction = Optional.empty(); private Optional initial = Optional.empty(); private Optional finalize = Optional.empty(); @@ -46,7 +46,7 @@ public GroupBy(String... keys) { document.put(key, 1); } - dboKeys = Optional.of(document); + this.keys = Optional.of(document); } // NOTE GroupByCommand does not handle keyfunction. @@ -58,7 +58,7 @@ public GroupBy(String key, boolean isKeyFunction) { keyFunction = Optional.ofNullable(key); } else { document.put(key, 1); - dboKeys = Optional.of(document); + keys = Optional.of(document); } } @@ -152,7 +152,7 @@ public Document getGroupByObject() { Document document = new Document(); - dboKeys.ifPresent(val -> document.append("key", val)); + keys.ifPresent(val -> document.append("key", val)); keyFunction.ifPresent(val -> document.append("$keyf", val)); document.put("$reduce", reduce); 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 index bf3c63061a..9334092788 100644 --- 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 @@ -17,15 +17,18 @@ import static org.assertj.core.api.Assertions.*; +import java.util.Locale; + 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; +import org.springframework.data.mongodb.core.Collation.CaseFirst; +import org.springframework.data.mongodb.core.Collation.CollationLocale; +import org.springframework.data.mongodb.core.Collation.ComparisonLevel; /** * @author Christoph Strobl + * @author Mark Paluch */ public class CollationUnitTests { @@ -59,7 +62,8 @@ public void justLocaleFromDocument() { @Test // DATAMONGO-1518 public void localeWithVariant() { - assertThat(Collation.of(ICULocale.of("de_AT").variant("phonebook")).toDocument()).isEqualTo(LOCALE_WITH_VARIANT); + assertThat(Collation.of(CollationLocale.of("de_AT").variant("phonebook")).toDocument()) + .isEqualTo(LOCALE_WITH_VARIANT); } @Test // DATAMONGO-1518 @@ -68,14 +72,15 @@ public void localeWithVariantFromDocument() { } @Test // DATAMONGO-1518 - public void lcaleFromJavaUtilLocale() { - assertThat(Collation.of(java.util.Locale.US).toDocument()).isEqualTo(new Document().append("locale", "en")); + public void localeFromJavaUtilLocale() { + + assertThat(Collation.of(java.util.Locale.US).toDocument()).isEqualTo(new Document().append("locale", "en_US")); + assertThat(Collation.of(Locale.ENGLISH).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); + assertThat(Collation.of("en_US").strength(ComparisonLevel.primary()).toDocument()).isEqualTo(WITH_STRENGTH_PRIMARY); } @Test // DATAMONGO-1518 @@ -86,7 +91,7 @@ public void withStrenghPrimaryFromDocument() { @Test // DATAMONGO-1518 public void withStrenghPrimaryAndIncludeCase() { - assertThat(Collation.of("en_US").strength(ICUComparisonLevel.primary().includeCase()).toDocument()) + assertThat(Collation.of("en_US").strength(ComparisonLevel.primary().includeCase()).toDocument()) .isEqualTo(WITH_STRENGTH_PRIMARY_INCLUDE_CASE); } @@ -129,7 +134,7 @@ public void withNumericOrderingFromDocument() { @Test // DATAMONGO-1518 public void withCaseFirst() { - assertThat(Collation.of("en_US").caseFirst(ICUCaseFirst.upper()).toDocument()).isEqualTo(WITH_CASE_FIRST_UPPER); + assertThat(Collation.of("en_US").caseFirst(CaseFirst.upper()).toDocument()).isEqualTo(WITH_CASE_FIRST_UPPER); } @Test // DATAMONGO-1518 @@ -164,8 +169,8 @@ public void withAlternateAndMaxVariableFromDocument() { @Test // DATAMONGO-1518 public void allTheThings() { - assertThat(Collation.of(ICULocale.of("de_AT").variant("phonebook")) - .strength(ICUComparisonLevel.primary().includeCase()).normalizationEnabled().backwardDiacriticSort() + assertThat(Collation.of(CollationLocale.of("de_AT").variant("phonebook")) + .strength(ComparisonLevel.primary().includeCase()).normalizationEnabled().backwardDiacriticSort() .numericOrderingEnabled().alternate(Alternate.shifted().punct()).toDocument()).isEqualTo(ALL_THE_THINGS); } @@ -176,7 +181,7 @@ public void allTheThingsFromDocument() { @Test // DATAMONGO-1518 public void justTheDefault() { - assertThat(Collation.binary().toDocument()).isEqualTo(BINARY_COMPARISON); + assertThat(Collation.simple().toDocument()).isEqualTo(BINARY_COMPARISON); } } 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 a13a0eecba..86f923c2fc 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 @@ -27,7 +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.Collation.CaseFirst; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.index.Index; import org.springframework.data.mongodb.core.index.IndexDefinition; @@ -45,6 +45,7 @@ * * @author Christoph Strobl * @author Oliver Gierke + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:infrastructure.xml") @@ -154,7 +155,7 @@ 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())); + .collation(Collation.of("de_AT").caseFirst(CaseFirst.off())); new DefaultIndexOperations(template.getMongoDbFactory(), this.template.getCollectionName(DefaultIndexOperationsIntegrationTestsSample.class), 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 index b2e0c90039..91fb175886 100644 --- 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 @@ -29,7 +29,7 @@ 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.Collation.CaseFirst; import org.springframework.data.mongodb.core.DefaultIndexOperationsIntegrationTests.DefaultIndexOperationsIntegrationTestsSample; import org.springframework.data.mongodb.core.index.Index; import org.springframework.data.mongodb.core.index.IndexDefinition; @@ -43,6 +43,7 @@ /** * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @@ -95,7 +96,7 @@ 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())); + .collation(Collation.of("de_AT").caseFirst(CaseFirst.off())); indexOps.ensureIndex(id).subscribe(); 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 index 04d47b9838..0bbfedfca1 100644 --- 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 @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import java.util.List; +import java.util.Locale; import org.bson.Document; import org.junit.Before; @@ -28,8 +29,7 @@ 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.core.Collation.ComparisonLevel; import org.springframework.data.mongodb.test.util.MongoVersionRule; import org.springframework.data.util.Version; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -38,6 +38,7 @@ /** * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) public class MongoTemplateCollationTests { @@ -79,7 +80,7 @@ public void createCollectionWithCollation() { public void createCollectionWithCollationHavingLocaleVariant() { template.createCollection(COLLECTION_NAME, - CollectionOptions.just(Collation.of(ICULocale.of("de_AT").variant("phonebook")))); + CollectionOptions.just(Collation.of(new Locale("de", "AT", "phonebook")))); Document collation = getCollationInfo(COLLECTION_NAME); assertThat(collation.get("locale")).isEqualTo("de_AT@collation=phonebook"); @@ -89,7 +90,7 @@ public void createCollectionWithCollationHavingLocaleVariant() { public void createCollectionWithCollationHavingStrength() { template.createCollection(COLLECTION_NAME, - CollectionOptions.just(Collation.of("en_US").strength(ICUComparisonLevel.primary().includeCase()))); + CollectionOptions.just(Collation.of("en_US").strength(ComparisonLevel.primary().includeCase()))); Document collation = getCollationInfo(COLLECTION_NAME); assertThat(collation.get("strength")).isEqualTo(1); 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 f226df4d91..3bdb0e0600 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 @@ -244,18 +244,4 @@ public void geoNearShouldUseCollationWhenPresent() { 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"))); - } - } From 75d9d0d32dd815c2949a9b96bc643702d269b9d9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 3 May 2017 13:32:21 +0200 Subject: [PATCH 4/4] DATAMONGO-1518 - Documentation. --- src/main/asciidoc/new-features.adoc | 1 + src/main/asciidoc/reference/mongodb.adoc | 83 ++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 3d9aba7476..905d4a2fe9 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,6 +7,7 @@ * Usage of the `Document` API instead of `DBObject`. * <>. * Support for aggregation result streaming via Java 8 `Stream`. +* Integration of collations for collection and index creation and query operations. [[new-features.1-10-0]] == What's new in Spring Data MongoDB 1.10 diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 415c9e61c6..8cb9813e3b 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1344,6 +1344,85 @@ TextQuery.searching(new TextCriteria().phrase("coffee cake")); The flags for `$caseSensitive` and `$diacriticSensitive` can be set via the according methods on `TextCriteria`. Please note that these two optional flags have been introduced in MongoDB 3.2 and will not be included in the query unless explicitly set. +[[mongo.collation]] +=== Collations + +MongoDB supports since 3.4 collations for collection and index creation and various query operations. Collations define string comparison rules based on the http://userguide.icu-project.org/collation/concepts[ICU collations]. A collation document consists of various properties that are encapsulated in `Collation`: + +==== +[source,java] +---- +Collation collation = Collation.of("fr") <1> + + .strength(ComparisonLevel.secondary() <2> + .includeCase()) + + .numericOrderingEnabled() <3> + + .alternate(Alternate.shifted().punct()) <4> + + .forwardDiacriticSort() <5> + + .normalizationEnabled(); <6> +---- +<1> `Collation` requires a locale for creation. This can be either a string representation of the locale, a `Locale` (considering language, country and variant) or a `CollationLocale`. The locale is mandatory for creation. +<2> Collation strength defines comparison levels denoting differences between characters. You can configure various options (case-sensitivity, case-ordering) depending on the selected strength. +<3> Specify whether to compare numeric strings as numbers or as strings. +<4> Specify whether the collation should consider whitespace and punctuation as base characters for purposes of comparison. +<5> Specify whether strings with diacritics sort from back of the string, such as with some French dictionary ordering. +<6> Specify whether to check if text requires normalization and to perform normalization. +==== + +Collations can be used to create collections and indexes. If you create a collection specifying a collation, the collation is applied to index creation and queries unless you specify a different collation. A collation is valid for a whole operation and cannot be specified on a per-field basis. + +[source,java] +---- +Collation french = Collation.of("fr"); +Collation german = Collation.of("de"); + +template.createCollection(Person.class, CollectionOptions.just(collation)); + +template.indexOps(Person.class).ensureIndex(new Index("name", Direction.ASC).collation(german)); +---- + +NOTE: MongoDB uses simple binary comparison if no collation is specified (`Collation.simple()`). + +Using collations with collection operations is a matter of specifying a `Collation` instance in your query or operation options. + +.Using collation with `find` +==== +[source,java] +---- +Collation collation = Collation.of("de"); + +Query query = new Query(Criteria.where("firstName").is("Amél")).collation(collation); + +List results = template.find(query, Person.class); +---- +==== + +.Using collation with `aggregate` +==== +[source,java] +---- +Collation collation = Collation.of("de"); + +AggregationOptions options = new AggregationOptions.Builder().collation(collation).build(); + +Aggregation aggregation = newAggregation( + project("tags"), + unwind("tags"), + group("tags") + .count().as("count") +).withOptions(options); + +AggregationResults results = template.aggregate(aggregation, "tags", TagCount.class); +---- +==== + +WARNING: Indexes are only used if the collation used for the operation and the index collation matches. + + include::../{spring-data-commons-docs}/query-by-example.adoc[leveloffset=+1] include::query-by-example.adoc[leveloffset=+1] @@ -2241,6 +2320,8 @@ You can create standard, geospatial and text indexes using the classes `IndexDef mongoTemplate.indexOps(Venue.class).ensureIndex(new GeospatialIndex("location")); ---- +NOTE: `Index` and `GeospatialIndex` support configuration of <>. + [[mongo-template.index-and-collections.access]] === Accessing index information @@ -2281,6 +2362,8 @@ mongoTemplate.dropCollection("MyNewCollection"); * *dropCollection* Drop the collection * *getCollection* Get a collection by name, creating it if it doesn't exist. +NOTE: Collection creation allows customization via `CollectionOptions` and supports <>. + [[mongo-template.commands]] == Executing Commands