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 01a252b80..393182f2c 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 @@ -114,6 +114,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; import org.springframework.data.mongodb.core.validation.Validator; +import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.Optionals; @@ -1156,7 +1157,13 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, options.skip((int) query.getSkip()); } if (StringUtils.hasText(query.getHint())) { - options.hint(Document.parse(query.getHint())); + + String hint = query.getHint(); + if(BsonUtils.isJsonDocument(hint)) { + options = options.hint(BsonUtils.parse(hint, mongoDbFactory)); + } else { + options = options.hintString(hint); + } } Document document = queryMapper.getMappedObject(query.getQueryObject(), @@ -3276,7 +3283,14 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } if (StringUtils.hasText(query.getHint())) { - cursorToUse = cursorToUse.hint(Document.parse(query.getHint())); + + String hint = query.getHint(); + + if(BsonUtils.isJsonDocument(hint)) { + cursorToUse = cursorToUse.hint(BsonUtils.parse(hint, mongoDbFactory)); + } else { + cursorToUse = cursorToUse.hintString(hint); + } } if (meta.hasValues()) { 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 257839f4a..600cceffd 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 @@ -21,6 +21,7 @@ import com.mongodb.client.result.InsertOneResult; import lombok.AccessLevel; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import org.springframework.data.mongodb.util.BsonUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -1279,7 +1280,13 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati options.skip((int) query.getSkip()); } if (StringUtils.hasText(query.getHint())) { - options.hint(Document.parse(query.getHint())); + + String hint = query.getHint(); + if(BsonUtils.isJsonDocument(hint)) { + options = options.hint(BsonUtils.parse(hint, mongoDatabaseFactory)); + } else { + options = options.hintString(hint); + } } operations.forType(entityClass).getCollation(query).map(Collation::toMongoCollation) // @@ -3276,7 +3283,14 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } if (StringUtils.hasText(query.getHint())) { - findPublisherToUse = findPublisherToUse.hint(Document.parse(query.getHint())); + + String hint = query.getHint(); + + if(BsonUtils.isJsonDocument(hint)) { + findPublisherToUse = findPublisherToUse.hint(BsonUtils.parse(hint, mongoDatabaseFactory)); + } else { + findPublisherToUse = findPublisherToUse.hintString(hint); + } } if (meta.hasValues()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java index 0de6f0076..6172a4198 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java @@ -141,14 +141,13 @@ public class Query { } /** - * Configures the query to use the given hint when being executed. {@code hint} is parsed as {@link Document}. + * Configures the query to use the given hint when being executed. The {@code hint} can either be an index name or a + * json {@link Document} representation. * * @param hint must not be {@literal null} or empty. * @return * @see Document#parse(String) - * @deprecated since 2.2, use {@link #withHint(Document)} */ - @Deprecated public Query withHint(String hint) { Assert.hasText(hint, "Hint must not be empty or null!"); @@ -312,10 +311,8 @@ public class Query { /** * @return - * @deprecated since 2.2. Return type to be changed to {@link Document}. */ @Nullable - @Deprecated public String getHint() { return hint; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java index 3e6f6887a..6ecd474d1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java @@ -25,11 +25,13 @@ import java.util.stream.StreamSupport; import org.bson.BsonValue; import org.bson.Document; +import org.bson.codecs.DocumentCodec; import org.bson.conversions.Bson; import org.bson.json.JsonParseException; - import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -175,6 +177,50 @@ public class BsonUtils { } } + /** + * Check if a given String looks like {@link Document#parse(String) parsable} json. + * + * @param value can be {@literal null}. + * @return {@literal true} if the given value looks like a json document. + * @since 3.0 + */ + public static boolean isJsonDocument(@Nullable String value) { + return StringUtils.hasText(value) && (value.startsWith("{") && value.endsWith("}")); + } + + /** + * Check if a given String looks like {@link org.bson.BsonArray#parse(String) parsable} json array. + * + * @param value can be {@literal null}. + * @return {@literal true} if the given value looks like a json array. + * @since 3.0 + */ + public static boolean isJsonArray(@Nullable String value) { + return StringUtils.hasText(value) && (value.startsWith("[") && value.endsWith("]")); + } + + /** + * Parse the given {@literal json} to {@link Document} applying transformations as specified by a potentially given + * {@link org.bson.codecs.Codec}. + * + * @param json must not be {@literal null}. + * @param codecRegistryProvider can be {@literal null}. In that case the default {@link DocumentCodec} is used. + * @return never {@literal null}. + * @throws IllegalArgumentException if the required argument is {@literal null}. + * @since 3.0 + */ + public static Document parse(String json, @Nullable CodecRegistryProvider codecRegistryProvider) { + + Assert.notNull(json, "Json must not be null!"); + + if (codecRegistryProvider == null) { + return Document.parse(json); + } + + return Document.parse(json, codecRegistryProvider.getCodecFor(Document.class) + .orElseGet(() -> new DocumentCodec(codecRegistryProvider.getCodecRegistry()))); + } + @Nullable private static String toJson(@Nullable Object value) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index b6a4ac6b4..31ca013cd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -19,6 +19,7 @@ import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.test.util.Assertions.*; +import com.mongodb.MongoClientSettings; import lombok.Data; import java.math.BigInteger; @@ -158,6 +159,7 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { when(findIterable.iterator()).thenReturn(cursor); when(factory.getMongoDatabase()).thenReturn(db); when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator); + when(factory.getCodecRegistry()).thenReturn(MongoClientSettings.getDefaultCodecRegistry()); when(db.getCollection(any(String.class), eq(Document.class))).thenReturn(collection); when(db.runCommand(any(), any(Class.class))).thenReturn(commandResultDocument); when(collection.find(any(org.bson.Document.class), any(Class.class))).thenReturn(findIterable); @@ -955,6 +957,17 @@ public class MongoTemplateUnitTests extends MongoOperationsUnitTests { assertThat(options.getValue().getHint()).isEqualTo(queryHint); } + @Test // DATAMONGO-2365 + public void countShouldApplyQueryHintAsIndexNameIfPresent() { + + template.count(new BasicQuery("{}").withHint("idx-1"), AutogenerateableId.class); + + ArgumentCaptor options = ArgumentCaptor.forClass(CountOptions.class); + verify(collection).countDocuments(any(), options.capture()); + + assertThat(options.getValue().getHintString()).isEqualTo("idx-1"); + } + @Test // DATAMONGO-1733 public void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryCursorPreparerUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryCursorPreparerUnitTests.java index 5404d7c00..c36fbd423 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryCursorPreparerUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryCursorPreparerUnitTests.java @@ -21,6 +21,7 @@ import static org.springframework.data.mongodb.core.query.Query.*; import java.util.concurrent.TimeUnit; +import com.mongodb.MongoClientSettings; import org.bson.Document; import org.junit.Before; import org.junit.Test; @@ -53,6 +54,7 @@ public class QueryCursorPreparerUnitTests { public void setUp() { when(factory.getExceptionTranslator()).thenReturn(exceptionTranslatorMock); + when(factory.getCodecRegistry()).thenReturn(MongoClientSettings.getDefaultCodecRegistry()); when(cursor.batchSize(anyInt())).thenReturn(cursor); when(cursor.comment(anyString())).thenReturn(cursor); when(cursor.maxTime(anyLong(), any())).thenReturn(cursor); @@ -70,6 +72,15 @@ public class QueryCursorPreparerUnitTests { verify(cursor).hint(new Document("age", 1)); } + @Test // DATAMONGO-2365 + public void appliesIndexNameAsHintCorrectly() { + + Query query = query(where("foo").is("bar")).withHint("idx-1"); + prepare(query); + + verify(cursor).hintString("idx-1"); + } + @Test // DATAMONGO-2319 public void appliesDocumentHintsCorrectly() { 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 6bd65c2f1..82ee81265 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 @@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; +import com.mongodb.MongoClientSettings; import lombok.Data; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -128,6 +129,7 @@ public class ReactiveMongoTemplateUnitTests { public void beforeEach() { when(factory.getExceptionTranslator()).thenReturn(exceptionTranslator); + when(factory.getCodecRegistry()).thenReturn(MongoClientSettings.getDefaultCodecRegistry()); when(factory.getMongoDatabase()).thenReturn(db); when(db.getCollection(any())).thenReturn(collection); when(db.getCollection(any(), any())).thenReturn(collection); @@ -434,6 +436,17 @@ public class ReactiveMongoTemplateUnitTests { assertThat(options.getValue().getHint()).isEqualTo(queryHint); } + @Test // DATAMONGO-2365 + public void countShouldApplyQueryHintAsIndexNameIfPresent() { + + template.count(new Query().withHint("idx-1"), Person.class, "star-wars").subscribe(); + + ArgumentCaptor options = ArgumentCaptor.forClass(CountOptions.class); + verify(collection).countDocuments(any(), options.capture()); + + assertThat(options.getValue().getHintString()).isEqualTo("idx-1"); + } + @Test // DATAMONGO-2215 public void updateShouldApplyArrayFilters() {