From 42672a6df99896096aa77ef1162763007436245e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 3 May 2017 11:57:55 +0200 Subject: [PATCH] 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(…). Update reference documentation. Original pull request: #459. --- .../data/mongodb/core/Collation.java | 479 ++++++++++-------- .../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 - src/main/asciidoc/new-features.adoc | 1 + src/main/asciidoc/reference/mongodb.adoc | 83 +++ 15 files changed, 490 insertions(+), 387 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 3dae36ee8..a0b7d9e51 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 @@ import com.mongodb.client.model.CollationStrength; * 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 class Collation { 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 class Collation { * @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 class Collation { /** * 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 class Collation { * @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 class Collation { * @return */ public Collation caseFirst(String caseFirst) { - return caseFirst(new ICUCaseFirst(caseFirst)); + return caseFirst(new CaseFirst(caseFirst)); } /** @@ -207,10 +214,10 @@ public class Collation { * @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 class Collation { * * @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 class Collation { */ 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 class Collation { */ 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 class Collation { 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 @@ public class Collation { * * @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 class Collation { * * @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(); } } + /** + * 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; + + ICUComparisonLevel(int level) { + this(level, Optional.empty(), Optional.empty()); + } + } + + /** + * Simple comparison levels. + */ + enum ComparisonLevels implements ComparisonLevel { + + 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 { + + 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 ICUComparisonLevel} + */ + public ComparisonLevel includeCase() { + return WITH_CASE_LEVEL; + } + + /** + * Exclude case comparison. + * + * @return new {@link ICUComparisonLevel} + */ + public ComparisonLevel excludeCase() { + return WITHOUT_CASE_LEVEL; + } + } + + /** + * Secondary-strength {@link ICUComparisonLevel}. + */ + public static class SecondaryICUComparisonLevel extends ICUComparisonLevel { + + 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 SecondaryICUComparisonLevel} + */ + public ComparisonLevel includeCase() { + return WITH_CASE_LEVEL; + } + + /** + * Exclude case comparison. + * + * @return new {@link SecondaryICUComparisonLevel} + */ + public ComparisonLevel excludeCase() { + return WITHOUT_CASE_LEVEL; + } + } + + /** + * Tertiary-strength {@link ICUComparisonLevel}. + */ public static class TertiaryICUComparisonLevel extends ICUComparisonLevel { - private TertiaryICUComparisonLevel(Integer level, ICUCaseFirst caseFirst) { - super(level, caseFirst, null); + static final TertiaryICUComparisonLevel DEFAULT = new TertiaryICUComparisonLevel(); + + private TertiaryICUComparisonLevel() { + super(3); + } + + private TertiaryICUComparisonLevel(CaseFirst caseFirst) { + super(3, Optional.of(caseFirst), Optional.empty()); } /** * Set the flag that determines sort order of case differences. * - * @param caseFirstSort must not be {@literal null}. - * @return + * @param caseFirst must not be {@literal null}. + * @return new {@link ICUComparisonLevel} */ - public TertiaryICUComparisonLevel caseFirst(ICUCaseFirst caseFirst) { + public ComparisonLevel caseFirst(CaseFirst 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); + return new TertiaryICUComparisonLevel(caseFirst); } } /** * @since 2.0 */ - public static class ICUCaseFirst { + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static class CaseFirst { + + 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 final String state; - private ICUCaseFirst(String state) { - this.state = 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 @@ public class Collation { * @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 class Collation { */ 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 @@ public class Collation { * * @return new {@link AlternateWithMaxVariable}. */ - public AlternateWithMaxVariable punct() { - return new AlternateWithMaxVariable(alternate, "punct"); + public Alternate punct() { + return SHIFTED_PUNCT; } /** @@ -627,10 +712,9 @@ public class Collation { * * @return new {@link AlternateWithMaxVariable}. */ - public AlternateWithMaxVariable space() { - return new AlternateWithMaxVariable(alternate, "space"); + public Alternate space() { + return SHIFTED_SPACE; } - } /** @@ -639,38 +723,34 @@ public class Collation { * @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 class Collation { 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 @@ public class Collation { 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 @@ public class Collation { 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 e694110f0..469fc1c94 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 @@ import org.springframework.util.Assert; * * @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 class CollectionOptions { } /** - * 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 class CollectionOptions { * @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 class CollectionOptions { * @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 class CollectionOptions { * @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 class CollectionOptions { * @since 2.0 */ public void setCollation(Collation collation) { - this.collation = collation; + this.collation = Optional.ofNullable(collation); } /** @@ -161,6 +166,6 @@ public class CollectionOptions { * @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 1142f9491..6a53f18fd 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 @@ import java.util.Optional; */ 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 class FindAndModifyOptions { */ 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 2dbaa3fc6..66ddfdb0e 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.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; /** @@ -129,39 +125,11 @@ abstract class IndexConverters { 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 d039a3286..7b418aafa 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.mongodb.core.query.SerializationUtils.*; 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.Distance; 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 class MongoTemplate implements MongoOperations, ApplicationContextAware, 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 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, 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 class MongoTemplate implements MongoOperations, ApplicationContextAware, 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 class MongoTemplate implements MongoOperations, ApplicationContextAware, 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 class MongoTemplate implements MongoOperations, ApplicationContextAware, 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 class MongoTemplate implements MongoOperations, ApplicationContextAware, } } - 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 class MongoTemplate implements MongoOperations, ApplicationContextAware, 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 class MongoTemplate implements MongoOperations, ApplicationContextAware, 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 class MongoTemplate implements MongoOperations, ApplicationContextAware, opts.upsert(true); } opts.projection(fields); - if (options.returnNew) { + if (options.isReturnNew()) { opts.returnDocument(ReturnDocument.AFTER); } @@ -2506,16 +2491,16 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, 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 0e21292c5..c658e428c 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.convert.EntityReader; 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 class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati 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 class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati 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 class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati 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 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati 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 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } 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 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } 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 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati public Publisher 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 class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati if (options.isRemove()) { FindOneAndDeleteOptions findOneAndDeleteOptions = convertToFindOneAndDeleteOptions(fields, sort); - if (options.getCollation().isPresent()) { - findOneAndDeleteOptions = findOneAndDeleteOptions - .collation(options.getCollation().map(Collation::toMongoCollation).get()); - } + findOneAndDeleteOptions = options.getCollation().map(Collation::toMongoCollation) + .map(findOneAndDeleteOptions::collation).orElse(findOneAndDeleteOptions); return collection.findOneAndDelete(query, findOneAndDeleteOptions); } @@ -2154,9 +2151,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati 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 class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati 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 5f7e985cc..7805c0f18 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 @@ import org.springframework.util.StringUtils; /** * @author Oliver Gierke * @author Christoph Strobl + * @author Mark Paluch */ @SuppressWarnings("deprecation") public class Index implements IndexDefinition { @@ -39,21 +40,13 @@ public class Index implements IndexDefinition { } 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 class Index implements IndexDefinition { /** * Build the index in background (non blocking). - * + * * @return * @since 1.5 */ @@ -110,7 +103,7 @@ public class Index implements IndexDefinition { /** * Specifies TTL in seconds. - * + * * @param value * @return * @since 1.5 @@ -121,7 +114,7 @@ public class Index implements IndexDefinition { /** * 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 63a0457c6..3bc139f9f 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 @@ 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 * 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 class GroupBy { document.put(key, 1); } - dboKeys = Optional.of(document); + this.keys = Optional.of(document); } // NOTE GroupByCommand does not handle keyfunction. @@ -58,7 +58,7 @@ public class GroupBy { keyFunction = Optional.ofNullable(key); } else { document.put(key, 1); - dboKeys = Optional.of(document); + keys = Optional.of(document); } } @@ -152,7 +152,7 @@ public class GroupBy { 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 bf3c63061..933409278 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 @@ package org.springframework.data.mongodb.core; 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 class CollationUnitTests { @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 class CollationUnitTests { } @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 class CollationUnitTests { @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 class CollationUnitTests { @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 class CollationUnitTests { @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 class CollationUnitTests { @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 a13a0eecb..86f923c2f 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.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Sort.Direction; -import org.springframework.data.mongodb.core.Collation.ICUCaseFirst; +import org.springframework.data.mongodb.core.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 @@ import com.mongodb.client.MongoCollection; * * @author Christoph Strobl * @author Oliver Gierke + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:infrastructure.xml") @@ -154,7 +155,7 @@ public class DefaultIndexOperationsIntegrationTests { 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 b2e0c9003..91fb17588 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.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.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 @@ import com.mongodb.reactivestreams.client.MongoCollection; /** * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration @@ -95,7 +96,7 @@ public class DefaultReactiveIndexOperationsTests { 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 04d47b983..0bbfedfca 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 @@ package org.springframework.data.mongodb.core; 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.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.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 @@ import com.mongodb.MongoClient; /** * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) public class MongoTemplateCollationTests { @@ -79,7 +80,7 @@ public class MongoTemplateCollationTests { 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 class MongoTemplateCollationTests { 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 f226df4d9..3bdb0e060 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 class ReactiveMongoTemplateUnitTests { 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/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 3d9aba747..905d4a2fe 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 415c9e61c..8cb9813e3 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