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