From 5e377bc931eabe8b0041458eb0b5215bb81a3086 Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin <18335884+vbabanin@users.noreply.github.com> Date: Fri, 10 Jan 2025 01:40:33 +0000 Subject: [PATCH 01/43] Version: bump 5.4.0-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index df2f70c49de..26734755984 100644 --- a/build.gradle +++ b/build.gradle @@ -76,7 +76,7 @@ configure(coreProjects) { apply plugin: 'idea' group = 'org.mongodb' - version = '5.3.0-SNAPSHOT' + version = '5.4.0-SNAPSHOT' repositories { mavenLocal() From d203c44b688e34acb80992bb3c613500d2838f41 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Mon, 13 Jan 2025 17:20:29 +0000 Subject: [PATCH 02/43] Fixing default module name for kotlin-extensions (#1602) --- driver-kotlin-extensions/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver-kotlin-extensions/build.gradle.kts b/driver-kotlin-extensions/build.gradle.kts index 25b437e0fad..e0192fa5abc 100644 --- a/driver-kotlin-extensions/build.gradle.kts +++ b/driver-kotlin-extensions/build.gradle.kts @@ -165,4 +165,4 @@ tasks.javadocJar.configure { // =========================== tasks.sourcesJar { from(project.sourceSets.main.map { it.kotlin }) } -afterEvaluate { tasks.jar { manifest { attributes["Automatic-Module-Name"] = "org.mongodb.driver.kotlin.core" } } } +afterEvaluate { tasks.jar { manifest { attributes["Automatic-Module-Name"] = "org.mongodb.driver.kotlin.extensions" } } } From 8c49e38091773438b07045ebcc674a73c5f98d7b Mon Sep 17 00:00:00 2001 From: joykim1005 <81274936+joykim1005@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:21:50 -0500 Subject: [PATCH 03/43] Add phrase operator for Atlas Search (#1586) Add phrase operator for Atlas Search There are several Atlas Search query operators that are not implemented with first class support in the Java driver. This PR adds the phrase operator to Atlas Search. JAVA-5724 --- .../PhraseConstructibleBsonElement.java | 53 +++++++++++++++ .../model/search/PhraseSearchOperator.java | 51 ++++++++++++++ .../client/model/search/SearchOperator.java | 30 ++++++++ .../AggregatesSearchIntegrationTest.java | 4 +- .../model/search/SearchOperatorTest.java | 68 +++++++++++++++++++ .../scala/model/search/SearchOperator.scala | 21 ++++++ .../mongodb/scala/model/search/package.scala | 8 +++ 7 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 driver-core/src/main/com/mongodb/client/model/search/PhraseConstructibleBsonElement.java create mode 100644 driver-core/src/main/com/mongodb/client/model/search/PhraseSearchOperator.java diff --git a/driver-core/src/main/com/mongodb/client/model/search/PhraseConstructibleBsonElement.java b/driver-core/src/main/com/mongodb/client/model/search/PhraseConstructibleBsonElement.java new file mode 100644 index 00000000000..0f18e2db7a9 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/PhraseConstructibleBsonElement.java @@ -0,0 +1,53 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb.client.model.search; + +import com.mongodb.internal.client.model.AbstractConstructibleBsonElement; + +import org.bson.conversions.Bson; + +import static com.mongodb.assertions.Assertions.notNull; + +final class PhraseConstructibleBsonElement extends AbstractConstructibleBsonElement implements + PhraseSearchOperator { + PhraseConstructibleBsonElement(final String name, final Bson value) { + super(name, value); + } + + private PhraseConstructibleBsonElement(final Bson baseElement, final Bson appendedElementValue) { + super(baseElement, appendedElementValue); + } + + @Override + protected PhraseConstructibleBsonElement newSelf(final Bson baseElement, final Bson appendedElementValue) { + return new PhraseConstructibleBsonElement(baseElement, appendedElementValue); + } + + @Override + public PhraseSearchOperator synonyms(final String name) { + return newWithAppendedValue("synonyms", notNull("name", name)); + } + + @Override + public PhraseSearchOperator slop(final int slop) { + return newWithAppendedValue("slop", slop); + } + + @Override + public PhraseConstructibleBsonElement score(final SearchScore modifier) { + return newWithAppendedValue("score", notNull("modifier", modifier)); + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/PhraseSearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/PhraseSearchOperator.java new file mode 100644 index 00000000000..3ac2abe05ad --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/PhraseSearchOperator.java @@ -0,0 +1,51 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb.client.model.search; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Reason; +import com.mongodb.annotations.Sealed; + +/** + * @see SearchOperator#phrase(SearchPath, String) + * @see SearchOperator#phrase(Iterable, Iterable) + * @since 5.3 + */ + +@Sealed +@Beta(Reason.CLIENT) +public interface PhraseSearchOperator extends SearchOperator { + @Override + PhraseSearchOperator score(SearchScore modifier); + + /** + * Creates a new {@link PhraseSearchOperator} that uses slop. The default value is 0. + * + * @param slop The allowable distance between words in the query phrase. + * @return A new {@link PhraseSearchOperator}. + */ + PhraseSearchOperator slop(int slop); + + /** + * Creates a new {@link PhraseSearchOperator} that uses synonyms. + * + * @param name The name of the synonym mapping. + * @return A new {@link PhraseSearchOperator}. + * + * @mongodb.atlas.manual atlas-search/synonyms/ Synonym mappings + */ + PhraseSearchOperator synonyms(String name); +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java index 9234db91c51..00961bc3c18 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java @@ -292,6 +292,36 @@ static GeoNearSearchOperator near(final Point origin, final Number pivot, final .append("pivot", notNull("pivot", pivot))); } + /** + * Returns a {@link SearchOperator} that performs a search for documents containing an ordered sequence of terms. + * + * @param path The field to be searched. + * @param query The string to search for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/phrase/ phrase operator + */ + static PhraseSearchOperator phrase(final SearchPath path, final String query) { + return phrase(singleton(notNull("path", path)), singleton(notNull("query", query))); + } + + /** + * Returns a {@link SearchOperator} that performs a search for documents containing an ordered sequence of terms. + * + * @param paths The non-empty fields to be searched. + * @param queries The non-empty strings to search for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/phrase/ phrase operator + */ + static PhraseSearchOperator phrase(final Iterable paths, final Iterable queries) { + Iterator pathIterator = notNull("paths", paths).iterator(); + isTrueArgument("paths must not be empty", pathIterator.hasNext()); + Iterator queryIterator = notNull("queries", queries).iterator(); + isTrueArgument("queries must not be empty", queryIterator.hasNext()); + String firstQuery = queryIterator.next(); + return new PhraseConstructibleBsonElement("phrase", new Document("path", combineToBsonValue(pathIterator, false)) + .append("query", queryIterator.hasNext() ? queries : firstQuery)); + } + /** * Creates a {@link SearchOperator} from a {@link Bson} in situations when there is no builder method that better satisfies your needs. * This method cannot be used to validate the syntax. diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java index 29de80dda32..0dd8ab387d2 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java @@ -82,6 +82,7 @@ import static com.mongodb.client.model.search.SearchOperator.exists; import static com.mongodb.client.model.search.SearchOperator.near; import static com.mongodb.client.model.search.SearchOperator.numberRange; +import static com.mongodb.client.model.search.SearchOperator.phrase; import static com.mongodb.client.model.search.SearchOperator.text; import static com.mongodb.client.model.search.SearchOptions.searchOptions; import static com.mongodb.client.model.search.SearchPath.fieldPath; @@ -608,7 +609,8 @@ private static Stream searchAndSearchMetaArgs() { dateRange(fieldPath("fieldName6")) .lte(Instant.ofEpochMilli(1)), near(0, 1.5, fieldPath("fieldName7"), fieldPath("fieldName8")), - near(Instant.ofEpochMilli(1), Duration.ofMillis(3), fieldPath("fieldName9")) + near(Instant.ofEpochMilli(1), Duration.ofMillis(3), fieldPath("fieldName9")), + phrase(fieldPath("fieldName10"), "term6") )) .minimumShouldMatch(1) .mustNot(singleton( diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java index c0ea645fb73..1b34e8cf15c 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java @@ -581,6 +581,74 @@ void near() { ); } + @Test + void phrase() { + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> + // queries must not be empty + SearchOperator.phrase(singleton(fieldPath("fieldName")), emptyList()) + ), + () -> assertThrows(IllegalArgumentException.class, () -> + // paths must not be empty + SearchOperator.phrase(emptyList(), singleton("term")) + ), + () -> assertEquals( + new BsonDocument("phrase", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("query", new BsonString("term")) + ), + SearchOperator.phrase( + fieldPath("fieldName"), + "term") + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("phrase", + new BsonDocument("path", new BsonArray(asList( + fieldPath("fieldName").toBsonValue(), + wildcardPath("wildc*rd").toBsonValue()))) + .append("query", new BsonArray(asList( + new BsonString("term1"), + new BsonString("term2")))) + ), + SearchOperator.phrase( + asList( + fieldPath("fieldName"), + wildcardPath("wildc*rd")), + asList( + "term1", + "term2")) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("phrase", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("query", new BsonString("term")) + .append("synonyms", new BsonString("synonymMappingName")) + ), + SearchOperator.phrase( + singleton(fieldPath("fieldName")), + singleton("term")) + .synonyms("synonymMappingName") + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("phrase", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("query", new BsonString("term")) + .append("synonyms", new BsonString("synonymMappingName")) + .append("slop", new BsonInt32(5)) + ), + SearchOperator.phrase( + singleton(fieldPath("fieldName")), + singleton("term")) + .synonyms("synonymMappingName") + .slop(5) + .toBsonDocument() + ) + ); + } + private static SearchOperator docExamplePredefined() { return SearchOperator.exists( fieldPath("fieldName")); diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala index 90f27092ebc..a72e5b3dbcc 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala @@ -228,6 +228,27 @@ object SearchOperator { def near(origin: Point, pivot: Number, paths: Iterable[_ <: FieldSearchPath]): GeoNearSearchOperator = JSearchOperator.near(origin, pivot, paths.asJava) + /** + * Returns a `SearchOperator` that performs a search for documents containing an ordered sequence of terms. + * + * @param path The field to be searched. + * @param query The string to search for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/phrase/ phrase operator]] + */ + def phrase(path: SearchPath, query: String): PhraseSearchOperator = JSearchOperator.phrase(path, query) + + /** + * Returns a `SearchOperator` that performs a search for documents containing an ordered sequence of terms. + * + * @param paths The non-empty fields to be searched. + * @param queries The non-empty strings to search for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/phrase/ phrase operator]] + */ + def phrase(paths: Iterable[_ <: SearchPath], queries: Iterable[String]): PhraseSearchOperator = + JSearchOperator.phrase(paths.asJava, queries.asJava) + /** * Creates a `SearchOperator` from a `Bson` in situations when there is no builder method that better satisfies your needs. * This method cannot be used to validate the syntax. diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala index 557060324cd..e9b86401471 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala @@ -119,6 +119,14 @@ package object search { @Beta(Array(Reason.CLIENT)) type TextSearchOperator = com.mongodb.client.model.search.TextSearchOperator + /** + * @see `SearchOperator.phrase(String, SearchPath)` + * @see `SearchOperator.phrase(Iterable, Iterable)` + */ + @Sealed + @Beta(Array(Reason.CLIENT)) + type PhraseSearchOperator = com.mongodb.client.model.search.PhraseSearchOperator + /** * @see `SearchOperator.autocomplete(String, FieldSearchPath)` * @see `SearchOperator.autocomplete(Iterable, FieldSearchPath)` From 3cea0abb877841f187c928e4956bbb35490a6038 Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin Date: Wed, 15 Jan 2025 12:46:29 -0800 Subject: [PATCH 04/43] Format Gradle script to comply with static checks. (#1607) JAVA-5751 --- driver-kotlin-extensions/build.gradle.kts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/driver-kotlin-extensions/build.gradle.kts b/driver-kotlin-extensions/build.gradle.kts index e0192fa5abc..76f36ca33b2 100644 --- a/driver-kotlin-extensions/build.gradle.kts +++ b/driver-kotlin-extensions/build.gradle.kts @@ -165,4 +165,6 @@ tasks.javadocJar.configure { // =========================== tasks.sourcesJar { from(project.sourceSets.main.map { it.kotlin }) } -afterEvaluate { tasks.jar { manifest { attributes["Automatic-Module-Name"] = "org.mongodb.driver.kotlin.extensions" } } } +afterEvaluate { + tasks.jar { manifest { attributes["Automatic-Module-Name"] = "org.mongodb.driver.kotlin.extensions" } } +} From 0922a24edc602d93673b240f4c9f1503c0a16c99 Mon Sep 17 00:00:00 2001 From: joykim1005 <81274936+joykim1005@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:54:38 -0500 Subject: [PATCH 05/43] Improve selection criteria for srvMaxHosts (#1590) Improve selection criteria for srvMaxHosts This PR improves the selection criteria for srvMaxHosts. The net result is that it will keep existing nodes, discard deleted nodes, and fill up to srvMaxHosts from a random shuffle of newly discovered nodes from the SRV response. It also updates the prose test to match the spec. JAVA-4400 --- .../connection/DnsMultiServerCluster.java | 37 ++++++++++++++----- .../connection/SrvPollingProseTests.java | 3 +- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/driver-core/src/main/com/mongodb/internal/connection/DnsMultiServerCluster.java b/driver-core/src/main/com/mongodb/internal/connection/DnsMultiServerCluster.java index 0589d0f7d19..51e28ee5c84 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/DnsMultiServerCluster.java +++ b/driver-core/src/main/com/mongodb/internal/connection/DnsMultiServerCluster.java @@ -21,6 +21,7 @@ import com.mongodb.connection.ClusterId; import com.mongodb.connection.ClusterSettings; import com.mongodb.connection.ClusterType; +import com.mongodb.connection.ServerDescription; import com.mongodb.lang.Nullable; import java.util.ArrayList; @@ -28,6 +29,7 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; import static com.mongodb.assertions.Assertions.assertNotNull; @@ -38,7 +40,6 @@ public final class DnsMultiServerCluster extends AbstractMultiServerCluster { private final DnsSrvRecordMonitor dnsSrvRecordMonitor; private volatile MongoException srvResolutionException; - public DnsMultiServerCluster(final ClusterId clusterId, final ClusterSettings settings, final ClusterableServerFactory serverFactory, final DnsSrvRecordMonitorFactory dnsSrvRecordMonitorFactory) { super(clusterId, settings, serverFactory); @@ -57,17 +58,33 @@ public void initialize(final Collection hosts) { } } - private Collection applySrvMaxHosts(final Collection hosts) { - Collection newHosts = hosts; + private Collection applySrvMaxHosts(final Collection latestSrvHosts) { Integer srvMaxHosts = getSettings().getSrvMaxHosts(); - if (srvMaxHosts != null && srvMaxHosts > 0) { - if (srvMaxHosts < hosts.size()) { - List newHostsList = new ArrayList<>(hosts); - Collections.shuffle(newHostsList, ThreadLocalRandom.current()); - newHosts = newHostsList.subList(0, srvMaxHosts); - } + if (srvMaxHosts == null || srvMaxHosts <= 0 || latestSrvHosts.size() <= srvMaxHosts) { + return new ArrayList<>(latestSrvHosts); } - return newHosts; + List activeHosts = getActivePriorHosts(latestSrvHosts); + int numNewHostsToAdd = srvMaxHosts - activeHosts.size(); + activeHosts.addAll(addShuffledHosts(latestSrvHosts, activeHosts, numNewHostsToAdd)); + + return activeHosts; + } + + private List getActivePriorHosts(final Collection latestSrvHosts) { + List priorHosts = DnsMultiServerCluster.this.getCurrentDescription().getServerDescriptions().stream() + .map(ServerDescription::getAddress).collect(Collectors.toList()); + priorHosts.removeIf(host -> !latestSrvHosts.contains(host)); + + return priorHosts; + } + + private List addShuffledHosts(final Collection latestSrvHosts, + final List activePriorHosts, final int numNewHostsToAdd) { + List addedHosts = new ArrayList<>(latestSrvHosts); + addedHosts.removeAll(activePriorHosts); + Collections.shuffle(addedHosts, ThreadLocalRandom.current()); + + return addedHosts.subList(0, numNewHostsToAdd); } @Override diff --git a/driver-core/src/test/unit/com/mongodb/internal/connection/SrvPollingProseTests.java b/driver-core/src/test/unit/com/mongodb/internal/connection/SrvPollingProseTests.java index a6605725cf8..a0f08a82360 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/connection/SrvPollingProseTests.java +++ b/driver-core/src/test/unit/com/mongodb/internal/connection/SrvPollingProseTests.java @@ -160,9 +160,10 @@ public void shouldUseAllRecordsWhenSrvMaxHostsIsGreaterThanOrEqualToNumSrvRecord public void shouldUseSrvMaxHostsWhenSrvMaxHostsIsLessThanNumSrvRecords() { int srvMaxHosts = 2; List updatedHosts = asList(firstHost, thirdHost, fourthHost); - initCluster(updatedHosts, srvMaxHosts); + assertEquals(srvMaxHosts, clusterHostsSet().size()); + assertTrue(updatedHosts.contains(firstHost)); assertTrue(updatedHosts.containsAll(clusterHostsSet())); } From e022dbd705a99db804a0d6a07f9cd85ece7b36c2 Mon Sep 17 00:00:00 2001 From: joykim1005 <81274936+joykim1005@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:56:25 -0500 Subject: [PATCH 06/43] Add regex operator to Atlas Search (#1587) Add regex operator to Atlas Search There are several Atlas Search query operators that are not implemented with first class support in the Java driver. This PR adds the regex operator to Atlas Search. JAVA-5726 --- .../model/search/RegexSearchOperator.java | 33 ++++++++++++ .../SearchConstructibleBsonElement.java | 2 +- .../client/model/search/SearchOperator.java | 30 +++++++++++ .../AggregatesSearchIntegrationTest.java | 4 +- .../model/search/SearchOperatorTest.java | 52 +++++++++++++++++++ .../scala/model/search/SearchOperator.scala | 21 ++++++++ .../mongodb/scala/model/search/package.scala | 8 +++ 7 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/client/model/search/RegexSearchOperator.java diff --git a/driver-core/src/main/com/mongodb/client/model/search/RegexSearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/RegexSearchOperator.java new file mode 100644 index 00000000000..c0286079714 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/RegexSearchOperator.java @@ -0,0 +1,33 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb.client.model.search; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Reason; +import com.mongodb.annotations.Sealed; + +/** + * @see SearchOperator#regex(SearchPath, String) + * @see SearchOperator#regex(Iterable, Iterable) + * @since 5.3 + */ + +@Sealed +@Beta(Reason.CLIENT) +public interface RegexSearchOperator extends SearchOperator { + @Override + RegexSearchOperator score(SearchScore modifier); +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java index 8f0b1e510c5..ee168689df3 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java @@ -31,7 +31,7 @@ final class SearchConstructibleBsonElement extends AbstractConstructibleBsonElement implements MustCompoundSearchOperator, MustNotCompoundSearchOperator, ShouldCompoundSearchOperator, FilterCompoundSearchOperator, ExistsSearchOperator, TextSearchOperator, AutocompleteSearchOperator, - NumberNearSearchOperator, DateNearSearchOperator, GeoNearSearchOperator, + NumberNearSearchOperator, DateNearSearchOperator, GeoNearSearchOperator, RegexSearchOperator, ValueBoostSearchScore, PathBoostSearchScore, ConstantSearchScore, FunctionSearchScore, GaussSearchScoreExpression, PathSearchScoreExpression, FacetSearchCollector, diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java index 00961bc3c18..90cb590d256 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java @@ -322,6 +322,36 @@ static PhraseSearchOperator phrase(final Iterable paths, f .append("query", queryIterator.hasNext() ? queries : firstQuery)); } + /** + * Returns a {@link SearchOperator} that performs a search using a regular expression. + * + * @param path The field to be searched. + * @param query The string to search for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/regex/ regex operator + */ + static RegexSearchOperator regex(final SearchPath path, final String query) { + return regex(singleton(notNull("path", path)), singleton(notNull("query", query))); + } + + /** + * Returns a {@link SearchOperator} that performs a search using a regular expression. + * + * @param paths The non-empty fields to be searched. + * @param queries The non-empty strings to search for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/regex/ regex operator + */ + static RegexSearchOperator regex(final Iterable paths, final Iterable queries) { + Iterator pathIterator = notNull("paths", paths).iterator(); + isTrueArgument("paths must not be empty", pathIterator.hasNext()); + Iterator queryIterator = notNull("queries", queries).iterator(); + isTrueArgument("queries must not be empty", queryIterator.hasNext()); + String firstQuery = queryIterator.next(); + return new SearchConstructibleBsonElement("regex", new Document("path", combineToBsonValue(pathIterator, false)) + .append("query", queryIterator.hasNext() ? queries : firstQuery)); + } + /** * Creates a {@link SearchOperator} from a {@link Bson} in situations when there is no builder method that better satisfies your needs. * This method cannot be used to validate the syntax. diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java index 0dd8ab387d2..790f596784a 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java @@ -82,6 +82,7 @@ import static com.mongodb.client.model.search.SearchOperator.exists; import static com.mongodb.client.model.search.SearchOperator.near; import static com.mongodb.client.model.search.SearchOperator.numberRange; +import static com.mongodb.client.model.search.SearchOperator.regex; import static com.mongodb.client.model.search.SearchOperator.phrase; import static com.mongodb.client.model.search.SearchOperator.text; import static com.mongodb.client.model.search.SearchOptions.searchOptions; @@ -610,7 +611,8 @@ private static Stream searchAndSearchMetaArgs() { .lte(Instant.ofEpochMilli(1)), near(0, 1.5, fieldPath("fieldName7"), fieldPath("fieldName8")), near(Instant.ofEpochMilli(1), Duration.ofMillis(3), fieldPath("fieldName9")), - phrase(fieldPath("fieldName10"), "term6") + phrase(fieldPath("fieldName10"), "term6"), + regex(fieldPath("fieldName11"), "term7") )) .minimumShouldMatch(1) .mustNot(singleton( diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java index 1b34e8cf15c..02dc8d3fa04 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java @@ -649,6 +649,58 @@ void phrase() { ); } + @Test + void regex() { + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> + // queries must not be empty + SearchOperator.regex(singleton(fieldPath("fieldName")), emptyList()) + ), + () -> assertThrows(IllegalArgumentException.class, () -> + // paths must not be empty + SearchOperator.regex(emptyList(), singleton("term")) + ), + () -> assertEquals( + new BsonDocument("regex", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("query", new BsonString("term")) + ), + SearchOperator.regex( + fieldPath("fieldName"), + "term") + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("regex", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("query", new BsonString("term")) + ), + SearchOperator.regex( + singleton(fieldPath("fieldName")), + singleton("term")) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("regex", + new BsonDocument("path", new BsonArray(asList( + fieldPath("fieldName").toBsonValue(), + wildcardPath("wildc*rd").toBsonValue()))) + .append("query", new BsonArray(asList( + new BsonString("term1"), + new BsonString("term2")))) + ), + SearchOperator.regex( + asList( + fieldPath("fieldName"), + wildcardPath("wildc*rd")), + asList( + "term1", + "term2")) + .toBsonDocument() + ) + ); + } + private static SearchOperator docExamplePredefined() { return SearchOperator.exists( fieldPath("fieldName")); diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala index a72e5b3dbcc..b76f707d3ae 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala @@ -249,6 +249,27 @@ object SearchOperator { def phrase(paths: Iterable[_ <: SearchPath], queries: Iterable[String]): PhraseSearchOperator = JSearchOperator.phrase(paths.asJava, queries.asJava) + /** + * Returns a `SearchOperator` that performs a search using a regular expression. + * + * @param path The field to be searched. + * @param query The string to search for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/regex/ regex operator]] + */ + def regex(path: SearchPath, query: String): RegexSearchOperator = JSearchOperator.regex(path, query) + + /** + * Returns a `SearchOperator` that performs a search using a regular expression. + * + * @param paths The non-empty fields to be searched. + * @param queries The non-empty strings to search for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/regex/ regex operator]] + */ + def regex(paths: Iterable[_ <: SearchPath], queries: Iterable[String]): RegexSearchOperator = + JSearchOperator.regex(paths.asJava, queries.asJava) + /** * Creates a `SearchOperator` from a `Bson` in situations when there is no builder method that better satisfies your needs. * This method cannot be used to validate the syntax. diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala index e9b86401471..8d6c1eff8e2 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala @@ -135,6 +135,14 @@ package object search { @Beta(Array(Reason.CLIENT)) type AutocompleteSearchOperator = com.mongodb.client.model.search.AutocompleteSearchOperator + /** + * @see `SearchOperator.regex(String, SearchPath)` + * @see `SearchOperator.regex(Iterable, Iterable)` + */ + @Sealed + @Beta(Array(Reason.CLIENT)) + type RegexSearchOperator = com.mongodb.client.model.search.RegexSearchOperator + /** * A base for a [[NumberRangeSearchOperatorBase]] which allows creating instances of this operator. * This interface is a technicality and does not represent a meaningful element of the full-text search query syntax. From 0d8750821e36b75e0d4123066a95e83a238083d8 Mon Sep 17 00:00:00 2001 From: joykim1005 <81274936+joykim1005@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:42:06 -0500 Subject: [PATCH 07/43] Add queryString operator to Atlas Search (#1588) Add queryString operator to Atlas Search There are several Atlas Search query operators that are not implemented with first class support in the Java driver. This PR adds the queryString operator to Atlas Search. JAVA-5727 --- .../search/QueryStringSearchOperator.java | 31 +++++++++++++++++++ .../SearchConstructibleBsonElement.java | 2 +- .../client/model/search/SearchOperator.java | 17 ++++++++++ .../AggregatesSearchIntegrationTest.java | 4 ++- .../model/search/SearchOperatorTest.java | 24 ++++++++++++++ .../scala/model/search/SearchOperator.scala | 11 +++++++ .../mongodb/scala/model/search/package.scala | 7 +++++ 7 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/client/model/search/QueryStringSearchOperator.java diff --git a/driver-core/src/main/com/mongodb/client/model/search/QueryStringSearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/QueryStringSearchOperator.java new file mode 100644 index 00000000000..eb32ee8c733 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/QueryStringSearchOperator.java @@ -0,0 +1,31 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb.client.model.search; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Reason; +import com.mongodb.annotations.Sealed; + +/** + * @see SearchOperator#queryString(FieldSearchPath, String) + * @since 5.3 + */ +@Sealed +@Beta(Reason.CLIENT) +public interface QueryStringSearchOperator extends SearchOperator { + @Override + QueryStringSearchOperator score(SearchScore modifier); +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java index ee168689df3..c5a7e03a40b 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java @@ -31,7 +31,7 @@ final class SearchConstructibleBsonElement extends AbstractConstructibleBsonElement implements MustCompoundSearchOperator, MustNotCompoundSearchOperator, ShouldCompoundSearchOperator, FilterCompoundSearchOperator, ExistsSearchOperator, TextSearchOperator, AutocompleteSearchOperator, - NumberNearSearchOperator, DateNearSearchOperator, GeoNearSearchOperator, RegexSearchOperator, + NumberNearSearchOperator, DateNearSearchOperator, GeoNearSearchOperator, RegexSearchOperator, QueryStringSearchOperator, ValueBoostSearchScore, PathBoostSearchScore, ConstantSearchScore, FunctionSearchScore, GaussSearchScoreExpression, PathSearchScoreExpression, FacetSearchCollector, diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java index 90cb590d256..0f7f89c732b 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java @@ -292,6 +292,23 @@ static GeoNearSearchOperator near(final Point origin, final Number pivot, final .append("pivot", notNull("pivot", pivot))); } + /** + * Returns a {@link SearchOperator} that supports querying a combination of indexed fields and values. + * + * @param defaultPath The field to be searched by default. + * @param query One or more indexed fields and values to search. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/queryString/ queryString operator + */ + static QueryStringSearchOperator queryString(final FieldSearchPath defaultPath, final String query) { + isTrueArgument("path must not be empty", defaultPath != null); + isTrueArgument("query must not be empty", query != null); + + return new SearchConstructibleBsonElement("queryString", + new Document("defaultPath", defaultPath.toBsonValue()) + .append("query", query)); + } + /** * Returns a {@link SearchOperator} that performs a search for documents containing an ordered sequence of terms. * diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java index 790f596784a..5926fbd7bfb 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java @@ -82,6 +82,7 @@ import static com.mongodb.client.model.search.SearchOperator.exists; import static com.mongodb.client.model.search.SearchOperator.near; import static com.mongodb.client.model.search.SearchOperator.numberRange; +import static com.mongodb.client.model.search.SearchOperator.queryString; import static com.mongodb.client.model.search.SearchOperator.regex; import static com.mongodb.client.model.search.SearchOperator.phrase; import static com.mongodb.client.model.search.SearchOperator.text; @@ -612,7 +613,8 @@ private static Stream searchAndSearchMetaArgs() { near(0, 1.5, fieldPath("fieldName7"), fieldPath("fieldName8")), near(Instant.ofEpochMilli(1), Duration.ofMillis(3), fieldPath("fieldName9")), phrase(fieldPath("fieldName10"), "term6"), - regex(fieldPath("fieldName11"), "term7") + regex(fieldPath("fieldName11"), "term7"), + queryString(fieldPath("fieldName12"), "term8") )) .minimumShouldMatch(1) .mustNot(singleton( diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java index 02dc8d3fa04..0221d20aa17 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java @@ -581,6 +581,30 @@ void near() { ); } + @Test + void queryString() { + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> + // queries must not be empty + SearchOperator.queryString(fieldPath("fieldName"), null) + ), + () -> assertThrows(IllegalArgumentException.class, () -> + // paths must not be empty + SearchOperator.queryString(null, "term1 AND (term2 OR term3)") + ), + () -> assertEquals( + new BsonDocument("queryString", + new BsonDocument("defaultPath", fieldPath("fieldName").toBsonValue()) + .append("query", new BsonString("term1 AND (term2 OR term3)")) + ), + SearchOperator.queryString( + fieldPath("fieldName"), + "term1 AND (term2 OR term3)") + .toBsonDocument() + ) + ); + } + @Test void phrase() { assertAll( diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala index b76f707d3ae..ba5c4c046c8 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala @@ -228,6 +228,17 @@ object SearchOperator { def near(origin: Point, pivot: Number, paths: Iterable[_ <: FieldSearchPath]): GeoNearSearchOperator = JSearchOperator.near(origin, pivot, paths.asJava) + /** + * Returns a `SearchOperator` that supports querying a combination of indexed fields and values. + * + * @param defaultPath The field to be searched by default. + * @param query One or more indexed fields and values to search. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/queryString/ queryString operator]] + */ + def queryString(defaultPath: FieldSearchPath, query: String): QueryStringSearchOperator = + JSearchOperator.queryString(defaultPath, query) + /** * Returns a `SearchOperator` that performs a search for documents containing an ordered sequence of terms. * diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala index 8d6c1eff8e2..b4e3c58d63a 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala @@ -198,6 +198,13 @@ package object search { @Beta(Array(Reason.CLIENT)) type GeoNearSearchOperator = com.mongodb.client.model.search.GeoNearSearchOperator + /** + * @see `SearchOperator.queryString` + */ + @Sealed + @Beta(Array(Reason.CLIENT)) + type QueryStringSearchOperator = com.mongodb.client.model.search.QueryStringSearchOperator + /** * Fuzzy search options that may be used with some [[SearchOperator]]s. * From 22006bd1b3a46925030d88dac85db156019b3bdb Mon Sep 17 00:00:00 2001 From: joykim1005 <81274936+joykim1005@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:04:36 -0500 Subject: [PATCH 08/43] Add wildcard operator to Atlas Search (#1596) Add wildcard operator to Atlas Search There are several Atlas Search query operators that are not implemented with first class support in the Java driver. This PR adds the wildcard operator to Atlas Search. JAVA-5745 --- .../SearchConstructibleBsonElement.java | 2 +- .../client/model/search/SearchOperator.java | 30 +++++++++++++ .../model/search/WildcardSearchOperator.java | 32 ++++++++++++++ .../AggregatesSearchIntegrationTest.java | 4 +- .../model/search/SearchOperatorTest.java | 42 +++++++++++++++++++ .../scala/model/search/SearchOperator.scala | 21 ++++++++++ .../mongodb/scala/model/search/package.scala | 8 ++++ 7 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/client/model/search/WildcardSearchOperator.java diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java index c5a7e03a40b..b16f4651577 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java @@ -31,7 +31,7 @@ final class SearchConstructibleBsonElement extends AbstractConstructibleBsonElement implements MustCompoundSearchOperator, MustNotCompoundSearchOperator, ShouldCompoundSearchOperator, FilterCompoundSearchOperator, ExistsSearchOperator, TextSearchOperator, AutocompleteSearchOperator, - NumberNearSearchOperator, DateNearSearchOperator, GeoNearSearchOperator, RegexSearchOperator, QueryStringSearchOperator, + NumberNearSearchOperator, DateNearSearchOperator, GeoNearSearchOperator, RegexSearchOperator, QueryStringSearchOperator, WildcardSearchOperator, ValueBoostSearchScore, PathBoostSearchScore, ConstantSearchScore, FunctionSearchScore, GaussSearchScoreExpression, PathSearchScoreExpression, FacetSearchCollector, diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java index 0f7f89c732b..7c0edeafd98 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java @@ -339,6 +339,36 @@ static PhraseSearchOperator phrase(final Iterable paths, f .append("query", queryIterator.hasNext() ? queries : firstQuery)); } + /** + * Returns a {@link SearchOperator} that performs a search using a special characters in the search string that can match any character. + * + * @param query The string to search for. + * @param path The indexed field to be searched. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/wildcard/ wildcard operator + */ + static WildcardSearchOperator wildcard(final String query, final SearchPath path) { + return wildcard(singleton(notNull("query", query)), singleton(notNull("path", path))); + } + + /** + * Returns a {@link SearchOperator} that performs a search using a special characters in the search string that can match any character. + * + * @param queries The non-empty strings to search for. + * @param paths The non-empty index fields to be searched. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/wildcard/ wildcard operator + */ + static WildcardSearchOperator wildcard(final Iterable queries, final Iterable paths) { + Iterator queryIterator = notNull("queries", queries).iterator(); + isTrueArgument("queries must not be empty", queryIterator.hasNext()); + String firstQuery = queryIterator.next(); + Iterator pathIterator = notNull("paths", paths).iterator(); + isTrueArgument("paths must not be empty", pathIterator.hasNext()); + return new SearchConstructibleBsonElement("wildcard", new Document("query", queryIterator.hasNext() ? queries : firstQuery) + .append("path", combineToBsonValue(pathIterator, false))); + } + /** * Returns a {@link SearchOperator} that performs a search using a regular expression. * diff --git a/driver-core/src/main/com/mongodb/client/model/search/WildcardSearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/WildcardSearchOperator.java new file mode 100644 index 00000000000..651d9ffa57c --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/WildcardSearchOperator.java @@ -0,0 +1,32 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb.client.model.search; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Reason; +import com.mongodb.annotations.Sealed; + +/** + * @see SearchOperator#wildcard(String, SearchPath) + * @see SearchOperator#wildcard(Iterable, Iterable) + * @since 4.7 + */ +@Sealed +@Beta(Reason.CLIENT) +public interface WildcardSearchOperator extends SearchOperator { + @Override + WildcardSearchOperator score(SearchScore modifier); +} diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java index 5926fbd7bfb..fec1b7586c5 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java @@ -86,6 +86,7 @@ import static com.mongodb.client.model.search.SearchOperator.regex; import static com.mongodb.client.model.search.SearchOperator.phrase; import static com.mongodb.client.model.search.SearchOperator.text; +import static com.mongodb.client.model.search.SearchOperator.wildcard; import static com.mongodb.client.model.search.SearchOptions.searchOptions; import static com.mongodb.client.model.search.SearchPath.fieldPath; import static com.mongodb.client.model.search.SearchPath.wildcardPath; @@ -614,7 +615,8 @@ private static Stream searchAndSearchMetaArgs() { near(Instant.ofEpochMilli(1), Duration.ofMillis(3), fieldPath("fieldName9")), phrase(fieldPath("fieldName10"), "term6"), regex(fieldPath("fieldName11"), "term7"), - queryString(fieldPath("fieldName12"), "term8") + queryString(fieldPath("fieldName12"), "term8"), + wildcard(asList("term10", "term11"), asList(wildcardPath("wildc*rd"), fieldPath("fieldName14"))) )) .minimumShouldMatch(1) .mustNot(singleton( diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java index 0221d20aa17..a986b5a5589 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java @@ -581,6 +581,48 @@ void near() { ); } + @Test + void wildcard() { + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> + // queries must not be empty + SearchOperator.wildcard(emptyList(), singleton(fieldPath("fieldName"))) + ), + () -> assertThrows(IllegalArgumentException.class, () -> + // paths must not be empty + SearchOperator.wildcard(singleton("term"), emptyList()) + ), + () -> assertEquals( + new BsonDocument("wildcard", + new BsonDocument("query", new BsonString("term")) + .append("path", fieldPath("fieldName").toBsonValue()) + ), + SearchOperator.wildcard( + "term", + fieldPath("fieldName")) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("wildcard", + new BsonDocument("query", new BsonArray(asList( + new BsonString("term1"), + new BsonString("term2")))) + .append("path", new BsonArray(asList( + fieldPath("fieldName").toBsonValue(), + wildcardPath("wildc*rd").toBsonValue()))) + ), + SearchOperator.wildcard( + asList( + "term1", + "term2"), + asList( + fieldPath("fieldName"), + wildcardPath("wildc*rd"))) + .toBsonDocument() + ) + ); + } + @Test void queryString() { assertAll( diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala index ba5c4c046c8..bccb75b3665 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala @@ -228,6 +228,27 @@ object SearchOperator { def near(origin: Point, pivot: Number, paths: Iterable[_ <: FieldSearchPath]): GeoNearSearchOperator = JSearchOperator.near(origin, pivot, paths.asJava) + /** + * Returns a `SearchOperator` that enables queries which use special characters in the search string that can match any character. + * + * @param query The string to search for. + * @param path The indexed field to be searched. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/wildcard/ wildcard operator]] + */ + def wildcard(query: String, path: SearchPath): WildcardSearchOperator = JSearchOperator.wildcard(query, path) + + /** + * Returns a `SearchOperator` that enables queries which use special characters in the search string that can match any character. + * + * @param queries The non-empty strings to search for. + * @param paths The non-empty indexed fields to be searched. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/wildcard/ wildcard operator]] + */ + def wildcard(queries: Iterable[String], paths: Iterable[_ <: SearchPath]): WildcardSearchOperator = + JSearchOperator.wildcard(queries.asJava, paths.asJava) + /** * Returns a `SearchOperator` that supports querying a combination of indexed fields and values. * diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala index b4e3c58d63a..9fb3c9f4f4a 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala @@ -198,6 +198,14 @@ package object search { @Beta(Array(Reason.CLIENT)) type GeoNearSearchOperator = com.mongodb.client.model.search.GeoNearSearchOperator + /** + * @see `SearchOperator.wildcard(String, SearchPath)` + * @see `SearchOperator.wildcard(Iterable, Iterable)` + */ + @Sealed + @Beta(Array(Reason.CLIENT)) + type WildcardSearchOperator = com.mongodb.client.model.search.WildcardSearchOperator + /** * @see `SearchOperator.queryString` */ From 927c73217d63d52131386fe9b9d68227880b26d1 Mon Sep 17 00:00:00 2001 From: joykim1005 <81274936+joykim1005@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:09:25 -0500 Subject: [PATCH 09/43] Fix Int Test for Regex and Wildcard Operator (#1611) Fix int test for regex and wildcard operator The merged PRs for the regex and wildcard causes the AggregatesSearchIntegrationTest to fail. This PR fixes the integration test, so it passes. JAVA-5726 --- .../client/model/search/AggregatesSearchIntegrationTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java index fec1b7586c5..91a0a9bd617 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java @@ -614,9 +614,10 @@ private static Stream searchAndSearchMetaArgs() { near(0, 1.5, fieldPath("fieldName7"), fieldPath("fieldName8")), near(Instant.ofEpochMilli(1), Duration.ofMillis(3), fieldPath("fieldName9")), phrase(fieldPath("fieldName10"), "term6"), - regex(fieldPath("fieldName11"), "term7"), + regex(fieldPath("title").multi("keyword"), "term7"), queryString(fieldPath("fieldName12"), "term8"), - wildcard(asList("term10", "term11"), asList(wildcardPath("wildc*rd"), fieldPath("fieldName14"))) + wildcard(asList("term10", "term11"), asList(wildcardPath("wildc*rd"), fieldPath("title").multi( + "keyword"))) )) .minimumShouldMatch(1) .mustNot(singleton( From 75cb0e6dd2c23fb290d5d8b68307e821644a0805 Mon Sep 17 00:00:00 2001 From: joykim1005 <81274936+joykim1005@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:35:58 -0500 Subject: [PATCH 10/43] Add moreLikeThis operator to Atlas Search (#1609) Add moreLikeThis operator to Atlas Search There are several Atlas Search query operators that are not implemented with first class support in the Java driver. This PR adds the moreLikeThis operator to Atlas Search. JAVA-5740 --- .../search/MoreLikeThisSearchOperator.java | 33 +++++++++++++++ .../SearchConstructibleBsonElement.java | 1 + .../client/model/search/SearchOperator.java | 26 ++++++++++++ .../AggregatesSearchIntegrationTest.java | 4 ++ .../model/search/SearchOperatorTest.java | 40 +++++++++++++++++++ .../scala/model/search/SearchOperator.scala | 20 ++++++++++ .../mongodb/scala/model/search/package.scala | 7 ++++ 7 files changed, 131 insertions(+) create mode 100644 driver-core/src/main/com/mongodb/client/model/search/MoreLikeThisSearchOperator.java diff --git a/driver-core/src/main/com/mongodb/client/model/search/MoreLikeThisSearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/MoreLikeThisSearchOperator.java new file mode 100644 index 00000000000..b9f9826d858 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/MoreLikeThisSearchOperator.java @@ -0,0 +1,33 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb.client.model.search; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Reason; +import com.mongodb.annotations.Sealed; +import org.bson.BsonDocument; + +/** + * @see SearchOperator#moreLikeThis(BsonDocument) + * @see SearchOperator#moreLikeThis(Iterable) + * @since 4.7 + */ +@Sealed +@Beta(Reason.CLIENT) +public interface MoreLikeThisSearchOperator extends SearchOperator { + @Override + TextSearchOperator score(SearchScore modifier); +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java index b16f4651577..6db68a7d6c4 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java @@ -32,6 +32,7 @@ final class SearchConstructibleBsonElement extends AbstractConstructibleBsonElem MustCompoundSearchOperator, MustNotCompoundSearchOperator, ShouldCompoundSearchOperator, FilterCompoundSearchOperator, ExistsSearchOperator, TextSearchOperator, AutocompleteSearchOperator, NumberNearSearchOperator, DateNearSearchOperator, GeoNearSearchOperator, RegexSearchOperator, QueryStringSearchOperator, WildcardSearchOperator, + MoreLikeThisSearchOperator, ValueBoostSearchScore, PathBoostSearchScore, ConstantSearchScore, FunctionSearchScore, GaussSearchScoreExpression, PathSearchScoreExpression, FacetSearchCollector, diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java index 7c0edeafd98..dada0331c4a 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java @@ -20,6 +20,7 @@ import com.mongodb.annotations.Sealed; import com.mongodb.client.model.Aggregates; import com.mongodb.client.model.geojson.Point; +import org.bson.BsonDocument; import org.bson.BsonType; import org.bson.Document; import org.bson.conversions.Bson; @@ -292,6 +293,31 @@ static GeoNearSearchOperator near(final Point origin, final Number pivot, final .append("pivot", notNull("pivot", pivot))); } + /** + * Returns a {@link SearchOperator} that returns documents similar to input document. + * + * @param like The BSON document that is used to extract representative terms to query for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/morelikethis/ moreLikeThis operator + */ + static MoreLikeThisSearchOperator moreLikeThis(final BsonDocument like) { + return moreLikeThis(singleton(notNull("like", like))); + } + + /** + * Returns a {@link SearchOperator} that returns documents similar to input documents. + * + * @param likes The BSON documents that are used to extract representative terms to query for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/morelikethis/ moreLikeThis operator + */ + static MoreLikeThisSearchOperator moreLikeThis(final Iterable likes) { + Iterator likesIterator = notNull("likes", likes).iterator(); + isTrueArgument("likes must not be empty", likesIterator.hasNext()); + BsonDocument firstLike = likesIterator.next(); + return new SearchConstructibleBsonElement("moreLikeThis", new Document("like", likesIterator.hasNext() ? likes : firstLike)); + } + /** * Returns a {@link SearchOperator} that supports querying a combination of indexed fields and values. * diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java index 91a0a9bd617..a8e0ad314b0 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java @@ -23,6 +23,7 @@ import com.mongodb.client.model.geojson.Position; import com.mongodb.client.test.CollectionHelper; import org.bson.BsonDocument; +import org.bson.BsonString; import org.bson.codecs.BsonDocumentCodec; import org.bson.conversions.Bson; import org.bson.json.JsonWriterSettings; @@ -80,6 +81,7 @@ import static com.mongodb.client.model.search.SearchOperator.compound; import static com.mongodb.client.model.search.SearchOperator.dateRange; import static com.mongodb.client.model.search.SearchOperator.exists; +import static com.mongodb.client.model.search.SearchOperator.moreLikeThis; import static com.mongodb.client.model.search.SearchOperator.near; import static com.mongodb.client.model.search.SearchOperator.numberRange; import static com.mongodb.client.model.search.SearchOperator.queryString; @@ -616,6 +618,8 @@ private static Stream searchAndSearchMetaArgs() { phrase(fieldPath("fieldName10"), "term6"), regex(fieldPath("title").multi("keyword"), "term7"), queryString(fieldPath("fieldName12"), "term8"), + moreLikeThis(new BsonDocument("like", new BsonDocument("fieldName10", + new BsonString("term6")))), wildcard(asList("term10", "term11"), asList(wildcardPath("wildc*rd"), fieldPath("title").multi( "keyword"))) )) diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java index a986b5a5589..ef481ea03c1 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java @@ -581,6 +581,46 @@ void near() { ); } + @Test + void moreLikeThis() { + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> + // likes must not be empty + SearchOperator.moreLikeThis(emptyList()) + ), + () -> assertEquals( + new BsonDocument("moreLikeThis", + new BsonDocument("like", new BsonDocument("fieldName", new BsonString("fieldValue"))) + ), + SearchOperator.moreLikeThis(new BsonDocument("fieldName", new BsonString("fieldValue"))) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("moreLikeThis", + new BsonDocument("like", new BsonDocument("fieldName", new BsonString("fieldValue")) + .append("fieldName2", new BsonString("fieldValue2"))) + ), + SearchOperator.moreLikeThis(new BsonDocument("fieldName", new BsonString("fieldValue")) + .append("fieldName2", new BsonString("fieldValue2"))) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("moreLikeThis", + new BsonDocument("like", new BsonArray(asList( + new BsonDocument("fieldName", new BsonString("fieldValue")) + .append("fieldName2", new BsonString("fieldValue2")), + new BsonDocument("fieldName3", new BsonString("fieldValue3")) + ))) + ), + SearchOperator.moreLikeThis(asList( + new BsonDocument("fieldName", new BsonString("fieldValue")) + .append("fieldName2", new BsonString("fieldValue2")), + new BsonDocument("fieldName3", new BsonString("fieldValue3")))) + .toBsonDocument() + ) + ); + } + @Test void wildcard() { assertAll( diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala index bccb75b3665..fc8f065b554 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala @@ -17,6 +17,7 @@ package org.mongodb.scala.model.search import com.mongodb.annotations.{ Beta, Reason } import com.mongodb.client.model.search.{ SearchOperator => JSearchOperator } +import org.mongodb.scala.bson.BsonDocument import org.mongodb.scala.bson.conversions.Bson import org.mongodb.scala.model.geojson.Point @@ -228,6 +229,25 @@ object SearchOperator { def near(origin: Point, pivot: Number, paths: Iterable[_ <: FieldSearchPath]): GeoNearSearchOperator = JSearchOperator.near(origin, pivot, paths.asJava) + /** + * Returns a `SearchOperator` that returns documents similar to input document. + * + * @param like The BSON document that is used to extract representative terms to query for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/morelikethis/ moreLikeThis operator]] + */ + def moreLikeThis(like: BsonDocument): MoreLikeThisSearchOperator = JSearchOperator.moreLikeThis(like) + + /** + * Returns a `SearchOperator` that returns documents similar to input documents. + * + * @param likes The BSON documents that are used to extract representative terms to query for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/morelikethis/ moreLikeThis operator]] + */ + def moreLikeThis(likes: Iterable[BsonDocument]): MoreLikeThisSearchOperator = + JSearchOperator.moreLikeThis(likes.asJava) + /** * Returns a `SearchOperator` that enables queries which use special characters in the search string that can match any character. * diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala index 9fb3c9f4f4a..135190347ba 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala @@ -198,6 +198,13 @@ package object search { @Beta(Array(Reason.CLIENT)) type GeoNearSearchOperator = com.mongodb.client.model.search.GeoNearSearchOperator + /** + * @see `SearchOperator.moreLikeThis` + */ + @Sealed + @Beta(Array(Reason.CLIENT)) + type MoreLikeThisSearchOperator = com.mongodb.client.model.search.MoreLikeThisSearchOperator + /** * @see `SearchOperator.wildcard(String, SearchPath)` * @see `SearchOperator.wildcard(Iterable, Iterable)` From 51c736e781d9788a67ecc0176a853bff129c0960 Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin Date: Tue, 21 Jan 2025 14:22:03 -0800 Subject: [PATCH 11/43] Fix test cleanup to prevent conflicts between tests. (#1608) JAVA-5761 --- .../ClientSideEncryptionExternalKeyVaultTest.java | 11 ++++++++--- .../functional/com/mongodb/client/CrudProseTest.java | 12 ++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionExternalKeyVaultTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionExternalKeyVaultTest.java index f4b6b336e96..da513bb2a9e 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionExternalKeyVaultTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryptionExternalKeyVaultTest.java @@ -20,9 +20,11 @@ import com.mongodb.ClientEncryptionSettings; import com.mongodb.MongoClientSettings; import com.mongodb.MongoCredential; +import com.mongodb.MongoNamespace; import com.mongodb.MongoSecurityException; import com.mongodb.WriteConcern; import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.test.CollectionHelper; import com.mongodb.client.vault.ClientEncryption; import com.mongodb.client.vault.ClientEncryptions; import org.bson.BsonBinary; @@ -57,6 +59,7 @@ public class ClientSideEncryptionExternalKeyVaultTest { private MongoClient client, clientEncrypted; private ClientEncryption clientEncryption; private final boolean withExternalKeyVault; + private static final MongoNamespace NAMESPACE = new MongoNamespace("db", ClientSideEncryptionExternalKeyVaultTest.class.getName()); public ClientSideEncryptionExternalKeyVaultTest(final boolean withExternalKeyVault) { this.withExternalKeyVault = withExternalKeyVault; @@ -84,7 +87,7 @@ public void setUp() throws IOException, URISyntaxException { + "UN3YkQ5aXRRMkhGRGdQV09wOGVNYUMxT2k3NjZKelhaQmRCZGJkTXVyZG9uSjFk"); localMasterkey.put("key", localMasterkeyBytes); kmsProviders.put("local", localMasterkey); - schemaMap.put("db.coll", bsonDocumentFromPath("external-schema.json")); + schemaMap.put(NAMESPACE.getFullName(), bsonDocumentFromPath("external-schema.json")); AutoEncryptionSettings.Builder autoEncryptionSettingsBuilder = AutoEncryptionSettings.builder() .keyVaultNamespace("keyvault.datakeys") @@ -123,8 +126,8 @@ public void setUp() throws IOException, URISyntaxException { public void testExternal() { boolean authExceptionThrown = false; MongoCollection coll = clientEncrypted - .getDatabase("db") - .getCollection("coll", BsonDocument.class); + .getDatabase(NAMESPACE.getDatabaseName()) + .getCollection(NAMESPACE.getCollectionName(), BsonDocument.class); try { coll.insertOne(new BsonDocument().append("encrypted", new BsonString("test"))); } catch (MongoSecurityException mse) { @@ -169,5 +172,7 @@ public void after() { // ignore } } + + CollectionHelper.drop(NAMESPACE); } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java index 7138cdfe67e..016887c33b7 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java @@ -34,6 +34,7 @@ import com.mongodb.client.model.bulk.ClientBulkWriteOptions; import com.mongodb.client.model.bulk.ClientBulkWriteResult; import com.mongodb.client.model.bulk.ClientNamespacedWriteModel; +import com.mongodb.client.test.CollectionHelper; import com.mongodb.event.CommandStartedEvent; import com.mongodb.internal.connection.TestCommandListener; import org.bson.BsonArray; @@ -47,6 +48,7 @@ import org.bson.RawBsonDocument; import org.bson.codecs.configuration.CodecRegistry; import org.bson.codecs.pojo.PojoCodecProvider; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -96,7 +98,7 @@ * CRUD Prose Tests. */ public class CrudProseTest { - private static final MongoNamespace NAMESPACE = new MongoNamespace("db", "coll"); + private static final MongoNamespace NAMESPACE = new MongoNamespace("db", CrudProseTest.class.getName()); @DisplayName("1. WriteConcernError.details exposes writeConcernError.errInfo") @Test @@ -367,7 +369,8 @@ private void testBulkWriteSplitsWhenExceedingMaxMessageSizeBytesDueToNsInfo( Document helloResponse = droppedDatabase(client).runCommand(new Document("hello", 1)); int maxBsonObjectSize = helloResponse.getInteger("maxBsonObjectSize"); int maxMessageSizeBytes = helloResponse.getInteger("maxMessageSizeBytes"); - int opsBytes = maxMessageSizeBytes - 1122; + // By the spec test, we have to subtract only 1122, however, we have different collection name. + int opsBytes = maxMessageSizeBytes - 1118 - NAMESPACE.getCollectionName().length(); int numModels = opsBytes / maxBsonObjectSize; int remainderBytes = opsBytes % maxBsonObjectSize; List models = new ArrayList<>(nCopies( @@ -613,4 +616,9 @@ private static ClientBulkWriteResult runInTransaction(final ClientSession sessio throw throwable; } } + + @AfterAll + public static void cleanUp() { + CollectionHelper.drop(NAMESPACE); + } } From 089a55beb41b6cd5f9f0fc6a030fb9ab7e6a5382 Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin Date: Wed, 22 Jan 2025 12:50:02 -0800 Subject: [PATCH 12/43] Add Scala Client Bulk Write API. (#1603) JAVA-5531 --- .../scala/syncadapter/SyncMongoCluster.scala | 21 +- .../org/mongodb/scala/MongoCluster.scala | 123 +++++++++ .../mongodb/scala/model/bulk/package.scala | 235 ++++++++++++++++++ .../scala/org/mongodb/scala/package.scala | 7 + .../scala/ApiAliasAndCompanionSpec.scala | 4 +- .../org/mongodb/scala/MongoClientSpec.scala | 40 ++- .../org/mongodb/scala/ScalaPackageSpec.scala | 1 - .../scala/model/bulk/BulkModelSpec.scala | 111 +++++++++ 8 files changed, 515 insertions(+), 27 deletions(-) create mode 100644 driver-scala/src/main/scala/org/mongodb/scala/model/bulk/package.scala create mode 100644 driver-scala/src/test/scala/org/mongodb/scala/model/bulk/BulkModelSpec.scala diff --git a/driver-scala/src/integration/scala/org/mongodb/scala/syncadapter/SyncMongoCluster.scala b/driver-scala/src/integration/scala/org/mongodb/scala/syncadapter/SyncMongoCluster.scala index 972831f197f..439188e3792 100644 --- a/driver-scala/src/integration/scala/org/mongodb/scala/syncadapter/SyncMongoCluster.scala +++ b/driver-scala/src/integration/scala/org/mongodb/scala/syncadapter/SyncMongoCluster.scala @@ -1,6 +1,5 @@ package org.mongodb.scala.syncadapter -import com.mongodb.assertions.Assertions import com.mongodb.client.model.bulk.{ ClientBulkWriteOptions, ClientBulkWriteResult, ClientNamespacedWriteModel } import com.mongodb.{ ClientSessionOptions, ReadConcern, ReadPreference, WriteConcern } import com.mongodb.client.{ ClientSession, MongoCluster => JMongoCluster, MongoDatabase => JMongoDatabase } @@ -129,33 +128,21 @@ class SyncMongoCluster(wrapped: MongoCluster) extends JMongoCluster { override def bulkWrite( models: util.List[_ <: ClientNamespacedWriteModel] - ): ClientBulkWriteResult = { - org.junit.Assume.assumeTrue("TODO-JAVA-5531 implement", java.lang.Boolean.parseBoolean(toString)) - throw Assertions.fail("TODO-JAVA-5531 implement") - } + ): ClientBulkWriteResult = wrapped.bulkWrite(models.asScala.toList).toFuture().get() override def bulkWrite( models: util.List[_ <: ClientNamespacedWriteModel], options: ClientBulkWriteOptions - ): ClientBulkWriteResult = { - org.junit.Assume.assumeTrue("TODO-JAVA-5531 implement", java.lang.Boolean.parseBoolean(toString)) - throw Assertions.fail("TODO-JAVA-5531 implement") - } + ): ClientBulkWriteResult = wrapped.bulkWrite(models.asScala.toList, options).toFuture().get() override def bulkWrite( clientSession: ClientSession, models: util.List[_ <: ClientNamespacedWriteModel] - ): ClientBulkWriteResult = { - org.junit.Assume.assumeTrue("TODO-JAVA-5531 implement", java.lang.Boolean.parseBoolean(toString)) - throw Assertions.fail("TODO-JAVA-5531 implement") - } + ): ClientBulkWriteResult = wrapped.bulkWrite(unwrap(clientSession), models.asScala.toList).toFuture().get() override def bulkWrite( clientSession: ClientSession, models: util.List[_ <: ClientNamespacedWriteModel], options: ClientBulkWriteOptions - ): ClientBulkWriteResult = { - org.junit.Assume.assumeTrue("TODO-JAVA-5531 implement", java.lang.Boolean.parseBoolean(toString)) - throw Assertions.fail("TODO-JAVA-5531 implement") - } + ): ClientBulkWriteResult = wrapped.bulkWrite(unwrap(clientSession), models.asScala.toList, options).toFuture().get() } diff --git a/driver-scala/src/main/scala/org/mongodb/scala/MongoCluster.scala b/driver-scala/src/main/scala/org/mongodb/scala/MongoCluster.scala index a7352d5ac41..94c75bda724 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/MongoCluster.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/MongoCluster.scala @@ -22,6 +22,9 @@ import com.mongodb.reactivestreams.client.{ MongoCluster => JMongoCluster } import org.bson.codecs.configuration.CodecRegistry import org.mongodb.scala.bson.DefaultHelper.DefaultsTo import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.model.bulk.ClientNamespacedUpdateManyModel +import org.mongodb.scala.model.bulk.ClientNamespacedDeleteManyModel +import org.mongodb.scala.model.bulk.{ ClientBulkWriteOptions, ClientBulkWriteResult, ClientNamespacedWriteModel } import scala.collection.JavaConverters._ import scala.concurrent.duration.{ Duration, MILLISECONDS } @@ -290,4 +293,124 @@ class MongoCluster(private val wrapped: JMongoCluster) { )(implicit e: C DefaultsTo Document, ct: ClassTag[C]): ChangeStreamObservable[C] = ChangeStreamObservable(wrapped.watch(clientSession, pipeline.asJava, ct)) + /** + * Executes a client-level bulk write operation. + * This method is functionally equivalent to `bulkWrite(List, ClientBulkWriteOptions)` + * with the [[ClientBulkWriteOptions.clientBulkWriteOptions default options]]. + * + * This operation supports retryable writes. + * Depending on the number of `models`, encoded size of `models`, and the size limits in effect, + * executing this operation may require multiple `bulkWrite` commands. + * The eligibility for retries is determined per each `bulkWrite` command: + * [[ClientNamespacedUpdateManyModel]], [[ClientNamespacedDeleteManyModel]] in a command render it non-retryable. + * + * This operation is not supported by MongoDB Atlas Serverless instances. + * + * [[https://www.mongodb.com/docs/manual/reference/command/bulkWrite/ bulkWrite]] + * @param models The [[ClientNamespacedWriteModel]] individual write operations. + * @return The [[SingleObservable]] signalling at most one element [[ClientBulkWriteResult]] if the operation is successful, + * or the following errors: + * - [[ClientBulkWriteException]]: If and only if the operation is unsuccessful or partially unsuccessful, + * and there is at least one of the following pieces of information to report: + * [[ClientBulkWriteException ClientBulkWriteException#getWriteConcernErrors]], + * [[ClientBulkWriteException ClientBulkWriteException#getWriteErrors]], + * [[ClientBulkWriteException ClientBulkWriteException#getPartialResult]]. + * - [[MongoException]]: Only if the operation is unsuccessful. + * @since 5.4 + * @note Requires MongoDB 8.0 or greater. + */ + def bulkWrite(models: List[_ <: ClientNamespacedWriteModel]): SingleObservable[ClientBulkWriteResult] = + wrapped.bulkWrite(models.asJava) + + /** + * Executes a client-level bulk write operation. + * + * This operation supports retryable writes. + * Depending on the number of `models`, encoded size of `models`, and the size limits in effect, + * executing this operation may require multiple `bulkWrite` commands. + * The eligibility for retries is determined per each `bulkWrite` command: + * [[ClientNamespacedUpdateManyModel]], [[ClientNamespacedDeleteManyModel]] in a command render it non-retryable. + * + * This operation is not supported by MongoDB Atlas Serverless instances. + * + * [[https://www.mongodb.com/docs/manual/reference/command/bulkWrite/ bulkWrite]] + * @param models The [[ClientNamespacedWriteModel]] individual write operations. + * @param options The options. + * @return The [[SingleObservable]] signalling at most one element [[ClientBulkWriteResult]] if the operation is successful, + * or the following errors: + * - [[ClientBulkWriteException]]: If and only if the operation is unsuccessful or partially unsuccessful, + * and there is at least one of the following pieces of information to report: + * [[ClientBulkWriteException ClientBulkWriteException#getWriteConcernErrors]], + * [[ClientBulkWriteException ClientBulkWriteException#getWriteErrors]], + * [[ClientBulkWriteException ClientBulkWriteException#getPartialResult]]. + * - [[MongoException]]: Only if the operation is unsuccessful. + * @since 5.4 + * @note Requires MongoDB 8.0 or greater. + */ + def bulkWrite( + models: List[_ <: ClientNamespacedWriteModel], + options: ClientBulkWriteOptions + ): SingleObservable[ClientBulkWriteResult] = wrapped.bulkWrite(models.asJava, options) + + /** + * Executes a client-level bulk write operation. + * This method is functionally equivalent to `bulkWrite(ClientSession, List, ClientBulkWriteOptions)` + * with the [[ClientBulkWriteOptions.clientBulkWriteOptions default options]]. + * + * This operation supports retryable writes. + * Depending on the number of `models`, encoded size of `models`, and the size limits in effect, + * executing this operation may require multiple `bulkWrite` commands. + * The eligibility for retries is determined per each `bulkWrite` command: + * [[ClientNamespacedUpdateManyModel]], [[ClientNamespacedDeleteManyModel]] in a command render it non-retryable. + * + * This operation is not supported by MongoDB Atlas Serverless instances. + * + * [[https://www.mongodb.com/docs/manual/reference/command/bulkWrite/ bulkWrite]] + * @param clientSession [[ClientSession client session]] with which to associate this operation. + * @param models The [[ClientNamespacedWriteModel]] individual write operations. + * @return The [[SingleObservable]] signalling at most one element [[ClientBulkWriteResult]] if the operation is successful, + * or the following errors: + * - [[ClientBulkWriteException]]: If and only if the operation is unsuccessful or partially unsuccessful, + * and there is at least one of the following pieces of information to report: + * [[ClientBulkWriteException ClientBulkWriteException#getWriteConcernErrors]], + * [[ClientBulkWriteException ClientBulkWriteException#getWriteErrors]], + * [[ClientBulkWriteException ClientBulkWriteException#getPartialResult]]. + * - [[MongoException]]: Only if the operation is unsuccessful. + * @since 5.4 + * @note Requires MongoDB 8.0 or greater. + */ + def bulkWrite( + clientSession: ClientSession, + models: List[_ <: ClientNamespacedWriteModel] + ): SingleObservable[ClientBulkWriteResult] = wrapped.bulkWrite(clientSession, models.asJava) + + /** + * Executes a client-level bulk write operation. + * + * This operation supports retryable writes. + * Depending on the number of `models`, encoded size of `models`, and the size limits in effect, + * executing this operation may require multiple `bulkWrite` commands. + * The eligibility for retries is determined per each `bulkWrite` command: + * [[ClientNamespacedUpdateManyModel]], [[ClientNamespacedDeleteManyModel]] in a command render it non-retryable. + * + * [[https://www.mongodb.com/docs/manual/reference/command/bulkWrite/ bulkWrite]] + * @param clientSession The [[ClientSession client session]] with which to associate this operation. + * @param models The [[ClientNamespacedWriteModel]] individual write operations. + * @param options The options. + * @return The [[SingleObservable]] signalling at most one element [[ClientBulkWriteResult]] if the operation is successful, + * or the following errors: + * - [[ClientBulkWriteException]]: If and only if the operation is unsuccessful or partially unsuccessful, + * and there is at least one of the following pieces of information to report: + * [[ClientBulkWriteException ClientBulkWriteException#getWriteConcernErrors]], + * [[ClientBulkWriteException ClientBulkWriteException#getWriteErrors]], + * [[ClientBulkWriteException ClientBulkWriteException#getPartialResult]]. + * - [[MongoException]]: Only if the operation is unsuccessful. + * @since 5.4 + * @note Requires MongoDB 8.0 or greater. + */ + def bulkWrite( + clientSession: ClientSession, + models: List[_ <: ClientNamespacedWriteModel], + options: ClientBulkWriteOptions + ): SingleObservable[ClientBulkWriteResult] = wrapped.bulkWrite(clientSession, models.asJava, options) } diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/bulk/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/bulk/package.scala new file mode 100644 index 00000000000..44dcd7e3c84 --- /dev/null +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/bulk/package.scala @@ -0,0 +1,235 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.mongodb.scala.model + +import org.mongodb.scala.MongoNamespace +import org.mongodb.scala.bson.conversions.Bson + +import scala.collection.JavaConverters._ + +/** + * Models, options, results for the client-level bulk write operation. + * + * @since 5.4 + */ +package object bulk { + + /** + * A model for inserting a document. + */ + type ClientNamespacedInsertOneModel = com.mongodb.client.model.bulk.ClientNamespacedInsertOneModel + + /** + * A model for updating at most one document matching a filter. + */ + type ClientNamespacedUpdateOneModel = com.mongodb.client.model.bulk.ClientNamespacedUpdateOneModel + + /** + * A model for updating all documents matching a filter. + */ + type ClientNamespacedUpdateManyModel = com.mongodb.client.model.bulk.ClientNamespacedUpdateManyModel + + /** + * A model for replacing at most one document matching a filter. + */ + type ClientNamespacedReplaceOneModel = com.mongodb.client.model.bulk.ClientNamespacedReplaceOneModel + + /** + * A model for deleting at most one document matching a filter. + */ + type ClientNamespacedDeleteOneModel = com.mongodb.client.model.bulk.ClientNamespacedDeleteOneModel + + /** + * A model for deleting all documents matching a filter. + */ + type ClientNamespacedDeleteManyModel = com.mongodb.client.model.bulk.ClientNamespacedDeleteManyModel + + /** + * A combination of an individual write operation and a [[MongoNamespace]] + * the operation is targeted at. + */ + type ClientNamespacedWriteModel = com.mongodb.client.model.bulk.ClientNamespacedWriteModel + + object ClientNamespacedWriteModel { + + def insertOne[TDocument](namespace: MongoNamespace, document: TDocument): ClientNamespacedInsertOneModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.insertOne(namespace, document) + + def updateOne(namespace: MongoNamespace, filter: Bson, update: Bson): ClientNamespacedUpdateOneModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.updateOne(namespace, filter, update) + + def updateOne( + namespace: MongoNamespace, + filter: Bson, + update: Bson, + options: ClientUpdateOneOptions + ): ClientNamespacedUpdateOneModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.updateOne(namespace, filter, update, options) + + def updateOne( + namespace: MongoNamespace, + filter: Bson, + updatePipeline: Iterable[_ <: Bson] + ): ClientNamespacedUpdateOneModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.updateOne(namespace, filter, updatePipeline.asJava) + + def updateOne( + namespace: MongoNamespace, + filter: Bson, + updatePipeline: Iterable[_ <: Bson], + options: ClientUpdateOneOptions + ): ClientNamespacedUpdateOneModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.updateOne( + namespace, + filter, + updatePipeline.asJava, + options + ) + + def updateMany(namespace: MongoNamespace, filter: Bson, update: Bson): ClientNamespacedUpdateManyModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.updateMany(namespace, filter, update) + + def updateMany( + namespace: MongoNamespace, + filter: Bson, + update: Bson, + options: ClientUpdateManyOptions + ): ClientNamespacedUpdateManyModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.updateMany(namespace, filter, update, options) + + def updateMany( + namespace: MongoNamespace, + filter: Bson, + updatePipeline: Iterable[_ <: Bson] + ): ClientNamespacedUpdateManyModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.updateMany(namespace, filter, updatePipeline.asJava) + + def updateMany( + namespace: MongoNamespace, + filter: Bson, + updatePipeline: Iterable[_ <: Bson], + options: ClientUpdateManyOptions + ): ClientNamespacedUpdateManyModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.updateMany( + namespace, + filter, + updatePipeline.asJava, + options + ) + + def replaceOne[TDocument]( + namespace: MongoNamespace, + filter: Bson, + replacement: TDocument + ): ClientNamespacedReplaceOneModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.replaceOne(namespace, filter, replacement) + + def replaceOne[TDocument]( + namespace: MongoNamespace, + filter: Bson, + replacement: TDocument, + options: ClientReplaceOneOptions + ): ClientNamespacedReplaceOneModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.replaceOne(namespace, filter, replacement, options) + + def deleteOne(namespace: MongoNamespace, filter: Bson): ClientNamespacedDeleteOneModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.deleteOne(namespace, filter) + + def deleteOne( + namespace: MongoNamespace, + filter: Bson, + options: ClientDeleteOneOptions + ): ClientNamespacedDeleteOneModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.deleteOne(namespace, filter, options) + + def deleteMany(namespace: MongoNamespace, filter: Bson): ClientNamespacedDeleteManyModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.deleteMany(namespace, filter) + + def deleteMany( + namespace: MongoNamespace, + filter: Bson, + options: ClientDeleteManyOptions + ): ClientNamespacedDeleteManyModel = + com.mongodb.client.model.bulk.ClientNamespacedWriteModel.deleteMany(namespace, filter, options) + } + + /** + * The options to apply when executing a client-level bulk write operation. + */ + type ClientBulkWriteOptions = com.mongodb.client.model.bulk.ClientBulkWriteOptions + + object ClientBulkWriteOptions { + def clientBulkWriteOptions(): ClientBulkWriteOptions = + com.mongodb.client.model.bulk.ClientBulkWriteOptions.clientBulkWriteOptions() + } + + /** + * The options to apply when updating a document. + */ + type ClientUpdateOneOptions = com.mongodb.client.model.bulk.ClientUpdateOneOptions + + object ClientUpdateOneOptions { + def clientUpdateOneOptions(): ClientUpdateOneOptions = + com.mongodb.client.model.bulk.ClientUpdateOneOptions.clientUpdateOneOptions() + } + + /** + * The options to apply when updating documents. + */ + type ClientUpdateManyOptions = com.mongodb.client.model.bulk.ClientUpdateManyOptions + + object ClientUpdateManyOptions { + def clientUpdateManyOptions(): ClientUpdateManyOptions = + com.mongodb.client.model.bulk.ClientUpdateManyOptions.clientUpdateManyOptions() + } + + /** + * The options to apply when replacing a document. + */ + type ClientReplaceOneOptions = com.mongodb.client.model.bulk.ClientReplaceOneOptions + + object ClientReplaceOneOptions { + def clientReplaceOneOptions(): ClientReplaceOneOptions = + com.mongodb.client.model.bulk.ClientReplaceOneOptions.clientReplaceOneOptions() + } + + /** + * The options to apply when deleting a document. + */ + type ClientDeleteOneOptions = com.mongodb.client.model.bulk.ClientDeleteOneOptions + + object ClientDeleteOneOptions { + def clientDeleteOneOptions(): ClientDeleteOneOptions = + com.mongodb.client.model.bulk.ClientDeleteOneOptions.clientDeleteOneOptions() + } + + /** + * The options to apply when deleting documents. + */ + type ClientDeleteManyOptions = com.mongodb.client.model.bulk.ClientDeleteManyOptions + + object ClientDeleteManyOptions { + def clientDeleteManyOptions(): ClientDeleteManyOptions = + com.mongodb.client.model.bulk.ClientDeleteManyOptions.clientDeleteManyOptions() + } + + /** + * The result of a successful or partially successful client-level bulk write operation. + * + */ + type ClientBulkWriteResult = com.mongodb.client.model.bulk.ClientBulkWriteResult +} diff --git a/driver-scala/src/main/scala/org/mongodb/scala/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/package.scala index 7da5578ff96..9a4cba0e35c 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/package.scala @@ -234,6 +234,13 @@ package object scala extends ClientSessionImplicits with ObservableImplicits wit */ type MongoBulkWriteException = com.mongodb.MongoBulkWriteException + /** + * The result of an unsuccessful or partially unsuccessful client-level bulk write operation. + * + * @since 5.4 + */ + type ClientBulkWriteException = com.mongodb.ClientBulkWriteException + /** * An exception indicating that a failure occurred when running a `\$changeStream`. * @since 2.2 diff --git a/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala b/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala index 5b8e46c598d..2e21d30526a 100644 --- a/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala +++ b/driver-scala/src/test/scala/org/mongodb/scala/ApiAliasAndCompanionSpec.scala @@ -153,9 +153,7 @@ class ApiAliasAndCompanionSpec extends BaseSpec { .asScala .map(_.getSimpleName) .toSet + - "MongoException" - "MongoGridFSException" - "MongoConfigurationException" - "MongoWriteConcernWithResponseException" - - // TODO-JAVA-5531 remove the `"ClientBulkWriteException"` exclusion - "ClientBulkWriteException" + "MongoException" - "MongoGridFSException" - "MongoConfigurationException" - "MongoWriteConcernWithResponseException" val objects = new Reflections( new ConfigurationBuilder() diff --git a/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSpec.scala b/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSpec.scala index 4e0189bfd5e..781007b24ad 100644 --- a/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSpec.scala +++ b/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSpec.scala @@ -19,6 +19,7 @@ package org.mongodb.scala import com.mongodb.reactivestreams.client.{ MongoClient => JMongoClient } import org.bson.BsonDocument import org.mockito.Mockito.verify +import org.mongodb.scala.model.bulk.{ ClientBulkWriteOptions, ClientBulkWriteResult, ClientNamespacedWriteModel } import org.scalatestplus.mockito.MockitoSugar import scala.collection.JavaConverters._ @@ -28,6 +29,7 @@ class MongoClientSpec extends BaseSpec with MockitoSugar { val wrapped = mock[JMongoClient] val clientSession = mock[ClientSession] val mongoClient = new MongoClient(wrapped) + val namespace = new MongoNamespace("db.coll") "MongoClient" should "have the same methods as the wrapped MongoClient" in { val wrapped = classOf[JMongoClient].getMethods.map(_.getName).toSet -- Seq("getSettings") @@ -35,12 +37,7 @@ class MongoClientSpec extends BaseSpec with MockitoSugar { wrapped.foreach((name: String) => { val cleanedName = name.stripPrefix("get") - - if (!cleanedName.contains("bulkWrite")) { - // TODO-JAVA-5531 remove this whole `if` block - assert(local.contains(name) | local.contains(cleanedName.head.toLower + cleanedName.tail), s"Missing: $name") - } - // TODO-JAVA-5531 uncomment: assert(local.contains(name) | local.contains(cleanedName.head.toLower + cleanedName.tail), s"Missing: $name") + assert(local.contains(name) | local.contains(cleanedName.head.toLower + cleanedName.tail), s"Missing: $name") }) } @@ -104,6 +101,37 @@ class MongoClientSpec extends BaseSpec with MockitoSugar { verify(wrapped).watch(clientSession, pipeline.asJava, classOf[BsonDocument]) } + it should "call the underlying bulkWrite with models only" in { + val models = List(ClientNamespacedWriteModel.insertOne(namespace, Document("key" -> "value"))) + mongoClient.bulkWrite(models) shouldBe a[SingleObservable[ClientBulkWriteResult]] + verify(wrapped).bulkWrite(models.asJava) + } + + it should "call the underlying bulkWrite with models and options" in { + val models = List(ClientNamespacedWriteModel.insertOne(namespace, Document("key" -> "value"))) + val options = ClientBulkWriteOptions.clientBulkWriteOptions() + + mongoClient.bulkWrite(models, options) + + verify(wrapped).bulkWrite(models.asJava, options) + } + + it should "call the underlying bulkWrite with clientSession and models" in { + val models = List(ClientNamespacedWriteModel.insertOne(namespace, Document("key" -> "value"))) + + mongoClient.bulkWrite(clientSession, models) + + verify(wrapped).bulkWrite(clientSession, models.asJava) + } + + it should "call the underlying bulkWrite with clientSession, models, and options" in { + val models = List(ClientNamespacedWriteModel.insertOne(namespace, Document("key" -> "value"))) + val options = ClientBulkWriteOptions.clientBulkWriteOptions() + + mongoClient.bulkWrite(clientSession, models, options) + verify(wrapped).bulkWrite(clientSession, models.asJava, options) + } + it should "call the underlying getClusterDescription" in { mongoClient.getClusterDescription verify(wrapped).getClusterDescription diff --git a/driver-scala/src/test/scala/org/mongodb/scala/ScalaPackageSpec.scala b/driver-scala/src/test/scala/org/mongodb/scala/ScalaPackageSpec.scala index 3a91b8c3034..19b1140e8f7 100644 --- a/driver-scala/src/test/scala/org/mongodb/scala/ScalaPackageSpec.scala +++ b/driver-scala/src/test/scala/org/mongodb/scala/ScalaPackageSpec.scala @@ -20,7 +20,6 @@ import java.util.concurrent.TimeUnit import _root_.scala.concurrent.duration.Duration import com.mongodb.{ MongoCredential => JMongoCredential } import org.bson.BsonDocumentWrapper -import org.bson.codecs.DocumentCodec import org.mongodb.scala import org.mongodb.scala.MongoClient.DEFAULT_CODEC_REGISTRY import org.mongodb.scala.bson._ diff --git a/driver-scala/src/test/scala/org/mongodb/scala/model/bulk/BulkModelSpec.scala b/driver-scala/src/test/scala/org/mongodb/scala/model/bulk/BulkModelSpec.scala new file mode 100644 index 00000000000..f96ca2d96ee --- /dev/null +++ b/driver-scala/src/test/scala/org/mongodb/scala/model/bulk/BulkModelSpec.scala @@ -0,0 +1,111 @@ +package org.mongodb.scala.model.bulk + +import org.mongodb.scala.bson.Document +import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.{ BaseSpec, MongoNamespace } + +class BulkModelSpec extends BaseSpec { + + val namespace = new MongoNamespace("db.coll") + val filter: Bson = Document("a" -> 1) + val update: Bson = Document("$set" -> Document("b" -> 2)) + val replacement = Document("b" -> 2) + val document = Document("a" -> 1) + val updatePipeline: Seq[Document] = Seq(Document("$set" -> Document("b" -> 2))) + + it should "be able to create ClientNamespacedInsertOneModel" in { + val insertOneModel = ClientNamespacedWriteModel.insertOne(namespace, document) + insertOneModel shouldBe a[ClientNamespacedInsertOneModel] + insertOneModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedInsertOneModel] + } + + it should "be able to create ClientNamespacedUpdateOneModel with filter and update" in { + val updateOneModel = ClientNamespacedWriteModel.updateOne(namespace, filter, update) + updateOneModel shouldBe a[ClientNamespacedUpdateOneModel] + updateOneModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedUpdateOneModel] + } + + it should "be able to create ClientNamespacedUpdateOneModel with filter, update, and options" in { + val options = ClientUpdateOneOptions.clientUpdateOneOptions() + val updateOneModel = ClientNamespacedWriteModel.updateOne(namespace, filter, update, options) + updateOneModel shouldBe a[ClientNamespacedUpdateOneModel] + updateOneModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedUpdateOneModel] + } + + it should "be able to create ClientNamespacedUpdateOneModel with update pipeline" in { + val updateOneModel = ClientNamespacedWriteModel.updateOne(namespace, filter, updatePipeline) + updateOneModel shouldBe a[ClientNamespacedUpdateOneModel] + updateOneModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedUpdateOneModel] + } + + it should "be able to create ClientNamespacedUpdateOneModel with update pipeline and options" in { + val options = ClientUpdateOneOptions.clientUpdateOneOptions() + val updateOneModel = ClientNamespacedWriteModel.updateOne(namespace, filter, updatePipeline, options) + updateOneModel shouldBe a[ClientNamespacedUpdateOneModel] + updateOneModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedUpdateOneModel] + } + + it should "be able to create ClientNamespacedUpdateManyModel with filter and update" in { + val updateManyModel = ClientNamespacedWriteModel.updateMany(namespace, filter, update) + updateManyModel shouldBe a[ClientNamespacedUpdateManyModel] + updateManyModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedUpdateManyModel] + } + it should "be able to create ClientNamespacedUpdateManyModel with filter, update and options" in { + val options = ClientUpdateManyOptions.clientUpdateManyOptions() + val updateManyModel = ClientNamespacedWriteModel.updateMany(namespace, filter, update, options) + updateManyModel shouldBe a[ClientNamespacedUpdateManyModel] + updateManyModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedUpdateManyModel] + } + + it should "be able to create ClientNamespacedUpdateManyModel with filter, updatePipeline" in { + val updateManyModel = ClientNamespacedWriteModel.updateMany(namespace, filter, updatePipeline) + updateManyModel shouldBe a[ClientNamespacedUpdateManyModel] + updateManyModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedUpdateManyModel] + } + + it should "be able to create ClientNamespacedUpdateManyModel with filter, updatePipeline and options" in { + val options = ClientUpdateManyOptions.clientUpdateManyOptions() + val updateManyModel = ClientNamespacedWriteModel.updateMany(namespace, filter, updatePipeline, options) + updateManyModel shouldBe a[ClientNamespacedUpdateManyModel] + updateManyModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedUpdateManyModel] + } + + it should "be able to create ClientNamespacedReplaceOneModel" in { + val replaceOneModel = ClientNamespacedWriteModel.replaceOne(namespace, filter, replacement) + replaceOneModel shouldBe a[ClientNamespacedReplaceOneModel] + replaceOneModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedReplaceOneModel] + } + + it should "be able to create ClientNamespacedReplaceOneModel with options" in { + val options = ClientReplaceOneOptions.clientReplaceOneOptions() + val replaceOneModel = ClientNamespacedWriteModel.replaceOne(namespace, filter, replacement, options) + replaceOneModel shouldBe a[ClientNamespacedReplaceOneModel] + replaceOneModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedReplaceOneModel] + } + + it should "be able to create ClientNamespacedDeleteOneModel" in { + val deleteOneModel = ClientNamespacedWriteModel.deleteOne(namespace, filter) + deleteOneModel shouldBe a[ClientNamespacedDeleteOneModel] + deleteOneModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedDeleteOneModel] + } + + it should "be able to create ClientNamespacedDeleteOneModel with options" in { + val options = ClientDeleteOneOptions.clientDeleteOneOptions() + val deleteOneModel = ClientNamespacedWriteModel.deleteOne(namespace, filter, options) + deleteOneModel shouldBe a[ClientNamespacedDeleteOneModel] + deleteOneModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedDeleteOneModel] + } + + it should "be able to create ClientNamespacedDeleteManyModel" in { + val deleteManyModel = ClientNamespacedWriteModel.deleteMany(namespace, filter) + deleteManyModel shouldBe a[ClientNamespacedDeleteManyModel] + deleteManyModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedDeleteManyModel] + } + + it should "be able to create ClientNamespacedDeleteManyModel with options" in { + val options = ClientDeleteManyOptions.clientDeleteManyOptions() + val deleteManyModel = ClientNamespacedWriteModel.deleteMany(namespace, filter, options) + deleteManyModel shouldBe a[ClientNamespacedDeleteManyModel] + deleteManyModel shouldBe a[com.mongodb.client.model.bulk.ClientNamespacedDeleteManyModel] + } +} From 3e8f9051af57db0d4e0bae058c77a4b96b1bf45d Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin Date: Mon, 27 Jan 2025 08:31:45 -0800 Subject: [PATCH 13/43] Remove deprecation warnings. (#1613) JAVA-5770 --- .../src/main/com/mongodb/ConnectionString.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/driver-core/src/main/com/mongodb/ConnectionString.java b/driver-core/src/main/com/mongodb/ConnectionString.java index 69db84eb072..062ca1d45c2 100644 --- a/driver-core/src/main/com/mongodb/ConnectionString.java +++ b/driver-core/src/main/com/mongodb/ConnectionString.java @@ -509,7 +509,6 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient credential = createCredentials(combinedOptionsMaps, userName, password); warnOnUnsupportedOptions(combinedOptionsMaps); - warnDeprecatedTimeouts(combinedOptionsMaps); } private static final Set GENERAL_OPTIONS_KEYS = new LinkedHashSet<>(); @@ -518,7 +517,6 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient private static final Set WRITE_CONCERN_KEYS = new HashSet<>(); private static final Set COMPRESSOR_KEYS = new HashSet<>(); private static final Set ALL_KEYS = new HashSet<>(); - private static final Set DEPRECATED_TIMEOUT_KEYS = new HashSet<>(); static { GENERAL_OPTIONS_KEYS.add("minpoolsize"); @@ -592,10 +590,6 @@ public ConnectionString(final String connectionString, @Nullable final DnsClient ALL_KEYS.addAll(READ_PREFERENCE_KEYS); ALL_KEYS.addAll(WRITE_CONCERN_KEYS); ALL_KEYS.addAll(COMPRESSOR_KEYS); - - DEPRECATED_TIMEOUT_KEYS.add("sockettimeoutms"); - DEPRECATED_TIMEOUT_KEYS.add("waitqueuetimeoutms"); - DEPRECATED_TIMEOUT_KEYS.add("wtimeoutms"); } // Any options contained in the connection string completely replace the corresponding options specified in TXT records, @@ -616,15 +610,6 @@ private void warnOnUnsupportedOptions(final Map> optionsMap .forEach(k -> LOGGER.warn(format("Connection string contains unsupported option '%s'.", k))); } } - private void warnDeprecatedTimeouts(final Map> optionsMap) { - if (LOGGER.isWarnEnabled()) { - optionsMap.keySet() - .stream() - .filter(DEPRECATED_TIMEOUT_KEYS::contains) - .forEach(k -> LOGGER.warn(format("Use of deprecated timeout option: '%s'. Prefer 'timeoutMS' instead.", k))); - } - } - private void translateOptions(final Map> optionsMap) { boolean tlsInsecureSet = false; From 0e256548e6040e0814ed9d44e6a50e8aa0447a91 Mon Sep 17 00:00:00 2001 From: joykim1005 <81274936+joykim1005@users.noreply.github.com> Date: Tue, 28 Jan 2025 10:52:24 -0500 Subject: [PATCH 14/43] Add equals operator to Atlas Search (#1606) JAVA-5729 --------- Co-authored-by: Maxim Katcharov --- .../model/search/EqualsSearchOperator.java | 43 +++++++ .../SearchConstructibleBsonElement.java | 5 +- .../client/model/search/SearchOperator.java | 97 ++++++++++++++++ .../AggregatesSearchIntegrationTest.java | 5 +- .../model/search/SearchOperatorTest.java | 108 ++++++++++++++++++ .../scala/model/search/SearchOperator.scala | 80 +++++++++++++ .../mongodb/scala/model/search/package.scala | 7 ++ 7 files changed, 342 insertions(+), 3 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/client/model/search/EqualsSearchOperator.java diff --git a/driver-core/src/main/com/mongodb/client/model/search/EqualsSearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/EqualsSearchOperator.java new file mode 100644 index 00000000000..b3aa4c278ea --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/EqualsSearchOperator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb.client.model.search; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Reason; +import com.mongodb.annotations.Sealed; + +import java.util.UUID; + +import java.time.Instant; + +import org.bson.types.ObjectId; + +/** + * @see SearchOperator#equals(FieldSearchPath, boolean) + * @see SearchOperator#equals(FieldSearchPath, ObjectId) + * @see SearchOperator#equals(FieldSearchPath, Number) + * @see SearchOperator#equals(FieldSearchPath, Instant) + * @see SearchOperator#equals(FieldSearchPath, String) + * @see SearchOperator#equals(FieldSearchPath, UUID) + * @see SearchOperator#equalsNull(FieldSearchPath) + * @since 5.3 + */ +@Sealed +@Beta(Reason.CLIENT) +public interface EqualsSearchOperator extends SearchOperator { + @Override + EqualsSearchOperator score(SearchScore modifier); +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java index 6db68a7d6c4..6c78d7e16d7 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java @@ -31,8 +31,9 @@ final class SearchConstructibleBsonElement extends AbstractConstructibleBsonElement implements MustCompoundSearchOperator, MustNotCompoundSearchOperator, ShouldCompoundSearchOperator, FilterCompoundSearchOperator, ExistsSearchOperator, TextSearchOperator, AutocompleteSearchOperator, - NumberNearSearchOperator, DateNearSearchOperator, GeoNearSearchOperator, RegexSearchOperator, QueryStringSearchOperator, WildcardSearchOperator, - MoreLikeThisSearchOperator, + NumberNearSearchOperator, DateNearSearchOperator, GeoNearSearchOperator, + EqualsSearchOperator, MoreLikeThisSearchOperator, + RegexSearchOperator, QueryStringSearchOperator, WildcardSearchOperator, ValueBoostSearchScore, PathBoostSearchScore, ConstantSearchScore, FunctionSearchScore, GaussSearchScoreExpression, PathSearchScoreExpression, FacetSearchCollector, diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java index dada0331c4a..e457454db0c 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java @@ -20,6 +20,11 @@ import com.mongodb.annotations.Sealed; import com.mongodb.client.model.Aggregates; import com.mongodb.client.model.geojson.Point; + +import java.util.UUID; + +import org.bson.BsonBinary; +import org.bson.BsonNull; import org.bson.BsonDocument; import org.bson.BsonType; import org.bson.Document; @@ -29,6 +34,8 @@ import java.time.Instant; import java.util.Iterator; +import org.bson.types.ObjectId; + import static com.mongodb.assertions.Assertions.isTrueArgument; import static com.mongodb.internal.Iterables.concat; import static com.mongodb.internal.client.model.Util.combineToBsonValue; @@ -293,6 +300,96 @@ static GeoNearSearchOperator near(final Point origin, final Number pivot, final .append("pivot", notNull("pivot", pivot))); } + /** + * Returns a {@link SearchOperator} that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The boolean value to query for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/equals/ equals operator + */ + static EqualsSearchOperator equals(final FieldSearchPath path, final boolean value) { + return new SearchConstructibleBsonElement("equals", new Document("path", notNull("path", path).toValue()) + .append("value", value)); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The object id value to query for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/equals/ equals operator + */ + static EqualsSearchOperator equals(final FieldSearchPath path, final ObjectId value) { + return new SearchConstructibleBsonElement("equals", new Document("path", notNull("path", path).toValue()) + .append("value", notNull("value", value))); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The number value to query for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/equals/ equals operator + */ + static EqualsSearchOperator equals(final FieldSearchPath path, final Number value) { + return new SearchConstructibleBsonElement("equals", new Document("path", notNull("path", path).toValue()) + .append("value", notNull("value", value))); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The instant date value to query for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/equals/ equals operator + */ + static EqualsSearchOperator equals(final FieldSearchPath path, final Instant value) { + return new SearchConstructibleBsonElement("equals", new Document("path", notNull("path", path).toValue()) + .append("value", notNull("value", value))); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The string value to query for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/equals/ equals operator + */ + static EqualsSearchOperator equals(final FieldSearchPath path, final String value) { + return new SearchConstructibleBsonElement("equals", new Document("path", notNull("path", path).toValue()) + .append("value", notNull("value", value))); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The uuid value to query for. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/equals/ equals operator + */ + static EqualsSearchOperator equals(final FieldSearchPath path, final UUID value) { + return new SearchConstructibleBsonElement("equals", new Document("path", notNull("path", path).toValue()) + .append("value", notNull("value", new BsonBinary(value)))); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where a field matches null. + * + * @param path The indexed field to be searched. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/equals/ equals operator + */ + static EqualsSearchOperator equalsNull(final FieldSearchPath path) { + return new SearchConstructibleBsonElement("equals", new Document("path", notNull("path", path).toValue()) + .append("value", BsonNull.VALUE)); + } + /** * Returns a {@link SearchOperator} that returns documents similar to input document. * diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java index a8e0ad314b0..d90f4a0575c 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java @@ -80,6 +80,7 @@ import static com.mongodb.client.model.search.SearchOperator.autocomplete; import static com.mongodb.client.model.search.SearchOperator.compound; import static com.mongodb.client.model.search.SearchOperator.dateRange; +import static com.mongodb.client.model.search.SearchOperator.equalsNull; import static com.mongodb.client.model.search.SearchOperator.exists; import static com.mongodb.client.model.search.SearchOperator.moreLikeThis; import static com.mongodb.client.model.search.SearchOperator.near; @@ -621,7 +622,9 @@ private static Stream searchAndSearchMetaArgs() { moreLikeThis(new BsonDocument("like", new BsonDocument("fieldName10", new BsonString("term6")))), wildcard(asList("term10", "term11"), asList(wildcardPath("wildc*rd"), fieldPath("title").multi( - "keyword"))) + "keyword"))), + SearchOperator.equals(fieldPath("fieldName11"), "term7"), + equalsNull(fieldPath("fieldName12")) )) .minimumShouldMatch(1) .mustNot(singleton( diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java index ef481ea03c1..06a825c6125 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java @@ -18,14 +18,22 @@ import com.mongodb.MongoClientSettings; import com.mongodb.client.model.geojson.Point; import com.mongodb.client.model.geojson.Position; + +import java.util.UUID; + import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonBoolean; import org.bson.BsonDateTime; import org.bson.BsonDocument; import org.bson.BsonDouble; import org.bson.BsonInt32; import org.bson.BsonInt64; +import org.bson.BsonNull; +import org.bson.BsonObjectId; import org.bson.BsonString; import org.bson.Document; +import org.bson.types.ObjectId; import org.junit.jupiter.api.Test; import java.time.Duration; @@ -581,6 +589,106 @@ void near() { ); } + @Test + void equals() { + ObjectId objectId = new ObjectId(); + UUID uuid = UUID.randomUUID(); + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> + // path must not be null + SearchOperator.equals(null, "term") + ), + () -> assertEquals( + new BsonDocument("equals", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("value", new BsonBoolean(true)) + ), + SearchOperator.equals( + fieldPath("fieldName"), + true) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("equals", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("value", new BsonObjectId(objectId)) + ), + SearchOperator.equals( + fieldPath("fieldName"), + objectId) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("equals", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("value", new BsonInt32(1)) + ), + SearchOperator.equals( + fieldPath("fieldName"), + 1) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("equals", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("value", new BsonInt64(Long.MAX_VALUE)) + ), + SearchOperator.equals( + fieldPath("fieldName"), + Long.MAX_VALUE) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("equals", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("value", new BsonDouble(Double.MAX_VALUE)) + ), + SearchOperator.equals( + fieldPath("fieldName"), + Double.MAX_VALUE) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("equals", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("value", new BsonDateTime(Instant.EPOCH.toEpochMilli())) + ), + SearchOperator.equals( + fieldPath("fieldName"), + Instant.EPOCH) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("equals", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("value", new BsonString("term")) + ), + SearchOperator.equals( + fieldPath("fieldName"), + "term") + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("equals", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("value", new BsonBinary(uuid)) + ), + SearchOperator.equals( + fieldPath("fieldName"), + uuid) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("equals", + new BsonDocument("path", fieldPath("fieldName").toBsonValue()) + .append("value", BsonNull.VALUE) + ), + SearchOperator.equalsNull(fieldPath("fieldName")) + .toBsonDocument() + ) + ); + } + @Test void moreLikeThis() { assertAll( diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala index fc8f065b554..500724d4d31 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala @@ -21,7 +21,10 @@ import org.mongodb.scala.bson.BsonDocument import org.mongodb.scala.bson.conversions.Bson import org.mongodb.scala.model.geojson.Point +import org.bson.types.ObjectId; + import java.time.{ Duration, Instant } +import java.util.UUID import collection.JavaConverters._ /** @@ -229,6 +232,83 @@ object SearchOperator { def near(origin: Point, pivot: Number, paths: Iterable[_ <: FieldSearchPath]): GeoNearSearchOperator = JSearchOperator.near(origin, pivot, paths.asJava) + /** + * Returns a `SearchOperator` that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The boolean value to query for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/equals/ equals operator]] + */ + def equals(path: FieldSearchPath, value: Boolean): EqualsSearchOperator = + JSearchOperator.equals(path, value) + + /** + * Returns a `SearchOperator` that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The object id value to query for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/equals/ equals operator]] + */ + def equals(path: FieldSearchPath, value: ObjectId): EqualsSearchOperator = + JSearchOperator.equals(path, value) + + /** + * Returns a `SearchOperator` that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The number value to query for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/equals/ equals operator]] + */ + def equals(path: FieldSearchPath, value: Number): EqualsSearchOperator = + JSearchOperator.equals(path, value) + + /** + * Returns a `SearchOperator` that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The instant date value to query for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/equals/ equals operator]] + */ + def equals(path: FieldSearchPath, value: Instant): EqualsSearchOperator = + JSearchOperator.equals(path, value) + + /** + * Returns a `SearchOperator` that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The string value to query for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/equals/ equals operator]] + */ + def equals(path: FieldSearchPath, value: String): EqualsSearchOperator = + JSearchOperator.equals(path, value) + + /** + * Returns a `SearchOperator` that searches for documents where a field matches the specified value. + * + * @param path The indexed field to be searched. + * @param value The uuid value to query for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/equals/ equals operator]] + */ + def equals(path: FieldSearchPath, value: UUID): EqualsSearchOperator = + JSearchOperator.equals(path, value) + + /** + * Returns a `SearchOperator` that searches for documents where a field matches null. + * + * @param path The indexed field to be searched. + * @param value The uuid value to query for. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/equals/ equals operator]] + */ + def equalsNull(path: FieldSearchPath): EqualsSearchOperator = + JSearchOperator.equalsNull(path) + /** * Returns a `SearchOperator` that returns documents similar to input document. * diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala index 135190347ba..0a0c7662c02 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala @@ -198,6 +198,13 @@ package object search { @Beta(Array(Reason.CLIENT)) type GeoNearSearchOperator = com.mongodb.client.model.search.GeoNearSearchOperator + /** + * @see `SearchOperator.equals` + */ + @Sealed + @Beta(Array(Reason.CLIENT)) + type EqualsSearchOperator = com.mongodb.client.model.search.EqualsSearchOperator + /** * @see `SearchOperator.moreLikeThis` */ From d3e81d85c5be5cbad72f9a8115b2ae46206d0f2a Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin Date: Mon, 3 Feb 2025 22:02:47 -0800 Subject: [PATCH 15/43] Client Bulk Write sort option. (#1612) - Add sort option to updateOne. - Add sort option to replaceOne. JAVA-5723 --------- Co-authored-by: Maxim Katcharov --- .../model/bulk/ClientReplaceOneOptions.java | 15 ++ .../model/bulk/ClientUpdateOneOptions.java | 15 ++ .../bulk/ConcreteClientReplaceOneOptions.java | 18 ++ .../bulk/ConcreteClientUpdateOneOptions.java | 23 +++ .../DefaultClusterableServerFactory.java | 1 + .../operation/ClientBulkWriteOperation.java | 12 ++ .../client-bulkWrite-replaceOne-sort.json | 163 +++++++++++++++++ .../crud/client-bulkWrite-updateOne-sort.json | 167 ++++++++++++++++++ .../client/unified/UnifiedCrudHelper.java | 21 ++- 9 files changed, 431 insertions(+), 4 deletions(-) create mode 100644 driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-replaceOne-sort.json create mode 100644 driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-updateOne-sort.json diff --git a/driver-core/src/main/com/mongodb/client/model/bulk/ClientReplaceOneOptions.java b/driver-core/src/main/com/mongodb/client/model/bulk/ClientReplaceOneOptions.java index 2142d736f60..4de01a94843 100644 --- a/driver-core/src/main/com/mongodb/client/model/bulk/ClientReplaceOneOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/bulk/ClientReplaceOneOptions.java @@ -74,4 +74,19 @@ static ClientReplaceOneOptions clientReplaceOneOptions() { */ @Override ClientReplaceOneOptions upsert(@Nullable Boolean upsert); + + /** + * Sets the sort criteria to apply to the operation. A null value means no sort criteria is set. + * + *

+ * The sort criteria determines which document the operation replaces if the query matches multiple documents. + * The first document matched by the specified sort criteria will be replaced. + * + * @param sort The sort criteria. {@code null} represents the server default. + * @return this + * @mongodb.driver.manual reference/method/db.collection.replaceOne/ Sort + * @mongodb.server.release 8.0 + * @since 5.4 + */ + ClientReplaceOneOptions sort(@Nullable Bson sort); } diff --git a/driver-core/src/main/com/mongodb/client/model/bulk/ClientUpdateOneOptions.java b/driver-core/src/main/com/mongodb/client/model/bulk/ClientUpdateOneOptions.java index 9b04ec6ef15..c5abea43b2a 100644 --- a/driver-core/src/main/com/mongodb/client/model/bulk/ClientUpdateOneOptions.java +++ b/driver-core/src/main/com/mongodb/client/model/bulk/ClientUpdateOneOptions.java @@ -85,4 +85,19 @@ static ClientUpdateOneOptions clientUpdateOneOptions() { */ @Override ClientUpdateOneOptions upsert(@Nullable Boolean upsert); + + /** + * Sets the sort criteria to apply to the operation. A null value means no sort criteria is set. + * + *

+ * The sort criteria determines which document the operation updates if the query matches multiple documents. + * The first document matched by the specified sort criteria will be updated. + * + * @param sort The sort criteria. {@code null} represents the server default. + * @return this + * @mongodb.driver.manual reference/method/db.collection.updateOne/ Sort + * @mongodb.server.release 8.0 + * @since 5.4 + */ + ClientUpdateOneOptions sort(@Nullable Bson sort); } diff --git a/driver-core/src/main/com/mongodb/internal/client/model/bulk/ConcreteClientReplaceOneOptions.java b/driver-core/src/main/com/mongodb/internal/client/model/bulk/ConcreteClientReplaceOneOptions.java index 18e9d060763..f7172488bfc 100644 --- a/driver-core/src/main/com/mongodb/internal/client/model/bulk/ConcreteClientReplaceOneOptions.java +++ b/driver-core/src/main/com/mongodb/internal/client/model/bulk/ConcreteClientReplaceOneOptions.java @@ -38,6 +38,8 @@ public final class ConcreteClientReplaceOneOptions implements ClientReplaceOneOp private String hintString; @Nullable private Boolean upsert; + @Nullable + private Bson sort; public ConcreteClientReplaceOneOptions() { } @@ -89,6 +91,21 @@ public ClientReplaceOneOptions upsert(@Nullable final Boolean upsert) { return this; } + /** + * @see ClientReplaceOneOptions#sort(Bson) + */ + public ClientReplaceOneOptions sort(final Bson sort) { + this.sort = sort; + return this; + } + + /** + * @see ClientReplaceOneOptions#sort(Bson) + */ + public Optional getSort() { + return ofNullable(sort); + } + /** * @see #upsert(Boolean) */ @@ -103,6 +120,7 @@ public String toString() { + ", hint=" + hint + ", hintString='" + hintString + '\'' + ", upsert=" + upsert + + ", sort=" + sort + '}'; } } diff --git a/driver-core/src/main/com/mongodb/internal/client/model/bulk/ConcreteClientUpdateOneOptions.java b/driver-core/src/main/com/mongodb/internal/client/model/bulk/ConcreteClientUpdateOneOptions.java index fdf960ed1df..3bd5f1451d7 100644 --- a/driver-core/src/main/com/mongodb/internal/client/model/bulk/ConcreteClientUpdateOneOptions.java +++ b/driver-core/src/main/com/mongodb/internal/client/model/bulk/ConcreteClientUpdateOneOptions.java @@ -20,12 +20,19 @@ import com.mongodb.lang.Nullable; import org.bson.conversions.Bson; +import java.util.Optional; + +import static java.util.Optional.ofNullable; + /** * This class is not part of the public API and may be removed or changed at any time. */ public final class ConcreteClientUpdateOneOptions extends AbstractClientUpdateOptions implements ClientUpdateOneOptions { static final ConcreteClientUpdateOneOptions MUTABLE_EMPTY = new ConcreteClientUpdateOneOptions(); + @Nullable + private Bson sort; + public ConcreteClientUpdateOneOptions() { } @@ -54,6 +61,21 @@ public ConcreteClientUpdateOneOptions upsert(@Nullable final Boolean upsert) { return (ConcreteClientUpdateOneOptions) super.upsert(upsert); } + /** + * @see ClientUpdateOneOptions#sort(Bson) + */ + public ConcreteClientUpdateOneOptions sort(final Bson sort) { + this.sort = sort; + return this; + } + + /** + * @see ClientUpdateOneOptions#sort(Bson) + */ + public Optional getSort() { + return ofNullable(sort); + } + @Override public String toString() { return "ClientUpdateOneOptions{" @@ -62,6 +84,7 @@ public String toString() { + ", hint=" + getHint().orElse(null) + ", hintString=" + getHintString().map(s -> '\'' + s + '\'') .orElse(null) + ", upsert=" + isUpsert().orElse(null) + + ", sort=" + getSort().orElse(null) + '}'; } } diff --git a/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterableServerFactory.java b/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterableServerFactory.java index 880e1db8521..aa8973ec092 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterableServerFactory.java +++ b/driver-core/src/main/com/mongodb/internal/connection/DefaultClusterableServerFactory.java @@ -93,6 +93,7 @@ public ClusterableServer create(final Cluster cluster, final ServerAddress serve new InternalStreamConnectionFactory(clusterMode, true, heartbeatStreamFactory, null, applicationName, mongoDriverInformation, emptyList(), loggerSettings, null, serverApi), clusterMode, serverApi, isFunctionAsAServiceEnvironment, sdamProvider, heartbeatOperationContextFactory); + ConnectionPool connectionPool = new DefaultConnectionPool(serverId, new InternalStreamConnectionFactory(clusterMode, streamFactory, credential, applicationName, mongoDriverInformation, compressorList, loggerSettings, commandListener, serverApi), diff --git a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java index ccd7f272e95..f6ff7632c8f 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java @@ -1247,6 +1247,14 @@ private void encodeWriteModelInternals( }); } + private void encodeWriteModelInternals(final BsonWriter writer, final ConcreteClientUpdateOneModel model) { + encodeWriteModelInternals(writer, (AbstractClientUpdateModel) model); + model.getOptions().getSort().ifPresent(value -> { + writer.writeName("sort"); + encodeUsingRegistry(writer, value); + }); + } + private void encodeWriteModelInternals(final BsonWriter writer, final AbstractClientUpdateModel model) { writer.writeName("filter"); encodeUsingRegistry(writer, model.getFilter()); @@ -1294,6 +1302,10 @@ private void encodeWriteModelInternals(final BsonBinaryWriter writer, final Conc }); options.getHintString().ifPresent(value -> writer.writeString("hint", value)); options.isUpsert().ifPresent(value -> writer.writeBoolean("upsert", value)); + options.getSort().ifPresent(value -> { + writer.writeName("sort"); + encodeUsingRegistry(writer, value); + }); } private void encodeWriteModelInternals(final BsonWriter writer, final AbstractClientDeleteModel model) { diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-replaceOne-sort.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-replaceOne-sort.json new file mode 100644 index 00000000000..b86bc5f9429 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-replaceOne-sort.json @@ -0,0 +1,163 @@ +{ + "description": "client bulkWrite updateOne-sort", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite replaceOne with sort option", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "replaceOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "replacement": { + "x": 1 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "ops": [ + { + "update": 0, + "filter": { + "_id": { + "$gt": 1 + } + }, + "updateMods": { + "x": 1 + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "nErrors": 0, + "nMatched": 1, + "nModified": 1 + }, + "commandName": "bulkWrite" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 1 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-updateOne-sort.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-updateOne-sort.json new file mode 100644 index 00000000000..ef75dcb3741 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-updateOne-sort.json @@ -0,0 +1,167 @@ +{ + "description": "client bulkWrite updateOne-sort", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite updateOne with sort option", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "ops": [ + { + "update": 0, + "filter": { + "_id": { + "$gt": 1 + } + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "sort": { + "_id": -1 + }, + "multi": { + "$$unsetOrMatches": false + }, + "upsert": { + "$$unsetOrMatches": false + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "nErrors": 0, + "nMatched": 1, + "nModified": 1 + }, + "commandName": "bulkWrite" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 34 + } + ] + } + ] + } + ] +} diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java index 5c925d97272..735c35dc9ed 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java @@ -134,6 +134,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.singleton; import static java.util.Objects.requireNonNull; +import static java.util.Optional.ofNullable; import static java.util.stream.Collectors.toList; @SuppressWarnings("deprecation") @@ -1607,7 +1608,7 @@ private static SearchIndexModel toIndexSearchModel(final BsonValue bsonValue) { BsonDocument model = bsonValue.asDocument(); BsonDocument definition = model.getDocument("definition"); SearchIndexType type = model.containsKey("type") ? getSearchIndexType(model.getString("type")) : null; - String name = Optional.ofNullable(model.getString("name", null)) + String name = ofNullable(model.getString("name", null)) .map(BsonString::getValue). orElse(null); return new SearchIndexModel(name, definition, type); @@ -1616,7 +1617,7 @@ private static SearchIndexModel toIndexSearchModel(final BsonValue bsonValue) { OperationResult executeListSearchIndexes(final BsonDocument operation) { MongoCollection collection = getMongoCollection(operation); - Optional arguments = Optional.ofNullable(operation.getOrDefault("arguments", null)).map(BsonValue::asDocument); + Optional arguments = ofNullable(operation.getOrDefault("arguments", null)).map(BsonValue::asDocument); if (arguments.isPresent()) { ListSearchIndexesIterable iterable = createListSearchIndexesIterable(collection, arguments.get()); @@ -1634,7 +1635,7 @@ OperationResult executeListSearchIndexes(final BsonDocument operation) { private ListSearchIndexesIterable createListSearchIndexesIterable(final MongoCollection collection, final BsonDocument arguments) { - Optional name = Optional.ofNullable(arguments.getOrDefault("name", null)) + Optional name = ofNullable(arguments.getOrDefault("name", null)) .map(BsonValue::asString).map(BsonString::getValue); ListSearchIndexesIterable iterable = collection.listSearchIndexes(BsonDocument.class); @@ -1930,6 +1931,9 @@ private static ClientReplaceOneOptions getClientReplaceOneOptions(final BsonDocu case "upsert": options.upsert(argument.asBoolean().getValue()); break; + case "sort": + options.sort(argument.asDocument()); + break; default: throw new UnsupportedOperationException(format("Unsupported argument: key=%s, argument=%s", key, argument)); } @@ -1938,7 +1942,16 @@ private static ClientReplaceOneOptions getClientReplaceOneOptions(final BsonDocu } private static ClientUpdateOneOptions getClientUpdateOneOptions(final BsonDocument arguments) { - return fillAbstractClientUpdateOptions(new ConcreteClientUpdateOneOptions(), arguments); + ConcreteClientUpdateOneOptions options = new ConcreteClientUpdateOneOptions(); + + if (arguments.containsKey("sort")) { + BsonDocument sort = arguments + .remove("sort") + .asDocument(); + options.sort(sort); + } + + return fillAbstractClientUpdateOptions(options, arguments); } private static ClientUpdateManyOptions getClientUpdateManyOptions(final BsonDocument arguments) { From 470fc2eafeced2ff0444e770b044c5f01a1b79c9 Mon Sep 17 00:00:00 2001 From: joykim1005 <81274936+joykim1005@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:59:10 -0500 Subject: [PATCH 16/43] Add in operator to Atlas Search (#1605) JAVA-5744 --------- Co-authored-by: Valentin Kovalenko Co-authored-by: Maxim Katcharov Co-authored-by: Valentin Kovalenko --- .../main/org/bson/codecs/BsonArrayCodec.java | 12 +- .../mongodb/client/model/mql/MqlValues.java | 30 ++--- .../client/model/search/InSearchOperator.java | 41 ++++++ .../SearchConstructibleBsonElement.java | 2 +- .../client/model/search/SearchOperator.java | 124 +++++++++++++++++- .../AggregatesSearchIntegrationTest.java | 3 + .../model/search/SearchOperatorTest.java | 93 ++++++++++++- .../scala/model/search/SearchOperator.scala | 95 +++++++++++++- .../mongodb/scala/model/search/package.scala | 7 + 9 files changed, 371 insertions(+), 36 deletions(-) create mode 100644 driver-core/src/main/com/mongodb/client/model/search/InSearchOperator.java diff --git a/bson/src/main/org/bson/codecs/BsonArrayCodec.java b/bson/src/main/org/bson/codecs/BsonArrayCodec.java index 2efe3147d8a..9b4bef5e4c5 100644 --- a/bson/src/main/org/bson/codecs/BsonArrayCodec.java +++ b/bson/src/main/org/bson/codecs/BsonArrayCodec.java @@ -23,9 +23,6 @@ import org.bson.BsonWriter; import org.bson.codecs.configuration.CodecRegistry; -import java.util.ArrayList; -import java.util.List; - import static org.bson.assertions.Assertions.notNull; import static org.bson.codecs.configuration.CodecRegistries.fromProviders; @@ -60,16 +57,13 @@ public BsonArrayCodec(final CodecRegistry codecRegistry) { @Override public BsonArray decode(final BsonReader reader, final DecoderContext decoderContext) { + BsonArray bsonArray = new BsonArray(); reader.readStartArray(); - - List list = new ArrayList<>(); while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { - list.add(readValue(reader, decoderContext)); + bsonArray.add(readValue(reader, decoderContext)); } - reader.readEndArray(); - - return new BsonArray(list); + return bsonArray; } @Override diff --git a/driver-core/src/main/com/mongodb/client/model/mql/MqlValues.java b/driver-core/src/main/com/mongodb/client/model/mql/MqlValues.java index a2d58fbc02b..e3e2bbd56a2 100644 --- a/driver-core/src/main/com/mongodb/client/model/mql/MqlValues.java +++ b/driver-core/src/main/com/mongodb/client/model/mql/MqlValues.java @@ -74,11 +74,11 @@ public static MqlBoolean of(final boolean of) { */ public static MqlArray ofBooleanArray(final boolean... array) { Assertions.notNull("array", array); - List list = new ArrayList<>(); + BsonArray bsonArray = new BsonArray(); for (boolean b : array) { - list.add(new BsonBoolean(b)); + bsonArray.add(new BsonBoolean(b)); } - return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list))); + return new MqlExpression<>((cr) -> new AstPlaceholder(bsonArray)); } /** @@ -102,11 +102,11 @@ public static MqlInteger of(final int of) { */ public static MqlArray ofIntegerArray(final int... array) { Assertions.notNull("array", array); - List list = new ArrayList<>(); + BsonArray bsonArray = new BsonArray(); for (int i : array) { - list.add(new BsonInt32(i)); + bsonArray.add(new BsonInt32(i)); } - return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list))); + return new MqlExpression<>((cr) -> new AstPlaceholder(bsonArray)); } /** @@ -130,11 +130,11 @@ public static MqlInteger of(final long of) { */ public static MqlArray ofIntegerArray(final long... array) { Assertions.notNull("array", array); - List list = new ArrayList<>(); + BsonArray bsonArray = new BsonArray(); for (long i : array) { - list.add(new BsonInt64(i)); + bsonArray.add(new BsonInt64(i)); } - return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list))); + return new MqlExpression<>((cr) -> new AstPlaceholder(bsonArray)); } /** @@ -158,11 +158,11 @@ public static MqlNumber of(final double of) { */ public static MqlArray ofNumberArray(final double... array) { Assertions.notNull("array", array); - List list = new ArrayList<>(); + BsonArray bsonArray = new BsonArray(); for (double n : array) { - list.add(new BsonDouble(n)); + bsonArray.add(new BsonDouble(n)); } - return new MqlExpression<>((cr) -> new AstPlaceholder(new BsonArray(list))); + return new MqlExpression<>((cr) -> new AstPlaceholder(bsonArray)); } /** @@ -310,12 +310,12 @@ public static MqlDocument current() { public static MqlArray ofArray(final T... array) { Assertions.notNull("array", array); return new MqlExpression<>((cr) -> { - List list = new ArrayList<>(); + BsonArray bsonArray = new BsonArray(); for (T v : array) { Assertions.notNull("elements of array", v); - list.add(((MqlExpression) v).toBsonValue(cr)); + bsonArray.add(((MqlExpression) v).toBsonValue(cr)); } - return new AstPlaceholder(new BsonArray(list)); + return new AstPlaceholder(bsonArray); }); } diff --git a/driver-core/src/main/com/mongodb/client/model/search/InSearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/InSearchOperator.java new file mode 100644 index 00000000000..4719d1b0bc6 --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/search/InSearchOperator.java @@ -0,0 +1,41 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.mongodb.client.model.search; + +import com.mongodb.annotations.Beta; +import com.mongodb.annotations.Reason; +import com.mongodb.annotations.Sealed; +import org.bson.types.ObjectId; + +import java.time.Instant; +import java.util.UUID; + +/** + * @see SearchOperator#in(FieldSearchPath, boolean, boolean...) + * @see SearchOperator#in(FieldSearchPath, ObjectId, ObjectId...) + * @see SearchOperator#in(FieldSearchPath, Number, Number...) + * @see SearchOperator#in(FieldSearchPath, Instant, Instant...) + * @see SearchOperator#in(FieldSearchPath, UUID, UUID...) + * @see SearchOperator#in(FieldSearchPath, String, String...) + * @see SearchOperator#in(FieldSearchPath, Iterable) + * @since 5.3 + */ +@Sealed +@Beta(Reason.CLIENT) +public interface InSearchOperator extends SearchOperator { + @Override + InSearchOperator score(SearchScore modifier); +} diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java index 6c78d7e16d7..c1a37597c1a 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchConstructibleBsonElement.java @@ -32,7 +32,7 @@ final class SearchConstructibleBsonElement extends AbstractConstructibleBsonElem MustCompoundSearchOperator, MustNotCompoundSearchOperator, ShouldCompoundSearchOperator, FilterCompoundSearchOperator, ExistsSearchOperator, TextSearchOperator, AutocompleteSearchOperator, NumberNearSearchOperator, DateNearSearchOperator, GeoNearSearchOperator, - EqualsSearchOperator, MoreLikeThisSearchOperator, + EqualsSearchOperator, InSearchOperator, MoreLikeThisSearchOperator, RegexSearchOperator, QueryStringSearchOperator, WildcardSearchOperator, ValueBoostSearchScore, PathBoostSearchScore, ConstantSearchScore, FunctionSearchScore, GaussSearchScoreExpression, PathSearchScoreExpression, diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java index e457454db0c..ef5c1239313 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java @@ -20,27 +20,26 @@ import com.mongodb.annotations.Sealed; import com.mongodb.client.model.Aggregates; import com.mongodb.client.model.geojson.Point; - -import java.util.UUID; - +import org.bson.BsonArray; import org.bson.BsonBinary; -import org.bson.BsonNull; +import org.bson.BsonBoolean; import org.bson.BsonDocument; +import org.bson.BsonNull; import org.bson.BsonType; import org.bson.Document; import org.bson.conversions.Bson; +import org.bson.types.ObjectId; import java.time.Duration; import java.time.Instant; import java.util.Iterator; - -import org.bson.types.ObjectId; +import java.util.UUID; import static com.mongodb.assertions.Assertions.isTrueArgument; +import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.Iterables.concat; import static com.mongodb.internal.client.model.Util.combineToBsonValue; import static java.util.Collections.singleton; -import static com.mongodb.assertions.Assertions.notNull; /** * The core part of the {@link Aggregates#search(SearchOperator, SearchOptions) $search} pipeline stage of an aggregation pipeline. @@ -300,6 +299,117 @@ static GeoNearSearchOperator near(final Point origin, final Number pivot, final .append("pivot", notNull("pivot", pivot))); } + /** + * Returns a {@link SearchOperator} that searches for documents where the + * value or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The boolean value to search for. + * @param values More fields to be searched. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/in/ in operator + */ + static InSearchOperator in(final FieldSearchPath path, final boolean value, final boolean... values) { + notNull("values", values); + BsonArray bsonArray = new BsonArray(); + bsonArray.add(new BsonBoolean(value)); + for (boolean v : values) { + bsonArray.add(new BsonBoolean(v)); + } + return in(notNull("path", path), bsonArray); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where the + * value or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The objectId value to search for. + * @param values More fields to be searched. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/in/ in operator + */ + static InSearchOperator in(final FieldSearchPath path, final ObjectId value, final ObjectId... values) { + return in(notNull("path", path), concat(notNull("value", value), values)); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where the + * value or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The number value to search for. + * @param values More fields to be searched. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/in/ in operator + */ + static InSearchOperator in(final FieldSearchPath path, final Number value, final Number... values) { + return in(notNull("path", path), concat(notNull("value", value), values)); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where the + * value or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The instant date value to search for. + * @param values More fields to be searched. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/in/ in operator + */ + static InSearchOperator in(final FieldSearchPath path, final Instant value, final Instant... values) { + return in(notNull("path", path), concat(notNull("value", value), values)); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where the + * value or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The uuid value to search for. + * @param values More fields to be searched. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/in/ in operator + */ + static InSearchOperator in(final FieldSearchPath path, final UUID value, final UUID... values) { + return in(notNull("path", path), concat(notNull("value", value), values)); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where the + * value or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The string value to search for. + * @param values More fields to be searched. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/in/ in operator + */ + static InSearchOperator in(final FieldSearchPath path, final String value, final String... values) { + return in(notNull("path", path), concat(notNull("value", value), values)); + } + + /** + * Returns a {@link SearchOperator} that searches for documents where the + * value or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param values The non-empty values to search for. Value can be either a single value or an array of values of only one of the supported BSON types and can't be a mix of different types. + * @param the type of elements in {@code values}. + * @return The requested {@link SearchOperator}. + * @mongodb.atlas.manual atlas-search/in/ in operator + */ + static InSearchOperator in(final FieldSearchPath path, final Iterable values) { + notNull("path", path); + Iterator valueIterator = notNull("values", values).iterator(); + isTrueArgument("values must not be empty", valueIterator.hasNext()); + T firstValue = valueIterator.next(); + boolean hasMore = valueIterator.hasNext(); + return new SearchConstructibleBsonElement("in", new Document() + .append("path", path.toValue()) + .append("value", hasMore ? values : firstValue)); + } + /** * Returns a {@link SearchOperator} that searches for documents where a field matches the specified value. * diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java index d90f4a0575c..3fe4229b498 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java @@ -82,6 +82,7 @@ import static com.mongodb.client.model.search.SearchOperator.dateRange; import static com.mongodb.client.model.search.SearchOperator.equalsNull; import static com.mongodb.client.model.search.SearchOperator.exists; +import static com.mongodb.client.model.search.SearchOperator.in; import static com.mongodb.client.model.search.SearchOperator.moreLikeThis; import static com.mongodb.client.model.search.SearchOperator.near; import static com.mongodb.client.model.search.SearchOperator.numberRange; @@ -617,6 +618,8 @@ private static Stream searchAndSearchMetaArgs() { near(0, 1.5, fieldPath("fieldName7"), fieldPath("fieldName8")), near(Instant.ofEpochMilli(1), Duration.ofMillis(3), fieldPath("fieldName9")), phrase(fieldPath("fieldName10"), "term6"), + in(fieldPath("fieldName10"), true), + in(fieldPath("fieldName11"), "term4", "term5"), regex(fieldPath("title").multi("keyword"), "term7"), queryString(fieldPath("fieldName12"), "term8"), moreLikeThis(new BsonDocument("like", new BsonDocument("fieldName10", diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java index 06a825c6125..9151768da31 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java @@ -18,9 +18,6 @@ import com.mongodb.MongoClientSettings; import com.mongodb.client.model.geojson.Point; import com.mongodb.client.model.geojson.Position; - -import java.util.UUID; - import org.bson.BsonArray; import org.bson.BsonBinary; import org.bson.BsonBoolean; @@ -33,11 +30,15 @@ import org.bson.BsonObjectId; import org.bson.BsonString; import org.bson.Document; +import org.bson.UuidRepresentation; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.conversions.Bson; import org.bson.types.ObjectId; import org.junit.jupiter.api.Test; import java.time.Duration; import java.time.Instant; +import java.util.UUID; import static com.mongodb.client.model.search.FuzzySearchOptions.fuzzySearchOptions; import static com.mongodb.client.model.search.SearchPath.fieldPath; @@ -589,6 +590,92 @@ void near() { ); } + @Test + void in() { + ObjectId objectId = new ObjectId(); + UUID uuid = UUID.randomUUID(); + assertAll( + () -> assertThrows(IllegalArgumentException.class, () -> + // paths must not be empty + SearchOperator.in(null, true) + ), + () -> assertEquals( + new BsonDocument("in", + new BsonDocument("path", fieldPath("fieldName1").toBsonValue()) + .append("value", new BsonBoolean(true)) + ), + SearchOperator.in(fieldPath("fieldName1"), true) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("in", + new BsonDocument("path", fieldPath("fieldName1").toBsonValue()) + .append("value", new BsonArray(asList(new BsonBoolean(true), new BsonBoolean(false)))) + ), + SearchOperator.in(fieldPath("fieldName1"), asList(true, false)) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("in", + new BsonDocument("path", fieldPath("fieldName1").toBsonValue()) + .append("value", new BsonObjectId(objectId)) + ), + SearchOperator.in(fieldPath("fieldName1"), objectId) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("in", + new BsonDocument("path", fieldPath("fieldName1").toBsonValue()) + .append("value", new BsonInt32(1)) + ), + SearchOperator.in(fieldPath("fieldName1"), 1) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("in", + new BsonDocument("path", fieldPath("fieldName1").toBsonValue()) + .append("value", new BsonInt64(Long.MAX_VALUE)) + ), + SearchOperator.in(fieldPath("fieldName1"), Long.MAX_VALUE) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("in", + new BsonDocument("path", fieldPath("fieldName1").toBsonValue()) + .append("value", new BsonDouble(Double.MAX_VALUE)) + ), + SearchOperator.in(fieldPath("fieldName1"), Double.MAX_VALUE) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("in", + new BsonDocument("path", fieldPath("fieldName1").toBsonValue()) + .append("value", new BsonDateTime(Instant.EPOCH.toEpochMilli())) + ), + SearchOperator.in(fieldPath("fieldName1"), Instant.EPOCH) + .toBsonDocument() + ), + () -> assertEquals( + new BsonDocument("in", + new BsonDocument("path", fieldPath("fieldName1").toBsonValue()) + .append("value", new BsonBinary(uuid)) + ), + SearchOperator.in(fieldPath("fieldName1"), uuid) + .toBsonDocument( + Document.class, + CodecRegistries.withUuidRepresentation(Bson.DEFAULT_CODEC_REGISTRY, UuidRepresentation.STANDARD)) + ), + () -> assertEquals( + new BsonDocument("in", + new BsonDocument("path", fieldPath("fieldName1").toBsonValue()) + .append("value", new BsonString("value")) + ), + SearchOperator.in(fieldPath("fieldName1"), "value") + .toBsonDocument() + ) + ); + } + @Test void equals() { ObjectId objectId = new ObjectId(); diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala index 500724d4d31..6f4f6a8860d 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala @@ -17,6 +17,9 @@ package org.mongodb.scala.model.search import com.mongodb.annotations.{ Beta, Reason } import com.mongodb.client.model.search.{ SearchOperator => JSearchOperator } + +import java.util.UUID + import org.mongodb.scala.bson.BsonDocument import org.mongodb.scala.bson.conversions.Bson import org.mongodb.scala.model.geojson.Point @@ -24,7 +27,7 @@ import org.mongodb.scala.model.geojson.Point import org.bson.types.ObjectId; import java.time.{ Duration, Instant } -import java.util.UUID + import collection.JavaConverters._ /** @@ -232,6 +235,96 @@ object SearchOperator { def near(origin: Point, pivot: Number, paths: Iterable[_ <: FieldSearchPath]): GeoNearSearchOperator = JSearchOperator.near(origin, pivot, paths.asJava) + /** + * Returns a `SearchOperator` that searches for documents where the value + * or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The boolean value to search for. + * @param values More fields to be searched. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/in/ in operator]] + */ + def in(path: FieldSearchPath, value: Boolean, values: Boolean*): InSearchOperator = + JSearchOperator.in(path, value, values: _*) + + /** + * Returns a `SearchOperator` that searches for documents where the value + * or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The objectId value to search for. + * @param values More fields to be searched. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/in/ in operator]] + */ + def in(path: FieldSearchPath, value: ObjectId, values: ObjectId*): InSearchOperator = + JSearchOperator.in(path, value, values: _*) + + /** + * Returns a `SearchOperator` that searches for documents where the value + * or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The number value to search for. + * @param values More fields to be searched. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/in/ in operator]] + */ + def in(path: FieldSearchPath, value: Number, values: Number*): InSearchOperator = + JSearchOperator.in(path, value, values: _*) + + /** + * Returns a `SearchOperator` that searches for documents where the value + * or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The instant date value to search for. + * @param values More fields to be searched. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/in/ in operator]] + */ + def in(path: FieldSearchPath, value: Instant, values: Instant*): InSearchOperator = + JSearchOperator.in(path, value, values: _*) + + /** + * Returns a `SearchOperator` that searches for documents where the value + * or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The uuid value to search for. + * @param values More fields to be searched. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/in/ in operator]] + */ + def in(path: FieldSearchPath, value: UUID, values: UUID*): InSearchOperator = + JSearchOperator.in(path, value, values: _*) + + /** + * Returns a `SearchOperator` that searches for documents where the value + * or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param value The string value to search for. + * @param values More fields to be searched. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/in/ in operator]] + */ + def in(path: FieldSearchPath, value: String, values: String*): InSearchOperator = + JSearchOperator.in(path, value, values: _*) + + /** + * Returns a `SearchOperator` that searches for documents where the value + * or array of values at a given path contains any of the specified values + * + * @param path The indexed field to be searched. + * @param values The non-empty values to search for. Value can be either a single value or an array of values of only one of the supported BSON types and can't be a mix of different types. + * @return The requested `SearchOperator`. + * @see [[https://www.mongodb.com/docs/atlas/atlas-search/in/ in operator]] + */ + def in[T](path: FieldSearchPath, values: Iterable[_ <: T]): InSearchOperator = + JSearchOperator.in(path, values.asJava) + /** * Returns a `SearchOperator` that searches for documents where a field matches the specified value. * diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala index 0a0c7662c02..771e800801d 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/package.scala @@ -198,6 +198,13 @@ package object search { @Beta(Array(Reason.CLIENT)) type GeoNearSearchOperator = com.mongodb.client.model.search.GeoNearSearchOperator + /** + * @see `SearchOperator.in` + */ + @Sealed + @Beta(Array(Reason.CLIENT)) + type InSearchOperator = com.mongodb.client.model.search.InSearchOperator + /** * @see `SearchOperator.equals` */ From 80fbcb134c67e30cd35f12554fc1a05b13cd2496 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 5 Feb 2025 10:44:15 +0000 Subject: [PATCH 17/43] Evergreen: Update to use stop-orchestration.sh (#1621) JAVA-5778 --- .evergreen/.evg.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index a57f6473b6f..ed22ca27972 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -794,15 +794,7 @@ functions: shell: bash script: | ${PREPARE_SHELL} - cd "$MONGO_ORCHESTRATION_HOME" - # source the mongo-orchestration virtualenv if it exists - if [ -f venv/bin/activate ]; then - . venv/bin/activate - elif [ -f venv/Scripts/activate ]; then - . venv/Scripts/activate - fi - mongo-orchestration stop || true - cd - + bash ${DRIVERS_TOOLS}/.evergreen/stop-orchestration.sh || true rm -rf $DRIVERS_TOOLS || true "fix absolute paths": From bedcdae5217eb8c447d3ae0c5818348665e85a6a Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Wed, 29 Jan 2025 17:23:13 +0000 Subject: [PATCH 18/43] Gradle bump to 7.6.4 --- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 59536 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 257 ++++++++++++++--------- 3 files changed, 154 insertions(+), 105 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..7454180f2ae8848c63b8b4dea2cb829da983f2fa 100644 GIT binary patch delta 18435 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 B8dv}T delta 17845 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 X49Luk)baa85H9$5YCsTPT`SVRWMtMW diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e750102e092..3994438e229 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 4f906e0c811..1b6c787337f 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,67 +17,101 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=`expr $i + 1` + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" From e80cb0bf803753bd17cb0df5c0f124e9d913db9a Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 30 Jan 2025 11:04:16 +0000 Subject: [PATCH 19/43] Gradle updated to use libs.versions.toml JAVA-5291 --- bson-kotlin/build.gradle.kts | 16 +- bson-kotlinx/build.gradle.kts | 32 ++-- build.gradle | 111 +++++--------- driver-benchmarks/build.gradle | 2 +- driver-core/build.gradle | 37 ++--- driver-kotlin-coroutine/build.gradle.kts | 33 ++-- driver-kotlin-extensions/build.gradle.kts | 22 +-- driver-kotlin-sync/build.gradle.kts | 25 ++- driver-lambda/build.gradle | 22 +-- driver-reactive-streams/build.gradle | 15 +- driver-sync/build.gradle | 2 +- driver-workload-executor/build.gradle | 20 +-- graalvm-native-image-app/build.gradle | 15 +- gradle/libs.versions.toml | 178 ++++++++++++++++++++++ mongodb-crypt/build.gradle.kts | 17 +-- 15 files changed, 326 insertions(+), 221 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/bson-kotlin/build.gradle.kts b/bson-kotlin/build.gradle.kts index 45e8c9c0e5d..84b6deabd31 100644 --- a/bson-kotlin/build.gradle.kts +++ b/bson-kotlin/build.gradle.kts @@ -18,12 +18,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.jetbrains.kotlin.jvm") - `java-library` + id("java-library") // Test based plugins - id("com.diffplug.spotless") - id("org.jetbrains.dokka") - id("io.gitlab.arturbosch.detekt") + alias(libs.plugins.spotless) + alias(libs.plugins.dokka) + alias(libs.plugins.detekt) } repositories { @@ -39,13 +39,13 @@ ext.set("pomName", "Bson Kotlin") dependencies { // Align versions of all Kotlin components - implementation(platform("org.jetbrains.kotlin:kotlin-bom")) - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(platform(libs.kotlin.bom)) + implementation(libs.kotlin.stdlib.jdk8) api(project(path = ":bson", configuration = "default")) - implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation(libs.kotlin.reflect) - testImplementation("org.jetbrains.kotlin:kotlin-test-junit") + testImplementation(libs.junit.kotlin) testImplementation(project(path = ":driver-core", configuration = "default")) } diff --git a/bson-kotlinx/build.gradle.kts b/bson-kotlinx/build.gradle.kts index ac0b07f18eb..d2b3e13919b 100644 --- a/bson-kotlinx/build.gradle.kts +++ b/bson-kotlinx/build.gradle.kts @@ -19,12 +19,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.jetbrains.kotlin.jvm") kotlin("plugin.serialization") - `java-library` + id("java-library") // Test based plugins - id("com.diffplug.spotless") - id("org.jetbrains.dokka") - id("io.gitlab.arturbosch.detekt") + alias(libs.plugins.spotless) + alias(libs.plugins.dokka) + alias(libs.plugins.detekt) } repositories { @@ -38,10 +38,6 @@ description = "Bson Kotlinx Codecs" ext.set("pomName", "Bson Kotlinx") -ext.set("kotlinxDatetimeVersion", "0.4.0") - -val kotlinxDatetimeVersion: String by ext - java { registerFeature("dateTimeSupport") { usingSourceSet(sourceSets["main"]) } registerFeature("jsonSupport") { usingSourceSet(sourceSets["main"]) } @@ -49,21 +45,21 @@ java { dependencies { // Align versions of all Kotlin components - implementation(platform("org.jetbrains.kotlin:kotlin-bom")) - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(platform(libs.kotlin.bom)) + implementation(libs.kotlin.stdlib.jdk8) - implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.5.0")) - implementation("org.jetbrains.kotlinx:kotlinx-serialization-core") - "dateTimeSupportImplementation"("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDatetimeVersion") - "jsonSupportImplementation"("org.jetbrains.kotlinx:kotlinx-serialization-json") + implementation(platform(libs.kotlinx.serialization)) + implementation(libs.kotlinx.serialization.core) + "dateTimeSupportImplementation"(libs.kotlinx.serialization.datetime) + "jsonSupportImplementation"(libs.kotlinx.serialization.json) api(project(path = ":bson", configuration = "default")) - implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation(libs.kotlin.reflect) - testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation(project(path = ":driver-core", configuration = "default")) - testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDatetimeVersion") - testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") + testImplementation(libs.junit.kotlin) + testImplementation(libs.kotlinx.serialization.datetime) + testImplementation(libs.kotlinx.serialization.json) } kotlin { explicitApi() } diff --git a/build.gradle b/build.gradle index 26734755984..b89624d4c4a 100644 --- a/build.gradle +++ b/build.gradle @@ -14,33 +14,21 @@ * limitations under the License. */ -apply plugin: 'eclipse' -apply plugin: 'idea' - -buildscript { - repositories { - mavenLocal() - mavenCentral() - maven { url "https://plugins.gradle.org/m2/" } - } - dependencies { - classpath 'com.netflix.nebula:gradle-extra-configurations-plugin:7.0.0' - classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.15" - classpath 'biz.aQute.bnd:biz.aQute.bnd.gradle:5.1.2' - - // Scala plugins - classpath "com.adtran:scala-multiversion-plugin:2.0.4" - classpath "com.diffplug.spotless:spotless-plugin-gradle:6.14.0" - - // kotlin plugins - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10" - classpath "org.jetbrains.kotlin:kotlin-serialization:1.8.10" - classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.8.10" - classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.21.0" - - // Test logging plugin - classpath 'com.adarshr:gradle-test-logger-plugin:2.1.0' - } +plugins { + id("eclipse") + id("idea") + + alias(libs.plugins.bnd) apply false + alias(libs.plugins.detekt) apply false + alias(libs.plugins.dokka) apply false + alias(libs.plugins.download) apply false + alias(libs.plugins.kotlin) apply false + alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.optional) apply false + alias(libs.plugins.scala.multiversion) apply false + alias(libs.plugins.spotbugs) apply false + alias(libs.plugins.spotless) apply false + alias(libs.plugins.test.logger) apply false } ////////////////////////////////////////// @@ -49,17 +37,6 @@ buildscript { ext { configDir = new File(rootDir, 'config') - jnrUnixsocketVersion = '0.38.17' - nettyVersion = '4.1.87.Final' - snappyVersion = '1.1.10.3' - zstdVersion = '1.5.5-3' - awsSdkV2Version = '2.18.9' - awsSdkV1Version = '1.12.337' - projectReactorVersion = '2022.0.0' - junitBomVersion = '5.10.2' - logbackVersion = '1.3.14' - graalSdkVersion = '24.0.0' - reflectionsVersion = '0.9.10' gitVersion = getGitVersion() } @@ -109,27 +86,21 @@ configure(javaProjects) { } configure(scalaProjects) { - apply plugin: 'scala' - apply plugin: 'java-library' - apply plugin: 'idea' + apply plugin: "java-library" + apply plugin: "scala" apply plugin: "com.adtran.scala-multiversion-plugin" apply plugin: "com.diffplug.spotless" group = 'org.mongodb.scala' dependencies { - api ('org.scala-lang:scala-library:%scala-version%') - api ('org.scala-lang:scala-reflect:%scala-version%') - - testImplementation(platform("org.junit:junit-bom:$junitBomVersion")) - testImplementation("org.junit.vintage:junit-vintage-engine") - - testImplementation('org.scalatest:scalatest-flatspec_%%:3.2.9') - testImplementation('org.scalatest:scalatest-shouldmatchers_%%:3.2.9') - testImplementation('org.scalatestplus:junit-4-13_%%:3.2.9.0') - testImplementation('org.scalatestplus:mockito-3-12_%%:3.2.10.0') - testImplementation("ch.qos.logback:logback-classic:$logbackVersion") - testImplementation("org.reflections:reflections:$reflectionsVersion") + api(libs.scala.library) + api(libs.scala.reflect) + + testImplementation(platform(libs.junit.bom)) + testImplementation(libs.bundles.junit.vintage) + testImplementation(libs.bundles.scalatest) + testImplementation(libs.reflections) } test{ @@ -177,12 +148,12 @@ configure(javaMainProjects) { apply plugin: 'java-library' dependencies { - compileOnly 'com.google.code.findbugs:jsr305:1.3.9' - api 'org.slf4j:slf4j-api:1.7.6', optional - testImplementation 'com.google.code.findbugs:jsr305:1.3.9' + compileOnly(libs.findbugs.jsr) + api(libs.slf4j, optional) + testImplementation(libs.findbugs.jsr) // https://issues.apache.org/jira/browse/GROOVY-10194 - testImplementation 'org.codehaus.groovy:groovy-all:3.0.9' + testImplementation(libs.groovy) } /* Compiling */ @@ -251,25 +222,19 @@ configure(javaCodeCheckedProjects) { apply plugin: 'com.adarshr.test-logger' dependencies { - testImplementation(platform("org.junit:junit-bom:$junitBomVersion")) - testImplementation('org.junit.jupiter:junit-jupiter') - testImplementation('org.junit.jupiter:junit-jupiter-params') - testImplementation('org.junit.jupiter:junit-jupiter-engine') - testImplementation('org.junit.vintage:junit-vintage-engine') - - testImplementation platform('org.spockframework:spock-bom:2.1-groovy-3.0') - testImplementation 'org.spockframework:spock-core' - testImplementation 'org.spockframework:spock-junit4' + testImplementation(platform(libs.junit.bom)) + testImplementation(libs.bundles.junit.vintage) + + testImplementation(platform(libs.spock.bom)) + testImplementation(libs.bundles.spock) + if ('8'.equals(findProperty("javaVersion"))) { - testImplementation("org.mockito:mockito-core:4.6.1") - testImplementation("org.mockito:mockito-inline:4.6.1") + testImplementation(libs.bundles.mockito.java8) } else { - testImplementation("org.mockito:mockito-core:5.11.0") + testImplementation(libs.bundles.mockito) } - testImplementation 'cglib:cglib-nodep:2.2.2' - testImplementation 'org.objenesis:objenesis:1.3' - testImplementation 'org.hamcrest:hamcrest-all:1.3' - testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + testImplementation(libs.cglib) + testImplementation(libs.objenesis) testImplementation project(':util:spock') //Adding categories to classpath } diff --git a/driver-benchmarks/build.gradle b/driver-benchmarks/build.gradle index 91d979cff68..b8c5ac0b62b 100644 --- a/driver-benchmarks/build.gradle +++ b/driver-benchmarks/build.gradle @@ -32,7 +32,7 @@ sourceSets { dependencies { api project(':driver-sync') api project(':mongodb-crypt') - implementation "ch.qos.logback:logback-classic:$logbackVersion" + implementation(libs.logback.classic) } javadoc { diff --git a/driver-core/build.gradle b/driver-core/build.gradle index a44f65bbc1b..900015bf79e 100644 --- a/driver-core/build.gradle +++ b/driver-core/build.gradle @@ -16,7 +16,7 @@ plugins { - id 'com.github.gmazzo.buildconfig' version '3.0.3' + alias(libs.plugins.build.config) } archivesBaseName = 'mongodb-driver-core' @@ -33,7 +33,6 @@ configurations { } } -def classifiers = ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"] dependencies { api project(path: ':bson', configuration: 'default') implementation project(path: ':bson-record-codec', configuration: 'default') @@ -41,30 +40,26 @@ dependencies { implementation project(path: ':bson-kotlinx', configuration: 'default'), optional implementation project(path: ':mongodb-crypt', configuration: 'default'), optional - implementation "com.github.jnr:jnr-unixsocket:$jnrUnixsocketVersion", optional - api platform("io.netty:netty-bom:$nettyVersion") - api "io.netty:netty-buffer", optional - api "io.netty:netty-transport", optional - api "io.netty:netty-handler", optional - compileOnly "org.graalvm.sdk:graal-sdk:$graalSdkVersion" + implementation(libs.jnr.unixsocket, optional) + api(platform(libs.netty.bom), optional) + api(libs.bundles.netty, optional) + compileOnly(libs.graal.sdk) - // Optionally depend on both AWS SDK v2 and v1. The driver will use v2 is present, v1 if present, or built-in functionality if - // neither are present - implementation "software.amazon.awssdk:auth:$awsSdkV2Version", optional - implementation "software.amazon.awssdk:sts:$awsSdkV2Version", optional - implementation "com.amazonaws:aws-java-sdk-core:$awsSdkV1Version", optional - implementation "com.amazonaws:aws-java-sdk-sts:$awsSdkV1Version", optional + // Optionally depend on both AWS SDK v2 and v1. + // The driver will use v2 is present, v1 if present, or built-in functionality if neither are present + implementation(libs.bundles.aws.java.sdk.v1, optional) + implementation(libs.bundles.aws.java.sdk.v2, optional) - implementation "org.xerial.snappy:snappy-java:$snappyVersion", optional - implementation "com.github.luben:zstd-jni:$zstdVersion", optional + implementation(libs.snappy.java, optional) + implementation(libs.zstd.jni, optional) testImplementation project(':bson').sourceSets.test.output - testImplementation('org.junit.jupiter:junit-jupiter-api') - testImplementation("org.reflections:reflections:$reflectionsVersion") - testRuntimeOnly "io.netty:netty-tcnative-boringssl-static" + testImplementation(libs.bundles.junit) + testImplementation(libs.reflections) - classifiers.forEach { - testRuntimeOnly "io.netty:netty-tcnative-boringssl-static::$it" + testRuntimeOnly(libs.netty.boringssl) + ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"].each { arch -> + testRuntimeOnly(variantOf(libs.netty.boringssl) { classifier(arch) }) } } diff --git a/driver-kotlin-coroutine/build.gradle.kts b/driver-kotlin-coroutine/build.gradle.kts index 96ac4cc31eb..59ef13952f2 100644 --- a/driver-kotlin-coroutine/build.gradle.kts +++ b/driver-kotlin-coroutine/build.gradle.kts @@ -18,12 +18,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.jetbrains.kotlin.jvm") - `java-library` + id("java-library") // Test based plugins - id("com.diffplug.spotless") - id("org.jetbrains.dokka") - id("io.gitlab.arturbosch.detekt") + alias(libs.plugins.spotless) + alias(libs.plugins.dokka) + alias(libs.plugins.detekt) } repositories { @@ -56,26 +56,25 @@ val integrationTestImplementation: Configuration by dependencies { // Align versions of all Kotlin components - implementation(platform("org.jetbrains.kotlin:kotlin-bom")) - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(platform(libs.kotlin.bom)) + implementation(libs.kotlin.stdlib.jdk8) - implementation(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) - api("org.jetbrains.kotlinx:kotlinx-coroutines-core") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive") + implementation(platform(libs.kotlinx.coroutines.bom)) + api(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.reactive) api(project(path = ":bson", configuration = "default")) api(project(path = ":driver-reactive-streams", configuration = "default")) implementation(project(path = ":bson-kotlin", configuration = "default")) - testImplementation("org.jetbrains.kotlin:kotlin-reflect") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit") - testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") - testImplementation("org.mockito:mockito-junit-jupiter:4.11.0") - testImplementation("org.assertj:assertj-core:3.24.2") - testImplementation("io.github.classgraph:classgraph:4.8.154") + testImplementation(libs.kotlin.reflect) + testImplementation(libs.junit.kotlin) + testImplementation(libs.bundles.mockito.kotlin) + testImplementation(libs.assertj) + testImplementation(libs.classgraph) - integrationTestImplementation("org.jetbrains.kotlin:kotlin-test-junit") - integrationTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test") + integrationTestImplementation(libs.junit.kotlin) + integrationTestImplementation(libs.kotlinx.coroutines.test) integrationTestImplementation(project(path = ":driver-sync")) integrationTestImplementation(project(path = ":driver-core")) integrationTestImplementation(project(path = ":bson")) diff --git a/driver-kotlin-extensions/build.gradle.kts b/driver-kotlin-extensions/build.gradle.kts index 76f36ca33b2..fc0e134749a 100644 --- a/driver-kotlin-extensions/build.gradle.kts +++ b/driver-kotlin-extensions/build.gradle.kts @@ -18,12 +18,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.jetbrains.kotlin.jvm") - `java-library` + id("java-library") // Test based plugins - id("com.diffplug.spotless") - id("org.jetbrains.dokka") - id("io.gitlab.arturbosch.detekt") + alias(libs.plugins.spotless) + alias(libs.plugins.dokka) + alias(libs.plugins.detekt) } repositories { @@ -41,8 +41,8 @@ java { registerFeature("kotlinDrivers") { usingSourceSet(sourceSets["main"]) } } dependencies { // Align versions of all Kotlin components - implementation(platform("org.jetbrains.kotlin:kotlin-bom")) - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(platform(libs.kotlin.bom)) + implementation(libs.kotlin.stdlib.jdk8) api(project(path = ":driver-core", configuration = "default")) @@ -51,11 +51,11 @@ dependencies { "kotlinDriversImplementation"(project(path = ":driver-kotlin-sync", configuration = "default")) "kotlinDriversImplementation"(project(path = ":driver-kotlin-coroutine", configuration = "default")) - testImplementation("org.jetbrains.kotlin:kotlin-reflect") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit") - testImplementation("org.assertj:assertj-core:3.24.2") - testImplementation("io.github.classgraph:classgraph:4.8.154") - testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") + testImplementation(libs.kotlin.reflect) + testImplementation(libs.junit.kotlin) + testImplementation(libs.bundles.mockito.kotlin) + testImplementation(libs.assertj) + testImplementation(libs.classgraph) } kotlin { explicitApi() } diff --git a/driver-kotlin-sync/build.gradle.kts b/driver-kotlin-sync/build.gradle.kts index e7fb132cb36..e0131613a97 100644 --- a/driver-kotlin-sync/build.gradle.kts +++ b/driver-kotlin-sync/build.gradle.kts @@ -18,12 +18,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.jetbrains.kotlin.jvm") - `java-library` + id("java-library") // Test based plugins - id("com.diffplug.spotless") - id("org.jetbrains.dokka") - id("io.gitlab.arturbosch.detekt") + alias(libs.plugins.spotless) + alias(libs.plugins.dokka) + alias(libs.plugins.detekt) } repositories { @@ -56,21 +56,20 @@ val integrationTestImplementation: Configuration by dependencies { // Align versions of all Kotlin components - implementation(platform("org.jetbrains.kotlin:kotlin-bom")) - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(platform(libs.kotlin.bom)) + implementation(libs.kotlin.stdlib.jdk8) api(project(path = ":bson", configuration = "default")) api(project(path = ":driver-sync", configuration = "default")) implementation(project(path = ":bson-kotlin", configuration = "default")) - testImplementation("org.jetbrains.kotlin:kotlin-reflect") - testImplementation("org.jetbrains.kotlin:kotlin-test-junit") - testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") - testImplementation("org.mockito:mockito-junit-jupiter:4.11.0") - testImplementation("org.assertj:assertj-core:3.24.2") - testImplementation("io.github.classgraph:classgraph:4.8.154") + testImplementation(libs.kotlin.reflect) + testImplementation(libs.junit.kotlin) + testImplementation(libs.bundles.mockito.kotlin) + testImplementation(libs.assertj) + testImplementation(libs.classgraph) - integrationTestImplementation("org.jetbrains.kotlin:kotlin-test-junit") + integrationTestImplementation(libs.junit.kotlin) integrationTestImplementation(project(path = ":driver-sync")) integrationTestImplementation(project(path = ":driver-core")) } diff --git a/driver-lambda/build.gradle b/driver-lambda/build.gradle index d7b9928e8f7..d1e4f624629 100644 --- a/driver-lambda/build.gradle +++ b/driver-lambda/build.gradle @@ -14,23 +14,13 @@ * limitations under the License. */ -buildscript { - repositories { - maven { url "https://plugins.gradle.org/m2/" } - } - dependencies { - classpath 'com.github.jengelman.gradle.plugins:shadow:6.1.0' - } -} plugins { + id("java") id("application") + alias(libs.plugins.shadow) } -apply plugin: 'application' -apply plugin: 'com.github.johnrengelman.shadow' -apply plugin: 'java' - compileJava { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -53,10 +43,10 @@ dependencies { implementation project(':driver-sync') implementation project(':bson') - implementation('com.amazonaws:aws-lambda-java-core:1.2.2') - implementation('com.amazonaws:aws-lambda-java-events:3.11.1') - implementation(platform("org.junit:junit-bom:$junitBomVersion")) - implementation('org.junit.jupiter:junit-jupiter-api') + implementation(libs.aws.lambda.core) + implementation(libs.aws.lambda.events) + implementation(platform(libs.junit.bom)) + implementation(libs.bundles.junit) } diff --git a/driver-reactive-streams/build.gradle b/driver-reactive-streams/build.gradle index 7b86f010484..f50a12de147 100644 --- a/driver-reactive-streams/build.gradle +++ b/driver-reactive-streams/build.gradle @@ -21,20 +21,21 @@ dependencies { api project(path: ':bson', configuration: 'default') api project(path: ':driver-core', configuration: 'default') - api 'org.reactivestreams:reactive-streams:1.0.4' - implementation platform("io.projectreactor:reactor-bom:$projectReactorVersion") - implementation 'io.projectreactor:reactor-core' + api(libs.reactive.streams) + implementation(platform(libs.project.reactor.bom)) + implementation(libs.project.reactor.core) testImplementation project(':bson').sourceSets.test.output testImplementation project(':driver-sync') testImplementation project(':driver-sync').sourceSets.test.output testImplementation project(':driver-core').sourceSets.test.output - testImplementation 'org.reactivestreams:reactive-streams-tck:1.0.4' - testImplementation 'io.projectreactor:reactor-test' + testImplementation(libs.reactive.streams.tck) + + testImplementation(libs.project.reactor.test) if ('8'.equals(findProperty("javaVersion"))) { - testImplementation 'org.mockito:mockito-junit-jupiter:4.6.1' + testImplementation(libs.bundles.mockito.java8) } else { - testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0' + testImplementation(libs.bundles.mockito) } testRuntimeOnly project(path: ':driver-core', configuration: 'consumableTestRuntimeOnly') diff --git a/driver-sync/build.gradle b/driver-sync/build.gradle index 1c2f3ac6c59..2ea76dbd829 100644 --- a/driver-sync/build.gradle +++ b/driver-sync/build.gradle @@ -28,7 +28,7 @@ dependencies { testImplementation project(':driver-core').sourceSets.test.output testRuntimeOnly project(path: ':driver-core', configuration: 'consumableTestRuntimeOnly') - testImplementation('com.amazonaws:aws-lambda-java-core:1.2.1') + testImplementation(libs.aws.lambda.core) } sourceSets { diff --git a/driver-workload-executor/build.gradle b/driver-workload-executor/build.gradle index 7c48e444dc2..8885e1fd59c 100644 --- a/driver-workload-executor/build.gradle +++ b/driver-workload-executor/build.gradle @@ -14,18 +14,12 @@ * limitations under the License. */ -buildscript { - repositories { - maven { url "https://plugins.gradle.org/m2/" } - } - dependencies { - classpath 'com.github.jengelman.gradle.plugins:shadow:6.1.0' - } +plugins { + id("java") + id("application") + alias(libs.plugins.shadow) } -apply plugin: 'application' -apply plugin: 'com.github.johnrengelman.shadow' - mainClassName = "com.mongodb.workload.WorkloadExecutor" sourceSets { @@ -43,10 +37,8 @@ dependencies { implementation project(':driver-sync') implementation project(':driver-core').sourceSets.test.output implementation project(':driver-sync').sourceSets.test.output - implementation "ch.qos.logback:logback-classic:$logbackVersion" - implementation(platform("org.junit:junit-bom:$junitBomVersion")) - implementation('org.junit.jupiter:junit-jupiter') - implementation('org.junit.vintage:junit-vintage-engine') + implementation(platform(libs.junit.bom)) + implementation(libs.bundles.junit.vintage) } javadoc { diff --git a/graalvm-native-image-app/build.gradle b/graalvm-native-image-app/build.gradle index b3d7335f9d9..e9a2c0433bb 100644 --- a/graalvm-native-image-app/build.gradle +++ b/graalvm-native-image-app/build.gradle @@ -17,8 +17,8 @@ // Note requires a Gradle project flag `-PincludeGraalvm` (see settings.gradle). plugins { - id 'application' - id 'org.graalvm.buildtools.native' version '0.9.23' + id("application") + alias(libs.plugins.graalvm.buildtools) } application { @@ -113,9 +113,10 @@ dependencies { implementation project(':driver-sync').sourceSets.test.output implementation project(':driver-legacy').sourceSets.test.output implementation project(':driver-reactive-streams').sourceSets.test.output - implementation 'org.slf4j:slf4j-api:2.0.12' - implementation "ch.qos.logback:logback-classic:$logbackVersion" - implementation platform("io.projectreactor:reactor-bom:$projectReactorVersion") - implementation 'io.projectreactor:reactor-core' - implementation "org.graalvm.sdk:nativeimage:$graalSdkVersion" + + implementation(libs.slf4j) + implementation(libs.logback) + implementation(platform(libs.reactor.bom)) + implementation(libs.reactor.core) + implementation(libs.graal.sdk.native) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000000..7eaf08263b4 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,178 @@ +# Copyright 2008-present MongoDB, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[versions] +aws-sdk-v1 = "1.12.337" +aws-sdk-v2 = "2.18.9" +graal-sdk = "24.0.0" +jna = "5.11.0" +jnr-unixsocket = "0.38.17" +netty-bom = "4.1.87.Final" +project-reactor-bom = "2022.0.0" +reactive-streams = "1.0.4" +snappy = "1.1.10.3" +zstd = "1.5.5-3" + +kotlin = "1.8.10" +kotlinx-coroutines-bom = "1.6.4" +kotlinx-datetime = "0.4.0" +kotlinx-serialization = "1.5.0" + +scala = "%scala-version%" # Handled by the scala multiversion plugin + +# Test +assertj = "3.24.2" +aws-lambda-core = "1.2.2" +aws-lambda-events = "3.11.1" +cglib = "2.2.2" +classgraph = "4.8.154" +findbugs-jsr = "1.3.9" +groovy = "3.0.9" +hamcrest = "1.3" +junit-bom = "5.10.2" +logback = "1.3.14" +mockito = "5.11.0" +mockito-java8 = "4.6.1" +mockito-kotlin = "4.1.0" +objenesis = "1.3" +reflections = "0.9.10" +scalatest = "3.2.9" +scalatest-plus = "3.2.10.0" +slf4j = "1.7.6" +spock-bom = "2.1-groovy-3.0" + +# Plugins +plugin-bnd = "5.1.2" +plugin-build-config = "3.0.3" +plugin-detekt = "1.21.0" +plugin-dokka = "1.8.10" +plugin-download = "5.6.0" +plugin-graalvm = "0.9.23" +plugin-optional-base = "7.0.0" +plugin-scala-multiversion = "2.0.4" +plugin-shadow = "6.1.0" +plugin-spotbugs = "6.0.15" +plugin-spotless = "6.14.0" +plugin-test-logger = "2.1.0" + +[libraries] +aws-java-sdk-v1-core = { module = "com.amazonaws:aws-java-sdk-core", version.ref = "aws-sdk-v1" } +aws-java-sdk-v1-sts = { module = "com.amazonaws:aws-java-sdk-sts", version.ref = "aws-sdk-v1" } +aws-java-sdk-v2-auth = { module = "software.amazon.awssdk:auth", version.ref = "aws-sdk-v2" } +aws-java-sdk-v2-sts = { module = "software.amazon.awssdk:sts", version.ref = "aws-sdk-v2" } + +jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } +jnr-unixsocket = { module = "com.github.jnr:jnr-unixsocket", version.ref = "jnr-unixsocket" } + +netty-bom = { module = "io.netty:netty-bom", version.ref = "netty-bom" } +netty-buffer = { module = "io.netty:netty-buffer" } +netty-handler = { module = "io.netty:netty-handler" } +netty-transport = { module = "io.netty:netty-transport" } + +project-reactor-bom = { module = "io.projectreactor:reactor-bom", version.ref = "project-reactor-bom" } +project-reactor-core = { module = "io.projectreactor:reactor-core" } +reactive-streams = { module = " org.reactivestreams:reactive-streams", version.ref = "reactive-streams" } + +slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } +snappy-java = { module = "org.xerial.snappy:snappy-java", version.ref = "snappy" } +zstd-jni = { module = "com.github.luben:zstd-jni", version.ref = "zstd" } + +graal-sdk = { module = "org.graalvm.sdk:graal-sdk", version.ref = "graal-sdk" } +graal-sdk-native = { module = "org.graalvm.sdk:native", version.ref = "graal-sdk" } + +kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom" } +kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } +kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "kotlinx-coroutines-bom" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } +kotlinx-coroutines-reactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } +kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-bom", version.ref = "kotlinx-serialization" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json" } +kotlinx-serialization-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } + +scala-library = {module = "org.scala-lang:scala-library", version.ref = "scala" } +scala-reflect = {module = "org.scala-lang:scala-reflect", version.ref = "scala" } + +# Test +junit-bom = { module = "org.junit:junit-bom", version.ref = "junit-bom" } +junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } +junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params" } +junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } +junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine" } +junit-kotlin = { module = "org.jetbrains.kotlin:kotlin-test-junit" } + +spock-bom = { module = "org.spockframework:spock-bom", version.ref = "spock-bom" } +spock-core = { module = "org.spockframework:spock-core" } +spock-junit4 = { module = "org.spockframework:spock-junit4" } + +mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } +mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } +mockito-junit-jupiter-java8 = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito-java8" } +mockito-core-java8 = { module = "org.mockito:mockito-core", version.ref = "mockito-java8" } +mockito-inline-java8 = { module = "org.mockito:mockito-inline", version.ref = "mockito-java8" } +mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" } + +scalatest-flatspec = { module = "org.scalatest:scalatest-flatspec_%%", version.ref = "scalatest" } +scalatest-should-matchers = { module = "org.scalatest:scalatest-shouldmatchers_%%", version.ref = "scalatest" } +scalatest-plus-junit4 = { module = "org.scalatestplus:junit-4-13_%%", version.ref = "scalatest-plus" } +scalatest-plus-mockito = { module = "org.scalatestplus:mockito-3-12_%%", version.ref = "scalatest-plus" } + +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" } + +assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } +aws-lambda-core = { module = " com.amazonaws:aws-lambda-java-core", version.ref = "aws-lambda-core" } +aws-lambda-events = { module = " com.amazonaws:aws-lambda-java-events", version.ref = "aws-lambda-events" } +cglib = { module = "cglib:cglib-nodep", version.ref = "cglib" } +classgraph = { module = "io.github.classgraph:classgraph", version.ref = "classgraph" } +findbugs-jsr = { module = "com.google.code.findbugs:jsr305", version.ref = "findbugs-jsr" } +groovy = { module = "org.codehaus.groovy:groovy-all", version.ref = "groovy" } +hamcrest-all = { module = "org.hamcrest:hamcrest-all", version.ref = "hamcrest" } +logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +netty-boringssl = { module = "io.netty:netty-tcnative-boringssl-static" } +objenesis = { module = "org.objenesis:objenesis", version.ref = "objenesis" } +project-reactor-test = { module = "io.projectreactor:reactor-test" } +reactive-streams-tck = { module = " org.reactivestreams:reactive-streams-tck", version.ref = "reactive-streams" } +reflections = { module = "org.reflections:reflections", version.ref = "reflections" } + +[bundles] +aws-java-sdk-v1 = ["aws-java-sdk-v1-core", "aws-java-sdk-v1-sts"] +aws-java-sdk-v2 = ["aws-java-sdk-v2-auth", "aws-java-sdk-v2-sts"] +netty = ["netty-buffer", "netty-handler", "netty-transport"] + +# Test +junit = ["junit-jupiter", "junit-jupiter-params", "junit-jupiter-engine", "junit-jupiter", "logback-classic", "hamcrest-all"] +junit-vintage = ["junit-vintage-engine", "junit-jupiter-params", "junit-jupiter-engine", "junit-jupiter", "logback-classic", "hamcrest-all"] +scalatest = ["scalatest-flatspec", "scalatest-should-matchers", "scalatest-plus-junit4", "scalatest-plus-mockito"] +spock = ["spock-core", "spock-junit4"] + +mockito = ["mockito-junit-jupiter", "mockito-core"] +mockito-java8 = ["mockito-junit-jupiter-java8", "mockito-core-java8", "mockito-inline-java8"] +mockito-kotlin = ["mockito-kotlin", "mockito-junit-jupiter-java8"] + +[plugins] +bnd = { id = "biz.aQute.bnd", version.ref = "plugin-bnd" } +build-config = { id = "com.github.gmazzo.buildconfig", version.ref = "plugin-build-config" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "plugin-detekt" } +dokka = { id = "org.jetbrains.dokka", version.ref = "plugin-dokka" } +download = { id = "de.undercouch.download", version.ref = "plugin-download" } +graalvm-buildtools = { id = "org.graalvm.buildtools.native", version.ref = "plugin-graalvm" } +kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +optional = { id = "nebula.optional-base", version.ref = "plugin-optional-base" } +scala-multiversion = { id = "com.adtran.scala-multiversion-plugin", version.ref = "plugin-scala-multiversion" } +shadow = { id = "com.github.johnrengelman.shadow", version.ref = "plugin-shadow" } +spotbugs = { id = "com.github.spotbugs", version.ref = "plugin-spotbugs" } +spotless = { id = "com.diffplug.spotless", version.ref = "plugin-spotless" } +test-logger = { id = "com.adarshr.test-logger", version.ref = "plugin-test-logger" } diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 6c07a315185..405d16a9e79 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -17,19 +17,8 @@ import de.undercouch.gradle.tasks.download.Download -buildscript { - repositories { - mavenCentral() - google() - } - dependencies { - "classpath"(group = "net.java.dev.jna", name = "jna", version = "5.11.0") - } -} - plugins { - // Needed to download libmongocrypt from s3. - id("de.undercouch.download") version "5.6.0" + alias(libs.plugins.download) } group = "org.mongodb" @@ -44,10 +33,10 @@ java { dependencies { api(project(path = ":bson", configuration = "default")) - api("net.java.dev.jna:jna:5.11.0") + api(libs.jna) // Tests - testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation(libs.bundles.junit) } /* From 84b4185c915f200e076a5ee2ce0a009e908f7b2e Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 6 Feb 2025 10:14:43 +0000 Subject: [PATCH 20/43] Revert "Gradle updated to use libs.versions.toml" This reverts commit e80cb0bf803753bd17cb0df5c0f124e9d913db9a. --- bson-kotlin/build.gradle.kts | 16 +- bson-kotlinx/build.gradle.kts | 32 ++-- build.gradle | 111 +++++++++----- driver-benchmarks/build.gradle | 2 +- driver-core/build.gradle | 37 +++-- driver-kotlin-coroutine/build.gradle.kts | 33 ++-- driver-kotlin-extensions/build.gradle.kts | 22 +-- driver-kotlin-sync/build.gradle.kts | 25 +-- driver-lambda/build.gradle | 22 ++- driver-reactive-streams/build.gradle | 15 +- driver-sync/build.gradle | 2 +- driver-workload-executor/build.gradle | 20 ++- graalvm-native-image-app/build.gradle | 15 +- gradle/libs.versions.toml | 178 ---------------------- mongodb-crypt/build.gradle.kts | 17 ++- 15 files changed, 221 insertions(+), 326 deletions(-) delete mode 100644 gradle/libs.versions.toml diff --git a/bson-kotlin/build.gradle.kts b/bson-kotlin/build.gradle.kts index 84b6deabd31..45e8c9c0e5d 100644 --- a/bson-kotlin/build.gradle.kts +++ b/bson-kotlin/build.gradle.kts @@ -18,12 +18,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.jetbrains.kotlin.jvm") - id("java-library") + `java-library` // Test based plugins - alias(libs.plugins.spotless) - alias(libs.plugins.dokka) - alias(libs.plugins.detekt) + id("com.diffplug.spotless") + id("org.jetbrains.dokka") + id("io.gitlab.arturbosch.detekt") } repositories { @@ -39,13 +39,13 @@ ext.set("pomName", "Bson Kotlin") dependencies { // Align versions of all Kotlin components - implementation(platform(libs.kotlin.bom)) - implementation(libs.kotlin.stdlib.jdk8) + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") api(project(path = ":bson", configuration = "default")) - implementation(libs.kotlin.reflect) + implementation("org.jetbrains.kotlin:kotlin-reflect") - testImplementation(libs.junit.kotlin) + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation(project(path = ":driver-core", configuration = "default")) } diff --git a/bson-kotlinx/build.gradle.kts b/bson-kotlinx/build.gradle.kts index d2b3e13919b..ac0b07f18eb 100644 --- a/bson-kotlinx/build.gradle.kts +++ b/bson-kotlinx/build.gradle.kts @@ -19,12 +19,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.jetbrains.kotlin.jvm") kotlin("plugin.serialization") - id("java-library") + `java-library` // Test based plugins - alias(libs.plugins.spotless) - alias(libs.plugins.dokka) - alias(libs.plugins.detekt) + id("com.diffplug.spotless") + id("org.jetbrains.dokka") + id("io.gitlab.arturbosch.detekt") } repositories { @@ -38,6 +38,10 @@ description = "Bson Kotlinx Codecs" ext.set("pomName", "Bson Kotlinx") +ext.set("kotlinxDatetimeVersion", "0.4.0") + +val kotlinxDatetimeVersion: String by ext + java { registerFeature("dateTimeSupport") { usingSourceSet(sourceSets["main"]) } registerFeature("jsonSupport") { usingSourceSet(sourceSets["main"]) } @@ -45,21 +49,21 @@ java { dependencies { // Align versions of all Kotlin components - implementation(platform(libs.kotlin.bom)) - implementation(libs.kotlin.stdlib.jdk8) + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - implementation(platform(libs.kotlinx.serialization)) - implementation(libs.kotlinx.serialization.core) - "dateTimeSupportImplementation"(libs.kotlinx.serialization.datetime) - "jsonSupportImplementation"(libs.kotlinx.serialization.json) + implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.5.0")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-core") + "dateTimeSupportImplementation"("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDatetimeVersion") + "jsonSupportImplementation"("org.jetbrains.kotlinx:kotlinx-serialization-json") api(project(path = ":bson", configuration = "default")) - implementation(libs.kotlin.reflect) + implementation("org.jetbrains.kotlin:kotlin-reflect") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation(project(path = ":driver-core", configuration = "default")) - testImplementation(libs.junit.kotlin) - testImplementation(libs.kotlinx.serialization.datetime) - testImplementation(libs.kotlinx.serialization.json) + testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDatetimeVersion") + testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") } kotlin { explicitApi() } diff --git a/build.gradle b/build.gradle index b89624d4c4a..26734755984 100644 --- a/build.gradle +++ b/build.gradle @@ -14,21 +14,33 @@ * limitations under the License. */ -plugins { - id("eclipse") - id("idea") - - alias(libs.plugins.bnd) apply false - alias(libs.plugins.detekt) apply false - alias(libs.plugins.dokka) apply false - alias(libs.plugins.download) apply false - alias(libs.plugins.kotlin) apply false - alias(libs.plugins.kotlin.serialization) apply false - alias(libs.plugins.optional) apply false - alias(libs.plugins.scala.multiversion) apply false - alias(libs.plugins.spotbugs) apply false - alias(libs.plugins.spotless) apply false - alias(libs.plugins.test.logger) apply false +apply plugin: 'eclipse' +apply plugin: 'idea' + +buildscript { + repositories { + mavenLocal() + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath 'com.netflix.nebula:gradle-extra-configurations-plugin:7.0.0' + classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:6.0.15" + classpath 'biz.aQute.bnd:biz.aQute.bnd.gradle:5.1.2' + + // Scala plugins + classpath "com.adtran:scala-multiversion-plugin:2.0.4" + classpath "com.diffplug.spotless:spotless-plugin-gradle:6.14.0" + + // kotlin plugins + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10" + classpath "org.jetbrains.kotlin:kotlin-serialization:1.8.10" + classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.8.10" + classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.21.0" + + // Test logging plugin + classpath 'com.adarshr:gradle-test-logger-plugin:2.1.0' + } } ////////////////////////////////////////// @@ -37,6 +49,17 @@ plugins { ext { configDir = new File(rootDir, 'config') + jnrUnixsocketVersion = '0.38.17' + nettyVersion = '4.1.87.Final' + snappyVersion = '1.1.10.3' + zstdVersion = '1.5.5-3' + awsSdkV2Version = '2.18.9' + awsSdkV1Version = '1.12.337' + projectReactorVersion = '2022.0.0' + junitBomVersion = '5.10.2' + logbackVersion = '1.3.14' + graalSdkVersion = '24.0.0' + reflectionsVersion = '0.9.10' gitVersion = getGitVersion() } @@ -86,21 +109,27 @@ configure(javaProjects) { } configure(scalaProjects) { - apply plugin: "java-library" - apply plugin: "scala" + apply plugin: 'scala' + apply plugin: 'java-library' + apply plugin: 'idea' apply plugin: "com.adtran.scala-multiversion-plugin" apply plugin: "com.diffplug.spotless" group = 'org.mongodb.scala' dependencies { - api(libs.scala.library) - api(libs.scala.reflect) - - testImplementation(platform(libs.junit.bom)) - testImplementation(libs.bundles.junit.vintage) - testImplementation(libs.bundles.scalatest) - testImplementation(libs.reflections) + api ('org.scala-lang:scala-library:%scala-version%') + api ('org.scala-lang:scala-reflect:%scala-version%') + + testImplementation(platform("org.junit:junit-bom:$junitBomVersion")) + testImplementation("org.junit.vintage:junit-vintage-engine") + + testImplementation('org.scalatest:scalatest-flatspec_%%:3.2.9') + testImplementation('org.scalatest:scalatest-shouldmatchers_%%:3.2.9') + testImplementation('org.scalatestplus:junit-4-13_%%:3.2.9.0') + testImplementation('org.scalatestplus:mockito-3-12_%%:3.2.10.0') + testImplementation("ch.qos.logback:logback-classic:$logbackVersion") + testImplementation("org.reflections:reflections:$reflectionsVersion") } test{ @@ -148,12 +177,12 @@ configure(javaMainProjects) { apply plugin: 'java-library' dependencies { - compileOnly(libs.findbugs.jsr) - api(libs.slf4j, optional) + compileOnly 'com.google.code.findbugs:jsr305:1.3.9' + api 'org.slf4j:slf4j-api:1.7.6', optional + testImplementation 'com.google.code.findbugs:jsr305:1.3.9' - testImplementation(libs.findbugs.jsr) // https://issues.apache.org/jira/browse/GROOVY-10194 - testImplementation(libs.groovy) + testImplementation 'org.codehaus.groovy:groovy-all:3.0.9' } /* Compiling */ @@ -222,19 +251,25 @@ configure(javaCodeCheckedProjects) { apply plugin: 'com.adarshr.test-logger' dependencies { - testImplementation(platform(libs.junit.bom)) - testImplementation(libs.bundles.junit.vintage) - - testImplementation(platform(libs.spock.bom)) - testImplementation(libs.bundles.spock) - + testImplementation(platform("org.junit:junit-bom:$junitBomVersion")) + testImplementation('org.junit.jupiter:junit-jupiter') + testImplementation('org.junit.jupiter:junit-jupiter-params') + testImplementation('org.junit.jupiter:junit-jupiter-engine') + testImplementation('org.junit.vintage:junit-vintage-engine') + + testImplementation platform('org.spockframework:spock-bom:2.1-groovy-3.0') + testImplementation 'org.spockframework:spock-core' + testImplementation 'org.spockframework:spock-junit4' if ('8'.equals(findProperty("javaVersion"))) { - testImplementation(libs.bundles.mockito.java8) + testImplementation("org.mockito:mockito-core:4.6.1") + testImplementation("org.mockito:mockito-inline:4.6.1") } else { - testImplementation(libs.bundles.mockito) + testImplementation("org.mockito:mockito-core:5.11.0") } - testImplementation(libs.cglib) - testImplementation(libs.objenesis) + testImplementation 'cglib:cglib-nodep:2.2.2' + testImplementation 'org.objenesis:objenesis:1.3' + testImplementation 'org.hamcrest:hamcrest-all:1.3' + testImplementation "ch.qos.logback:logback-classic:$logbackVersion" testImplementation project(':util:spock') //Adding categories to classpath } diff --git a/driver-benchmarks/build.gradle b/driver-benchmarks/build.gradle index b8c5ac0b62b..91d979cff68 100644 --- a/driver-benchmarks/build.gradle +++ b/driver-benchmarks/build.gradle @@ -32,7 +32,7 @@ sourceSets { dependencies { api project(':driver-sync') api project(':mongodb-crypt') - implementation(libs.logback.classic) + implementation "ch.qos.logback:logback-classic:$logbackVersion" } javadoc { diff --git a/driver-core/build.gradle b/driver-core/build.gradle index 900015bf79e..a44f65bbc1b 100644 --- a/driver-core/build.gradle +++ b/driver-core/build.gradle @@ -16,7 +16,7 @@ plugins { - alias(libs.plugins.build.config) + id 'com.github.gmazzo.buildconfig' version '3.0.3' } archivesBaseName = 'mongodb-driver-core' @@ -33,6 +33,7 @@ configurations { } } +def classifiers = ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"] dependencies { api project(path: ':bson', configuration: 'default') implementation project(path: ':bson-record-codec', configuration: 'default') @@ -40,26 +41,30 @@ dependencies { implementation project(path: ':bson-kotlinx', configuration: 'default'), optional implementation project(path: ':mongodb-crypt', configuration: 'default'), optional - implementation(libs.jnr.unixsocket, optional) - api(platform(libs.netty.bom), optional) - api(libs.bundles.netty, optional) - compileOnly(libs.graal.sdk) + implementation "com.github.jnr:jnr-unixsocket:$jnrUnixsocketVersion", optional + api platform("io.netty:netty-bom:$nettyVersion") + api "io.netty:netty-buffer", optional + api "io.netty:netty-transport", optional + api "io.netty:netty-handler", optional + compileOnly "org.graalvm.sdk:graal-sdk:$graalSdkVersion" - // Optionally depend on both AWS SDK v2 and v1. - // The driver will use v2 is present, v1 if present, or built-in functionality if neither are present - implementation(libs.bundles.aws.java.sdk.v1, optional) - implementation(libs.bundles.aws.java.sdk.v2, optional) + // Optionally depend on both AWS SDK v2 and v1. The driver will use v2 is present, v1 if present, or built-in functionality if + // neither are present + implementation "software.amazon.awssdk:auth:$awsSdkV2Version", optional + implementation "software.amazon.awssdk:sts:$awsSdkV2Version", optional + implementation "com.amazonaws:aws-java-sdk-core:$awsSdkV1Version", optional + implementation "com.amazonaws:aws-java-sdk-sts:$awsSdkV1Version", optional - implementation(libs.snappy.java, optional) - implementation(libs.zstd.jni, optional) + implementation "org.xerial.snappy:snappy-java:$snappyVersion", optional + implementation "com.github.luben:zstd-jni:$zstdVersion", optional testImplementation project(':bson').sourceSets.test.output - testImplementation(libs.bundles.junit) - testImplementation(libs.reflections) + testImplementation('org.junit.jupiter:junit-jupiter-api') + testImplementation("org.reflections:reflections:$reflectionsVersion") + testRuntimeOnly "io.netty:netty-tcnative-boringssl-static" - testRuntimeOnly(libs.netty.boringssl) - ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"].each { arch -> - testRuntimeOnly(variantOf(libs.netty.boringssl) { classifier(arch) }) + classifiers.forEach { + testRuntimeOnly "io.netty:netty-tcnative-boringssl-static::$it" } } diff --git a/driver-kotlin-coroutine/build.gradle.kts b/driver-kotlin-coroutine/build.gradle.kts index 59ef13952f2..96ac4cc31eb 100644 --- a/driver-kotlin-coroutine/build.gradle.kts +++ b/driver-kotlin-coroutine/build.gradle.kts @@ -18,12 +18,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.jetbrains.kotlin.jvm") - id("java-library") + `java-library` // Test based plugins - alias(libs.plugins.spotless) - alias(libs.plugins.dokka) - alias(libs.plugins.detekt) + id("com.diffplug.spotless") + id("org.jetbrains.dokka") + id("io.gitlab.arturbosch.detekt") } repositories { @@ -56,25 +56,26 @@ val integrationTestImplementation: Configuration by dependencies { // Align versions of all Kotlin components - implementation(platform(libs.kotlin.bom)) - implementation(libs.kotlin.stdlib.jdk8) + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - implementation(platform(libs.kotlinx.coroutines.bom)) - api(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.coroutines.reactive) + implementation(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.6.4")) + api("org.jetbrains.kotlinx:kotlinx-coroutines-core") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactive") api(project(path = ":bson", configuration = "default")) api(project(path = ":driver-reactive-streams", configuration = "default")) implementation(project(path = ":bson-kotlin", configuration = "default")) - testImplementation(libs.kotlin.reflect) - testImplementation(libs.junit.kotlin) - testImplementation(libs.bundles.mockito.kotlin) - testImplementation(libs.assertj) - testImplementation(libs.classgraph) + testImplementation("org.jetbrains.kotlin:kotlin-reflect") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") + testImplementation("org.mockito:mockito-junit-jupiter:4.11.0") + testImplementation("org.assertj:assertj-core:3.24.2") + testImplementation("io.github.classgraph:classgraph:4.8.154") - integrationTestImplementation(libs.junit.kotlin) - integrationTestImplementation(libs.kotlinx.coroutines.test) + integrationTestImplementation("org.jetbrains.kotlin:kotlin-test-junit") + integrationTestImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test") integrationTestImplementation(project(path = ":driver-sync")) integrationTestImplementation(project(path = ":driver-core")) integrationTestImplementation(project(path = ":bson")) diff --git a/driver-kotlin-extensions/build.gradle.kts b/driver-kotlin-extensions/build.gradle.kts index fc0e134749a..76f36ca33b2 100644 --- a/driver-kotlin-extensions/build.gradle.kts +++ b/driver-kotlin-extensions/build.gradle.kts @@ -18,12 +18,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.jetbrains.kotlin.jvm") - id("java-library") + `java-library` // Test based plugins - alias(libs.plugins.spotless) - alias(libs.plugins.dokka) - alias(libs.plugins.detekt) + id("com.diffplug.spotless") + id("org.jetbrains.dokka") + id("io.gitlab.arturbosch.detekt") } repositories { @@ -41,8 +41,8 @@ java { registerFeature("kotlinDrivers") { usingSourceSet(sourceSets["main"]) } } dependencies { // Align versions of all Kotlin components - implementation(platform(libs.kotlin.bom)) - implementation(libs.kotlin.stdlib.jdk8) + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") api(project(path = ":driver-core", configuration = "default")) @@ -51,11 +51,11 @@ dependencies { "kotlinDriversImplementation"(project(path = ":driver-kotlin-sync", configuration = "default")) "kotlinDriversImplementation"(project(path = ":driver-kotlin-coroutine", configuration = "default")) - testImplementation(libs.kotlin.reflect) - testImplementation(libs.junit.kotlin) - testImplementation(libs.bundles.mockito.kotlin) - testImplementation(libs.assertj) - testImplementation(libs.classgraph) + testImplementation("org.jetbrains.kotlin:kotlin-reflect") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") + testImplementation("org.assertj:assertj-core:3.24.2") + testImplementation("io.github.classgraph:classgraph:4.8.154") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") } kotlin { explicitApi() } diff --git a/driver-kotlin-sync/build.gradle.kts b/driver-kotlin-sync/build.gradle.kts index e0131613a97..e7fb132cb36 100644 --- a/driver-kotlin-sync/build.gradle.kts +++ b/driver-kotlin-sync/build.gradle.kts @@ -18,12 +18,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id("org.jetbrains.kotlin.jvm") - id("java-library") + `java-library` // Test based plugins - alias(libs.plugins.spotless) - alias(libs.plugins.dokka) - alias(libs.plugins.detekt) + id("com.diffplug.spotless") + id("org.jetbrains.dokka") + id("io.gitlab.arturbosch.detekt") } repositories { @@ -56,20 +56,21 @@ val integrationTestImplementation: Configuration by dependencies { // Align versions of all Kotlin components - implementation(platform(libs.kotlin.bom)) - implementation(libs.kotlin.stdlib.jdk8) + implementation(platform("org.jetbrains.kotlin:kotlin-bom")) + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") api(project(path = ":bson", configuration = "default")) api(project(path = ":driver-sync", configuration = "default")) implementation(project(path = ":bson-kotlin", configuration = "default")) - testImplementation(libs.kotlin.reflect) - testImplementation(libs.junit.kotlin) - testImplementation(libs.bundles.mockito.kotlin) - testImplementation(libs.assertj) - testImplementation(libs.classgraph) + testImplementation("org.jetbrains.kotlin:kotlin-reflect") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") + testImplementation("org.mockito:mockito-junit-jupiter:4.11.0") + testImplementation("org.assertj:assertj-core:3.24.2") + testImplementation("io.github.classgraph:classgraph:4.8.154") - integrationTestImplementation(libs.junit.kotlin) + integrationTestImplementation("org.jetbrains.kotlin:kotlin-test-junit") integrationTestImplementation(project(path = ":driver-sync")) integrationTestImplementation(project(path = ":driver-core")) } diff --git a/driver-lambda/build.gradle b/driver-lambda/build.gradle index d1e4f624629..d7b9928e8f7 100644 --- a/driver-lambda/build.gradle +++ b/driver-lambda/build.gradle @@ -14,13 +14,23 @@ * limitations under the License. */ +buildscript { + repositories { + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:6.1.0' + } +} plugins { - id("java") id("application") - alias(libs.plugins.shadow) } +apply plugin: 'application' +apply plugin: 'com.github.johnrengelman.shadow' +apply plugin: 'java' + compileJava { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -43,10 +53,10 @@ dependencies { implementation project(':driver-sync') implementation project(':bson') - implementation(libs.aws.lambda.core) - implementation(libs.aws.lambda.events) - implementation(platform(libs.junit.bom)) - implementation(libs.bundles.junit) + implementation('com.amazonaws:aws-lambda-java-core:1.2.2') + implementation('com.amazonaws:aws-lambda-java-events:3.11.1') + implementation(platform("org.junit:junit-bom:$junitBomVersion")) + implementation('org.junit.jupiter:junit-jupiter-api') } diff --git a/driver-reactive-streams/build.gradle b/driver-reactive-streams/build.gradle index f50a12de147..7b86f010484 100644 --- a/driver-reactive-streams/build.gradle +++ b/driver-reactive-streams/build.gradle @@ -21,21 +21,20 @@ dependencies { api project(path: ':bson', configuration: 'default') api project(path: ':driver-core', configuration: 'default') - api(libs.reactive.streams) - implementation(platform(libs.project.reactor.bom)) - implementation(libs.project.reactor.core) + api 'org.reactivestreams:reactive-streams:1.0.4' + implementation platform("io.projectreactor:reactor-bom:$projectReactorVersion") + implementation 'io.projectreactor:reactor-core' testImplementation project(':bson').sourceSets.test.output testImplementation project(':driver-sync') testImplementation project(':driver-sync').sourceSets.test.output testImplementation project(':driver-core').sourceSets.test.output - testImplementation(libs.reactive.streams.tck) - - testImplementation(libs.project.reactor.test) + testImplementation 'org.reactivestreams:reactive-streams-tck:1.0.4' + testImplementation 'io.projectreactor:reactor-test' if ('8'.equals(findProperty("javaVersion"))) { - testImplementation(libs.bundles.mockito.java8) + testImplementation 'org.mockito:mockito-junit-jupiter:4.6.1' } else { - testImplementation(libs.bundles.mockito) + testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0' } testRuntimeOnly project(path: ':driver-core', configuration: 'consumableTestRuntimeOnly') diff --git a/driver-sync/build.gradle b/driver-sync/build.gradle index 2ea76dbd829..1c2f3ac6c59 100644 --- a/driver-sync/build.gradle +++ b/driver-sync/build.gradle @@ -28,7 +28,7 @@ dependencies { testImplementation project(':driver-core').sourceSets.test.output testRuntimeOnly project(path: ':driver-core', configuration: 'consumableTestRuntimeOnly') - testImplementation(libs.aws.lambda.core) + testImplementation('com.amazonaws:aws-lambda-java-core:1.2.1') } sourceSets { diff --git a/driver-workload-executor/build.gradle b/driver-workload-executor/build.gradle index 8885e1fd59c..7c48e444dc2 100644 --- a/driver-workload-executor/build.gradle +++ b/driver-workload-executor/build.gradle @@ -14,12 +14,18 @@ * limitations under the License. */ -plugins { - id("java") - id("application") - alias(libs.plugins.shadow) +buildscript { + repositories { + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath 'com.github.jengelman.gradle.plugins:shadow:6.1.0' + } } +apply plugin: 'application' +apply plugin: 'com.github.johnrengelman.shadow' + mainClassName = "com.mongodb.workload.WorkloadExecutor" sourceSets { @@ -37,8 +43,10 @@ dependencies { implementation project(':driver-sync') implementation project(':driver-core').sourceSets.test.output implementation project(':driver-sync').sourceSets.test.output - implementation(platform(libs.junit.bom)) - implementation(libs.bundles.junit.vintage) + implementation "ch.qos.logback:logback-classic:$logbackVersion" + implementation(platform("org.junit:junit-bom:$junitBomVersion")) + implementation('org.junit.jupiter:junit-jupiter') + implementation('org.junit.vintage:junit-vintage-engine') } javadoc { diff --git a/graalvm-native-image-app/build.gradle b/graalvm-native-image-app/build.gradle index e9a2c0433bb..b3d7335f9d9 100644 --- a/graalvm-native-image-app/build.gradle +++ b/graalvm-native-image-app/build.gradle @@ -17,8 +17,8 @@ // Note requires a Gradle project flag `-PincludeGraalvm` (see settings.gradle). plugins { - id("application") - alias(libs.plugins.graalvm.buildtools) + id 'application' + id 'org.graalvm.buildtools.native' version '0.9.23' } application { @@ -113,10 +113,9 @@ dependencies { implementation project(':driver-sync').sourceSets.test.output implementation project(':driver-legacy').sourceSets.test.output implementation project(':driver-reactive-streams').sourceSets.test.output - - implementation(libs.slf4j) - implementation(libs.logback) - implementation(platform(libs.reactor.bom)) - implementation(libs.reactor.core) - implementation(libs.graal.sdk.native) + implementation 'org.slf4j:slf4j-api:2.0.12' + implementation "ch.qos.logback:logback-classic:$logbackVersion" + implementation platform("io.projectreactor:reactor-bom:$projectReactorVersion") + implementation 'io.projectreactor:reactor-core' + implementation "org.graalvm.sdk:nativeimage:$graalSdkVersion" } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml deleted file mode 100644 index 7eaf08263b4..00000000000 --- a/gradle/libs.versions.toml +++ /dev/null @@ -1,178 +0,0 @@ -# Copyright 2008-present MongoDB, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -[versions] -aws-sdk-v1 = "1.12.337" -aws-sdk-v2 = "2.18.9" -graal-sdk = "24.0.0" -jna = "5.11.0" -jnr-unixsocket = "0.38.17" -netty-bom = "4.1.87.Final" -project-reactor-bom = "2022.0.0" -reactive-streams = "1.0.4" -snappy = "1.1.10.3" -zstd = "1.5.5-3" - -kotlin = "1.8.10" -kotlinx-coroutines-bom = "1.6.4" -kotlinx-datetime = "0.4.0" -kotlinx-serialization = "1.5.0" - -scala = "%scala-version%" # Handled by the scala multiversion plugin - -# Test -assertj = "3.24.2" -aws-lambda-core = "1.2.2" -aws-lambda-events = "3.11.1" -cglib = "2.2.2" -classgraph = "4.8.154" -findbugs-jsr = "1.3.9" -groovy = "3.0.9" -hamcrest = "1.3" -junit-bom = "5.10.2" -logback = "1.3.14" -mockito = "5.11.0" -mockito-java8 = "4.6.1" -mockito-kotlin = "4.1.0" -objenesis = "1.3" -reflections = "0.9.10" -scalatest = "3.2.9" -scalatest-plus = "3.2.10.0" -slf4j = "1.7.6" -spock-bom = "2.1-groovy-3.0" - -# Plugins -plugin-bnd = "5.1.2" -plugin-build-config = "3.0.3" -plugin-detekt = "1.21.0" -plugin-dokka = "1.8.10" -plugin-download = "5.6.0" -plugin-graalvm = "0.9.23" -plugin-optional-base = "7.0.0" -plugin-scala-multiversion = "2.0.4" -plugin-shadow = "6.1.0" -plugin-spotbugs = "6.0.15" -plugin-spotless = "6.14.0" -plugin-test-logger = "2.1.0" - -[libraries] -aws-java-sdk-v1-core = { module = "com.amazonaws:aws-java-sdk-core", version.ref = "aws-sdk-v1" } -aws-java-sdk-v1-sts = { module = "com.amazonaws:aws-java-sdk-sts", version.ref = "aws-sdk-v1" } -aws-java-sdk-v2-auth = { module = "software.amazon.awssdk:auth", version.ref = "aws-sdk-v2" } -aws-java-sdk-v2-sts = { module = "software.amazon.awssdk:sts", version.ref = "aws-sdk-v2" } - -jna = { module = "net.java.dev.jna:jna", version.ref = "jna" } -jnr-unixsocket = { module = "com.github.jnr:jnr-unixsocket", version.ref = "jnr-unixsocket" } - -netty-bom = { module = "io.netty:netty-bom", version.ref = "netty-bom" } -netty-buffer = { module = "io.netty:netty-buffer" } -netty-handler = { module = "io.netty:netty-handler" } -netty-transport = { module = "io.netty:netty-transport" } - -project-reactor-bom = { module = "io.projectreactor:reactor-bom", version.ref = "project-reactor-bom" } -project-reactor-core = { module = "io.projectreactor:reactor-core" } -reactive-streams = { module = " org.reactivestreams:reactive-streams", version.ref = "reactive-streams" } - -slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } -snappy-java = { module = "org.xerial.snappy:snappy-java", version.ref = "snappy" } -zstd-jni = { module = "com.github.luben:zstd-jni", version.ref = "zstd" } - -graal-sdk = { module = "org.graalvm.sdk:graal-sdk", version.ref = "graal-sdk" } -graal-sdk-native = { module = "org.graalvm.sdk:native", version.ref = "graal-sdk" } - -kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom" } -kotlin-stdlib-jdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8" } -kotlinx-coroutines-bom = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-bom", version.ref = "kotlinx-coroutines-bom" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core" } -kotlinx-coroutines-reactive = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-reactive" } -kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" } -kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-bom", version.ref = "kotlinx-serialization" } -kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json" } -kotlinx-serialization-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } - -scala-library = {module = "org.scala-lang:scala-library", version.ref = "scala" } -scala-reflect = {module = "org.scala-lang:scala-reflect", version.ref = "scala" } - -# Test -junit-bom = { module = "org.junit:junit-bom", version.ref = "junit-bom" } -junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } -junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params" } -junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine" } -junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine" } -junit-kotlin = { module = "org.jetbrains.kotlin:kotlin-test-junit" } - -spock-bom = { module = "org.spockframework:spock-bom", version.ref = "spock-bom" } -spock-core = { module = "org.spockframework:spock-core" } -spock-junit4 = { module = "org.spockframework:spock-junit4" } - -mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" } -mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" } -mockito-junit-jupiter-java8 = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito-java8" } -mockito-core-java8 = { module = "org.mockito:mockito-core", version.ref = "mockito-java8" } -mockito-inline-java8 = { module = "org.mockito:mockito-inline", version.ref = "mockito-java8" } -mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" } - -scalatest-flatspec = { module = "org.scalatest:scalatest-flatspec_%%", version.ref = "scalatest" } -scalatest-should-matchers = { module = "org.scalatest:scalatest-shouldmatchers_%%", version.ref = "scalatest" } -scalatest-plus-junit4 = { module = "org.scalatestplus:junit-4-13_%%", version.ref = "scalatest-plus" } -scalatest-plus-mockito = { module = "org.scalatestplus:mockito-3-12_%%", version.ref = "scalatest-plus" } - -kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test" } - -assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } -aws-lambda-core = { module = " com.amazonaws:aws-lambda-java-core", version.ref = "aws-lambda-core" } -aws-lambda-events = { module = " com.amazonaws:aws-lambda-java-events", version.ref = "aws-lambda-events" } -cglib = { module = "cglib:cglib-nodep", version.ref = "cglib" } -classgraph = { module = "io.github.classgraph:classgraph", version.ref = "classgraph" } -findbugs-jsr = { module = "com.google.code.findbugs:jsr305", version.ref = "findbugs-jsr" } -groovy = { module = "org.codehaus.groovy:groovy-all", version.ref = "groovy" } -hamcrest-all = { module = "org.hamcrest:hamcrest-all", version.ref = "hamcrest" } -logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } -netty-boringssl = { module = "io.netty:netty-tcnative-boringssl-static" } -objenesis = { module = "org.objenesis:objenesis", version.ref = "objenesis" } -project-reactor-test = { module = "io.projectreactor:reactor-test" } -reactive-streams-tck = { module = " org.reactivestreams:reactive-streams-tck", version.ref = "reactive-streams" } -reflections = { module = "org.reflections:reflections", version.ref = "reflections" } - -[bundles] -aws-java-sdk-v1 = ["aws-java-sdk-v1-core", "aws-java-sdk-v1-sts"] -aws-java-sdk-v2 = ["aws-java-sdk-v2-auth", "aws-java-sdk-v2-sts"] -netty = ["netty-buffer", "netty-handler", "netty-transport"] - -# Test -junit = ["junit-jupiter", "junit-jupiter-params", "junit-jupiter-engine", "junit-jupiter", "logback-classic", "hamcrest-all"] -junit-vintage = ["junit-vintage-engine", "junit-jupiter-params", "junit-jupiter-engine", "junit-jupiter", "logback-classic", "hamcrest-all"] -scalatest = ["scalatest-flatspec", "scalatest-should-matchers", "scalatest-plus-junit4", "scalatest-plus-mockito"] -spock = ["spock-core", "spock-junit4"] - -mockito = ["mockito-junit-jupiter", "mockito-core"] -mockito-java8 = ["mockito-junit-jupiter-java8", "mockito-core-java8", "mockito-inline-java8"] -mockito-kotlin = ["mockito-kotlin", "mockito-junit-jupiter-java8"] - -[plugins] -bnd = { id = "biz.aQute.bnd", version.ref = "plugin-bnd" } -build-config = { id = "com.github.gmazzo.buildconfig", version.ref = "plugin-build-config" } -detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "plugin-detekt" } -dokka = { id = "org.jetbrains.dokka", version.ref = "plugin-dokka" } -download = { id = "de.undercouch.download", version.ref = "plugin-download" } -graalvm-buildtools = { id = "org.graalvm.buildtools.native", version.ref = "plugin-graalvm" } -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -optional = { id = "nebula.optional-base", version.ref = "plugin-optional-base" } -scala-multiversion = { id = "com.adtran.scala-multiversion-plugin", version.ref = "plugin-scala-multiversion" } -shadow = { id = "com.github.johnrengelman.shadow", version.ref = "plugin-shadow" } -spotbugs = { id = "com.github.spotbugs", version.ref = "plugin-spotbugs" } -spotless = { id = "com.diffplug.spotless", version.ref = "plugin-spotless" } -test-logger = { id = "com.adarshr.test-logger", version.ref = "plugin-test-logger" } diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 405d16a9e79..6c07a315185 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -17,8 +17,19 @@ import de.undercouch.gradle.tasks.download.Download +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + "classpath"(group = "net.java.dev.jna", name = "jna", version = "5.11.0") + } +} + plugins { - alias(libs.plugins.download) + // Needed to download libmongocrypt from s3. + id("de.undercouch.download") version "5.6.0" } group = "org.mongodb" @@ -33,10 +44,10 @@ java { dependencies { api(project(path = ":bson", configuration = "default")) - api(libs.jna) + api("net.java.dev.jna:jna:5.11.0") // Tests - testImplementation(libs.bundles.junit) + testImplementation("org.junit.jupiter:junit-jupiter") } /* From 53da75837be0e3eddfcc6582a7956f21f6ab5046 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 6 Feb 2025 10:14:43 +0000 Subject: [PATCH 21/43] Revert "Gradle bump to 7.6.4" This reverts commit bedcdae5217eb8c447d3ae0c5818348665e85a6a. --- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 257 +++++++++-------------- 3 files changed, 105 insertions(+), 154 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 17845 zcmV)CK*GO}(F4QI1F(Jx4W$DjNjn4p0N4ir06~)x5+0MO2`GQvQyWzj|J`gh3(E#l zNGO!HfVMRRN~%`0q^)g%XlN*vP!O#;m*h5VyX@j-1N|HN;8S1vqEAj=eCdn`)tUB9 zXZjcT^`bL6qvL}gvXj%9vrOD+x!Gc_0{$Zg+6lTXG$bmoEBV z*%y^c-mV0~Rjzv%e6eVI)yl>h;TMG)Ft8lqpR`>&IL&`>KDi5l$AavcVh9g;CF0tY zw_S0eIzKD?Nj~e4raA8wxiiImTRzv6;b6|LFmw)!E4=CiJ4I%&axSey4zE-MIh@*! z*P;K2Mx{xVYPLeagKA}Hj=N=1VrWU`ukuBnc14iBG?B}Uj>?=2UMk4|42=()8KOnc zrJzAxxaEIfjw(CKV6F$35u=1qyf(%cY8fXaS9iS?yetY{mQ#Xyat*7sSoM9fJlZqq zyasQ3>D>6p^`ck^Y|kYYZB*G})uAbQ#7)Jeb~glGz@2rPu}zBWDzo5K$tP<|meKV% z{Swf^eq6NBioF)v&~9NLIxHMTKe6gJ@QQ^A6fA!n#u1C&n`aG7TDXKM1Jly-DwTB` z+6?=Y)}hj;C#r5>&x;MCM4U13nuXVK*}@yRY~W3X%>U>*CB2C^K6_OZsXD!nG2RSX zQg*0)$G3%Es$otA@p_1N!hIPT(iSE=8OPZG+t)oFyD~{nevj0gZen$p>U<7}uRE`t5Mk1f4M0K*5 zbn@3IG5I2mk;8K>*RZ zPV6iL006)S001s%0eYj)9hu1 z9o)iQT9(v*sAuZ|ot){RrZ0Qw4{E0A+!Yx_M~#Pj&OPUM&i$RU=Uxu}e*6Sr2ror= z&?lmvFCO$)BY+^+21E>ENWe`I0{02H<-lz&?})gIVFyMWxX0B|0b?S6?qghp3lDgz z2?0|ALJU=7s-~Lb3>9AA5`#UYCl!Xeh^i@bxs5f&SdiD!WN}CIgq&WI4VCW;M!UJL zX2};d^sVj5oVl)OrkapV-C&SrG)*x=X*ru!2s04TjZ`pY$jP)4+%)7&MlpiZ`lgoF zo_p>^4qGz^(Y*uB10dY2kcIbt=$FIdYNqk;~47wf@)6|nJp z1cocL3zDR9N2Pxkw)dpi&_rvMW&Dh0@T*_}(1JFSc0S~Ph2Sr=vy)u*=TY$i_IHSo zR+&dtWFNxHE*!miRJ%o5@~GK^G~4$LzEYR-(B-b(L*3jyTq}M3d0g6sdx!X3-m&O% zK5g`P179KHJKXpIAAX`A2MFUA;`nXx^b?mboVbQgigIHTU8FI>`q53AjWaD&aowtj z{XyIX>c)*nLO~-WZG~>I)4S1d2q@&?nwL)CVSWqWi&m1&#K1!gt`g%O4s$u^->Dwq ziKc&0O9KQ7000OG0000%03-m(e&Y`S09YWC4iYDSty&3q8^?8ij|8zxaCt!zCFq1@ z9TX4Hl68`nY>}cQNW4Ullqp$~SHO~l1!CdFLKK}ij_t^a?I?C^CvlvnZkwiVn>dl2 z2$V(JN{`5`-8ShF_ek6HNRPBlPuIPYu>TAeAV5O2)35r3*_k(Q-h1+h5pb(Zu%oJ__pBsW0n5ILw`!&QR&YV`g0Fe z(qDM!FX_7;`U3rxX#QHT{f%h;)Eursw=*#qvV)~y%^Uo^% zi-%sMe^uz;#Pe;@{JUu05zT*i=u7mU9{MkT`ft(vPdQZoK&2mg=tnf8FsaNQ+QcPg zB>vP8Rd6Z0JoH5_Q`zldg;hx4azQCq*rRZThqlqTRMzn1O3_rQTrHk8LQ<{5UYN~` zM6*~lOGHyAnx&#yCK{i@%N1Us@=6cw=UQxpSE;<(LnnES%6^q^QhBYQ-VCSmIu8wh z@_LmwcFDfAhIn>`%h7L{)iGBzu`Md4dj-m3C8mA9+BL*<>q z#$7^ttIBOE-=^|zmG`K8yUKT{yjLu2SGYsreN0*~9yhFxn4U};Nv1XXj1fH*v-g=3 z@tCPc`YdzQGLp%zXwo*o$m9j-+~nSWls#s|?PyrHO%SUGdk**X9_=|b)Y%^j_V$3S z>mL2A-V)Q}qb(uZipEFVm?}HWc+%G6_K+S+87g-&RkRQ8-{0APDil115eG|&>WQhU zufO*|e`hFks^cJJmx_qNx{ltSp3aT|XgD5-VxGGXb7gkiOG$w^qMVBDjR8%!Sbh72niHRDV* ziFy8LE+*$j?t^6aZP9qt-ow;hzkmhvy*Hn-X^6?yVMbtNbyqZQ^rXg58`gk+I%Wv} zn_)dRq+3xjc8D%}EQ%nnTF7L7m}o9&*^jf`_qvUhVKY7w9Zgxr-0YHWFRd3$l_6UX zpXt^U&TiC*qZWx#pOG6k?3Tg)pra*fw(O6_45>lUBN1U5Qmc>^DHt)5b~Ntjsw!NI z1n4{$HWFeIi)*qvgK^ui;(81VQc1(wJ8C#tjR>Dkjf{xYC^_B^#qrdCc)uZxtgua6 zk98UGQF|;;k`c+0_z)tQ&9DwLB~&12@D1!*mTz_!3Mp=cg;B7Oq4cKN>5v&dW7q@H zal=g6Ipe`siZN4NZiBrkJCU*x216gmbV(FymgHuG@%%|8sgD?gR&0*{y4n=pukZnd z4=Nl~_>jVfbIehu)pG)WvuUpLR}~OKlW|)=S738Wh^a&L+Vx~KJU25o6%G7+Cy5mB zgmYsgkBC|@K4Jm_PwPoz`_|5QSk}^p`XV`649#jr4Lh^Q>Ne~#6Cqxn$7dNMF=%Va z%z9Ef6QmfoXAlQ3)PF8#3Y% zadcE<1`fd1&Q9fMZZnyI;&L;YPuy#TQ8b>AnXr*SGY&xUb>2678A+Y z8K%HOdgq_4LRFu_M>Ou|kj4W%sPPaV)#zDzN~25klE!!PFz_>5wCxglj7WZI13U5| zEq_YLKPH;v8sEhyG`dV_jozR);a6dBvkauhC;1dk%mr+J*Z6MMH9jqxFk@)&h{mHl zrf^i_d-#mTF=6-T8Rk?(1+rPGgl$9=j%#dkf@x6>czSc`jk7$f!9SrV{do%m!t8{? z_iAi$Qe&GDR#Nz^#uJ>-_?(E$ns)(3)X3cYY)?gFvU+N>nnCoBSmwB2<4L|xH19+4 z`$u#*Gt%mRw=*&|em}h_Y`Pzno?k^8e*hEwfM`A_yz-#vJtUfkGb=s>-!6cHfR$Mz z`*A8jVcz7T{n8M>ZTb_sl{EZ9Ctau4naX7TX?&g^VLE?wZ+}m)=YW4ODRy*lV4%-0 zG1XrPs($mVVfpnqoSihnIFkLdxG9um&n-U|`47l{bnr(|8dmglO7H~yeK7-wDwZXq zaHT($Qy2=MMuj@lir(iyxI1HnMlaJwpX86je}e=2n|Esb6hB?SmtDH3 z2qH6o`33b{;M{mDa5@@~1or8+Zcio*97pi1Jkx6v5MXCaYsb~Ynq)eWpKnF{n)FXZ z?Xd;o7ESu&rtMFr5(yJ(B7V>&0gnDdL*4MZH&eO+r*t!TR98ssbMRaw`7;`SLI8mT z=)hSAt~F=mz;JbDI6g~J%w!;QI(X14AnOu;uve^4wyaP3>(?jSLp+LQ7uU(iib%IyB(d&g@+hg;78M>h7yAeq$ALRoHGkKXA+E z$Sk-hd$Fs2nL4w9p@O*Y$c;U)W#d~)&8Js;i^Dp^* z0*7*zEGj~VehF4sRqSGny*K_CxeF=T^8;^lb}HF125G{kMRV?+hYktZWfNA^Mp7y8 zK~Q?ycf%rr+wgLaHQ|_<6z^eTG7izr@99SG9Q{$PCjJabSz`6L_QJJe7{LzTc$P&pwTy<&3RRUlSHmK;?}=QAhQaDW3#VWcNAH3 zeBPRTDf3?3mfdI$&WOg(nr9Gyzg`&u^o!f2rKJ57D_>p z6|?Vg?h(@(*X=o071{g^le>*>qSbVam`o}sAK8>b|11%e&;%`~b2OP7--q%0^2YDS z`2M`{2QYr1VC)sIW9WOu8<~7Q>^$*Og{KF+kI;wFegvaIDkB%3*%PWtWKSq7l`1YcDxQQ2@nv{J!xWV?G+w6C zhUUxUYVf%(Q(40_xrZB@rbxL=Dj3RV^{*yHd>4n-TOoHVRnazDOxxkS9kiZyN}IN3 zB^5N=* zRSTO+rA<{*P8-$GZdyUNOB=MzddG$*@q>mM;pUIiQ_z)hbE#Ze-IS)9G}Rt$5PSB{ zZZ;#h9nS7Rf1ecW&n(Gpu9}{vXQZ-f`UHIvD?cTbF`YvH*{rgE(zE22pLAQfhg-`U zuh612EpByB(~{w7svCylrBk%5$LCIyuhrGi=yOfca`=8ltKxHcSNfDRt@62QH^R_0 z&eQL6rRk>Dvf6rjMQv5ZXzg}S`HqV69hJT^pPHtdhqsrPJWs|IT9>BvpQa@*(FX6v zG}TYjreQCnH(slMt5{NgUf)qsS1F&Bb(M>$X}tWI&yt2I&-rJbqveuj?5J$`Dyfa2 z)m6Mq0XH@K)Y2v8X=-_4=4niodT&Y7W?$KLQhjA<+R}WTdYjX9>kD+SRS^oOY1{A= zZTId-(@wF^UEWso($wZtrs%e7t<}YaC_;#@`r0LUzKY&|qPJz*y~RHG`E6bypP5AX zN!p0^AUu8uDR>xM-ALFzBxXM~Q3z=}fHWCIG>0&I6x2Iu7&U)49j7qeMI&?qb$=4I zdMmhAJrO%@0f%YW! z^gLByEGSk+R0v4*d4w*N$Ju6z#j%HBI}6y$2en=-@S3=6+yZX94m&1j@s- z7T6|#0$c~dYq9IkA!P)AGkp~S$zYJ1SXZ#RM0|E~Q0PSm?DsT4N3f^)b#h(u9%_V5 zX*&EIX|gD~P!vtx?ra71pl%v)F!W~X2hcE!h8cu@6uKURdmo1-7icN4)ej4H1N~-C zjXgOK+mi#aJv4;`DZ%QUbVVZclkx;9`2kgbAhL^d{@etnm+5N8pB#fyH)bxtZGCAv z(%t0kPgBS{Q2HtjrfI0B$$M0c?{r~2T=zeXo7V&&aprCzww=i*}Atu7g^(*ivauMz~kkB%Vt{Wydlz%%2c26%>0PAbZO zVHx%tK(uzDl#ZZK`cW8TD2)eD77wB@gum{B2bO_jnqGl~01EF_^jx4Uqu1yfA~*&g zXJ`-N?D-n~5_QNF_5+Un-4&l$1b zVlHFqtluoN85b^C{A==lp#hS9J(npJ#6P4aY41r) zzCmv~c77X5L}H%sj>5t&@0heUDy;S1gSOS>JtH1v-k5l}z2h~i3^4NF6&iMb;ZYVE zMw*0%-9GdbpF1?HHim|4+)Zed=Fk<2Uz~GKc^P(Ig@x0&XuX0<-K(gA*KkN&lY2Xu zG054Q8wbK~$jE32#Ba*Id2vkqmfV{U$Nx9vJ;jeI`X+j1kh7hB8$CBTe@ANmT^tI8 z%U>zrTKuECin-M|B*gy(SPd`(_xvxjUL?s137KOyH>U{z01cBcFFt=Fp%d+BK4U;9 zQG_W5i)JASNpK)Q0wQpL<+Ml#cei41kCHe&P9?>p+KJN>I~`I^vK1h`IKB7k^xi`f z$H_mtr_+@M>C5+_xt%v}{#WO{86J83;VS@Ei3JLtp<*+hsY1oGzo z0?$?OJO$79;{|@aP!fO6t9TJ!?8i&|c&UPWRMbkwT3nEeFH`Yyyh6b%Rm^nBuTt@9 z+$&-4lf!G|@LCo3<8=yN@5dYbc%uq|Hz|0tiiLQKiUoM9g14zyECKGv0}3AWv2WJ zUAXGUhvkNk`0-H%ACsRSmy4fJ@kxBD3ZKSj6g(n1KPw?g{v19phcBr3BEF>J%lL|d zud3LNuL;cR*xS+;X+N^Br+x2{&hDMhb-$6_fKU(Pt0FQUXgNrZvzsVCnsFqv?#L z4-FYsQ-?D>;LdjHu_TT1CHN~aGkmDjWJkJg4G^!+V_APd%_48tErDv6BW5;ji^UDD zRu5Sw7wwplk`w{OGEKWJM&61c-AWn!SeUP8G#+beH4_Ov*)NUV?eGw&GHNDI6G(1Y zTfCv?T*@{QyK|!Q09wbk5koPD>=@(cA<~i4pSO?f(^5sSbdhUc+K$DW#_7^d7i%At z?KBg#vm$?P4h%?T=XymU;w*AsO_tJr)`+HUll+Uk_zx6vNw>G3jT){w3ck+Z=>7f0 zZVkM*!k^Z_E@_pZK6uH#|vzoL{-j1VFlUHP&5~q?j=UvJJNQG ztQdiCF$8_EaN_Pu8+afN6n8?m5UeR_p_6Log$5V(n9^W)-_vS~Ws`RJhQNPb1$C?| zd9D_ePe*`aI9AZ~Ltbg)DZ;JUo@-tu*O7CJ=T)ZI1&tn%#cisS85EaSvpS~c#CN9B z#Bx$vw|E@gm{;cJOuDi3F1#fxWZ9+5JCqVRCz5o`EDW890NUfNCuBn)3!&vFQE{E$L`Cf7FMSSX%ppLH+Z}#=p zSow$)$z3IL7frW#M>Z4|^9T!=Z8}B0h*MrWXXiVschEA=$a|yX9T~o!=%C?T+l^Cc zJx&MB$me(a*@lLLWZ=>PhKs!}#!ICa0! zq%jNgnF$>zrBZ3z%)Y*yOqHbKzEe_P=@<5$u^!~9G2OAzi#}oP&UL9JljG!zf{JIK z++G*8j)K=$#57N)hj_gSA8golO7xZP|KM?elUq)qLS)i(?&lk{oGMJh{^*FgklBY@Xfl<_Q zXP~(}ST6V01$~VfOmD6j!Hi}lsE}GQikW1YmBH)`f_+)KI!t#~B7=V;{F*`umxy#2Wt8(EbQ~ks9wZS(KV5#5Tn3Ia90r{}fI%pfbqBAG zhZ)E7)ZzqA672%@izC5sBpo>dCcpXi$VNFztSQnmI&u`@zQ#bqFd9d&ls?RomgbSh z9a2rjfNiKl2bR!$Y1B*?3Ko@s^L5lQN|i6ZtiZL|w5oq%{Fb@@E*2%%j=bcma{K~9 z*g1%nEZ;0g;S84ZZ$+Rfurh;Nhq0;{t~(EIRt}D@(Jb7fbe+_@H=t&)I)gPCtj*xI z9S>k?WEAWBmJZ|gs}#{3*pR`-`!HJ)1Dkx8vAM6Tv1bHZhH=MLI;iC#Y!$c|$*R>h zjP{ETat(izXB{@tTOAC4nWNhh1_%7AVaf!kVI5D=Jf5I1!?}stbx_Yv23hLf$iUTb z-)WrTtd2X+;vBW_q*Z6}B!10fs=2FA=3gy*dljsE43!G*3Uw(Is>(-a*5E!T4}b-Y zfvOC)-HYjNfcpi`=kG%(X3XcP?;p&=pz+F^6LKqRom~pA}O* zitR+Np{QZ(D2~p_Jh-k|dL!LPmexLM?tEqI^qRDq9Mg z5XBftj3z}dFir4oScbB&{m5>s{v&U=&_trq#7i&yQN}Z~OIu0}G)>RU*`4<}@7bB% zKYxGx0#L#u199YKSWZwV$nZd>D>{mDTs4qDNyi$4QT6z~D_%Bgf?>3L#NTtvX;?2D zS3IT*2i$Snp4fjDzR#<)A``4|dA(}wv^=L?rB!;kiotwU_gma`w+@AUtkSyhwp{M} z!e`jbUR3AG4XvnBVcyIZht6Vi~?pCC!$XF2 z*V~)DBVm8H7$*OZQJYl3482hadhsI2NCz~_NINtpC?|KI6H3`SG@1d%PsDdw{u}hq zN;OU~F7L1jT&KAitilb&Fl3X12zfSuFm;X)xQWOHL&7d)Q5wgn{78QJ6k5J;is+XP zCPO8_rlGMJB-kuQ*_=Yo1TswG4xnZd&eTjc8=-$6J^8TAa~kEnRQ@Zp-_W&B(4r@F zA==}0vBzsF1mB~743XqBmL9=0RSkGn$cvHf*hyc{<2{@hW+jKjbC|y%CNupHY_NC% zivz^btBLP-cDyV8j>u)=loBs>HoI5ME)xg)oK-Q0wAy|8WD$fm>K{-`0|W{H00;;G z000j`0OWQ8aHA9e04^;603eeQIvtaXMG=2tcr1y8Fl-J;AS+=<0%DU8Bp3oEEDhA^ zOY)M8%o5+cF$rC?trfMcty*f)R;^v=f~}||Xe!#;T3eTDZELN&-50xk+J1heP5AQ>h5O#S_uO;O@;~REd*_G$x$hVeE#bchX)otXQy|S5(oB)2a2%Sc(iDHm z=d>V|a!BLp9^#)o7^EQ2kg=K4%nI^sK2w@-kmvB+ARXYdq?xC2age6)e4$^UaY=wn zgLD^{X0A+{ySY+&7RpldwpC6=E zSPq?y(rl8ZN%(A*sapd4PU+dIakIwT0=zxIJEUW0kZSo|(zFEWdETY*ZjIk9uNMUA ze11=mHu8lUUlgRx!hItf0dAF#HfdIB+#aOuY--#QN9Ry zbx|XkG?PrBb@l6Owl{9Oa9w{x^R}%GwcEEfY;L-6OU8|9RXvu`-ECS`jcO1x1MP{P zcr;Bw##*Dod9K@pEx9z9G~MiNi>8v1OU-}vk*HbI)@CM? zn~b=jWUF%HP=CS+VCP>GiAU_UOz$aq3%%Z2laq^Gx`WAEmuNScCN)OlW>YHGYFgV2 z42lO5ZANs5VMXLS-RZTvBJkWy*OeV#L;7HwWg51*E|RpFR=H}h(|N+79g)tIW!RBK ze08bg^hlygY$C2`%N>7bDm`UZ(5M~DTanh3d~dg+OcNdUanr8azO?})g}EfnUB;5- zE1FX=ru?X=zAk4_6@__o1fE+ml1r&u^f1Kb24Jf-)zKla%-dbd>UZ1 zrj3!RR!Jg`ZnllKJ)4Yfg)@z>(fFepeOcp=F-^VHv?3jSxfa}-NB~*qkJ5Uq(yn+( z<8)qbZh{C!xnO@-XC~XMNVnr-Z+paowv!$H7>`ypMwA(X4(knx7z{UcWWe-wXM!d? zYT}xaVy|7T@yCbNOoy)$D=E%hUNTm(lPZqL)?$v+-~^-1P8m@Jm2t^L%4#!JK#Vtg zyUjM+Y*!$);1<)0MUqL00L0*EZcsE&usAK-?|{l|-)b7|PBKl}?TM6~#j9F+eZq25_L&oSl}DOMv^-tacpDI)l*Ws3u+~jO@;t(T)P=HCEZ#s_5q=m zOsVY!QsOJn)&+Ge6Tm)Ww_Bd@0PY(78ZJ)7_eP-cnXYk`>j9q`x2?Xc6O@55wF+6R zUPdIX!2{VGA;FSivN@+;GNZ7H2(pTDnAOKqF*ARg+C54vZ@Ve`i?%nDDvQRh?m&`1 zq46gH)wV=;UrwfCT3F(m!Q5qYpa!#f6qr0wF=5b9rk%HF(ITc!*R3wIFaCcftGwPt z(kzx{$*>g5L<;u}HzS4XD%ml zmdStbJcY@pn`!fUmkzJ8N>*8Y+DOO^r}1f4ix-`?x|khoRvF%jiA)8)P{?$8j2_qN zcl3Lm9-s$xdYN9)>3j6BPFK)Jbovl|Sf_p((CHe!4hx@F)hd&&*Xb&{TBj>%pT;-n z{3+hA^QZYnjXxtF2XwxPZ`S#J8h>5qLwtwM-{5abbEnRS z`9_`Zq8FJiI#0syE_V_3M&trw$P=ezkHosV$8&I5c0(*-9KBE5DJOC-Xv zw}1bq~AD0_Xerm`%ryiG9_$S z5G|btfiAUNdV09SO2l9v+e#(H6HYOdQs=^ z@xwZQU)~;p1L*~ciC}9ao{nQ-@B>rpUzKBxv=cUusOP5Trs3QnvHxGh9e>s7AM{V1|HfYe z3QwH;nHHR49fYzuGc3W3l5xrDAI392SFXx>lWE3V9Ds9il3PyZaN5>oC3>9W-^7vC z3~KZ-@iD?tIkhg+6t{m;RGk2%>@I0&kf)o$+-^ls0(YABNbM(=l#ad@nKp_j=b~Xs ziR;xu_+)lxy6|+af!@}gO2H_x)p;nZ-tYxW5Omq=l`GzMp*GTLr>vZN1?e}^C$t*Z zvzEdIc2|HA2RFN_4#EkzMqKnbbw!?!?%B@M0^^5Z;K?x-%lg?Z>}wMV8zEqHZ$cr~Y#Wv>9+)KMUZatUqbRU8 z8t9qrek(H^C0Tuzq|cP2$WL7tzj+Dj5y^2SF1D154CnsB$xbz`$wV||n-cG%rsT$p z+3RHdadK(3-noj(2L#8c5lODg)V8pv(GEnNb@F>dEHQr>!qge@L>#qg)RAUtiOYqF ziiV_ETExwD)bQ<))?-9$)E(FiRBYyC@}issHS!j9n)~I1tarxnQ2LfjdIJ)*jp{0E z&1oTd%!Qbw$W58s!6ms>F z=p0!~_Mv~8jyaicOS*t(ntw`5uFi0Bc4*mH8kSkk$>!f0;FM zX_t14I55!ZVsg0O$D2iuEDb7(J>5|NKW^Z~kzm@dax z9(|As$U7^}LF%#`6r&UPB*6`!Rf74h~*C=ami6xUxYCwiJxdr$+`z zKSC4A%8!s%R&j*2si(OEc*fy!q)?%=TjDZJ2}O zxT6o>jlKXz_7_Y$N})}IG`*#KfMzs#R(SI#)3*ZEzCv%_tu(VTZ5J| zw2$5kK)xTa>xGFgS0?X(NecjzFVKG%VVn?neu=&eQ+DJ1APlY1E?Q1s!Kk=yf7Uho z>8mg_!U{cKqpvI3ucSkC2V`!d^XMDk;>GG~>6>&X_z75-kv0UjevS5ORHV^e8r{tr z-9z*y&0eq3k-&c_AKw~<`8dtjsP0XgFv6AnG?0eo5P14T{xW#b*Hn2gEnt5-KvN1z zy!TUSi>IRbD3u+h@;fn7fy{F&hAKx7dG4i!c?5_GnvYV|_d&F16p;)pzEjB{zL-zr z(0&AZUkQ!(A>ghC5U-)t7(EXb-3)tNgb=z`>8m8n+N?vtl-1i&*ftMbE~0zsKG^I$ zSbh+rUiucsb!Ax@yB}j>yGeiKIZk1Xj!i#K^I*LZW_bWQIA-}FmJ~^}>p=K$bX9F{}z{s^KWc~OK(zl_X57aB^J9v}yQ5h#BE$+C)WOglV)nd0WWtaF{7`_Ur`my>4*NleQG#xae4fIo(b zW(&|g*#YHZNvDtE|6}yHvu(hDekJ-t*f!2RK;FZHRMb*l@Qwkh*~CqQRNLaepXypX z1?%ATf_nHIu3z6gK<7Dmd;{`0a!|toT0ck|TL$U;7Wr-*piO@R)KrbUz8SXO0vr1K z>76arfrqImq!ny+VkH!4?x*IR$d6*;ZA}Mhro(mzUa?agrFZpHi*)P~4~4N;XoIvH z9N%4VK|j4mV2DRQUD!_-9fmfA2(YVYyL#S$B;vqu7fnTbAFMqH``wS7^B5=|1O&fL z)qq(oV6_u4x(I(**#mD}MnAy(C&B4a1n6V%$&=vrIDq^F_KhE5Uw8_@{V`_#M0vCu zaNUXB=n0HT@D+ppDXi8-vp{tj)?7+k>1j}VvEKRgQ~DWva}8*pp`W8~KRo*kJ*&X} zP!~2fxQr@dM*q0dI|)Fux=pZWBk==RI7i{^BQf`kWlD2%|@R9!JA7& zLbM$uJ12y}_62$|T|{)@OJZtzfpL^t@1nMTYHutrF#D+^?~CN~9`YQ@#&&@c_Zf)( zbC~y8!2LO8jHwQXv>G~1q?c68ipT*%dY&c{8wd_!Y#~tMJ7yk!F8| zt?m_CLVw6cU@@p(#h4cY&Qsfz2Xp3w^4Cg%m03Tmq~9n%hyoMH^KY7{(QkRyn_!YB zzZa!Tgr~5$MAG$x)Fs71#6j}Kvcv3=9VUX8CH< zbP3|fY8f#$K*<5JQ7whM(v=GN2k26Xsh)#0!HKS(koLgAp-;)8z0w&_Z=nG4v6n8u z&Tm0Fi){4_!Y5Kp?!zv$FKfUifQ{%c82uYfrvE{%ejUd72aNYmI*0z3-a-EYr+bB->oH3#t(AY3 zV{Z=(SJr;D#0(`u*dc*~9T7D8Pudw894%!>c4wU&V1m<~0InidR6fbi?yPl(z+sKa zdF*kS>_4^1UO>y4T%Ar>epSr5&vp`$KdY7B(F%P0@VyHk@1fJ=6X0=aGjD-)BrOJD zW}IU@hg~^2r>a1fQvjTtvL*mKJ7q;pfP*U2=URL`VB_Y_JojbZ+MS=vaVN0C6L_MV zG1#5=35-E`KsD%r>-Q_ndvJ2tOYcMMP9f*t0iJ`(Z`^+YP)h>@lR(@Wvrt-`0tHG+ zuP2R@@mx=T@fPoQ1s`e^1I0H*kQPBGDky@!ZQG@8jY-+2ihreG5q$6i{3vmDTg0j$ zzRb*-nKN@{_wD`V6+i*YS)?$XfrA-sW?js?SYU8#vXxxQCc|*K!EbpWfu)3~jwq6_@KC0m;3A%jH^18_a0;ksC2DEwa@2{9@{ z9@T??<4QwR69zk{UvcHHX;`ICOwrF;@U;etd@YE)4MzI1WCsadP=`%^B>xPS-{`=~ zZ+2im8meb#4p~XIL9}ZOBg7D8R=PC8V}ObDcxEEK(4yGKcyCQWUe{9jCs+@k!_y|I z%s{W(&>P4w@hjQ>PQL$zY+=&aDU6cWr#hG)BVCyfP)h>@3IG5I2mk;8K>)Ppba*!h z005B=001VF5fT=Y4_ytCUk`sv8hJckqSy&Gc2Jx^WJ$J~08N{il-M$fz_ML$)Cpil z(nOv_nlZB^c4s&&O3h=OLiCz&(|f0 zxWU_-JZy>hxP*gvR>CLnNeQ1~g;6{g#-}AbkIzWR;j=8=6!AHpKQCbjFYxf9h%bov zVi;eNa1>t-<14KERUW>^KwoF+8zNo`Y*WiQwq}3m0_2RYtL9Wmu`JaRaQMQ)`Si^6+VbM`!rH~T?DX2=(n4nT zf`G`(Rpq*pDk*v~wMYPZ@vMNZDMPnxMYmU!lA{Xfo?n=Ibb4y3eyY1@Dut4|Y^ml& zqs$r}jAo=B(Ml>ogeEjyv(E`=kBzPf2uv9TQtO$~bamD#=Tv`lNy(K|w$J2O6jS51 zzZtOCHDWz7W0=L1XDW5WR5mtLGc~W+>*vX5{e~U@rE~?7e>vKU-v8bj;F4#abtcV(3ZtwXo9ia93HiETyQXwW4a-0){;$OU*l` zW^bjkyZTJ6_DL^0}`*)#EZ|2nvKRzMLH9-~@Z6$v#t8Dm%(qpP+DgzNe6d)1q zBqhyF$jJTyYFvl_=a>#I8jhJ)d6SBNPg#xg2^kZ3NX8kQ74ah(Y5Z8mlXyzTD&}Q8 ziY(pj-N-V2f>&hZQJ`Di%wp2fN(I%F@l)3M8GcSdNy+#HuO{$I8NXubRlFkL)cY@b z#`v{}-^hRXEq*8B_cG=%PZvI$eo(|8Wc(2o8L#0_GX9L$1@yV>%7mGk)QTD1R*OvS z4OW;ym1)%k9Bfem0tOqq3yyAUWp&q|LsN!RDnxa|j;>R|Mm2rIv7=tej5GFaa+`#| z;7u9Z_^XV+vD@2hF8Xe63+Qd`oig6S9jX(*DbjzPb*K-H7c^7E-(~!R6E%TrgW;RvG;WS{Ziv*W*a*`9Bb;$Er3?MyF~5GcXv`k>U)n}lwv$Sp+H@IKA5$mKk0g*4Ln{!tfvITeY zzr%8JJ5BdcEYsR9eGzJ4B&$}4FMmbRU6{8{_w7Kl77@PNe7|Bc#c?5(C5&Z=kJ#(oM90D4`rh2S!|^L!P#e#1hkD5@~-- z`63GV0~*rOZSqw7k^#-Y$Q4z3Oa2SPRURqEahB1B^h{7~+p03SwzqL9QU#$3-X zdYtQ?-K5xDAdfomEd6(yPtZ!yY_<35bMedeq`z2JWorljz5-f9<^93HM-$#+acw%9r!JOM%O<|BR`W& zd-%j_?b^q7Kl6{q^N{cg2u;11rFB5EP+oqG9&pHD#_Mo@aNMj;LUvsl&nK(ca(hT( zzFc2oHC6WQv8g7jo+3ZSwK+9G$cvfRnql)?g=XeQ3+LTh3)79nhEle8OqS3T$qn(> z(=5Bg?EWq-ldEywgzXW965%H(9^ik*rH(8dNdkbcS9|ow&_r`X~R^R?B+(oTiMzzlx8KnHqUi z8Rh-)VAnS-CO+3}yxqm8)X+N+uzieFVm-F#syP#M1p5&$wX3MJ8 z+R@grZ*5G^Uh4I@VT=>C4RJNc^~3mx$kS1F{L?3)BzdduD2MZKdu#jNno&f2&d{?` zW(>$oktzY@GO{|Ln~Bt^A4)(%?l-&(Dm!iL#$K_xOyhwAf=K2<+Bom zw7|hl6E5}B$d%n0sfZvfQRy9Fyz2~ z83#=#LaHnf1th^k*p|ux8!!8pfHE!)x*%=_hAddl)P%4h4%&8!5-W#xqqb}c=H(i|wqcIS&oDQ{ zhI7N-$f$ra3=RjPmMh?-IEkJYQ<}R9Z!}wmp$#~Uc%u1oh#TP}wF*kJJmQX2#27kL z_dz(yKufo<=m71bZfLp^Ll#t3(IHkrgMcvx@~om%Ib(h(<$Da7urTI`x|%`wD--sN zJEEa>4DGSEG?0ulkosfj8IMNN4)B=ZtvGG{|4Fp=Xhg!wPNgYzS>{Bp%%Qa+624X@ X49Luk)baa85H9$5YCsTPT`SVRWMtMW delta 18435 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 B8dv}T diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3994438e229..e750102e092 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787337f..4f906e0c811 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/bin/sh +#!/usr/bin/env sh # -# Copyright © 2015-2021 the original authors. +# Copyright 2015 the original author or authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,101 +17,67 @@ # ############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# +## +## Gradle start up script for UN*X +## ############################################################################## # Attempt to set APP_HOME - # Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi done - -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" -APP_BASE_NAME=${0##*/} +APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum +MAX_FD="maximum" warn () { echo "$*" -} >&2 +} die () { echo echo "$*" echo exit 1 -} >&2 +} # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -121,9 +87,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java + JAVACMD="$JAVA_HOME/jre/sh/java" else - JAVACMD=$JAVA_HOME/bin/java + JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -132,7 +98,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD=java + JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -140,95 +106,80 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi fi -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi # For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg + i=`expr $i + 1` done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac fi -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" exec "$JAVACMD" "$@" From 20494819a112f3a4aa4abf04cb2f69a973d72f7b Mon Sep 17 00:00:00 2001 From: Evan Darke Date: Thu, 6 Feb 2025 08:42:53 -0800 Subject: [PATCH 22/43] Optimize ObjectID sort and encoding/decoding (#1582) JAVA-5388 Co-authored-by: Viacheslav Babanin --- bson/src/main/org/bson/types/ObjectId.java | 178 +++++------------- .../unit/org/bson/types/ObjectIdTest.java | 121 +++++++++++- 2 files changed, 165 insertions(+), 134 deletions(-) diff --git a/bson/src/main/org/bson/types/ObjectId.java b/bson/src/main/org/bson/types/ObjectId.java index 7c1b1d29540..927d3ab0c31 100644 --- a/bson/src/main/org/bson/types/ObjectId.java +++ b/bson/src/main/org/bson/types/ObjectId.java @@ -16,17 +16,18 @@ package org.bson.types; +import static org.bson.assertions.Assertions.isTrueArgument; +import static org.bson.assertions.Assertions.notNull; + import java.io.InvalidObjectException; import java.io.ObjectInputStream; import java.io.Serializable; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.security.SecureRandom; import java.util.Date; import java.util.concurrent.atomic.AtomicInteger; -import static org.bson.assertions.Assertions.isTrueArgument; -import static org.bson.assertions.Assertions.notNull; - /** *

A globally unique identifier for objects.

* @@ -53,9 +54,8 @@ public final class ObjectId implements Comparable, Serializable { private static final int OBJECT_ID_LENGTH = 12; private static final int LOW_ORDER_THREE_BYTES = 0x00ffffff; - // Use primitives to represent the 5-byte random value. - private static final int RANDOM_VALUE1; - private static final short RANDOM_VALUE2; + // Use upper bytes of a long to represent the 5-byte random value. + private static final long RANDOM_VALUE; private static final AtomicInteger NEXT_COUNTER; @@ -67,18 +67,12 @@ public final class ObjectId implements Comparable, Serializable { * The timestamp */ private final int timestamp; + /** - * The counter. - */ - private final int counter; - /** - * the first four bits of randomness. - */ - private final int randomValue1; - /** - * The last two bits of randomness. + * The final 8 bytes of the ObjectID are 5 bytes probabilistically unique to the machine and + * process, followed by a 3 byte incrementing counter initialized to a random value. */ - private final short randomValue2; + private final long nonce; /** * Gets a new object id. @@ -101,7 +95,7 @@ public static ObjectId get() { * @since 4.1 */ public static ObjectId getSmallestWithDate(final Date date) { - return new ObjectId(dateToTimestampSeconds(date), 0, (short) 0, 0, false); + return new ObjectId(dateToTimestampSeconds(date), 0L); } /** @@ -152,7 +146,7 @@ public ObjectId() { * @param date the date */ public ObjectId(final Date date) { - this(dateToTimestampSeconds(date), NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES, false); + this(dateToTimestampSeconds(date), RANDOM_VALUE | (NEXT_COUNTER.getAndIncrement() & LOW_ORDER_THREE_BYTES)); } /** @@ -163,7 +157,7 @@ public ObjectId(final Date date) { * @throws IllegalArgumentException if the high order byte of counter is not zero */ public ObjectId(final Date date, final int counter) { - this(dateToTimestampSeconds(date), counter, true); + this(dateToTimestampSeconds(date), getNonceFromUntrustedCounter(counter)); } /** @@ -174,25 +168,19 @@ public ObjectId(final Date date, final int counter) { * @throws IllegalArgumentException if the high order byte of counter is not zero */ public ObjectId(final int timestamp, final int counter) { - this(timestamp, counter, true); + this(timestamp, getNonceFromUntrustedCounter(counter)); } - private ObjectId(final int timestamp, final int counter, final boolean checkCounter) { - this(timestamp, RANDOM_VALUE1, RANDOM_VALUE2, counter, checkCounter); + private ObjectId(final int timestamp, final long nonce) { + this.timestamp = timestamp; + this.nonce = nonce; } - private ObjectId(final int timestamp, final int randomValue1, final short randomValue2, final int counter, - final boolean checkCounter) { - if ((randomValue1 & 0xff000000) != 0) { - throw new IllegalArgumentException("The random value must be between 0 and 16777215 (it must fit in three bytes)."); - } - if (checkCounter && ((counter & 0xff000000) != 0)) { + private static long getNonceFromUntrustedCounter(final int counter) { + if ((counter & 0xff000000) != 0) { throw new IllegalArgumentException("The counter must be between 0 and 16777215 (it must fit in three bytes)."); } - this.timestamp = timestamp; - this.counter = counter & LOW_ORDER_THREE_BYTES; - this.randomValue1 = randomValue1; - this.randomValue2 = randomValue2; + return RANDOM_VALUE | counter; } /** @@ -226,12 +214,14 @@ public ObjectId(final ByteBuffer buffer) { notNull("buffer", buffer); isTrueArgument("buffer.remaining() >=12", buffer.remaining() >= OBJECT_ID_LENGTH); - // Note: Cannot use ByteBuffer.getInt because it depends on tbe buffer's byte order - // and ObjectId's are always in big-endian order. - timestamp = makeInt(buffer.get(), buffer.get(), buffer.get(), buffer.get()); - randomValue1 = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get()); - randomValue2 = makeShort(buffer.get(), buffer.get()); - counter = makeInt((byte) 0, buffer.get(), buffer.get(), buffer.get()); + ByteOrder originalOrder = buffer.order(); + try { + buffer.order(ByteOrder.BIG_ENDIAN); + this.timestamp = buffer.getInt(); + this.nonce = buffer.getLong(); + } finally { + buffer.order(originalOrder); + } } /** @@ -240,9 +230,11 @@ public ObjectId(final ByteBuffer buffer) { * @return the byte array */ public byte[] toByteArray() { - ByteBuffer buffer = ByteBuffer.allocate(OBJECT_ID_LENGTH); - putToByteBuffer(buffer); - return buffer.array(); // using .allocate ensures there is a backing array that can be returned + // using .allocate ensures there is a backing array that can be returned + return ByteBuffer.allocate(OBJECT_ID_LENGTH) + .putInt(this.timestamp) + .putLong(this.nonce) + .array(); } /** @@ -257,18 +249,14 @@ public void putToByteBuffer(final ByteBuffer buffer) { notNull("buffer", buffer); isTrueArgument("buffer.remaining() >=12", buffer.remaining() >= OBJECT_ID_LENGTH); - buffer.put(int3(timestamp)); - buffer.put(int2(timestamp)); - buffer.put(int1(timestamp)); - buffer.put(int0(timestamp)); - buffer.put(int2(randomValue1)); - buffer.put(int1(randomValue1)); - buffer.put(int0(randomValue1)); - buffer.put(short1(randomValue2)); - buffer.put(short0(randomValue2)); - buffer.put(int2(counter)); - buffer.put(int1(counter)); - buffer.put(int0(counter)); + ByteOrder originalOrder = buffer.order(); + try { + buffer.order(ByteOrder.BIG_ENDIAN); + buffer.putInt(this.timestamp); + buffer.putLong(this.nonce); + } finally { + buffer.order(originalOrder); + } } /** @@ -313,49 +301,26 @@ public boolean equals(final Object o) { return false; } - ObjectId objectId = (ObjectId) o; - - if (counter != objectId.counter) { - return false; - } - if (timestamp != objectId.timestamp) { - return false; - } - - if (randomValue1 != objectId.randomValue1) { + ObjectId other = (ObjectId) o; + if (timestamp != other.timestamp) { return false; } - - if (randomValue2 != objectId.randomValue2) { - return false; - } - - return true; + return nonce == other.nonce; } @Override public int hashCode() { - int result = timestamp; - result = 31 * result + counter; - result = 31 * result + randomValue1; - result = 31 * result + randomValue2; - return result; + return 31 * timestamp + Long.hashCode(nonce); } @Override public int compareTo(final ObjectId other) { - if (other == null) { - throw new NullPointerException(); + int cmp = Integer.compareUnsigned(this.timestamp, other.timestamp); + if (cmp != 0) { + return cmp; } - byte[] byteArray = toByteArray(); - byte[] otherByteArray = other.toByteArray(); - for (int i = 0; i < OBJECT_ID_LENGTH; i++) { - if (byteArray[i] != otherByteArray[i]) { - return ((byteArray[i] & 0xff) < (otherByteArray[i] & 0xff)) ? -1 : 1; - } - } - return 0; + return Long.compareUnsigned(nonce, other.nonce); } @Override @@ -407,8 +372,7 @@ private Object readResolve() { static { try { SecureRandom secureRandom = new SecureRandom(); - RANDOM_VALUE1 = secureRandom.nextInt(0x01000000); - RANDOM_VALUE2 = (short) secureRandom.nextInt(0x00008000); + RANDOM_VALUE = secureRandom.nextLong() & ~LOW_ORDER_THREE_BYTES; NEXT_COUNTER = new AtomicInteger(secureRandom.nextInt()); } catch (Exception e) { throw new RuntimeException(e); @@ -443,46 +407,4 @@ private static int hexCharToInt(final char c) { private static int dateToTimestampSeconds(final Date time) { return (int) (time.getTime() / 1000); } - - // Big-Endian helpers, in this class because all other BSON numbers are little-endian - - private static int makeInt(final byte b3, final byte b2, final byte b1, final byte b0) { - // CHECKSTYLE:OFF - return (((b3) << 24) | - ((b2 & 0xff) << 16) | - ((b1 & 0xff) << 8) | - ((b0 & 0xff))); - // CHECKSTYLE:ON - } - - private static short makeShort(final byte b1, final byte b0) { - // CHECKSTYLE:OFF - return (short) (((b1 & 0xff) << 8) | ((b0 & 0xff))); - // CHECKSTYLE:ON - } - - private static byte int3(final int x) { - return (byte) (x >> 24); - } - - private static byte int2(final int x) { - return (byte) (x >> 16); - } - - private static byte int1(final int x) { - return (byte) (x >> 8); - } - - private static byte int0(final int x) { - return (byte) (x); - } - - private static byte short1(final short x) { - return (byte) (x >> 8); - } - - private static byte short0(final short x) { - return (byte) (x); - } - } diff --git a/bson/src/test/unit/org/bson/types/ObjectIdTest.java b/bson/src/test/unit/org/bson/types/ObjectIdTest.java index 14c8241f55a..cfe04623b90 100644 --- a/bson/src/test/unit/org/bson/types/ObjectIdTest.java +++ b/bson/src/test/unit/org/bson/types/ObjectIdTest.java @@ -17,36 +17,72 @@ package org.bson.types; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.nio.Buffer; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.Locale; import java.util.Random; +import static org.junit.Assert.assertFalse; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; public class ObjectIdTest { - @Test - public void testToBytes() { + + /** Calls the base method of ByteBuffer.position(int) since the override is not available in jdk8. */ + private static ByteBuffer setPosition(final ByteBuffer buf, final int pos) { + ((Buffer) buf).position(pos); + return buf; + } + + /** + * MethodSource for valid ByteBuffers that can hold an ObjectID + */ + public static List validOutputBuffers() { + List result = new ArrayList<>(); + result.add(ByteBuffer.allocate(12)); + result.add(ByteBuffer.allocate(12).order(ByteOrder.LITTLE_ENDIAN)); + result.add(ByteBuffer.allocate(24).put(new byte[12])); + result.add(ByteBuffer.allocateDirect(12)); + result.add(ByteBuffer.allocateDirect(12).order(ByteOrder.LITTLE_ENDIAN)); + return result; + } + + @MethodSource("validOutputBuffers") + @ParameterizedTest + public void testToBytes(final ByteBuffer output) { + int originalPosition = output.position(); + ByteOrder originalOrder = output.order(); byte[] expectedBytes = {81, 6, -4, -102, -68, -126, 55, 85, -127, 54, -46, -119}; + byte[] result = new byte[12]; ObjectId objectId = new ObjectId(expectedBytes); assertArrayEquals(expectedBytes, objectId.toByteArray()); - ByteBuffer buffer = ByteBuffer.allocate(12); - objectId.putToByteBuffer(buffer); - assertArrayEquals(expectedBytes, buffer.array()); + objectId.putToByteBuffer(output); + ((Buffer) output).position(output.position() - 12); + output.get(result); // read last 12 bytes leaving position intact + + assertArrayEquals(expectedBytes, result); + assertEquals(originalPosition + 12, output.position()); + assertEquals(originalOrder, output.order()); } @Test @@ -136,8 +172,64 @@ public void testTime() { } @Test - public void testDateCons() { + public void testDateConstructor() { assertEquals(new Date().getTime() / 1000, new ObjectId(new Date()).getDate().getTime() / 1000); + assertNotEquals(new ObjectId(new Date(1_000)), new ObjectId(new Date(1_000))); + assertEquals("00000001", new ObjectId(new Date(1_000)).toHexString().substring(0, 8)); + } + + @Test + public void testDateConstructorWithCounter() { + assertEquals(new ObjectId(new Date(1_000), 1), new ObjectId(new Date(1_000), 1)); + assertEquals("00000001", new ObjectId(new Date(1_000), 1).toHexString().substring(0, 8)); + assertThrows(NullPointerException.class, () -> new ObjectId(null, Integer.MAX_VALUE)); + assertThrows(IllegalArgumentException.class, () -> new ObjectId(new Date(1_000), Integer.MAX_VALUE)); + } + + @Test + public void testTimestampConstructor() { + assertEquals(1_000, new ObjectId(1_000, 1).getTimestamp()); + assertEquals(new ObjectId(1_000, 1), new ObjectId(1_000, 1)); + assertEquals("7fffffff", new ObjectId(Integer.MAX_VALUE, 1).toHexString().substring(0, 8)); + assertThrows(IllegalArgumentException.class, () -> new ObjectId(Integer.MAX_VALUE, Integer.MAX_VALUE)); + } + + /** + * MethodSource for valid ByteBuffers containing an ObjectID at the current position. + */ + public static List validInputBuffers() { + byte[] data = new byte[12]; + for (byte i = 0; i < data.length; ++i) { + data[i] = i; + } + + List result = new ArrayList<>(); + result.add(ByteBuffer.wrap(data)); + result.add(ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)); + result.add(setPosition(ByteBuffer.allocateDirect(data.length).put(data), 0)); + result.add(setPosition(ByteBuffer.allocateDirect(data.length).put(data).order(ByteOrder.LITTLE_ENDIAN), 0)); + result.add(setPosition(ByteBuffer.allocate(2 * data.length).put(data), 0)); + result.add(setPosition(ByteBuffer.allocate(2 * data.length).put(new byte[12]).put(data), 12)); + return result; + } + + @ParameterizedTest + @MethodSource(value = "validInputBuffers") + public void testByteBufferConstructor(final ByteBuffer input) { + ByteOrder order = input.order(); + int position = input.position(); + + byte[] result = new ObjectId(input).toByteArray(); + + assertArrayEquals(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, result); + assertEquals(order, input.order()); + assertEquals(position + 12, input.position()); + } + + @Test + public void testInvalidByteBufferConstructor() { + assertThrows(IllegalArgumentException.class, () -> new ObjectId((ByteBuffer) null)); + assertThrows(IllegalArgumentException.class, () -> new ObjectId(ByteBuffer.allocate(11))); } @Test @@ -162,6 +254,23 @@ public void testCompareTo() { assertEquals(-1, first.compareTo(third)); assertEquals(1, second.compareTo(first)); assertEquals(1, third.compareTo(first)); + assertThrows(NullPointerException.class, () -> first.compareTo(null)); + } + + @Test + public void testEquals() { + Date dateOne = new Date(); + Date dateTwo = new Date(dateOne.getTime() + 10000); + ObjectId first = new ObjectId(dateOne, 0); + ObjectId second = new ObjectId(dateOne, 1); + ObjectId third = new ObjectId(dateTwo, 0); + ObjectId fourth = new ObjectId(first.toByteArray()); + assertEquals(first, first); + assertEquals(first, fourth); + assertNotEquals(first, second); + assertNotEquals(first, third); + assertNotEquals(second, third); + assertFalse(first.equals(null)); } @Test From e4e5e6962535ef068ba3a49efcd27f1d1a9dadcb Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Tue, 11 Feb 2025 07:58:25 -0700 Subject: [PATCH 23/43] Stop ignoring id parameter (#1622) JAVA-5728 --- .../client/internal/gridfs/GridFSBucketImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/gridfs/GridFSBucketImpl.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/gridfs/GridFSBucketImpl.java index 1e81db2045e..948c666489c 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/gridfs/GridFSBucketImpl.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/gridfs/GridFSBucketImpl.java @@ -208,7 +208,7 @@ public GridFSUploadPublisher uploadFromPublisher(final ClientSession clien final Publisher source, final GridFSUploadOptions options) { return createGridFSUploadPublisher(chunkSizeBytes, filesCollection, chunksCollection, - notNull("clientSession", clientSession), new BsonObjectId(), filename, options, source); + notNull("clientSession", clientSession), id, filename, options, source); } @Override From 1c9942ca9cfdad296c80a5b0f3aa6d10e54cb9e1 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Tue, 11 Feb 2025 08:57:45 -0700 Subject: [PATCH 24/43] JAVA-5781 Add env var override (#1623) JAVA-5781 --- .../connection/ClientMetadataHelper.java | 2 +- .../internal/connection/FaasEnvironment.java | 30 ++++++++++++------ .../com/mongodb/client/WithWrapper.java | 18 ++--------- .../connection/FaasEnvironmentAccessor.java | 31 +++++++++++++++++++ 4 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 driver-core/src/test/functional/com/mongodb/internal/connection/FaasEnvironmentAccessor.java diff --git a/driver-core/src/main/com/mongodb/internal/connection/ClientMetadataHelper.java b/driver-core/src/main/com/mongodb/internal/connection/ClientMetadataHelper.java index 36d2d891829..825af685c10 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/ClientMetadataHelper.java +++ b/driver-core/src/main/com/mongodb/internal/connection/ClientMetadataHelper.java @@ -224,7 +224,7 @@ private enum Orchestrator { K8S("kubernetes") { @Override boolean isCurrentOrchestrator() { - return System.getenv("KUBERNETES_SERVICE_HOST") != null; + return FaasEnvironment.getEnv("KUBERNETES_SERVICE_HOST") != null; } }, UNKNOWN(null); diff --git a/driver-core/src/main/com/mongodb/internal/connection/FaasEnvironment.java b/driver-core/src/main/com/mongodb/internal/connection/FaasEnvironment.java index 6627722097b..a54c1efb066 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/FaasEnvironment.java +++ b/driver-core/src/main/com/mongodb/internal/connection/FaasEnvironment.java @@ -20,7 +20,9 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; enum FaasEnvironment { AWS_LAMBDA("aws.lambda"), @@ -29,21 +31,23 @@ enum FaasEnvironment { VERCEL("vercel"), UNKNOWN(null); + static final Map ENV_OVERRIDES_FOR_TESTING = new HashMap<>(); + static FaasEnvironment getFaasEnvironment() { List result = new ArrayList<>(); - String awsExecutionEnv = System.getenv("AWS_EXECUTION_ENV"); + String awsExecutionEnv = getEnv("AWS_EXECUTION_ENV"); - if (System.getenv("VERCEL") != null) { + if (getEnv("VERCEL") != null) { result.add(FaasEnvironment.VERCEL); } if ((awsExecutionEnv != null && awsExecutionEnv.startsWith("AWS_Lambda_")) - || System.getenv("AWS_LAMBDA_RUNTIME_API") != null) { + || getEnv("AWS_LAMBDA_RUNTIME_API") != null) { result.add(FaasEnvironment.AWS_LAMBDA); } - if (System.getenv("FUNCTIONS_WORKER_RUNTIME") != null) { + if (getEnv("FUNCTIONS_WORKER_RUNTIME") != null) { result.add(FaasEnvironment.AZURE_FUNC); } - if (System.getenv("K_SERVICE") != null || System.getenv("FUNCTION_NAME") != null) { + if (getEnv("K_SERVICE") != null || getEnv("FUNCTION_NAME") != null) { result.add(FaasEnvironment.GCP_FUNC); } // vercel takes precedence over aws.lambda @@ -56,6 +60,14 @@ static FaasEnvironment getFaasEnvironment() { return result.get(0); } + @Nullable + public static String getEnv(final String key) { + if (ENV_OVERRIDES_FOR_TESTING.containsKey(key)) { + return ENV_OVERRIDES_FOR_TESTING.get(key); + } + return System.getenv(key); + } + @Nullable private final String name; @@ -95,11 +107,11 @@ public Integer getMemoryMb() { public String getRegion() { switch (this) { case AWS_LAMBDA: - return System.getenv("AWS_REGION"); + return getEnv("AWS_REGION"); case GCP_FUNC: - return System.getenv("FUNCTION_REGION"); + return getEnv("FUNCTION_REGION"); case VERCEL: - return System.getenv("VERCEL_REGION"); + return getEnv("VERCEL_REGION"); default: return null; } @@ -108,7 +120,7 @@ public String getRegion() { @Nullable private static Integer getEnvInteger(final String name) { try { - String value = System.getenv(name); + String value = getEnv(name); return Integer.parseInt(value); } catch (NumberFormatException e) { return null; diff --git a/driver-core/src/test/functional/com/mongodb/client/WithWrapper.java b/driver-core/src/test/functional/com/mongodb/client/WithWrapper.java index 6484f642a1a..e610f578112 100644 --- a/driver-core/src/test/functional/com/mongodb/client/WithWrapper.java +++ b/driver-core/src/test/functional/com/mongodb/client/WithWrapper.java @@ -16,13 +16,11 @@ package com.mongodb.client; +import com.mongodb.internal.connection.FaasEnvironmentAccessor; import com.mongodb.lang.Nullable; -import java.lang.reflect.Field; import java.util.Map; -import static java.lang.System.getenv; - @FunctionalInterface public interface WithWrapper { @@ -34,7 +32,7 @@ static WithWrapper withWrapper() { default WithWrapper withEnvironmentVariable(final String name, @Nullable final String value) { return runnable -> { - Map innerMap = getEnvInnerMap(); + Map innerMap = FaasEnvironmentAccessor.getFaasEnvMap(); String original = innerMap.get(name); if (value == null) { innerMap.remove(name); @@ -65,16 +63,4 @@ default WithWrapper withSystemProperty(final String name, final String value) { }; } - static Map getEnvInnerMap() { - try { - Map env = getenv(); - Field field = env.getClass().getDeclaredField("m"); - field.setAccessible(true); - @SuppressWarnings("unchecked") - Map result = (Map) field.get(env); - return result; - } catch (IllegalAccessException | NoSuchFieldException e) { - throw new RuntimeException(e); - } - } } diff --git a/driver-core/src/test/functional/com/mongodb/internal/connection/FaasEnvironmentAccessor.java b/driver-core/src/test/functional/com/mongodb/internal/connection/FaasEnvironmentAccessor.java new file mode 100644 index 00000000000..ccc71f718ba --- /dev/null +++ b/driver-core/src/test/functional/com/mongodb/internal/connection/FaasEnvironmentAccessor.java @@ -0,0 +1,31 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.internal.connection; + +import java.util.Map; + +/** + * In the same package as FaasEnvironment, to access package-private + */ +public final class FaasEnvironmentAccessor { + private FaasEnvironmentAccessor() { + } + + public static Map getFaasEnvMap() { + return FaasEnvironment.ENV_OVERRIDES_FOR_TESTING; + } +} From 36448cd550f03aac0e02d12f586da72e585fef39 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Thu, 13 Feb 2025 06:54:09 -0700 Subject: [PATCH 25/43] JAVA-5771 Add search integration tests (#1616) JAVA-5771 --- .evergreen/.evg.yml | 161 ++++------- .evergreen/run-atlas-search-tests.sh | 4 +- .../client/model/search/SearchOperator.java | 9 +- .../model/search/WildcardSearchOperator.java | 2 +- ...atesBinaryVectorSearchIntegrationTest.java | 2 +- .../AggregatesSearchIntegrationTest.java | 3 + .../model/search/SearchOperatorTest.java | 4 +- .../scala/model/search/SearchOperator.scala | 2 +- .../com/mongodb/client/DatabaseTestCase.java | 23 +- .../com/mongodb/client/Fixture.java | 45 ++- .../AggregatesSearchFunctionalTest.java | 262 ++++++++++++++++++ 11 files changed, 356 insertions(+), 161 deletions(-) create mode 100644 driver-sync/src/test/functional/com/mongodb/client/model/search/AggregatesSearchFunctionalTest.java diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index ed22ca27972..e511c022186 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -509,24 +509,6 @@ functions: mongo --nodb setup.js aws_e2e_ecs.js cd - - "run atlas data lake test": - - command: shell.exec - type: test - params: - working_dir: "src" - script: | - ${PREPARE_SHELL} - JAVA_VERSION=${JAVA_VERSION} .evergreen/run-atlas-data-lake-test.sh - - "run atlas search test": - - command: shell.exec - type: test - params: - working_dir: "src" - script: | - ${PREPARE_SHELL} - MONGODB_URI="${atlas_search_uri}" .evergreen/run-atlas-search-tests.sh - "run-ocsp-test": - command: shell.exec type: test @@ -627,19 +609,6 @@ functions: ${PREPARE_SHELL} PROJECT_DIRECTORY=${PROJECT_DIRECTORY} JAVA_VERSION=${JAVA_VERSION} TOPOLOGY=${TOPOLOGY} STORAGE_ENGINE=${STORAGE_ENGINE} MONGODB_URI="${MONGODB_URI}" .evergreen/run-mmapv1-storage-test.sh - "run atlas test": - - command: shell.exec - type: test - params: - silent: true - working_dir: "src" - script: | - # DO NOT ECHO WITH XTRACE (which PREPARE_SHELL does) - # The connection strings are pipe-delimited - JAVA_VERSION="8" \ - MONGODB_URIS="${atlas_free_tier_uri}|${atlas_replica_set_uri}|${atlas_sharded_uri}|${atlas_tls_v11_uri}|${atlas_tls_v12_uri}|${atlas_free_tier_uri_srv}|${atlas_replica_set_uri_srv}|${atlas_sharded_uri_srv}|${atlas_tls_v11_uri_srv}|${atlas_tls_v12_uri_srv}|${atlas_serverless_uri}|${atlas_serverless_uri_srv}" \ - .evergreen/run-connectivity-tests.sh - run socks5 tests: - command: shell.exec type: test @@ -1454,14 +1423,50 @@ tasks: OCSP_MUST_STAPLE: "false" OCSP_TLS_SHOULD_SUCCEED: "0" - - name: "atlas-data-lake-test" + - name: "atlas-data-lake-task" commands: - func: "bootstrap mongohoused" - - func: "run atlas data lake test" + - command: shell.exec + type: test + params: + working_dir: "src" + script: | + ${PREPARE_SHELL} + JAVA_VERSION=${JAVA_VERSION} .evergreen/run-atlas-data-lake-test.sh - - name: "atlas-search-test" + - name: "atlas-search-task" commands: - - func: "run atlas search test" + - command: shell.exec + type: test + params: + working_dir: "src" + script: | + ${PREPARE_SHELL} + MONGODB_URI="${atlas_search_uri}" .evergreen/run-atlas-search-tests.sh + + - name: "atlas-connectivity-task" + commands: + - command: shell.exec + type: test + params: + silent: true + working_dir: "src" + script: | + # DO NOT ECHO WITH XTRACE (which PREPARE_SHELL does) + # The connection strings are pipe-delimited + MONGODB_URIS="${atlas_free_tier_uri}|${atlas_replica_set_uri}|${atlas_sharded_uri}|${atlas_tls_v11_uri}|${atlas_tls_v12_uri}|${atlas_free_tier_uri_srv}|${atlas_replica_set_uri_srv}|${atlas_sharded_uri_srv}|${atlas_tls_v11_uri_srv}|${atlas_tls_v12_uri_srv}|${atlas_serverless_uri}|${atlas_serverless_uri_srv}" \ + JAVA_VERSION="8" \ + .evergreen/run-connectivity-tests.sh + + - name: "atlas-search-index-management-task" + commands: + - command: subprocess.exec + params: + working_dir: src + binary: bash + add_expansions_to_env: true + args: + - .evergreen/run-atlas-search-index-management-tests.sh - name: "gssapi-auth-test" commands: @@ -1482,20 +1487,6 @@ tasks: - func: "bootstrap mongo-orchestration" - func: "run netty tests" - - name: "atlas-test" - commands: - - func: "run atlas test" - - - name: "test-atlas-search-index-helpers" - commands: - - command: subprocess.exec - params: - working_dir: src - binary: bash - add_expansions_to_env: true - args: - - .evergreen/run-atlas-search-index-management-tests.sh - - name: publish-snapshot depends_on: - variant: "static-checks" @@ -1528,7 +1519,7 @@ tasks: - func: "run perf tests" - func: "send dashboard data" - - name: "test-aws-lambda-deployed" + - name: "aws-lambda-deployed-task" commands: - command: ec2.assume_role params: @@ -1622,7 +1613,6 @@ tasks: echo "Untarring file ... begin" GCPKMS_CMD="tar xf mongo-java-driver.tgz" $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/run-command.sh echo "Untarring file ... end" - - command: shell.exec type: test params: @@ -1910,11 +1900,12 @@ axes: batchtime: 10080 # 7 days task_groups: - - name: test_atlas_task_group_search_indexes + - name: "atlas-deployed-task-group" setup_group: - func: fetch source - func: prepare resources - command: subprocess.exec + type: setup params: working_dir: src binary: bash @@ -1926,6 +1917,7 @@ task_groups: file: src/atlas-expansion.yml teardown_group: - command: subprocess.exec + type: setup params: working_dir: src binary: bash @@ -1935,7 +1927,9 @@ task_groups: setup_group_can_fail_task: true setup_group_timeout_secs: 1800 tasks: - - test-atlas-search-index-helpers + - "atlas-search-index-management-task" + - "aws-lambda-deployed-task" + - name: testgcpkms_task_group setup_group_can_fail_task: true setup_group_timeout_secs: 1800 # 30 minutes @@ -1971,6 +1965,7 @@ task_groups: $DRIVERS_TOOLS/.evergreen/csfle/gcpkms/delete-instance.sh tasks: - testgcpkms-task + - name: testazurekms_task_group setup_group_can_fail_task: true setup_group_timeout_secs: 1800 # 30 minutes @@ -2015,32 +2010,6 @@ task_groups: $DRIVERS_TOOLS/.evergreen/csfle/azurekms/delete-vm.sh tasks: - testazurekms-task - - name: test_atlas_task_group - setup_group: - - func: fetch source - - func: prepare resources - - command: subprocess.exec - params: - working_dir: src - binary: bash - add_expansions_to_env: true - args: - - ${DRIVERS_TOOLS}/.evergreen/atlas/setup-atlas-cluster.sh - - command: expansions.update - params: - file: src/atlas-expansion.yml - teardown_group: - - command: subprocess.exec - params: - working_dir: src - binary: bash - add_expansions_to_env: true - args: - - ${DRIVERS_TOOLS}/.evergreen/atlas/teardown-atlas-cluster.sh - setup_group_can_fail_task: true - setup_group_timeout_secs: 1800 - tasks: - - test-aws-lambda-deployed - name: testoidc_task_group setup_group: @@ -2275,24 +2244,12 @@ buildvariants: tasks: - name: "perf" -- name: rhel8-test-atlas - display_name: Atlas Cluster Tests - run_on: rhel80-large - tasks: - - test_atlas_task_group - - name: plain-auth-test display_name: "PLAIN (LDAP) Auth test" run_on: rhel80-small tasks: - name: "plain-auth-test" -- name: rhel80-test-search-indexes - display_name: Atlas Search Index Management Tests - run_on: rhel80-small - tasks: - - name: "test_atlas_task_group_search_indexes" - - name: "oidc-auth-test" display_name: "OIDC Auth" run_on: ubuntu2204-small @@ -2347,23 +2304,19 @@ buildvariants: tasks: - name: ".ocsp" -- name: atlas-data-lake-test - display_name: "Atlas Data Lake test" - run_on: ubuntu2004-small - tasks: - - name: "atlas-data-lake-test" - -- name: atlas-test - display_name: "Atlas test" +- name: "atlas-search-variant" + display_name: "Atlas Tests" run_on: rhel80-small tasks: - - name: "atlas-test" + - name: "atlas-deployed-task-group" + - name: "atlas-search-task" + - name: "atlas-connectivity-task" -- name: atlas-search-test - display_name: "Atlas Search test" - run_on: rhel80-small +- name: atlas-data-lake-test + display_name: "Atlas Data Lake test" + run_on: ubuntu2004-small tasks: - - name: "atlas-search-test" + - name: "atlas-data-lake-task" - name: "reactive-streams-tck-test" display_name: "Reactive Streams TCK tests" diff --git a/.evergreen/run-atlas-search-tests.sh b/.evergreen/run-atlas-search-tests.sh index 36cc981b3f4..f207647825f 100755 --- a/.evergreen/run-atlas-search-tests.sh +++ b/.evergreen/run-atlas-search-tests.sh @@ -16,4 +16,6 @@ echo "Running Atlas Search tests" ./gradlew --stacktrace --info \ -Dorg.mongodb.test.atlas.search=true \ -Dorg.mongodb.test.uri=${MONGODB_URI} \ - driver-core:test --tests AggregatesSearchIntegrationTest --tests AggregatesVectorSearchIntegrationTest + driver-core:test --tests AggregatesSearchIntegrationTest \ + --tests AggregatesBinaryVectorSearchIntegrationTest \ + --tests AggregatesSearchTest \ diff --git a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java index ef5c1239313..aa8b01b29d4 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java +++ b/driver-core/src/main/com/mongodb/client/model/search/SearchOperator.java @@ -22,9 +22,9 @@ import com.mongodb.client.model.geojson.Point; import org.bson.BsonArray; import org.bson.BsonBinary; -import org.bson.BsonBoolean; import org.bson.BsonDocument; import org.bson.BsonNull; +import org.bson.BsonBoolean; import org.bson.BsonType; import org.bson.Document; import org.bson.conversions.Bson; @@ -573,14 +573,15 @@ static PhraseSearchOperator phrase(final Iterable paths, f } /** - * Returns a {@link SearchOperator} that performs a search using a special characters in the search string that can match any character. + * Returns a {@link SearchOperator} that performs a search using a special + * characters in the search string that can match any character. * + * @param path The indexed field to be searched. * @param query The string to search for. - * @param path The indexed field to be searched. * @return The requested {@link SearchOperator}. * @mongodb.atlas.manual atlas-search/wildcard/ wildcard operator */ - static WildcardSearchOperator wildcard(final String query, final SearchPath path) { + static WildcardSearchOperator wildcard(final SearchPath path, final String query) { return wildcard(singleton(notNull("query", query)), singleton(notNull("path", path))); } diff --git a/driver-core/src/main/com/mongodb/client/model/search/WildcardSearchOperator.java b/driver-core/src/main/com/mongodb/client/model/search/WildcardSearchOperator.java index 651d9ffa57c..95d4a5caad5 100644 --- a/driver-core/src/main/com/mongodb/client/model/search/WildcardSearchOperator.java +++ b/driver-core/src/main/com/mongodb/client/model/search/WildcardSearchOperator.java @@ -20,7 +20,7 @@ import com.mongodb.annotations.Sealed; /** - * @see SearchOperator#wildcard(String, SearchPath) + * @see SearchOperator#wildcard(SearchPath, String) * @see SearchOperator#wildcard(Iterable, Iterable) * @since 4.7 */ diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesBinaryVectorSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesBinaryVectorSearchIntegrationTest.java index 0d5aad1085a..a242367992f 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesBinaryVectorSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesBinaryVectorSearchIntegrationTest.java @@ -22,9 +22,9 @@ import com.mongodb.client.model.SearchIndexType; import com.mongodb.client.test.CollectionHelper; import com.mongodb.internal.operation.SearchIndexRequest; +import org.bson.BinaryVector; import org.bson.BsonDocument; import org.bson.Document; -import org.bson.BinaryVector; import org.bson.codecs.DocumentCodec; import org.bson.conversions.Bson; import org.junit.jupiter.api.AfterAll; diff --git a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java index 3fe4229b498..bc34cb0060c 100644 --- a/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java +++ b/driver-core/src/test/functional/com/mongodb/client/model/search/AggregatesSearchIntegrationTest.java @@ -229,6 +229,9 @@ * * * + *

+ * Use this class when needing to test against MFLIX specifically. Otherwise, + * see AggregatesSearchTest. */ final class AggregatesSearchIntegrationTest { private static final MongoNamespace MFLIX_MOVIES_NS = new MongoNamespace("sample_mflix", "movies"); diff --git a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java index 9151768da31..ccf5a44cd1f 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/search/SearchOperatorTest.java @@ -833,8 +833,8 @@ void wildcard() { .append("path", fieldPath("fieldName").toBsonValue()) ), SearchOperator.wildcard( - "term", - fieldPath("fieldName")) + fieldPath("fieldName"), "term" + ) .toBsonDocument() ), () -> assertEquals( diff --git a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala index 6f4f6a8860d..1fa47a54e1b 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/model/search/SearchOperator.scala @@ -429,7 +429,7 @@ object SearchOperator { * @return The requested `SearchOperator`. * @see [[https://www.mongodb.com/docs/atlas/atlas-search/wildcard/ wildcard operator]] */ - def wildcard(query: String, path: SearchPath): WildcardSearchOperator = JSearchOperator.wildcard(query, path) + def wildcard(query: String, path: SearchPath): WildcardSearchOperator = JSearchOperator.wildcard(path, query) /** * Returns a `SearchOperator` that enables queries which use special characters in the search string that can match any character. diff --git a/driver-sync/src/test/functional/com/mongodb/client/DatabaseTestCase.java b/driver-sync/src/test/functional/com/mongodb/client/DatabaseTestCase.java index f2f1c5382cd..70479c4670b 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/DatabaseTestCase.java +++ b/driver-sync/src/test/functional/com/mongodb/client/DatabaseTestCase.java @@ -16,11 +16,8 @@ package com.mongodb.client; -import com.mongodb.MongoNamespace; import com.mongodb.client.test.CollectionHelper; import com.mongodb.internal.connection.ServerHelper; -import org.bson.BsonDocument; -import org.bson.BsonDocumentWrapper; import org.bson.Document; import org.bson.codecs.DocumentCodec; import org.junit.jupiter.api.AfterEach; @@ -40,7 +37,7 @@ public class DatabaseTestCase { @BeforeEach public void setUp() { - client = getMongoClient(); + client = getMongoClient(); database = client.getDatabase(getDefaultDatabaseName()); collection = database.getCollection(getClass().getName()); collection.drop(); @@ -58,23 +55,7 @@ public void tearDown() { } } - protected String getDatabaseName() { - return database.getName(); - } - - protected String getCollectionName() { - return collection.getNamespace().getCollectionName(); - } - - protected MongoNamespace getNamespace() { - return collection.getNamespace(); - } - protected CollectionHelper getCollectionHelper() { - return new CollectionHelper<>(new DocumentCodec(), getNamespace()); - } - - protected BsonDocument wrap(final Document document) { - return new BsonDocumentWrapper<>(document, new DocumentCodec()); + return new CollectionHelper<>(new DocumentCodec(), collection.getNamespace()); } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/Fixture.java b/driver-sync/src/test/functional/com/mongodb/client/Fixture.java index 3b0d45dca88..8114d62e41a 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/Fixture.java +++ b/driver-sync/src/test/functional/com/mongodb/client/Fixture.java @@ -16,6 +16,7 @@ package com.mongodb.client; +import com.mongodb.ClusterFixture; import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; import com.mongodb.ServerAddress; @@ -24,7 +25,6 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import static com.mongodb.ClusterFixture.getConnectionString; import static com.mongodb.ClusterFixture.getMultiMongosConnectionString; import static com.mongodb.ClusterFixture.getServerApi; import static com.mongodb.internal.connection.ClusterDescriptionHelper.getPrimaries; @@ -34,7 +34,6 @@ * Helper class for the acceptance tests. */ public final class Fixture { - private static final String DEFAULT_DATABASE_NAME = "JavaDriverTest"; private static final long MIN_HEARTBEAT_FREQUENCY_MS = 50L; private static MongoClient mongoClient; @@ -44,10 +43,23 @@ private Fixture() { } public static synchronized MongoClient getMongoClient() { - if (mongoClient == null) { - mongoClient = MongoClients.create(getMongoClientSettings()); - Runtime.getRuntime().addShutdownHook(new ShutdownHook()); + if (mongoClient != null) { + return mongoClient; } + MongoClientSettings mongoClientSettings = getMongoClientSettings(); + mongoClient = MongoClients.create(mongoClientSettings); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + synchronized (Fixture.class) { + if (mongoClient == null) { + return; + } + if (defaultDatabase != null) { + defaultDatabase.drop(); + } + mongoClient.close(); + mongoClient = null; + } + })); return mongoClient; } @@ -59,34 +71,15 @@ public static synchronized MongoDatabase getDefaultDatabase() { } public static String getDefaultDatabaseName() { - return DEFAULT_DATABASE_NAME; - } - - static class ShutdownHook extends Thread { - @Override - public void run() { - synchronized (Fixture.class) { - if (mongoClient != null) { - if (defaultDatabase != null) { - defaultDatabase.drop(); - } - mongoClient.close(); - mongoClient = null; - } - } - } + return ClusterFixture.getDefaultDatabaseName(); } public static MongoClientSettings getMongoClientSettings() { return getMongoClientSettingsBuilder().build(); } - public static MongoClientSettings getMultiMongosMongoClientSettings() { - return getMultiMongosMongoClientSettingsBuilder().build(); - } - public static MongoClientSettings.Builder getMongoClientSettingsBuilder() { - return getMongoClientSettings(getConnectionString()); + return getMongoClientSettings(ClusterFixture.getConnectionString()); } public static MongoClientSettings.Builder getMultiMongosMongoClientSettingsBuilder() { diff --git a/driver-sync/src/test/functional/com/mongodb/client/model/search/AggregatesSearchFunctionalTest.java b/driver-sync/src/test/functional/com/mongodb/client/model/search/AggregatesSearchFunctionalTest.java new file mode 100644 index 00000000000..1513d5495bc --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/model/search/AggregatesSearchFunctionalTest.java @@ -0,0 +1,262 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client.model.search; + +import com.mongodb.client.AggregateIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.SearchIndexModel; +import com.mongodb.internal.connection.ServerHelper; +import org.bson.BsonArray; +import org.bson.BsonDocument; +import org.bson.Document; +import org.bson.codecs.DecoderContext; +import org.bson.conversions.Bson; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static com.mongodb.ClusterFixture.isAtlasSearchTest; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry; +import static com.mongodb.client.Fixture.getMongoClient; +import static com.mongodb.client.Fixture.getPrimary; +import static com.mongodb.client.model.Aggregates.search; +import static com.mongodb.client.model.Aggregates.sort; +import static com.mongodb.client.model.Sorts.ascending; +import static com.mongodb.client.model.search.SearchOptions.searchOptions; +import static com.mongodb.client.model.search.SearchPath.fieldPath; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class AggregatesSearchFunctionalTest { + public static final String ATLAS_SEARCH_DATABASE = "javaVectorSearchTest"; + private static MongoClient client; + private static MongoDatabase database; + private static MongoCollection collection; + private static String searchIndexName; + + @BeforeAll + public static void beforeAll() { + assumeTrue(isAtlasSearchTest()); + assumeTrue(serverVersionAtLeast(8, 0)); + + client = getMongoClient(); + database = client.getDatabase(ATLAS_SEARCH_DATABASE); + String collectionName = AggregatesSearchFunctionalTest.class.getName(); + collection = database.getCollection(collectionName); + collection.drop(); + + // We insert documents first. The ensuing indexing guarantees that all + // data present at the time indexing commences will be indexed before + // the index enters the READY state. + insertDocuments("[\n" + + " { _id: 1 },\n" + + " { _id: 2, title: null },\n" + + " { _id: 3, title: 'test' },\n" + + " { _id: 4, title: ['test', 'xyz'] },\n" + + " { _id: 5, title: 'not test' },\n" + + " { _id: 6, description: 'desc 1' },\n" + + " { _id: 7, description: 'desc 8' },\n" + + " { _id: 8, summary: 'summary 1 one five' },\n" + + " { _id: 9, summary: 'summary 2 one two three four five' },\n" + + "]"); + + searchIndexName = "not_default"; + // Index creation can take disproportionately long, so we create it once + // for all tests. + // We set dynamic to true to index unspecified fields. Different kinds + // of fields are needed for different tests. + collection.createSearchIndexes(Arrays.asList(new SearchIndexModel(searchIndexName, Document.parse( + "{\n" + + " \"mappings\": {\n" + + " \"dynamic\": true,\n" + + " \"fields\": {\n" + + " \"title\": {\n" + + " \"type\": \"token\"\n" + + " },\n" + + " \"description\": {\n" + + " \"analyzer\": \"lucene.keyword\"," + + " \"type\": \"string\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}")))); + waitForIndex(collection, searchIndexName); + } + + @AfterAll + public static void afterAll() { + if (collection != null) { + collection.drop(); + } + try { + ServerHelper.checkPool(getPrimary()); + } catch (InterruptedException e) { + // ignore + } + } + + @Test + public void testExists() { + List pipeline = Arrays.asList( + search(SearchOperator.exists(fieldPath("title")), + searchOptions().index(searchIndexName))); + assertResults(pipeline, "[\n" + + " { _id: 2, title: null },\n" + + " { _id: 3, title: 'test' },\n" + + " { _id: 4, title: ['test', 'xyz'] },\n" + + " { _id: 5, title: 'not test' },\n" + + "]"); + } + + @Test + public void testEquals() { + List pipeline1 = Arrays.asList( + search(SearchOperator.equals(fieldPath("title"), "test"), + searchOptions().index(searchIndexName))); + assertResults(pipeline1, "[\n" + + " { _id: 3, title: 'test' }\n" + + " { _id: 4, title: ['test', 'xyz'] }\n" + + "]"); + + // equals null does not match non-existent fields + List pipeline2 = Arrays.asList( + search(SearchOperator.equalsNull(fieldPath("title")), + searchOptions().index(searchIndexName))); + assertResults(pipeline2, "[\n" + + " { _id: 2, title: null }\n" + + "]"); + } + + @Test + public void testMoreLikeThis() { + List pipeline = Arrays.asList( + search(SearchOperator.moreLikeThis(Document.parse("{ summary: 'summary' }").toBsonDocument()), + searchOptions().index(searchIndexName))); + assertResults(pipeline, "[\n" + + " { _id: 8, summary: 'summary 1 one five' },\n" + + " { _id: 9, summary: 'summary 2 one two three four five' },\n" + + "]"); + } + + @Test + public void testRegex() { + List pipeline = Arrays.asList( + search(SearchOperator.regex(fieldPath("description"), "des[c]+ <1-4>"), + searchOptions().index(searchIndexName))); + assertResults(pipeline, "[\n" + + " { _id: 6, description: 'desc 1' },\n" + + "]"); + } + + @Test + public void testWildcard() { + List pipeline = Arrays.asList( + search(SearchOperator.wildcard(fieldPath("description"), "desc*"), + searchOptions().index(searchIndexName))); + assertResults(pipeline, "[\n" + + " { _id: 6, description: 'desc 1' },\n" + + " { _id: 7, description: 'desc 8' },\n" + + "]"); + } + + @Test + public void testPhrase() { + List pipeline = Arrays.asList( + search(SearchOperator.phrase(fieldPath("summary"), "one five").slop(2), + searchOptions().index(searchIndexName))); + assertResults(pipeline, "[\n" + + " { _id: 8, summary: 'summary 1 one five' },\n" + + "]"); + } + + @Test + public void testQueryString() { + List pipeline = Arrays.asList( + search(SearchOperator.queryString(fieldPath("summary"), "summary: one AND summary: three"), + searchOptions().index(searchIndexName))); + assertResults(pipeline, "[\n" + + " { _id: 9, summary: 'summary 2 one two three four five' },\n" + + "]"); + } + + private static void insertDocuments(final String s) { + List documents = BsonArray.parse(s).stream() + .map(v -> new Document(v.asDocument())) + .collect(Collectors.toList()); + collection.insertMany(documents); + } + + private static void assertResults(final List pipeline, final String expectedResultsAsString) { + ArrayList pipeline2 = new ArrayList<>(pipeline); + pipeline2.add(sort(ascending("_id"))); + + List expectedResults = parseToList(expectedResultsAsString); + List actualResults = aggregate(pipeline2); + assertEquals(expectedResults, actualResults); + } + + private static List aggregate(final List stages) { + AggregateIterable result = collection.aggregate(stages); + List results = new ArrayList<>(); + result.forEach(r -> results.add(r.toBsonDocument())); + return results; + } + + public static List parseToList(final String s) { + return BsonArray.parse(s).stream().map(v -> toBsonDocument(v.asDocument())).collect(Collectors.toList()); + } + + public static BsonDocument toBsonDocument(final BsonDocument bsonDocument) { + return getDefaultCodecRegistry().get(BsonDocument.class).decode(bsonDocument.asBsonReader(), DecoderContext.builder().build()); + } + + public static boolean waitForIndex(final MongoCollection collection, final String indexName) { + long startTime = System.nanoTime(); + long timeoutNanos = TimeUnit.SECONDS.toNanos(60); + while (System.nanoTime() - startTime < timeoutNanos) { + Document indexRecord = StreamSupport.stream(collection.listSearchIndexes().spliterator(), false) + .filter(index -> indexName.equals(index.getString("name"))) + .findAny().orElse(null); + if (indexRecord != null) { + if ("FAILED".equals(indexRecord.getString("status"))) { + throw new RuntimeException("Search index has failed status."); + } + if (indexRecord.getBoolean("queryable")) { + return true; + } + } + try { + Thread.sleep(100); // busy-wait, avoid in production + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } + return false; + } + +} From c95ea47eae2c635049fd57b54c5894aff5bbdb1a Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Tue, 18 Feb 2025 22:29:23 -0700 Subject: [PATCH 26/43] Fix warnings (#1633) --- .../org/mongodb/scala/MongoCluster.scala | 30 +++++++++---------- .../org/mongodb/scala/MongoClientSpec.scala | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/driver-scala/src/main/scala/org/mongodb/scala/MongoCluster.scala b/driver-scala/src/main/scala/org/mongodb/scala/MongoCluster.scala index 94c75bda724..bd0422761b5 100644 --- a/driver-scala/src/main/scala/org/mongodb/scala/MongoCluster.scala +++ b/driver-scala/src/main/scala/org/mongodb/scala/MongoCluster.scala @@ -22,8 +22,6 @@ import com.mongodb.reactivestreams.client.{ MongoCluster => JMongoCluster } import org.bson.codecs.configuration.CodecRegistry import org.mongodb.scala.bson.DefaultHelper.DefaultsTo import org.mongodb.scala.bson.conversions.Bson -import org.mongodb.scala.model.bulk.ClientNamespacedUpdateManyModel -import org.mongodb.scala.model.bulk.ClientNamespacedDeleteManyModel import org.mongodb.scala.model.bulk.{ ClientBulkWriteOptions, ClientBulkWriteResult, ClientNamespacedWriteModel } import scala.collection.JavaConverters._ @@ -296,19 +294,19 @@ class MongoCluster(private val wrapped: JMongoCluster) { /** * Executes a client-level bulk write operation. * This method is functionally equivalent to `bulkWrite(List, ClientBulkWriteOptions)` - * with the [[ClientBulkWriteOptions.clientBulkWriteOptions default options]]. + * with the [[org.mongodb.scala.model.bulk.ClientBulkWriteOptions.clientBulkWriteOptions default options]]. * * This operation supports retryable writes. * Depending on the number of `models`, encoded size of `models`, and the size limits in effect, * executing this operation may require multiple `bulkWrite` commands. * The eligibility for retries is determined per each `bulkWrite` command: - * [[ClientNamespacedUpdateManyModel]], [[ClientNamespacedDeleteManyModel]] in a command render it non-retryable. + * [[org.mongodb.scala.model.bulk.ClientNamespacedUpdateManyModel]], [[org.mongodb.scala.model.bulk.ClientNamespacedDeleteManyModel]] in a command render it non-retryable. * * This operation is not supported by MongoDB Atlas Serverless instances. * * [[https://www.mongodb.com/docs/manual/reference/command/bulkWrite/ bulkWrite]] - * @param models The [[ClientNamespacedWriteModel]] individual write operations. - * @return The [[SingleObservable]] signalling at most one element [[ClientBulkWriteResult]] if the operation is successful, + * @param models The [[org.mongodb.scala.model.bulk.ClientNamespacedWriteModel]] individual write operations. + * @return The [[SingleObservable]] signalling at most one element [[org.mongodb.scala.model.bulk.ClientBulkWriteResult]] if the operation is successful, * or the following errors: * - [[ClientBulkWriteException]]: If and only if the operation is unsuccessful or partially unsuccessful, * and there is at least one of the following pieces of information to report: @@ -329,14 +327,14 @@ class MongoCluster(private val wrapped: JMongoCluster) { * Depending on the number of `models`, encoded size of `models`, and the size limits in effect, * executing this operation may require multiple `bulkWrite` commands. * The eligibility for retries is determined per each `bulkWrite` command: - * [[ClientNamespacedUpdateManyModel]], [[ClientNamespacedDeleteManyModel]] in a command render it non-retryable. + * [[org.mongodb.scala.model.bulk.ClientNamespacedUpdateManyModel]], [[org.mongodb.scala.model.bulk.ClientNamespacedDeleteManyModel]] in a command render it non-retryable. * * This operation is not supported by MongoDB Atlas Serverless instances. * * [[https://www.mongodb.com/docs/manual/reference/command/bulkWrite/ bulkWrite]] - * @param models The [[ClientNamespacedWriteModel]] individual write operations. + * @param models The [[org.mongodb.scala.model.bulk.ClientNamespacedWriteModel]] individual write operations. * @param options The options. - * @return The [[SingleObservable]] signalling at most one element [[ClientBulkWriteResult]] if the operation is successful, + * @return The [[SingleObservable]] signalling at most one element [[org.mongodb.scala.model.bulk.ClientBulkWriteResult]] if the operation is successful, * or the following errors: * - [[ClientBulkWriteException]]: If and only if the operation is unsuccessful or partially unsuccessful, * and there is at least one of the following pieces of information to report: @@ -355,20 +353,20 @@ class MongoCluster(private val wrapped: JMongoCluster) { /** * Executes a client-level bulk write operation. * This method is functionally equivalent to `bulkWrite(ClientSession, List, ClientBulkWriteOptions)` - * with the [[ClientBulkWriteOptions.clientBulkWriteOptions default options]]. + * with the [[org.mongodb.scala.model.bulk.ClientBulkWriteOptions.clientBulkWriteOptions default options]]. * * This operation supports retryable writes. * Depending on the number of `models`, encoded size of `models`, and the size limits in effect, * executing this operation may require multiple `bulkWrite` commands. * The eligibility for retries is determined per each `bulkWrite` command: - * [[ClientNamespacedUpdateManyModel]], [[ClientNamespacedDeleteManyModel]] in a command render it non-retryable. + * [[org.mongodb.scala.model.bulk.ClientNamespacedUpdateManyModel]], [[org.mongodb.scala.model.bulk.ClientNamespacedDeleteManyModel]] in a command render it non-retryable. * * This operation is not supported by MongoDB Atlas Serverless instances. * * [[https://www.mongodb.com/docs/manual/reference/command/bulkWrite/ bulkWrite]] * @param clientSession [[ClientSession client session]] with which to associate this operation. - * @param models The [[ClientNamespacedWriteModel]] individual write operations. - * @return The [[SingleObservable]] signalling at most one element [[ClientBulkWriteResult]] if the operation is successful, + * @param models The [[org.mongodb.scala.model.bulk.ClientNamespacedWriteModel]] individual write operations. + * @return The [[SingleObservable]] signalling at most one element [[org.mongodb.scala.model.bulk.ClientBulkWriteResult]] if the operation is successful, * or the following errors: * - [[ClientBulkWriteException]]: If and only if the operation is unsuccessful or partially unsuccessful, * and there is at least one of the following pieces of information to report: @@ -391,13 +389,13 @@ class MongoCluster(private val wrapped: JMongoCluster) { * Depending on the number of `models`, encoded size of `models`, and the size limits in effect, * executing this operation may require multiple `bulkWrite` commands. * The eligibility for retries is determined per each `bulkWrite` command: - * [[ClientNamespacedUpdateManyModel]], [[ClientNamespacedDeleteManyModel]] in a command render it non-retryable. + * [[org.mongodb.scala.model.bulk.ClientNamespacedUpdateManyModel]], [[org.mongodb.scala.model.bulk.ClientNamespacedDeleteManyModel]] in a command render it non-retryable. * * [[https://www.mongodb.com/docs/manual/reference/command/bulkWrite/ bulkWrite]] * @param clientSession The [[ClientSession client session]] with which to associate this operation. - * @param models The [[ClientNamespacedWriteModel]] individual write operations. + * @param models The [[org.mongodb.scala.model.bulk.ClientNamespacedWriteModel]] individual write operations. * @param options The options. - * @return The [[SingleObservable]] signalling at most one element [[ClientBulkWriteResult]] if the operation is successful, + * @return The [[SingleObservable]] signalling at most one element [[org.mongodb.scala.model.bulk.ClientBulkWriteResult]] if the operation is successful, * or the following errors: * - [[ClientBulkWriteException]]: If and only if the operation is unsuccessful or partially unsuccessful, * and there is at least one of the following pieces of information to report: diff --git a/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSpec.scala b/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSpec.scala index 781007b24ad..a888e33ae7f 100644 --- a/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSpec.scala +++ b/driver-scala/src/test/scala/org/mongodb/scala/MongoClientSpec.scala @@ -103,7 +103,7 @@ class MongoClientSpec extends BaseSpec with MockitoSugar { it should "call the underlying bulkWrite with models only" in { val models = List(ClientNamespacedWriteModel.insertOne(namespace, Document("key" -> "value"))) - mongoClient.bulkWrite(models) shouldBe a[SingleObservable[ClientBulkWriteResult]] + mongoClient.bulkWrite(models) shouldBe a[SingleObservable[_]] verify(wrapped).bulkWrite(models.asJava) } From ff7bae8ba5bd818076c670e33c88aed3dc8948ed Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin Date: Thu, 20 Feb 2025 00:01:30 -0800 Subject: [PATCH 27/43] Add BOM generation. (#1569) JAVA-3808 --------- Co-authored-by: Ross Lawley --- .evergreen/publish.sh | 2 +- bom/build.gradle.kts | 23 ++++++++++ build.gradle | 5 ++- gradle.properties | 1 + gradle/publish.gradle | 102 ++++++++++++++++++++++++++++++++++++++++-- settings.gradle | 1 + 6 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 bom/build.gradle.kts diff --git a/.evergreen/publish.sh b/.evergreen/publish.sh index bfecc0ae865..e3f9f365d42 100755 --- a/.evergreen/publish.sh +++ b/.evergreen/publish.sh @@ -26,6 +26,6 @@ fi SYSTEM_PROPERTIES="-Dorg.gradle.internal.publish.checksums.insecure=true -Dorg.gradle.internal.http.connectionTimeout=120000 -Dorg.gradle.internal.http.socketTimeout=120000" ./gradlew -version -./gradlew ${SYSTEM_PROPERTIES} --stacktrace --info ${TASK} +./gradlew ${SYSTEM_PROPERTIES} --stacktrace --info ${TASK} # Scala 2.13 is published as result of this gradle execution. ./gradlew ${SYSTEM_PROPERTIES} --stacktrace --info :bson-scala:${TASK} :driver-scala:${TASK} -PdefaultScalaVersions=2.12.12 ./gradlew ${SYSTEM_PROPERTIES} --stacktrace --info :bson-scala:${TASK} :driver-scala:${TASK} -PdefaultScalaVersions=2.11.12 diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts new file mode 100644 index 00000000000..5d1fb81c384 --- /dev/null +++ b/bom/build.gradle.kts @@ -0,0 +1,23 @@ +group = "org.mongodb" +description = "This Bill of Materials POM simplifies dependency management when referencing multiple" + + " MongoDB Java Driver artifacts in projects using Gradle or Maven." + +dependencies { + constraints { + api(project(":mongodb-crypt")) + api(project(":driver-core")) + api(project(":bson")) + api(project(":bson-record-codec")) + + api(project(":driver-sync")) + api(project(":driver-reactive-streams")) + + api(project(":bson-kotlin")) + api(project(":bson-kotlinx")) + api(project(":driver-kotlin-coroutine")) + api(project(":driver-kotlin-sync")) + + api(project(":bson-scala")) + api(project(":driver-scala")) + } +} diff --git a/build.gradle b/build.gradle index 26734755984..4715dfb27ff 100644 --- a/build.gradle +++ b/build.gradle @@ -65,9 +65,10 @@ ext { def configDir = ext.configDir def utilProjects = project(":util").allprojects +def bomProjects = project(":bom") def coreProjects = subprojects - utilProjects -def scalaProjects = subprojects.findAll { it.name.contains('scala') } -def javaProjects = subprojects - scalaProjects +def scalaProjects = subprojects.findAll { it.name.contains('scala') } - bomProjects +def javaProjects = subprojects - scalaProjects - bomProjects def javaMainProjects = javaProjects - utilProjects def javaCodeCheckedProjects = javaMainProjects.findAll { !['driver-benchmarks', 'driver-workload-executor', 'driver-lambda'].contains(it.name) } def javaAndScalaTestedProjects = javaCodeCheckedProjects + scalaProjects diff --git a/gradle.properties b/gradle.properties index 12f1750c442..d3514e32f68 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,6 +16,7 @@ org.gradle.daemon=true org.gradle.jvmargs=-Duser.country=US -Duser.language=en +## NOTE: This property is also used to generate scala compile versions in BOM. scalaVersions=2.11.12,2.12.20,2.13.15 defaultScalaVersions=2.13.15 runOnceTasks=clean,release diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 9add25f9261..fa56f09f138 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -76,9 +76,10 @@ ext { def projectNamesNotToBePublished = ["driver-benchmarks", "driver-lambda", "driver-workload-executor", "graalvm-native-image-app", "util", "spock", "taglets"] def publishedProjects = subprojects.findAll { !projectNamesNotToBePublished.contains(it.name) } -def scalaProjects = publishedProjects.findAll { it.name.contains('scala') } -def javaProjects = publishedProjects - scalaProjects -def projectsWithManifest = publishedProjects.findAll {it.name != 'driver-legacy' } +def bomProjects = project(":bom") +def scalaProjects = publishedProjects.findAll { it.name.contains('scala') } - bomProjects +def javaProjects = publishedProjects - scalaProjects - bomProjects +def projectsWithManifest = publishedProjects.findAll {it.name != 'driver-legacy' } - bomProjects configure(javaProjects) { project -> apply plugin: 'maven-publish' @@ -169,3 +170,98 @@ configure(projectsWithManifest) { project -> jar configureJarManifestAttributes(project) } } + +configure(bomProjects) { project -> + apply plugin: 'maven-publish' + apply plugin: 'signing' + apply plugin: 'java-platform' + + // Get the Scala versions from the project property. Only major.minor versions. + def scalaVersions = project.findProperty("scalaVersions")?.split(",") + ?.collect { it.split("\\.")[0] + "." + it.split("\\.")[1] } + + assert scalaVersions != null && !scalaVersions.isEmpty() : "Scala versions must be provided as a comma-separated list" + + " in the 'scalaVersions' project property" + + publishing { + publications { + mavenJava(MavenPublication) { + artifactId = "bom".equals(project.archivesBaseName) ? "mongodb-driver-bom" : project.archivesBaseName + from components.javaPlatform + + // Modify the generated POM to add multiple compile versions of driver-scala or bson-scala. + // Scala multi-version support generates only one for BOM. + pom.withXml { + def pomXml = asNode() + + def dependencyManagementNode = pomXml.get("dependencyManagement")?.getAt(0) + assert dependencyManagementNode : " node not found in the generated BOM POM" + + def dependenciesNode = dependencyManagementNode.get("dependencies")?.getAt(0) + assert dependenciesNode : " node not found inside " + + // Check if scala dependencies are present in the BOM. + def existingScalaDeps = dependenciesNode.children().findAll { + it.artifactId.text().contains("scala") + } + + existingScalaDeps.each { existingDep -> + String groupId = existingDep.groupId.text() + String originalArtifactId = existingDep.artifactId.text() + String artifactVersion = existingDep.version.text() + + // Add multiple versions with Scala suffixes for each Scala-related dependency. + scalaVersions.each { scalaVersion -> + // Remove existing Scala version suffix (_2.12, _2.13, etc.) + String baseArtifactId = originalArtifactId.replaceAll("_\\d+\\.\\d+(\\.\\d+)?\$", "") + String newArtifactId = "${baseArtifactId}_${scalaVersion}" + + // Skip if Scala dependency with this scalaVersion already exists in BOM. + if(newArtifactId != originalArtifactId) { + def dependencyNode = dependenciesNode.appendNode("dependency") + dependencyNode.appendNode("groupId", groupId) + dependencyNode.appendNode("artifactId", newArtifactId) + dependencyNode.appendNode("version", artifactVersion) + } + } + } + } + } + } + + repositories configureMavenRepositories(project) + } + + afterEvaluate { + publishing.publications.mavenJava.pom configurePom(project) + signing { + useInMemoryPgpKeys(findProperty("signingKey"), findProperty("signingPassword")) + sign publishing.publications.mavenJava + } + } + + tasks.withType(GenerateModuleMetadata) { + enabled = false + } + + tasks.withType(GenerateMavenPom).configureEach { + doLast { + def xml = file(destination).text + def root = new groovy.xml.XmlSlurper().parseText(xml) + + def dependencies = root.dependencyManagement.dependencies.children() + assert dependencies.children().size() > 1 : "BOM must contain more then one element:\n$destination" + + dependencies.each { dependency -> + def groupId = dependency.groupId.text() + assert groupId.startsWith('org.mongodb') : "BOM must contain only 'org.mongodb' dependencies, but found '$groupId':\n$destination" + /* The and tags should be omitted in BOM dependencies. + This ensures that consuming projects have the flexibility to decide whether a + dependency is optional in their context. The BOM's role is to provide version information, + not to dictate inclusion or exclusion of dependencies. */ + assert dependency.scope.size() == 0 : "BOM must not contain elements in dependency:\n$destination" + assert dependency.optional.size() == 0 : "BOM must not contain elements in dependency:\n$destination" + } + } + } +} diff --git a/settings.gradle b/settings.gradle index c8a32bd7df5..e390791d5d5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,6 +33,7 @@ include ':driver-scala' include ':mongodb-crypt' include 'util:spock' include 'util:taglets' +include ':bom' if(hasProperty("includeGraalvm")) { include ':graalvm-native-image-app' From fb3f30b79aecfa5b17404c33a29fb1f5fb6f4ffb Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Thu, 20 Feb 2025 11:14:30 -0500 Subject: [PATCH 28/43] Sync `non-lb-connection-establishment` (#1634) Sync test to https://github.com/mongodb/specifications/commit/d05c33e0a6124ee7d1a9de665084d540b2ff06c5 JAVA-5791 --- .../non-lb-connection-establishment.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json b/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json index 6aaa7bdf98b..f4fed13cc23 100644 --- a/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json +++ b/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json @@ -57,6 +57,19 @@ "tests": [ { "description": "operations against non-load balanced clusters fail if URI contains loadBalanced=true", + "runOnRequirements": [ + { + "maxServerVersion": "8.0.99", + "topologies": [ + "single" + ] + }, + { + "topologies": [ + "sharded" + ] + } + ], "operations": [ { "name": "runCommand", From 3b3ebc9b8b5b72cffdf260ec25393fe444248dcc Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Wed, 26 Feb 2025 10:11:05 -0700 Subject: [PATCH 29/43] Add OIDC k8s provider (#1576) JAVA-5405 --- .evergreen/.evg.yml | 90 +++++++++++++++++-- .evergreen/run-mongodb-oidc-test.sh | 10 +++ .../src/main/com/mongodb/MongoCredential.java | 9 +- .../connection/OidcAuthenticator.java | 33 ++++++- .../com/mongodb/ClusterFixture.java | 9 +- .../auth/legacy/connection-string.json | 22 ++++- .../OidcAuthenticationProseTests.java | 3 + 7 files changed, 162 insertions(+), 14 deletions(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index e511c022186..7c1ae2250cc 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -798,6 +798,30 @@ functions: ${PREPARE_SHELL} MONGODB_URI="${MONGODB_URI}" JAVA_VERSION="${JAVA_VERSION}" .evergreen/run-graalvm-native-image-app.sh + "oidc-auth-test-k8s-func": + - command: shell.exec + type: test + params: + shell: bash + include_expansions_in_env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] + script: |- + set -o errexit + ${PREPARE_SHELL} + export K8S_VARIANT=${VARIANT} + cd src + git add . + git commit --allow-empty -m "add files" + # uncompressed tar used to allow appending .git folder + export K8S_DRIVERS_TAR_FILE=/tmp/mongo-java-driver.tar + git archive -o $K8S_DRIVERS_TAR_FILE HEAD + tar -rf $K8S_DRIVERS_TAR_FILE .git + export K8S_TEST_CMD="OIDC_ENV=k8s VARIANT=${VARIANT} ./.evergreen/run-mongodb-oidc-test.sh" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/setup-pod.sh + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/run-self-test.sh + source $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/secrets-export.sh + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/run-driver-test.sh + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/k8s/teardown-pod.sh + # Anchors pre: @@ -921,6 +945,22 @@ tasks: export GCPOIDC_TEST_CMD="OIDC_ENV=gcp ./.evergreen/run-mongodb-oidc-test.sh" bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh + - name: "oidc-auth-test-k8s" + commands: + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} + duration_seconds: 1800 + - func: "oidc-auth-test-k8s-func" + vars: + VARIANT: eks + - func: "oidc-auth-test-k8s-func" + vars: + VARIANT: aks + - func: "oidc-auth-test-k8s-func" + vars: + VARIANT: gke + - name: serverless-test commands: - func: "run serverless" @@ -2011,7 +2051,7 @@ task_groups: tasks: - testazurekms-task - - name: testoidc_task_group + - name: test-oidc-task-group setup_group: - func: fetch source - func: prepare resources @@ -2036,7 +2076,7 @@ task_groups: tasks: - oidc-auth-test - - name: testazureoidc_task_group + - name: test-oidc-azure-task-group setup_group: - func: fetch source - func: prepare resources @@ -2059,7 +2099,7 @@ task_groups: tasks: - oidc-auth-test-azure - - name: testgcpoidc_task_group + - name: test-oidc-gcp-task-group setup_group: - func: fetch source - func: prepare resources @@ -2083,6 +2123,33 @@ task_groups: tasks: - oidc-auth-test-gcp + - name: test-oidc-k8s-task-group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + teardown_task_can_fail_task: true + teardown_group_timeout_secs: 180 + setup_group: + - func: fetch source + - func: prepare resources + - func: fix absolute paths + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} + - command: subprocess.exec + params: + binary: bash + include_expansions_in_env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/setup.sh + teardown_group: + - command: subprocess.exec + params: + binary: bash + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/k8s/teardown.sh + tasks: + - oidc-auth-test-k8s + buildvariants: # Test packaging and other release related routines @@ -2254,21 +2321,28 @@ buildvariants: display_name: "OIDC Auth" run_on: ubuntu2204-small tasks: - - name: testoidc_task_group + - name: test-oidc-task-group batchtime: 20160 # 14 days -- name: testazureoidc-variant +- name: test-oidc-azure-variant display_name: "OIDC Auth Azure" run_on: ubuntu2204-small tasks: - - name: testazureoidc_task_group + - name: test-oidc-azure-task-group batchtime: 20160 # 14 days -- name: testgcpoidc-variant +- name: test-oidc-gcp-variant display_name: "OIDC Auth GCP" run_on: ubuntu2204-small tasks: - - name: testgcpoidc_task_group + - name: test-oidc-gcp-task-group + batchtime: 20160 # 14 days + +- name: test-oidc-k8s-variant + display_name: "OIDC Auth K8S" + run_on: ubuntu2204-small + tasks: + - name: test-oidc-k8s-task-group batchtime: 20160 # 14 days - matrix_name: "aws-auth-test" diff --git a/.evergreen/run-mongodb-oidc-test.sh b/.evergreen/run-mongodb-oidc-test.sh index ec2b2c19610..55b0599fd02 100755 --- a/.evergreen/run-mongodb-oidc-test.sh +++ b/.evergreen/run-mongodb-oidc-test.sh @@ -19,6 +19,16 @@ elif [ $OIDC_ENV == "azure" ]; then source ./env.sh elif [ $OIDC_ENV == "gcp" ]; then source ./secrets-export.sh +elif [ $OIDC_ENV == "k8s" ]; then + # Make sure K8S_VARIANT is set. + if [ -z "$K8S_VARIANT" ]; then + echo "Must specify K8S_VARIANT" + popd + exit 1 + fi + + # fix for git permissions issue: + git config --global --add safe.directory /tmp/test else echo "Unrecognized OIDC_ENV $OIDC_ENV" exit 1 diff --git a/driver-core/src/main/com/mongodb/MongoCredential.java b/driver-core/src/main/com/mongodb/MongoCredential.java index f55251a7603..6e83e54a3cf 100644 --- a/driver-core/src/main/com/mongodb/MongoCredential.java +++ b/driver-core/src/main/com/mongodb/MongoCredential.java @@ -189,7 +189,7 @@ public final class MongoCredential { /** * Mechanism property key for specifying the environment for OIDC, which is * the name of a built-in OIDC application environment integration to use - * to obtain credentials. The value must be either "gcp" or "azure". + * to obtain credentials. The value must be either "k8s", "gcp", or "azure". * This is an alternative to supplying a callback. *

* The "gcp" and "azure" environments require @@ -199,6 +199,11 @@ public final class MongoCredential { * {@link MongoCredential#OIDC_CALLBACK_KEY} and * {@link MongoCredential#OIDC_HUMAN_CALLBACK_KEY} * must not be provided. + *

+ * The "k8s" environment will check the env vars + * {@code AZURE_FEDERATED_TOKEN_FILE}, and then {@code AWS_WEB_IDENTITY_TOKEN_FILE}, + * for the token file path, and if neither is set will then use the path + * {@code /var/run/secrets/kubernetes.io/serviceaccount/token}. * * @see #createOidcCredential(String) * @see MongoCredential#TOKEN_RESOURCE_KEY @@ -265,7 +270,7 @@ public final class MongoCredential { "*.mongodb.net", "*.mongodb-qa.net", "*.mongodb-dev.net", "*.mongodbgov.net", "localhost", "127.0.0.1", "::1")); /** - * Mechanism property key for specifying he URI of the target resource (sometimes called the audience), + * Mechanism property key for specifying the URI of the target resource (sometimes called the audience), * used in some OIDC environments. * *

A map from a BSON types to the Class to which it should be decoded. This class is useful if, for example, @@ -71,7 +70,7 @@ */ public class BsonTypeClassMap { static final BsonTypeClassMap DEFAULT_BSON_TYPE_CLASS_MAP = new BsonTypeClassMap(); - private final Map> map = new HashMap<>(); + private final Class[] bsonTypeOrdinalToClassMap = new Class[256]; /** * Construct an instance with the default mapping, but replacing the default mapping with any values contained in the given map. @@ -81,7 +80,7 @@ public class BsonTypeClassMap { */ public BsonTypeClassMap(final Map> replacementsForDefaults) { addDefaults(); - map.putAll(replacementsForDefaults); + replacementsForDefaults.forEach((key, value) -> bsonTypeOrdinalToClassMap[key.getValue()] = value); } /** @@ -91,10 +90,6 @@ public BsonTypeClassMap() { this(Collections.emptyMap()); } - Set keys() { - return map.keySet(); - } - /** * Gets the Class that is mapped to the given BSON type. * @@ -102,30 +97,30 @@ Set keys() { * @return the Class that is mapped to the BSON type */ public Class get(final BsonType bsonType) { - return map.get(bsonType); + return bsonTypeOrdinalToClassMap[bsonType.getValue()]; } private void addDefaults() { - map.put(BsonType.ARRAY, List.class); - map.put(BsonType.BINARY, Binary.class); - map.put(BsonType.BOOLEAN, Boolean.class); - map.put(BsonType.DATE_TIME, Date.class); - map.put(BsonType.DB_POINTER, BsonDbPointer.class); - map.put(BsonType.DOCUMENT, Document.class); - map.put(BsonType.DOUBLE, Double.class); - map.put(BsonType.INT32, Integer.class); - map.put(BsonType.INT64, Long.class); - map.put(BsonType.DECIMAL128, Decimal128.class); - map.put(BsonType.MAX_KEY, MaxKey.class); - map.put(BsonType.MIN_KEY, MinKey.class); - map.put(BsonType.JAVASCRIPT, Code.class); - map.put(BsonType.JAVASCRIPT_WITH_SCOPE, CodeWithScope.class); - map.put(BsonType.OBJECT_ID, ObjectId.class); - map.put(BsonType.REGULAR_EXPRESSION, BsonRegularExpression.class); - map.put(BsonType.STRING, String.class); - map.put(BsonType.SYMBOL, Symbol.class); - map.put(BsonType.TIMESTAMP, BsonTimestamp.class); - map.put(BsonType.UNDEFINED, BsonUndefined.class); + bsonTypeOrdinalToClassMap[BsonType.ARRAY.getValue()] = List.class; + bsonTypeOrdinalToClassMap[BsonType.BINARY.getValue()] = Binary.class; + bsonTypeOrdinalToClassMap[BsonType.BOOLEAN.getValue()] = Boolean.class; + bsonTypeOrdinalToClassMap[BsonType.DATE_TIME.getValue()] = Date.class; + bsonTypeOrdinalToClassMap[BsonType.DB_POINTER.getValue()] = BsonDbPointer.class; + bsonTypeOrdinalToClassMap[BsonType.DOCUMENT.getValue()] = Document.class; + bsonTypeOrdinalToClassMap[BsonType.DOUBLE.getValue()] = Double.class; + bsonTypeOrdinalToClassMap[BsonType.INT32.getValue()] = Integer.class; + bsonTypeOrdinalToClassMap[BsonType.INT64.getValue()] = Long.class; + bsonTypeOrdinalToClassMap[BsonType.DECIMAL128.getValue()] = Decimal128.class; + bsonTypeOrdinalToClassMap[BsonType.MAX_KEY.getValue()] = MaxKey.class; + bsonTypeOrdinalToClassMap[BsonType.MIN_KEY.getValue()] = MinKey.class; + bsonTypeOrdinalToClassMap[BsonType.JAVASCRIPT.getValue()] = Code.class; + bsonTypeOrdinalToClassMap[BsonType.JAVASCRIPT_WITH_SCOPE.getValue()] = CodeWithScope.class; + bsonTypeOrdinalToClassMap[BsonType.OBJECT_ID.getValue()] = ObjectId.class; + bsonTypeOrdinalToClassMap[BsonType.REGULAR_EXPRESSION.getValue()] = BsonRegularExpression.class; + bsonTypeOrdinalToClassMap[BsonType.STRING.getValue()] = String.class; + bsonTypeOrdinalToClassMap[BsonType.SYMBOL.getValue()] = Symbol.class; + bsonTypeOrdinalToClassMap[BsonType.TIMESTAMP.getValue()] = BsonTimestamp.class; + bsonTypeOrdinalToClassMap[BsonType.UNDEFINED.getValue()] = BsonUndefined.class; } @Override @@ -139,15 +134,11 @@ public boolean equals(final Object o) { BsonTypeClassMap that = (BsonTypeClassMap) o; - if (!map.equals(that.map)) { - return false; - } - - return true; + return Arrays.equals(bsonTypeOrdinalToClassMap, that.bsonTypeOrdinalToClassMap); } @Override public int hashCode() { - return map.hashCode(); + return Arrays.hashCode(bsonTypeOrdinalToClassMap); } } diff --git a/bson/src/main/org/bson/codecs/BsonTypeCodecMap.java b/bson/src/main/org/bson/codecs/BsonTypeCodecMap.java index 510a6041a0b..3a3def7ca7f 100644 --- a/bson/src/main/org/bson/codecs/BsonTypeCodecMap.java +++ b/bson/src/main/org/bson/codecs/BsonTypeCodecMap.java @@ -40,7 +40,7 @@ public class BsonTypeCodecMap { public BsonTypeCodecMap(final BsonTypeClassMap bsonTypeClassMap, final CodecRegistry codecRegistry) { this.bsonTypeClassMap = notNull("bsonTypeClassMap", bsonTypeClassMap); notNull("codecRegistry", codecRegistry); - for (BsonType cur : bsonTypeClassMap.keys()) { + for (BsonType cur : BsonType.values()) { Class clazz = bsonTypeClassMap.get(cur); if (clazz != null) { try { diff --git a/bson/src/main/org/bson/codecs/DocumentCodec.java b/bson/src/main/org/bson/codecs/DocumentCodec.java index 3559e93fcae..0c4161f53fd 100644 --- a/bson/src/main/org/bson/codecs/DocumentCodec.java +++ b/bson/src/main/org/bson/codecs/DocumentCodec.java @@ -156,7 +156,7 @@ public void encode(final BsonWriter writer, final Document document, final Encod beforeFields(writer, encoderContext, document); - for (final Map.Entry entry : ((Map) document).entrySet()) { + for (final Map.Entry entry : document.entrySet()) { if (skipField(encoderContext, entry.getKey())) { continue; } diff --git a/bson/src/test/unit/org/bson/codecs/IterableCodecProviderSpecification.groovy b/bson/src/test/unit/org/bson/codecs/IterableCodecProviderSpecification.groovy index b0eae796fc4..b5217676871 100644 --- a/bson/src/test/unit/org/bson/codecs/IterableCodecProviderSpecification.groovy +++ b/bson/src/test/unit/org/bson/codecs/IterableCodecProviderSpecification.groovy @@ -16,6 +16,7 @@ package org.bson.codecs +import org.bson.BsonType import spock.lang.Specification import static org.bson.codecs.configuration.CodecRegistries.fromProviders @@ -57,7 +58,7 @@ class IterableCodecProviderSpecification extends Specification { def 'unidentical instances should not be equal'() { given: def first = new IterableCodecProvider() - def second = new IterableCodecProvider(new BsonTypeClassMap([BOOLEAN: String])) + def second = new IterableCodecProvider(new BsonTypeClassMap([(BsonType.BOOLEAN): String])) def third = new IterableCodecProvider(new BsonTypeClassMap(), { Object from -> from }) diff --git a/driver-benchmarks/build.gradle b/driver-benchmarks/build.gradle index 91d979cff68..84e32c42e52 100644 --- a/driver-benchmarks/build.gradle +++ b/driver-benchmarks/build.gradle @@ -33,6 +33,16 @@ dependencies { api project(':driver-sync') api project(':mongodb-crypt') implementation "ch.qos.logback:logback-classic:$logbackVersion" + + implementation 'org.openjdk.jmh:jmh-core:1.37' + annotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37' +} + +tasks.register("jmh", JavaExec) { + group = 'benchmark' + description = 'Run JMH benchmarks.' + mainClass = 'org.openjdk.jmh.Main' + classpath = sourceSets.main.runtimeClasspath } javadoc { diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonArrayCodecBenchmark.java b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonArrayCodecBenchmark.java new file mode 100644 index 00000000000..19df4bd8269 --- /dev/null +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonArrayCodecBenchmark.java @@ -0,0 +1,99 @@ +/* + * Copyright 2016-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.benchmark.jmh.codec; + +import com.mongodb.internal.connection.ByteBufferBsonOutput; +import com.mongodb.internal.connection.PowerOfTwoBufferPool; +import org.bson.BsonArray; +import org.bson.BsonBinaryReader; +import org.bson.BsonBinaryWriter; +import org.bson.BsonDocument; +import org.bson.BsonDouble; +import org.bson.codecs.BsonArrayCodec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; +import org.jetbrains.annotations.NotNull; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import static com.mongodb.benchmark.jmh.codec.BsonUtils.getDocumentAsBuffer; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 20, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 20, time = 2, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(3) +public class BsonArrayCodecBenchmark { + + @State(Scope.Benchmark) + public static class Input { + protected final PowerOfTwoBufferPool bufferPool = PowerOfTwoBufferPool.DEFAULT; + protected final BsonArrayCodec bsonArrayCodec = new BsonArrayCodec(); + protected BsonDocument document; + protected byte[] documentBytes; + private BsonBinaryReader reader; + private BsonBinaryWriter writer; + private BsonArray bsonValues; + + @Setup + public void setup() throws IOException { + bsonValues = new BsonArray(); + document = new BsonDocument("array", bsonValues); + + for (int i = 0; i < 1000; i++) { + bsonValues.add(new BsonDouble(i)); + } + + documentBytes = getDocumentAsBuffer(document); + } + + @Setup(Level.Invocation) + public void beforeIteration() { + reader = new BsonBinaryReader(ByteBuffer.wrap(documentBytes)); + writer = new BsonBinaryWriter(new ByteBufferBsonOutput(bufferPool)); + + reader.readStartDocument(); + writer.writeStartDocument(); + writer.writeName("array"); + } + } + + @Benchmark + public void decode(@NotNull Input input, @NotNull Blackhole blackhole) { + blackhole.consume(input.bsonArrayCodec.decode(input.reader, DecoderContext.builder().build())); + } + + @Benchmark + public void encode(@NotNull Input input, @NotNull Blackhole blackhole) { + input.bsonArrayCodec.encode(input.writer, input.bsonValues, EncoderContext.builder().build()); + blackhole.consume(input); + } +} diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonDocumentBenchmark.java b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonDocumentBenchmark.java new file mode 100644 index 00000000000..22739c511b8 --- /dev/null +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonDocumentBenchmark.java @@ -0,0 +1,86 @@ +/* + * Copyright 2016-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.benchmark.jmh.codec; + +import com.mongodb.internal.connection.ByteBufferBsonOutput; +import com.mongodb.internal.connection.PowerOfTwoBufferPool; +import org.bson.BsonBinaryReader; +import org.bson.BsonBinaryWriter; +import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; +import org.jetbrains.annotations.NotNull; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; +import static com.mongodb.benchmark.jmh.codec.BsonUtils.getDocumentAsBuffer; + +/** + * Benchmark with minimal dependency on other codecs to evaluate BsonDocumentCodec's internal performance. + */ +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 20, time = 2, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 20, time = 2, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.SECONDS) +@Fork(3) +public class BsonDocumentBenchmark { + + @State(Scope.Benchmark) + public static class Input { + protected final PowerOfTwoBufferPool bufferPool = PowerOfTwoBufferPool.DEFAULT; + protected final BsonDocumentCodec bsonDocumentCodec = new BsonDocumentCodec(); + protected BsonDocument document; + protected byte[] documentBytes; + + @Setup + public void setup() throws IOException { + document = new BsonDocument(); + + for (int i = 0; i < 500; i++) { + document.append(Integer.toString(i), new BsonInt32(i)); + } + + documentBytes = getDocumentAsBuffer(document); + } + } + + @Benchmark + public void decode(@NotNull Input input, @NotNull Blackhole blackhole) { + blackhole.consume(input.bsonDocumentCodec.decode(new BsonBinaryReader(ByteBuffer.wrap(input.documentBytes)), DecoderContext.builder().build())); + } + + @Benchmark + public void encode(@NotNull Input input, @NotNull Blackhole blackhole) { + input.bsonDocumentCodec.encode(new BsonBinaryWriter(new ByteBufferBsonOutput(input.bufferPool)), input.document, EncoderContext.builder().build()); + blackhole.consume(input); + } +} diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonUtils.java b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonUtils.java new file mode 100644 index 00000000000..58ad034788b --- /dev/null +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonUtils.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.mongodb.benchmark.jmh.codec; + +import org.bson.BsonBinaryWriter; +import org.bson.BsonDocument; +import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.Codec; +import org.bson.codecs.EncoderContext; +import org.bson.io.BasicOutputBuffer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +public class BsonUtils { + + private static final Codec BSON_DOCUMENT_CODEC = new BsonDocumentCodec(); + + private BsonUtils(){ + //NOP + } + + public static byte[] getDocumentAsBuffer(final BsonDocument document) throws IOException { + BasicOutputBuffer buffer = new BasicOutputBuffer(); + BSON_DOCUMENT_CODEC.encode(new BsonBinaryWriter(buffer), document, EncoderContext.builder().build()); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(buffer.getSize()); + buffer.pipe(baos); + return baos.toByteArray(); + } +} diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/package-info.java b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/package-info.java new file mode 100644 index 00000000000..4c2731a218f --- /dev/null +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright 2016-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +/** + * Contains JMH benchmarks for targeted components and code paths. + * + *

When changes are made, the existing benchmarks can be quickly executed to assess + * any performance impact. These benchmarks are intended for targeted evaluation in a local environment or spawn host + * and are not currently executed on the Evergreen. If a benchmark for a particular code path or component does not yet + * exist, this package provides a convenient location to set up a new one + * for performance testing.

+ */ +package com.mongodb.benchmark.jmh.codec; From fa4a9edd7e88ef494f42a130a7b59f05d31c8154 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Tue, 4 Mar 2025 14:27:39 -0700 Subject: [PATCH 35/43] Split atlas-deployed-task-group across multiple hosts (#1640) --- .evergreen/.evg.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index 7c1ae2250cc..c1fba35e36d 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -1941,6 +1941,7 @@ axes: task_groups: - name: "atlas-deployed-task-group" + max_hosts: -1 setup_group: - func: fetch source - func: prepare resources From 005652e27abc5399812d52fa2f1d848981793361 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Wed, 5 Mar 2025 11:25:14 -0700 Subject: [PATCH 36/43] Fix driver-lambda evg failure (#1643) --- .evergreen/.evg.yml | 4 ++-- driver-lambda/template.yaml | 20 -------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/.evergreen/.evg.yml b/.evergreen/.evg.yml index c1fba35e36d..66b809e38a0 100644 --- a/.evergreen/.evg.yml +++ b/.evergreen/.evg.yml @@ -1942,6 +1942,8 @@ axes: task_groups: - name: "atlas-deployed-task-group" max_hosts: -1 + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 setup_group: - func: fetch source - func: prepare resources @@ -1965,8 +1967,6 @@ task_groups: add_expansions_to_env: true args: - ${DRIVERS_TOOLS}/.evergreen/atlas/teardown-atlas-cluster.sh - setup_group_can_fail_task: true - setup_group_timeout_secs: 1800 tasks: - "atlas-search-index-management-task" - "aws-lambda-deployed-task" diff --git a/driver-lambda/template.yaml b/driver-lambda/template.yaml index 7a53bb20272..9441f804f20 100644 --- a/driver-lambda/template.yaml +++ b/driver-lambda/template.yaml @@ -37,26 +37,6 @@ Resources: Properties: Path: /mongodb Method: get - ApplicationResourceGroup: - Type: AWS::ResourceGroups::Group - Properties: - Name: - Fn::Join: - - '' - - - ApplicationInsights-SAM- - - Ref: AWS::StackName - ResourceQuery: - Type: CLOUDFORMATION_STACK_1_0 - ApplicationInsightsMonitoring: - Type: AWS::ApplicationInsights::Application - Properties: - ResourceGroupName: - Fn::Join: - - '' - - - ApplicationInsights-SAM- - - Ref: AWS::StackName - AutoConfigurationEnabled: true - DependsOn: ApplicationResourceGroup Outputs: LambdaTestApi: Description: API Gateway endpoint URL for Prod stage for Lambda Test function From 304f854d675e048306a5289472938700e130ed79 Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Thu, 6 Mar 2025 02:57:19 -0700 Subject: [PATCH 37/43] Replace `org.jetbrains.annotations.NotNull` with `com.mongodb.lang.NonNull` (#1644) --- .../benchmark/jmh/codec/BsonArrayCodecBenchmark.java | 6 +++--- .../benchmark/jmh/codec/BsonDocumentBenchmark.java | 6 +++--- .../src/main/com/mongodb/internal/time/Timeout.java | 4 ++-- .../internal/async/SameThreadAsyncFunctionsTest.java | 8 ++++---- .../client/internal/crypt/KeyManagementService.java | 4 ++-- .../com/mongodb/client/internal/KeyManagementService.java | 6 +++--- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonArrayCodecBenchmark.java b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonArrayCodecBenchmark.java index 19df4bd8269..75cc9dab337 100644 --- a/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonArrayCodecBenchmark.java +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonArrayCodecBenchmark.java @@ -27,7 +27,7 @@ import org.bson.codecs.BsonArrayCodec; import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; -import org.jetbrains.annotations.NotNull; +import com.mongodb.lang.NonNull; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -87,12 +87,12 @@ public void beforeIteration() { } @Benchmark - public void decode(@NotNull Input input, @NotNull Blackhole blackhole) { + public void decode(@NonNull Input input, @NonNull Blackhole blackhole) { blackhole.consume(input.bsonArrayCodec.decode(input.reader, DecoderContext.builder().build())); } @Benchmark - public void encode(@NotNull Input input, @NotNull Blackhole blackhole) { + public void encode(@NonNull Input input, @NonNull Blackhole blackhole) { input.bsonArrayCodec.encode(input.writer, input.bsonValues, EncoderContext.builder().build()); blackhole.consume(input); } diff --git a/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonDocumentBenchmark.java b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonDocumentBenchmark.java index 22739c511b8..b050f19007e 100644 --- a/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonDocumentBenchmark.java +++ b/driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonDocumentBenchmark.java @@ -26,7 +26,7 @@ import org.bson.codecs.BsonDocumentCodec; import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; -import org.jetbrains.annotations.NotNull; +import com.mongodb.lang.NonNull; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; @@ -74,12 +74,12 @@ public void setup() throws IOException { } @Benchmark - public void decode(@NotNull Input input, @NotNull Blackhole blackhole) { + public void decode(@NonNull Input input, @NonNull Blackhole blackhole) { blackhole.consume(input.bsonDocumentCodec.decode(new BsonBinaryReader(ByteBuffer.wrap(input.documentBytes)), DecoderContext.builder().build())); } @Benchmark - public void encode(@NotNull Input input, @NotNull Blackhole blackhole) { + public void encode(@NonNull Input input, @NonNull Blackhole blackhole) { input.bsonDocumentCodec.encode(new BsonBinaryWriter(new ByteBufferBsonOutput(input.bufferPool)), input.document, EncoderContext.builder().build()); blackhole.consume(input); } diff --git a/driver-core/src/main/com/mongodb/internal/time/Timeout.java b/driver-core/src/main/com/mongodb/internal/time/Timeout.java index 85b92d9fde1..3dba42e580f 100644 --- a/driver-core/src/main/com/mongodb/internal/time/Timeout.java +++ b/driver-core/src/main/com/mongodb/internal/time/Timeout.java @@ -22,7 +22,7 @@ import com.mongodb.internal.function.CheckedRunnable; import com.mongodb.internal.function.CheckedSupplier; import com.mongodb.lang.Nullable; -import org.jetbrains.annotations.NotNull; +import com.mongodb.lang.NonNull; import java.util.Arrays; import java.util.Collections; @@ -82,7 +82,7 @@ static Timeout nullAsInfinite(@Nullable final Timeout timeout) { * @param zeroSemantics what to interpret a 0 duration as (infinite or expired) * @return a timeout that expires in the specified duration after now. */ - @NotNull + @NonNull static Timeout expiresIn(final long duration, final TimeUnit unit, final ZeroSemantics zeroSemantics) { if (duration < 0) { throw new AssertionError("Timeouts must not be in the past"); diff --git a/driver-core/src/test/unit/com/mongodb/internal/async/SameThreadAsyncFunctionsTest.java b/driver-core/src/test/unit/com/mongodb/internal/async/SameThreadAsyncFunctionsTest.java index 04b9290af55..7dd4c0f37ac 100644 --- a/driver-core/src/test/unit/com/mongodb/internal/async/SameThreadAsyncFunctionsTest.java +++ b/driver-core/src/test/unit/com/mongodb/internal/async/SameThreadAsyncFunctionsTest.java @@ -16,7 +16,7 @@ package com.mongodb.internal.async; -import org.jetbrains.annotations.NotNull; +import com.mongodb.lang.NonNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -62,7 +62,7 @@ void testInvalid() { private static class SameThreadExecutorService extends AbstractExecutorService { @Override - public void execute(@NotNull final Runnable command) { + public void execute(@NonNull final Runnable command) { command.run(); } @@ -70,7 +70,7 @@ public void execute(@NotNull final Runnable command) { public void shutdown() { } - @NotNull + @NonNull @Override public List shutdownNow() { return Collections.emptyList(); @@ -87,7 +87,7 @@ public boolean isTerminated() { } @Override - public boolean awaitTermination(final long timeout, @NotNull final TimeUnit unit) { + public boolean awaitTermination(final long timeout, @NonNull final TimeUnit unit) { return true; } } diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementService.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementService.java index 019445e6cde..b82dd590618 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementService.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/KeyManagementService.java @@ -39,7 +39,7 @@ import com.mongodb.lang.Nullable; import org.bson.ByteBuf; import org.bson.ByteBufNIO; -import org.jetbrains.annotations.NotNull; +import com.mongodb.lang.NonNull; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoSink; @@ -179,7 +179,7 @@ private OperationContext createOperationContext(@Nullable final Timeout operatio return OperationContext.simpleOperationContext(new TimeoutContext(timeoutSettings)); } - @NotNull + @NonNull private static TimeoutSettings createTimeoutSettings(final SocketSettings socketSettings, @Nullable final Long ms) { return new TimeoutSettings( diff --git a/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java b/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java index fee5ddac729..806f768a923 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java +++ b/driver-sync/src/main/com/mongodb/client/internal/KeyManagementService.java @@ -23,7 +23,7 @@ import com.mongodb.internal.diagnostics.logging.Loggers; import com.mongodb.internal.time.Timeout; import com.mongodb.lang.Nullable; -import org.jetbrains.annotations.NotNull; +import com.mongodb.lang.NonNull; import javax.net.SocketFactory; import javax.net.ssl.SSLContext; @@ -149,13 +149,13 @@ public int read() throws IOException { } @Override - public int read(@NotNull final byte[] b) throws IOException { + public int read(@NonNull final byte[] b) throws IOException { setSocketSoTimeoutToOperationTimeout(); return wrapped.read(b); } @Override - public int read(@NotNull final byte[] b, final int off, final int len) throws IOException { + public int read(@NonNull final byte[] b, final int off, final int len) throws IOException { setSocketSoTimeoutToOperationTimeout(); return wrapped.read(b, off, len); } From 410dd53833cfb787f8ae6e75e86763e442b2cd70 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 13 Mar 2025 11:49:01 +0000 Subject: [PATCH 38/43] Test IPv6 literal in SDAM Added secondary_ipv6_literal.json test JAVA-5810 --- .../rs/secondary_ipv6_literal.json | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 driver-core/src/test/resources/server-discovery-and-monitoring/rs/secondary_ipv6_literal.json diff --git a/driver-core/src/test/resources/server-discovery-and-monitoring/rs/secondary_ipv6_literal.json b/driver-core/src/test/resources/server-discovery-and-monitoring/rs/secondary_ipv6_literal.json new file mode 100644 index 00000000000..c23d8dc4c95 --- /dev/null +++ b/driver-core/src/test/resources/server-discovery-and-monitoring/rs/secondary_ipv6_literal.json @@ -0,0 +1,38 @@ +{ + "description": "Secondary with IPv6 literal", + "uri": "mongodb://[::1]/?replicaSet=rs", + "phases": [ + { + "responses": [ + [ + "[::1]:27017", + { + "ok": 1, + "helloOk": true, + "isWritablePrimary": false, + "secondary": true, + "setName": "rs", + "me": "[::1]:27017", + "hosts": [ + "[::1]:27017" + ], + "minWireVersion": 0, + "maxWireVersion": 26 + } + ] + ], + "outcome": { + "servers": { + "[::1]:27017": { + "type": "RSSecondary", + "setName": "rs" + } + }, + "topologyType": "ReplicaSetNoPrimary", + "setName": "rs", + "logicalSessionTimeoutMinutes": null, + "compatible": true + } + } + ] +} From 42b1a373017f9709a5c0ac6f2ddd90dccbb45c90 Mon Sep 17 00:00:00 2001 From: Jens Suhr Date: Wed, 29 Jan 2025 15:23:39 +0100 Subject: [PATCH 39/43] JAVA-5776 Make KProperty.path() public --- .../main/kotlin/com/mongodb/kotlin/client/model/Properties.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt index b35b44d4d93..fc8a4e94e87 100644 --- a/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt +++ b/driver-kotlin-extensions/src/main/kotlin/com/mongodb/kotlin/client/model/Properties.kt @@ -67,7 +67,7 @@ public operator fun KProperty1?>.div( * - BsonProperty annotation * - Property name */ -internal fun KProperty.path(): String { +public fun KProperty.path(): String { return if (this is KPropertyPath<*, T>) { this.name } else { From ab300f8a3db31d1e9c1c08696cd65ac6846eb4b6 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Wed, 19 Mar 2025 07:16:46 -0600 Subject: [PATCH 40/43] Retry flaky unified tests (#1565) JAVA-5795 --- .../com/mongodb/assertions/Assertions.java | 13 ++ .../client/coroutine/UnifiedCrudTest.kt | 2 +- .../mongodb/kotlin/client/UnifiedCrudTest.kt | 2 +- .../ClientSideOperationTimeoutTest.java | 9 +- .../unified/UnifiedReactiveStreamsTest.java | 11 ++ .../client/unified/UnifiedSyncTest.java | 11 ++ .../mongodb/client/unified/UnifiedTest.java | 142 +++++++++++++----- .../unified/UnifiedTestFailureValidator.java | 12 ++ .../unified/UnifiedTestModifications.java | 84 +++++++++-- .../mongodb/workload/WorkloadExecutor.java | 6 + 10 files changed, 238 insertions(+), 54 deletions(-) diff --git a/driver-core/src/main/com/mongodb/assertions/Assertions.java b/driver-core/src/main/com/mongodb/assertions/Assertions.java index a40b4e4b7b6..bf38638dc6d 100644 --- a/driver-core/src/main/com/mongodb/assertions/Assertions.java +++ b/driver-core/src/main/com/mongodb/assertions/Assertions.java @@ -179,6 +179,19 @@ public static boolean assertTrue(final boolean value) throws AssertionError { return true; } + /** + * @param value A value to check. + * @param message The message. + * @return {@code true}. + * @throws AssertionError If {@code value} is {@code false}. + */ + public static boolean assertTrue(final boolean value, final String message) throws AssertionError { + if (!value) { + throw new AssertionError(message); + } + return true; + } + /** * @param value A value to check. * @return {@code false}. diff --git a/driver-kotlin-coroutine/src/integration/kotlin/com/mongodb/kotlin/client/coroutine/UnifiedCrudTest.kt b/driver-kotlin-coroutine/src/integration/kotlin/com/mongodb/kotlin/client/coroutine/UnifiedCrudTest.kt index 036ec5afcc4..5091058573e 100644 --- a/driver-kotlin-coroutine/src/integration/kotlin/com/mongodb/kotlin/client/coroutine/UnifiedCrudTest.kt +++ b/driver-kotlin-coroutine/src/integration/kotlin/com/mongodb/kotlin/client/coroutine/UnifiedCrudTest.kt @@ -24,7 +24,7 @@ internal class UnifiedCrudTest() : UnifiedTest() { @JvmStatic @Throws(URISyntaxException::class, IOException::class) fun data(): Collection? { - return getTestData("unified-test-format/crud") + return getTestData("unified-test-format/crud", true) } } } diff --git a/driver-kotlin-sync/src/integration/kotlin/com/mongodb/kotlin/client/UnifiedCrudTest.kt b/driver-kotlin-sync/src/integration/kotlin/com/mongodb/kotlin/client/UnifiedCrudTest.kt index eb06f5c1875..f030cb54645 100644 --- a/driver-kotlin-sync/src/integration/kotlin/com/mongodb/kotlin/client/UnifiedCrudTest.kt +++ b/driver-kotlin-sync/src/integration/kotlin/com/mongodb/kotlin/client/UnifiedCrudTest.kt @@ -24,7 +24,7 @@ internal class UnifiedCrudTest() : UnifiedTest() { @JvmStatic @Throws(URISyntaxException::class, IOException::class) fun data(): Collection? { - return getTestData("unified-test-format/crud") + return getTestData("unified-test-format/crud", false) } } } diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/ClientSideOperationTimeoutTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/ClientSideOperationTimeoutTest.java index 168ff4b8f81..a1063f05362 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/ClientSideOperationTimeoutTest.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/ClientSideOperationTimeoutTest.java @@ -99,18 +99,25 @@ The Reactive Streams specification prevents us from allowing a subsequent next c @MethodSource("data") @Override public void shouldPassAllOutcomes( + final String testName, @Nullable final String fileDescription, @Nullable final String testDescription, @Nullable final String directoryName, + final int attemptNumber, + final int totalAttempts, final String schemaVersion, @Nullable final BsonArray runOnRequirements, final BsonArray entitiesArray, final BsonArray initialData, final BsonDocument definition) { try { - super.shouldPassAllOutcomes(fileDescription, + super.shouldPassAllOutcomes( + testName, + fileDescription, testDescription, directoryName, + attemptNumber, + totalAttempts, schemaVersion, runOnRequirements, entitiesArray, diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/UnifiedReactiveStreamsTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/UnifiedReactiveStreamsTest.java index 62c1315e240..28c8a27f8fa 100644 --- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/UnifiedReactiveStreamsTest.java +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/unified/UnifiedReactiveStreamsTest.java @@ -24,6 +24,7 @@ import com.mongodb.client.unified.UnifiedTest; import com.mongodb.client.unified.UnifiedTestModifications; import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.lang.NonNull; import com.mongodb.reactivestreams.client.MongoClients; import com.mongodb.reactivestreams.client.gridfs.GridFSBuckets; import com.mongodb.reactivestreams.client.internal.vault.ClientEncryptionImpl; @@ -31,6 +32,11 @@ import com.mongodb.reactivestreams.client.syncadapter.SyncGridFSBucket; import com.mongodb.reactivestreams.client.syncadapter.SyncMongoClient; import com.mongodb.reactivestreams.client.syncadapter.SyncMongoDatabase; +import org.junit.jupiter.params.provider.Arguments; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Collection; import static com.mongodb.client.unified.UnifiedTestModifications.Modifier; import static com.mongodb.client.unified.UnifiedTestModifications.TestDef; @@ -94,4 +100,9 @@ protected void postCleanUp(final TestDef testDef) { disableSleep(); } } + + @NonNull + protected static Collection getTestData(final String directory) throws URISyntaxException, IOException { + return getTestData(directory, true); + } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedSyncTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedSyncTest.java index 37db7cfe907..afcc8e4f1a3 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedSyncTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedSyncTest.java @@ -25,6 +25,12 @@ import com.mongodb.client.gridfs.GridFSBuckets; import com.mongodb.client.internal.ClientEncryptionImpl; import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.lang.NonNull; +import org.junit.jupiter.params.provider.Arguments; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Collection; public abstract class UnifiedSyncTest extends UnifiedTest { protected UnifiedSyncTest() { @@ -44,4 +50,9 @@ protected GridFSBucket createGridFSBucket(final MongoDatabase database) { protected ClientEncryption createClientEncryption(final MongoClient keyVaultClient, final ClientEncryptionSettings clientEncryptionSettings) { return new ClientEncryptionImpl(keyVaultClient, clientEncryptionSettings); } + + @NonNull + protected static Collection getTestData(final String directory) throws URISyntaxException, IOException { + return getTestData(directory, false); + } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java index 7ee16484df1..a437084ac1d 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java @@ -62,9 +62,11 @@ import java.io.File; import java.io.IOException; import java.net.URISyntaxException; +import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -81,6 +83,8 @@ import static com.mongodb.client.test.CollectionHelper.getCurrentClusterTime; import static com.mongodb.client.test.CollectionHelper.killAllSessions; import static com.mongodb.client.unified.RunOnRequirementsMatcher.runOnRequirementsMet; +import static com.mongodb.client.unified.UnifiedTestModifications.Modifier; +import static com.mongodb.client.unified.UnifiedTestModifications.applyCustomizations; import static com.mongodb.client.unified.UnifiedTestModifications.testDef; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; @@ -91,6 +95,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.abort; import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static util.JsonPoweredTestHelper.getTestDocument; @@ -101,6 +106,10 @@ public abstract class UnifiedTest { private static final Set PRESTART_POOL_ASYNC_WORK_MANAGER_FILE_DESCRIPTIONS = Collections.singleton( "wait queue timeout errors include details about checked out connections"); + public static final int RETRY_ATTEMPTS = 3; + public static final int FORCE_FLAKY_ATTEMPTS = 10; + private static final Set ATTEMPTED_TESTS_TO_HENCEFORTH_IGNORE = new HashSet<>(); + @Nullable private String fileDescription; private String schemaVersion; @@ -156,32 +165,49 @@ public Entities getEntities() { } @NonNull - protected static Collection getTestData(final String directory) throws URISyntaxException, IOException { + protected static Collection getTestData(final String directory, final boolean isReactive) + throws URISyntaxException, IOException { List data = new ArrayList<>(); for (File file : getTestFiles("/" + directory + "/")) { BsonDocument fileDocument = getTestDocument(file); - for (BsonValue cur : fileDocument.getArray("tests")) { - data.add(UnifiedTest.createTestData(directory, fileDocument, cur.asDocument())); + + final BsonDocument testDocument = cur.asDocument(); + String testDescription = testDocument.getString("description").getValue(); + String fileDescription = fileDocument.getString("description").getValue(); + TestDef testDef = testDef(directory, fileDescription, testDescription, isReactive); + applyCustomizations(testDef); + + boolean forceFlaky = testDef.wasAssignedModifier(Modifier.FORCE_FLAKY); + boolean retry = forceFlaky || testDef.wasAssignedModifier(Modifier.RETRY); + + int attempts; + if (retry) { + attempts = forceFlaky ? FORCE_FLAKY_ATTEMPTS : RETRY_ATTEMPTS; + } else { + attempts = 1; + } + + for (int attempt = 1; attempt <= attempts; attempt++) { + String testName = MessageFormat.format("{0}: {1}", fileDescription, testDescription); + data.add(Arguments.of( + testName, + fileDescription, + testDescription, + directory, + attempt, + attempts, + fileDocument.getString("schemaVersion").getValue(), + fileDocument.getArray("runOnRequirements", null), + fileDocument.getArray("createEntities", new BsonArray()), + fileDocument.getArray("initialData", new BsonArray()), + testDocument.clone())); + } } } return data; } - @NonNull - private static Arguments createTestData( - final String directory, final BsonDocument fileDocument, final BsonDocument testDocument) { - return Arguments.of( - fileDocument.getString("description").getValue(), - testDocument.getString("description").getValue(), - directory, - fileDocument.getString("schemaVersion").getValue(), - fileDocument.getArray("runOnRequirements", null), - fileDocument.getArray("createEntities", new BsonArray()), - fileDocument.getArray("initialData", new BsonArray()), - testDocument); - } - protected BsonDocument getDefinition() { return definition; } @@ -194,9 +220,12 @@ protected BsonDocument getDefinition() { @BeforeEach public void setUp( + final String testName, @Nullable final String fileDescription, @Nullable final String testDescription, @Nullable final String directoryName, + final int attemptNumber, + final int totalAttempts, final String schemaVersion, @Nullable final BsonArray runOnRequirements, final BsonArray entitiesArray, @@ -218,9 +247,9 @@ public void setUp( ignoreExtraEvents = false; if (directoryName != null && fileDescription != null && testDescription != null) { testDef = testDef(directoryName, fileDescription, testDescription, isReactive()); - UnifiedTestModifications.doSkips(testDef); + applyCustomizations(testDef); - boolean skip = testDef.wasAssignedModifier(UnifiedTestModifications.Modifier.SKIP); + boolean skip = testDef.wasAssignedModifier(Modifier.SKIP); assumeFalse(skip, "Skipping test"); } skips(fileDescription, testDescription); @@ -295,8 +324,9 @@ protected void postCleanUp(final TestDef testDef) { } /** - * This method is called once per {@link #setUp(String, String, String, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)}, - * unless {@link #setUp(String, String, String, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)} fails unexpectedly. + * This method is called once per + * {@link #setUp(String, String, String, String, int, int, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)}, unless + * {@link #setUp(String, String, String, String, int, int, String, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonArray, org.bson.BsonDocument)} fails unexpectedly. */ protected void skips(final String fileDescription, final String testDescription) { } @@ -305,40 +335,72 @@ protected boolean isReactive() { return false; } - @ParameterizedTest(name = "{0}: {1}") + @ParameterizedTest(name = "{0}") @MethodSource("data") public void shouldPassAllOutcomes( + final String testName, @Nullable final String fileDescription, @Nullable final String testDescription, @Nullable final String directoryName, + final int attemptNumber, + final int totalAttempts, final String schemaVersion, @Nullable final BsonArray runOnRequirements, final BsonArray entitiesArray, final BsonArray initialData, final BsonDocument definition) { - BsonArray operations = definition.getArray("operations"); - for (int i = 0; i < operations.size(); i++) { - BsonValue cur = operations.get(i); - assertOperation(rootContext, cur.asDocument(), i); + boolean forceFlaky = testDef.wasAssignedModifier(Modifier.FORCE_FLAKY); + if (!forceFlaky) { + boolean ignoreThisTest = ATTEMPTED_TESTS_TO_HENCEFORTH_IGNORE.contains(testName); + assumeFalse(ignoreThisTest, "Skipping a retryable test that already succeeded"); + // The attempt is what counts, since a test may fail with + // something like "ignored", and would not be retried. + // Only failures should trigger another attempt. + ATTEMPTED_TESTS_TO_HENCEFORTH_IGNORE.add(testName); } + try { + BsonArray operations = definition.getArray("operations"); + for (int i = 0; i < operations.size(); i++) { + BsonValue cur = operations.get(i); + assertOperation(rootContext, cur.asDocument(), i); + } - if (definition.containsKey("outcome")) { - assertOutcome(rootContext); - } + if (definition.containsKey("outcome")) { + assertOutcome(rootContext); + } - if (definition.containsKey("expectEvents")) { - compareEvents(rootContext, definition); - } + if (definition.containsKey("expectEvents")) { + compareEvents(rootContext, definition); + } - if (definition.containsKey("expectLogMessages")) { - ArrayList tweaks = new ArrayList<>(singletonList( - // `LogMessage.Entry.Name.OPERATION` is not supported, therefore we skip matching its value - LogMatcher.Tweak.skip(LogMessage.Entry.Name.OPERATION))); - if (getMongoClientSettings().getClusterSettings() - .getHosts().stream().anyMatch(serverAddress -> serverAddress instanceof UnixServerAddress)) { - tweaks.add(LogMatcher.Tweak.skip(LogMessage.Entry.Name.SERVER_PORT)); + if (definition.containsKey("expectLogMessages")) { + ArrayList tweaks = new ArrayList<>(singletonList( + // `LogMessage.Entry.Name.OPERATION` is not supported, therefore we skip matching its value + LogMatcher.Tweak.skip(LogMessage.Entry.Name.OPERATION))); + if (getMongoClientSettings().getClusterSettings() + .getHosts().stream().anyMatch(serverAddress -> serverAddress instanceof UnixServerAddress)) { + tweaks.add(LogMatcher.Tweak.skip(LogMessage.Entry.Name.SERVER_PORT)); + } + compareLogMessages(rootContext, definition, tweaks); + } + } catch (TestAbortedException e) { + // if a test is ignored, we do not retry + throw e; + } catch (Throwable e) { + if (forceFlaky) { + throw e; + } + if (testDef != null && !testDef.matchesThrowable(e)) { + // if the throwable is not matched, test definitions were not intended to apply; rethrow it + throw e; } - compareLogMessages(rootContext, definition, tweaks); + boolean isLastAttempt = attemptNumber == totalAttempts; + if (isLastAttempt) { + throw e; + } + + ATTEMPTED_TESTS_TO_HENCEFORTH_IGNORE.remove(testName); + abort("Ignoring failure and retrying attempt " + attemptNumber); } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestFailureValidator.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestFailureValidator.java index 88458f8af8e..0472ef8e6ce 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestFailureValidator.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestFailureValidator.java @@ -36,9 +36,12 @@ final class UnifiedTestFailureValidator extends UnifiedSyncTest { @Override @BeforeEach public void setUp( + final String testName, @Nullable final String fileDescription, @Nullable final String testDescription, final String directoryName, + final int attemptNumber, + final int totalAttempts, final String schemaVersion, @Nullable final BsonArray runOnRequirements, final BsonArray entitiesArray, @@ -46,9 +49,12 @@ public void setUp( final BsonDocument definition) { try { super.setUp( + testName, fileDescription, testDescription, directoryName, + attemptNumber, + totalAttempts, schemaVersion, runOnRequirements, entitiesArray, @@ -63,9 +69,12 @@ public void setUp( @ParameterizedTest @MethodSource("data") public void shouldPassAllOutcomes( + final String testName, @Nullable final String fileDescription, @Nullable final String testDescription, @Nullable final String directoryName, + final int attemptNumber, + final int totalAttempts, final String schemaVersion, @Nullable final BsonArray runOnRequirements, final BsonArray entitiesArray, @@ -74,9 +83,12 @@ public void shouldPassAllOutcomes( if (exception == null) { try { super.shouldPassAllOutcomes( + testName, fileDescription, testDescription, directoryName, + attemptNumber, + totalAttempts, schemaVersion, runOnRequirements, entitiesArray, diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java index 5184fd699be..df9706b8dd7 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTestModifications.java @@ -16,11 +16,12 @@ package com.mongodb.client.unified; -import com.mongodb.assertions.Assertions; +import org.opentest4j.AssertionFailedError; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; import static com.mongodb.ClusterFixture.isDataLakeTest; @@ -29,14 +30,17 @@ import static com.mongodb.ClusterFixture.isSharded; import static com.mongodb.ClusterFixture.serverVersionLessThan; import static com.mongodb.assertions.Assertions.assertNotNull; +import static com.mongodb.assertions.Assertions.assertTrue; import static com.mongodb.client.unified.UnifiedTestModifications.Modifier.IGNORE_EXTRA_EVENTS; +import static com.mongodb.client.unified.UnifiedTestModifications.Modifier.RETRY; import static com.mongodb.client.unified.UnifiedTestModifications.Modifier.SKIP; import static com.mongodb.client.unified.UnifiedTestModifications.Modifier.SLEEP_AFTER_CURSOR_CLOSE; import static com.mongodb.client.unified.UnifiedTestModifications.Modifier.SLEEP_AFTER_CURSOR_OPEN; import static com.mongodb.client.unified.UnifiedTestModifications.Modifier.WAIT_FOR_BATCH_CURSOR_CREATION; +import static java.lang.String.format; public final class UnifiedTestModifications { - public static void doSkips(final TestDef def) { + public static void applyCustomizations(final TestDef def) { // atlas-data-lake @@ -45,7 +49,7 @@ public static void doSkips(final TestDef def) { .directory("atlas-data-lake-testing"); // change-streams - def.skipNoncompliantReactive("error required from change stream initialization") // TODO reason? + def.skipNoncompliantReactive("error required from change stream initialization") // TODO-JAVA-5711 reason? .test("change-streams", "change-streams", "Test with document comment - pre 4.4"); def.skipNoncompliantReactive("event sensitive tests. We can't guarantee the amount of GetMore commands sent in the reactive driver") .test("change-streams", "change-streams", "Test that comment is set on getMore") @@ -61,17 +65,17 @@ public static void doSkips(final TestDef def) { // client-side-operation-timeout (CSOT) - // TODO + // TODO-JAVA-5712 // collection-management - def.skipNoncompliant("") // TODO reason? + def.skipNoncompliant("") // TODO-JAVA-5711 reason? .test("collection-management", "modifyCollection-pre_and_post_images", "modifyCollection to changeStreamPreAndPostImages enabled"); // command-logging-and-monitoring - def.skipNoncompliant("TODO") - .when(() -> !def.isReactive() && isServerlessTest()) // TODO why reactive check? + def.skipNoncompliant("") // TODO-JAVA-5711 + .when(() -> !def.isReactive() && isServerlessTest()) // TODO-JAVA-5711 why reactive check? .directory("command-logging") .directory("command-monitoring"); @@ -84,7 +88,7 @@ public static void doSkips(final TestDef def) { // connection-monitoring-and-pooling - // TODO reason, jira + // TODO-JAVA-5711 reason, jira // added as part of https://jira.mongodb.org/browse/JAVA-4976 , but unknown Jira to complete // The implementation of the functionality related to clearing the connection pool before closing the connection // will be carried out once the specification is finalized and ready. @@ -259,6 +263,7 @@ public static final class TestDef { private final boolean reactive; private final List modifiers = new ArrayList<>(); + private Function matchesThrowable; private TestDef(final String dir, final String file, final String test, final boolean reactive) { this.dir = assertNotNull(dir); @@ -274,7 +279,7 @@ private TestDef(final String dir, final String file, final String test, final bo * @param ticket reason for skipping the test; must start with a Jira URL */ public TestApplicator skipJira(final String ticket) { - Assertions.assertTrue(ticket.startsWith("https://jira.mongodb.org/browse/JAVA-")); + assertTrue(ticket.startsWith("https://jira.mongodb.org/browse/JAVA-")); return new TestApplicator(this, ticket, SKIP); } @@ -322,6 +327,21 @@ public TestApplicator skipUnknownReason(final String reason) { return new TestApplicator(this, reason, SKIP); } + /** + * The test will be retried, for the reason provided + */ + public TestApplicator retry(final String reason) { + return new TestApplicator(this, reason, RETRY); + } + + /** + * The reactive test will be retried, for the reason provided + */ + public TestApplicator retryReactive(final String reason) { + return new TestApplicator(this, reason, RETRY) + .when(this::isReactive); + } + public TestApplicator modify(final Modifier... modifiers) { return new TestApplicator(this, null, modifiers); } @@ -333,6 +353,13 @@ public boolean isReactive() { public boolean wasAssignedModifier(final Modifier modifier) { return this.modifiers.contains(modifier); } + + public boolean matchesThrowable(final Throwable e) { + if (matchesThrowable != null) { + return matchesThrowable.apply(e); + } + return false; + } } /** @@ -340,17 +367,19 @@ public boolean wasAssignedModifier(final Modifier modifier) { */ public static final class TestApplicator { private final TestDef testDef; - private final List modifiersToApply; private Supplier precondition; private boolean matchWasPerformed = false; + private final List modifiersToApply; + private Function matchesThrowable; + private TestApplicator( final TestDef testDef, final String reason, final Modifier... modifiersToApply) { this.testDef = testDef; this.modifiersToApply = Arrays.asList(modifiersToApply); - if (this.modifiersToApply.contains(SKIP)) { + if (this.modifiersToApply.contains(SKIP) || this.modifiersToApply.contains(RETRY)) { assertNotNull(reason); } } @@ -362,6 +391,7 @@ private TestApplicator onMatch(final boolean match) { } if (match) { this.testDef.modifiers.addAll(this.modifiersToApply); + this.testDef.matchesThrowable = this.matchesThrowable; } return this; } @@ -453,6 +483,26 @@ public TestApplicator when(final Supplier precondition) { this.precondition = precondition; return this; } + + /** + * The modification, if it is a RETRY, will only be applied when the + * failure message contains the provided message fragment. If an + * {@code AssertionFailedError} occurs, and has a cause, the cause's + * message will be checked. Otherwise, the throwable will be checked. + */ + public TestApplicator whenFailureContains(final String messageFragment) { + assertTrue(this.modifiersToApply.contains(RETRY), + format("Modifier %s was not specified before calling whenFailureContains", RETRY)); + this.matchesThrowable = (final Throwable e) -> { + // inspect the cause for failed assertions with a cause + if (e instanceof AssertionFailedError && e.getCause() != null) { + return e.getCause().getMessage().contains(messageFragment); + } else { + return e.getMessage().contains(messageFragment); + } + }; + return this; + } } public enum Modifier { @@ -478,5 +528,17 @@ public enum Modifier { * Skip the test. */ SKIP, + /** + * Ignore results and retry the test on failure. Will not repeat the + * test if the test succeeds. Multiple copies of the test are used to + * facilitate retries. + */ + RETRY, + /** + * The test will be retried multiple times, without the results being + * ignored. This is a helper that can be used, in patches, to check + * if certain tests are (still) flaky. + */ + FORCE_FLAKY, } } diff --git a/driver-workload-executor/src/main/com/mongodb/workload/WorkloadExecutor.java b/driver-workload-executor/src/main/com/mongodb/workload/WorkloadExecutor.java index 0e995cb34fd..7aba736aebc 100644 --- a/driver-workload-executor/src/main/com/mongodb/workload/WorkloadExecutor.java +++ b/driver-workload-executor/src/main/com/mongodb/workload/WorkloadExecutor.java @@ -98,18 +98,24 @@ protected boolean terminateLoop() { BsonArray createEntities = fileDocument.getArray("createEntities", new BsonArray()); BsonArray initialData = fileDocument.getArray("initialData", new BsonArray()); unifiedTest.setUp( + "", null, null, null, + 1, + 1, schemaVersion, runOnRequirements, createEntities, initialData, testDocument); unifiedTest.shouldPassAllOutcomes( + "", null, null, null, + 1, + 1, schemaVersion, runOnRequirements, createEntities, From 2849421837f8e9b5a6501f298377c813768f9771 Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Thu, 13 Mar 2025 11:55:49 +0000 Subject: [PATCH 41/43] Test Load balancers Updated unified tests JAVA-5805 - revert skip test JAVA-5228 - Unpin session test --- .../non-lb-connection-establishment.json | 13 ------ .../load-balancers/transactions.json | 44 +++++++++++++++++++ 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json b/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json index f4fed13cc23..6aaa7bdf98b 100644 --- a/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json +++ b/driver-core/src/test/resources/unified-test-format/load-balancers/non-lb-connection-establishment.json @@ -57,19 +57,6 @@ "tests": [ { "description": "operations against non-load balanced clusters fail if URI contains loadBalanced=true", - "runOnRequirements": [ - { - "maxServerVersion": "8.0.99", - "topologies": [ - "single" - ] - }, - { - "topologies": [ - "sharded" - ] - } - ], "operations": [ { "name": "runCommand", diff --git a/driver-core/src/test/resources/unified-test-format/load-balancers/transactions.json b/driver-core/src/test/resources/unified-test-format/load-balancers/transactions.json index 0dd04ee8540..ca9c1452179 100644 --- a/driver-core/src/test/resources/unified-test-format/load-balancers/transactions.json +++ b/driver-core/src/test/resources/unified-test-format/load-balancers/transactions.json @@ -1616,6 +1616,50 @@ ] } ] + }, + { + "description": "pinned connection is released when session ended", + "operations": [ + { + "name": "startTransaction", + "object": "session0" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "x": 1 + }, + "session": "session0" + } + }, + { + "name": "commitTransaction", + "object": "session0" + }, + { + "name": "endSession", + "object": "session0" + } + ], + "expectEvents": [ + { + "client": "client0", + "eventType": "cmap", + "events": [ + { + "connectionReadyEvent": {} + }, + { + "connectionCheckedOutEvent": {} + }, + { + "connectionCheckedInEvent": {} + } + ] + } + ] } ] } From 48fdf2a4e392f086221c6eb6834981f304354a52 Mon Sep 17 00:00:00 2001 From: Nabil Hachicha Date: Wed, 19 Mar 2025 14:16:04 +0000 Subject: [PATCH 42/43] JAVA-5736 Add bsonNamingStrategy option to support snake_case (#1627) Add bsonNamingStrategy option to support snake_case naming strategy --------- Co-authored-by: leesungbin Co-authored-by: Ross Lawley --- THIRD-PARTY-NOTICES | 16 ++++ .../bson/codecs/kotlinx/BsonConfiguration.kt | 15 ++++ .../org/bson/codecs/kotlinx/BsonDecoder.kt | 11 ++- .../org/bson/codecs/kotlinx/BsonEncoder.kt | 11 ++- .../bson/codecs/kotlinx/JsonBsonDecoder.kt | 2 + .../bson/codecs/kotlinx/JsonBsonEncoder.kt | 2 + .../codecs/kotlinx/utils/BsonCodecUtils.kt | 75 +++++++++++++++++++ .../kotlinx/KotlinSerializerCodecTest.kt | 37 +++++++++ .../codecs/kotlinx/samples/DataClasses.kt | 25 +++++++ 9 files changed, 192 insertions(+), 2 deletions(-) diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index f881b103544..acca60ca973 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -184,3 +184,19 @@ https://github.com/mongodb/mongo-java-driver. See the License for the specific language governing permissions and limitations under the License. +9) The following files: BsonCodecUtils.kt + + Copyright 2008-present MongoDB, Inc. + Copyright 2017-2021 JetBrains s.r.o. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonConfiguration.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonConfiguration.kt index 027fe8925da..8a163f42f83 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonConfiguration.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonConfiguration.kt @@ -31,4 +31,19 @@ public data class BsonConfiguration( val encodeDefaults: Boolean = true, val explicitNulls: Boolean = false, val classDiscriminator: String = "_t", + val bsonNamingStrategy: BsonNamingStrategy? = null ) + +/** + * Optional BSON naming strategy for a field. + * + * @since 5.4 + */ +public enum class BsonNamingStrategy { + + /** + * A strategy that transforms serial names from camel case to snake case — lowercase characters with words separated + * by underscores. + */ + SNAKE_CASE, +} diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt index 99e5d2acb17..c00d09345d0 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt @@ -37,11 +37,13 @@ import org.bson.BsonType import org.bson.BsonValue import org.bson.codecs.BsonValueCodec import org.bson.codecs.DecoderContext +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.cacheElementNamesByDescriptor import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonArrayDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonDocumentDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonMapDecoder import org.bson.codecs.kotlinx.utils.BsonCodecUtils.createBsonPolymorphicDecoder +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.getCachedElementNamesByDescriptor import org.bson.internal.NumberCodecHelper import org.bson.internal.StringCodecHelper import org.bson.types.ObjectId @@ -102,6 +104,7 @@ internal sealed class AbstractBsonDecoder( elementDescriptor.serialName, elementDescriptor.isNullable && !descriptor.isElementOptional(it)) } this.elementsMetadata = elementsMetadata + cacheElementNamesByDescriptor(descriptor, configuration) } override fun decodeElementIndex(descriptor: SerialDescriptor): Int { @@ -129,7 +132,13 @@ internal sealed class AbstractBsonDecoder( } return name?.let { - val index = descriptor.getElementIndex(it) + val index = + if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) { + getCachedElementNamesByDescriptor(descriptor)[it]?.let { name -> descriptor.getElementIndex(name) } + ?: UNKNOWN_NAME + } else { + descriptor.getElementIndex(it) + } return if (index == UNKNOWN_NAME) { reader.skipValue() decodeElementIndexImpl(descriptor) diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt index 1470bbb76a5..8a34bccdb36 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt @@ -31,6 +31,7 @@ import org.bson.BsonValue import org.bson.BsonWriter import org.bson.codecs.BsonValueCodec import org.bson.codecs.EncoderContext +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.convertCamelCase import org.bson.types.ObjectId /** @@ -203,7 +204,15 @@ internal open class BsonEncoderImpl( } internal fun encodeName(value: Any) { - writer.writeName(value.toString()) + val name = + value.toString().let { + if (configuration.bsonNamingStrategy == BsonNamingStrategy.SNAKE_CASE) { + convertCamelCase(it, '_') + } else { + it + } + } + writer.writeName(name) state = STATE.VALUE } diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt index 4b0eee8213a..bd8b6739958 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt @@ -31,6 +31,7 @@ import org.bson.AbstractBsonReader import org.bson.BsonBinarySubType import org.bson.BsonType import org.bson.UuidRepresentation +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toJsonNamingStrategy import org.bson.internal.UuidHelper @OptIn(ExperimentalSerializationApi::class) @@ -42,6 +43,7 @@ internal interface JsonBsonDecoder : BsonDecoder, JsonDecoder { explicitNulls = configuration.explicitNulls encodeDefaults = configuration.encodeDefaults classDiscriminator = configuration.classDiscriminator + namingStrategy = configuration.bsonNamingStrategy.toJsonNamingStrategy() serializersModule = this@JsonBsonDecoder.serializersModule } diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt index 6cff36a0909..4a754834e6d 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt @@ -30,6 +30,7 @@ import kotlinx.serialization.json.int import kotlinx.serialization.json.long import kotlinx.serialization.modules.SerializersModule import org.bson.BsonWriter +import org.bson.codecs.kotlinx.utils.BsonCodecUtils.toJsonNamingStrategy import org.bson.types.Decimal128 @OptIn(ExperimentalSerializationApi::class) @@ -52,6 +53,7 @@ internal class JsonBsonEncoder( explicitNulls = configuration.explicitNulls encodeDefaults = configuration.encodeDefaults classDiscriminator = configuration.classDiscriminator + namingStrategy = configuration.bsonNamingStrategy.toJsonNamingStrategy() serializersModule = this@JsonBsonEncoder.serializersModule } diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt index eabfebc5833..daf6c7df6f9 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/utils/BsonCodecUtils.kt @@ -16,7 +16,10 @@ package org.bson.codecs.kotlinx.utils import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationException import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.elementNames +import kotlinx.serialization.json.JsonNamingStrategy import kotlinx.serialization.modules.SerializersModule import org.bson.AbstractBsonReader import org.bson.BsonWriter @@ -28,6 +31,7 @@ import org.bson.codecs.kotlinx.BsonDocumentDecoder import org.bson.codecs.kotlinx.BsonEncoder import org.bson.codecs.kotlinx.BsonEncoderImpl import org.bson.codecs.kotlinx.BsonMapDecoder +import org.bson.codecs.kotlinx.BsonNamingStrategy import org.bson.codecs.kotlinx.BsonPolymorphicDecoder import org.bson.codecs.kotlinx.JsonBsonArrayDecoder import org.bson.codecs.kotlinx.JsonBsonDecoderImpl @@ -59,6 +63,8 @@ internal object BsonCodecUtils { } } + private val cachedElementNamesByDescriptor: MutableMap> = mutableMapOf() + internal fun createBsonEncoder( writer: BsonWriter, serializersModule: SerializersModule, @@ -116,4 +122,73 @@ internal object BsonCodecUtils { return if (hasJsonDecoder) JsonBsonMapDecoder(descriptor, reader, serializersModule, configuration) else BsonMapDecoder(descriptor, reader, serializersModule, configuration) } + + internal fun cacheElementNamesByDescriptor(descriptor: SerialDescriptor, configuration: BsonConfiguration) { + val convertedNameMap = + when (configuration.bsonNamingStrategy) { + BsonNamingStrategy.SNAKE_CASE -> { + val snakeCasedNames = descriptor.elementNames.associateWith { name -> convertCamelCase(name, '_') } + + snakeCasedNames.entries + .groupBy { entry -> entry.value } + .filter { group -> group.value.size > 1 } + .entries + .fold(StringBuilder("")) { acc, group -> + val keys = group.value.joinToString(", ") { entry -> entry.key } + acc.append("$keys in ${descriptor.serialName} generate same name: ${group.key}.\n") + } + .toString() + .takeIf { it.trim().isNotEmpty() } + ?.let { errorMessage: String -> throw SerializationException(errorMessage) } + + snakeCasedNames.entries.associate { it.value to it.key } + } + else -> emptyMap() + } + + cachedElementNamesByDescriptor[descriptor.serialName] = convertedNameMap + } + + internal fun getCachedElementNamesByDescriptor(descriptor: SerialDescriptor): Map { + return cachedElementNamesByDescriptor[descriptor.serialName] ?: emptyMap() + } + + // https://github.com/Kotlin/kotlinx.serialization/blob/f9f160a680da9f92c3bb121ae3644c96e57ba42e/formats/json/commonMain/src/kotlinx/serialization/json/JsonNamingStrategy.kt#L142-L174 + internal fun convertCamelCase(value: String, delimiter: Char) = + buildString(value.length * 2) { + var bufferedChar: Char? = null + var previousUpperCharsCount = 0 + + value.forEach { c -> + if (c.isUpperCase()) { + if (previousUpperCharsCount == 0 && isNotEmpty() && last() != delimiter) append(delimiter) + + bufferedChar?.let(::append) + + previousUpperCharsCount++ + bufferedChar = c.lowercaseChar() + } else { + if (bufferedChar != null) { + if (previousUpperCharsCount > 1 && c.isLetter()) { + append(delimiter) + } + append(bufferedChar) + previousUpperCharsCount = 0 + bufferedChar = null + } + append(c) + } + } + + if (bufferedChar != null) { + append(bufferedChar) + } + } + + internal fun BsonNamingStrategy?.toJsonNamingStrategy(): JsonNamingStrategy? { + return when (this) { + BsonNamingStrategy.SNAKE_CASE -> JsonNamingStrategy.SnakeCase + else -> null + } + } } diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt index aa749368e04..f9b3eb753c5 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt @@ -81,6 +81,7 @@ import org.bson.codecs.kotlinx.samples.DataClassWithBsonId import org.bson.codecs.kotlinx.samples.DataClassWithBsonIgnore import org.bson.codecs.kotlinx.samples.DataClassWithBsonProperty import org.bson.codecs.kotlinx.samples.DataClassWithBsonRepresentation +import org.bson.codecs.kotlinx.samples.DataClassWithCamelCase import org.bson.codecs.kotlinx.samples.DataClassWithCollections import org.bson.codecs.kotlinx.samples.DataClassWithContextualDateValues import org.bson.codecs.kotlinx.samples.DataClassWithDataClassMapKey @@ -94,6 +95,7 @@ import org.bson.codecs.kotlinx.samples.DataClassWithFailingInit import org.bson.codecs.kotlinx.samples.DataClassWithJsonElement import org.bson.codecs.kotlinx.samples.DataClassWithJsonElements import org.bson.codecs.kotlinx.samples.DataClassWithJsonElementsNullable +import org.bson.codecs.kotlinx.samples.DataClassWithKotlinAllowedName import org.bson.codecs.kotlinx.samples.DataClassWithListThatLastItemDefaultsToNull import org.bson.codecs.kotlinx.samples.DataClassWithMutableList import org.bson.codecs.kotlinx.samples.DataClassWithMutableMap @@ -105,6 +107,7 @@ import org.bson.codecs.kotlinx.samples.DataClassWithNulls import org.bson.codecs.kotlinx.samples.DataClassWithPair import org.bson.codecs.kotlinx.samples.DataClassWithParameterizedDataClass import org.bson.codecs.kotlinx.samples.DataClassWithRequired +import org.bson.codecs.kotlinx.samples.DataClassWithSameSnakeCaseName import org.bson.codecs.kotlinx.samples.DataClassWithSequence import org.bson.codecs.kotlinx.samples.DataClassWithSimpleValues import org.bson.codecs.kotlinx.samples.DataClassWithTriple @@ -1126,6 +1129,40 @@ class KotlinSerializerCodecTest { } } + @Test + fun testSnakeCaseNamingStrategy() { + val expected = + """{"two_words": "", "my_property": "", "camel_case_underscores": "", "url_mapping": "", + | "my_http_auth": "", "my_http2_api_key": "", "my_http2fast_api_key": ""}""" + .trimMargin() + val dataClass = DataClassWithCamelCase() + assertRoundTrips(expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE)) + } + + @Test + fun testSameSnakeCaseName() { + val expected = """{"my_http_auth": "", "my_http_auth1": ""}""" + val dataClass = DataClassWithSameSnakeCaseName() + val exception = + assertThrows { + assertRoundTrips( + expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE)) + } + assertEquals( + "myHTTPAuth, myHttpAuth in org.bson.codecs.kotlinx.samples.DataClassWithSameSnakeCaseName" + + " generate same name: my_http_auth.\n" + + "myHTTPAuth1, myHttpAuth1 in org.bson.codecs.kotlinx.samples.DataClassWithSameSnakeCaseName" + + " generate same name: my_http_auth1.\n", + exception.message) + } + + @Test + fun testKotlinAllowedName() { + val expected = """{"имя_переменной": "", "variable _name": ""}""" + val dataClass = DataClassWithKotlinAllowedName() + assertRoundTrips(expected, dataClass, BsonConfiguration(bsonNamingStrategy = BsonNamingStrategy.SNAKE_CASE)) + } + private inline fun assertRoundTrips( expected: String, value: T, diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt index e7a06600d20..773af52cd96 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt @@ -102,6 +102,31 @@ data class DataClassWithDefaults( val listSimple: List = listOf("a", "b", "c") ) +@Serializable +data class DataClassWithCamelCase( + val twoWords: String = "", + @Suppress("ConstructorParameterNaming") val MyProperty: String = "", + @Suppress("ConstructorParameterNaming") val camel_Case_Underscores: String = "", + @Suppress("ConstructorParameterNaming") val URLMapping: String = "", + val myHTTPAuth: String = "", + val myHTTP2ApiKey: String = "", + val myHTTP2fastApiKey: String = "", +) + +@Serializable +data class DataClassWithSameSnakeCaseName( + val myHTTPAuth: String = "", + val myHttpAuth: String = "", + val myHTTPAuth1: String = "", + val myHttpAuth1: String = "", +) + +@Serializable +data class DataClassWithKotlinAllowedName( + @Suppress("ConstructorParameterNaming") val имяПеременной: String = "", + @Suppress("ConstructorParameterNaming") val `variable Name`: String = "", +) + @Serializable data class DataClassWithNulls(val boolean: Boolean?, val string: String?, val listSimple: List?) @Serializable From 28e51aa5e7550b0ec9e082205e1232704a70a6c2 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov <10205+katcharov@users.noreply.github.com> Date: Thu, 20 Mar 2025 20:40:48 +0000 Subject: [PATCH 43/43] Version: bump 5.4.0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b979842f50c..baf9769015e 100644 --- a/build.gradle +++ b/build.gradle @@ -77,7 +77,7 @@ configure(coreProjects) { apply plugin: 'idea' group = 'org.mongodb' - version = '5.4.0-SNAPSHOT' + version = '5.4.0' repositories { mavenLocal()

A TOKEN_RESOURCE with a comma character must be given as a `MongoClient` configuration and not as diff --git a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java index 3d778ae0349..99fcee788ed 100644 --- a/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java +++ b/driver-core/src/main/com/mongodb/internal/connection/OidcAuthenticator.java @@ -76,10 +76,11 @@ public final class OidcAuthenticator extends SaslAuthenticator { private static final String TEST_ENVIRONMENT = "test"; private static final String AZURE_ENVIRONMENT = "azure"; private static final String GCP_ENVIRONMENT = "gcp"; + private static final String K8S_ENVIRONMENT = "k8s"; private static final List IMPLEMENTED_ENVIRONMENTS = Arrays.asList( - AZURE_ENVIRONMENT, GCP_ENVIRONMENT, TEST_ENVIRONMENT); + AZURE_ENVIRONMENT, GCP_ENVIRONMENT, K8S_ENVIRONMENT, TEST_ENVIRONMENT); private static final List USER_SUPPORTED_ENVIRONMENTS = Arrays.asList( - AZURE_ENVIRONMENT, GCP_ENVIRONMENT); + AZURE_ENVIRONMENT, GCP_ENVIRONMENT, K8S_ENVIRONMENT); private static final List REQUIRES_TOKEN_RESOURCE = Arrays.asList( AZURE_ENVIRONMENT, GCP_ENVIRONMENT); private static final List ALLOWS_USERNAME = Arrays.asList( @@ -90,6 +91,10 @@ public final class OidcAuthenticator extends SaslAuthenticator { public static final String OIDC_TOKEN_FILE = "OIDC_TOKEN_FILE"; + private static final String K8S_FALLBACK_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token"; + private static final String K8S_AZURE_FILE = "AZURE_FEDERATED_TOKEN_FILE"; + private static final String K8S_AWS_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE"; + private static final int CALLBACK_API_VERSION_NUMBER = 1; @Nullable @@ -192,6 +197,8 @@ private OidcCallback getRequestCallback() { machine = getAzureCallback(getMongoCredential()); } else if (GCP_ENVIRONMENT.equals(environment)) { machine = getGcpCallback(getMongoCredential()); + } else if (K8S_ENVIRONMENT.equals(environment)) { + machine = getK8sCallback(); } else { machine = getOidcCallbackMechanismProperty(OIDC_CALLBACK_KEY); } @@ -206,6 +213,24 @@ private static OidcCallback getTestCallback() { }; } + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) + static OidcCallback getK8sCallback() { + return (context) -> { + String azure = System.getenv(K8S_AZURE_FILE); + String aws = System.getenv(K8S_AWS_FILE); + String path; + if (azure != null) { + path = azure; + } else if (aws != null) { + path = aws; + } else { + path = K8S_FALLBACK_FILE; + } + String accessToken = readTokenFromFile(path); + return new OidcCallbackResult(accessToken); + }; + } + @VisibleForTesting(otherwise = VisibleForTesting.AccessModifier.PRIVATE) static OidcCallback getAzureCallback(final MongoCredential credential) { return (context) -> { @@ -499,6 +524,10 @@ private static String readTokenFromFile() { throw new MongoClientException( format("Environment variable must be specified: %s", OIDC_TOKEN_FILE)); } + return readTokenFromFile(path); + } + + private static String readTokenFromFile(final String path) { try { return new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8); } catch (IOException e) { diff --git a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java index dde9682de8d..f0004cd9e03 100644 --- a/driver-core/src/test/functional/com/mongodb/ClusterFixture.java +++ b/driver-core/src/test/functional/com/mongodb/ClusterFixture.java @@ -261,7 +261,14 @@ static class ShutdownHook extends Thread { @Override public void run() { if (cluster != null) { - new DropDatabaseOperation(getDefaultDatabaseName(), WriteConcern.ACKNOWLEDGED).execute(getBinding()); + try { + new DropDatabaseOperation(getDefaultDatabaseName(), WriteConcern.ACKNOWLEDGED).execute(getBinding()); + } catch (MongoCommandException e) { + // if we do not have permission to drop the database, assume it is cleaned up in some other way + if (!e.getMessage().contains("Command dropDatabase requires authentication")) { + throw e; + } + } cluster.close(); } } diff --git a/driver-core/src/test/resources/auth/legacy/connection-string.json b/driver-core/src/test/resources/auth/legacy/connection-string.json index f8b0f9426c1..dfed11656d4 100644 --- a/driver-core/src/test/resources/auth/legacy/connection-string.json +++ b/driver-core/src/test/resources/auth/legacy/connection-string.json @@ -481,7 +481,7 @@ }, { "description": "should throw an exception if username is specified for test (MONGODB-OIDC)", - "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&ENVIRONMENT:test", + "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test", "valid": false, "credential": null }, @@ -631,6 +631,26 @@ "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp", "valid": false, "credential": null + }, + { + "description": "should recognise the mechanism with k8s provider (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "k8s" + } + } + }, + { + "description": "should throw an error for a username and password with k8s provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:k8s", + "valid": false, + "credential": null } ] } diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 2d82ecf3d92..85722fade52 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -826,6 +826,7 @@ private MongoClientSettings createSettings( String cleanedConnectionString = callback == null ? connectionString : connectionString .replace("ENVIRONMENT:azure,", "") .replace("ENVIRONMENT:gcp,", "") + .replace("&authMechanismProperties=ENVIRONMENT:k8s", "") .replace("ENVIRONMENT:test,", ""); return createSettings(cleanedConnectionString, callback, commandListener, OIDC_CALLBACK_KEY); } @@ -1042,6 +1043,8 @@ private OidcCallbackResult callback(final OidcCallbackContext context) { c = OidcAuthenticator.getAzureCallback(credential); } else if (oidcEnv.contains("gcp")) { c = OidcAuthenticator.getGcpCallback(credential); + } else if (oidcEnv.contains("k8s")) { + c = OidcAuthenticator.getK8sCallback(); } else { c = getProseTestCallback(); } From eeceb6323d9c2af055bf12baf681f4d540855c40 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov Date: Thu, 27 Feb 2025 12:31:46 -0700 Subject: [PATCH 30/43] JAVA-5767 Support $lookup in CSFLE and QE (#1638) JAVA-5767 --- .../lookup/key-doc.json | 30 +++ .../lookup/schema-csfle.json | 19 ++ .../lookup/schema-csfle2.json | 19 ++ .../lookup/schema-qe.json | 20 ++ .../lookup/schema-qe2.json | 20 ++ .../bulk/BaseClientDeleteOptionsTest.java | 2 +- .../bulk/BaseClientUpdateOptionsTest.java | 2 +- ...ClientUpsertableWriteModelOptionsTest.java | 2 +- .../bulk/BaseClientWriteModelOptionsTest.java | 2 +- .../com/mongodb/testing/MongoAssertions.java | 43 +++ .../MongoBaseInterfaceAssertions.java | 2 +- .../crypt/CollectionInfoRetriever.java | 6 +- .../client/internal/crypt/Crypt.java | 6 +- ...lientSideEncryption25LookupProseTests.java | 39 +++ .../internal/CollectionInfoRetriever.java | 12 +- .../com/mongodb/client/internal/Crypt.java | 8 +- ...lientSideEncryption25LookupProseTests.java | 255 ++++++++++++++++++ .../OidcAuthenticationProseTests.java | 17 +- mongodb-crypt/build.gradle.kts | 2 +- .../com/mongodb/internal/crypt/capi/CAPI.java | 8 + .../internal/crypt/capi/MongoCryptImpl.java | 3 + 21 files changed, 481 insertions(+), 36 deletions(-) create mode 100644 driver-core/src/test/resources/client-side-encryption-data/lookup/key-doc.json create mode 100644 driver-core/src/test/resources/client-side-encryption-data/lookup/schema-csfle.json create mode 100644 driver-core/src/test/resources/client-side-encryption-data/lookup/schema-csfle2.json create mode 100644 driver-core/src/test/resources/client-side-encryption-data/lookup/schema-qe.json create mode 100644 driver-core/src/test/resources/client-side-encryption-data/lookup/schema-qe2.json create mode 100644 driver-core/src/test/unit/com/mongodb/testing/MongoAssertions.java rename driver-core/src/test/unit/com/mongodb/{ => testing}/MongoBaseInterfaceAssertions.java (98%) create mode 100644 driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryption25LookupProseTests.java create mode 100644 driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryption25LookupProseTests.java diff --git a/driver-core/src/test/resources/client-side-encryption-data/lookup/key-doc.json b/driver-core/src/test/resources/client-side-encryption-data/lookup/key-doc.json new file mode 100644 index 00000000000..566b56c354f --- /dev/null +++ b/driver-core/src/test/resources/client-side-encryption-data/lookup/key-doc.json @@ -0,0 +1,30 @@ +{ + "_id": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "keyMaterial": { + "$binary": { + "base64": "sHe0kz57YW7v8g9VP9sf/+K1ex4JqKc5rf/URX3n3p8XdZ6+15uXPaSayC6adWbNxkFskuMCOifDoTT+rkqMtFkDclOy884RuGGtUysq3X7zkAWYTKi8QAfKkajvVbZl2y23UqgVasdQu3OVBQCrH/xY00nNAs/52e958nVjBuzQkSb1T8pKJAyjZsHJ60+FtnfafDZSTAIBJYn7UWBCwQ==", + "subType": "00" + } + }, + "creationDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "updateDate": { + "$date": { + "$numberLong": "1648914851981" + } + }, + "status": { + "$numberInt": "0" + }, + "masterKey": { + "provider": "local" + } +} diff --git a/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-csfle.json b/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-csfle.json new file mode 100644 index 00000000000..29ac9ad5da4 --- /dev/null +++ b/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-csfle.json @@ -0,0 +1,19 @@ +{ + "properties": { + "csfle": { + "encrypt": { + "keyId": [ + { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + } + ], + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + } + } + }, + "bsonType": "object" +} diff --git a/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-csfle2.json b/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-csfle2.json new file mode 100644 index 00000000000..3f1c02781c5 --- /dev/null +++ b/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-csfle2.json @@ -0,0 +1,19 @@ +{ + "properties": { + "csfle2": { + "encrypt": { + "keyId": [ + { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + } + ], + "bsonType": "string", + "algorithm": "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic" + } + } + }, + "bsonType": "object" +} diff --git a/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-qe.json b/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-qe.json new file mode 100644 index 00000000000..9428ea1b458 --- /dev/null +++ b/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-qe.json @@ -0,0 +1,20 @@ +{ + "escCollection": "enxcol_.qe.esc", + "ecocCollection": "enxcol_.qe.ecoc", + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "qe", + "bsonType": "string", + "queries": { + "queryType": "equality", + "contention": 0 + } + } + ] +} diff --git a/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-qe2.json b/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-qe2.json new file mode 100644 index 00000000000..77d5bd37cbb --- /dev/null +++ b/driver-core/src/test/resources/client-side-encryption-data/lookup/schema-qe2.json @@ -0,0 +1,20 @@ +{ + "escCollection": "enxcol_.qe2.esc", + "ecocCollection": "enxcol_.qe2.ecoc", + "fields": [ + { + "keyId": { + "$binary": { + "base64": "EjRWeBI0mHYSNBI0VniQEg==", + "subType": "04" + } + }, + "path": "qe2", + "bsonType": "string", + "queries": { + "queryType": "equality", + "contention": 0 + } + } + ] +} diff --git a/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientDeleteOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientDeleteOptionsTest.java index e9832c24b21..fdcba01c2d3 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientDeleteOptionsTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientDeleteOptionsTest.java @@ -16,7 +16,7 @@ package com.mongodb.client.model.bulk; -import com.mongodb.MongoBaseInterfaceAssertions; +import com.mongodb.testing.MongoBaseInterfaceAssertions; import org.junit.jupiter.api.Test; class BaseClientDeleteOptionsTest { diff --git a/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientUpdateOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientUpdateOptionsTest.java index 43ba8e0967e..c9131452063 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientUpdateOptionsTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientUpdateOptionsTest.java @@ -16,7 +16,7 @@ package com.mongodb.client.model.bulk; -import com.mongodb.MongoBaseInterfaceAssertions; +import com.mongodb.testing.MongoBaseInterfaceAssertions; import org.junit.jupiter.api.Test; class BaseClientUpdateOptionsTest { diff --git a/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientUpsertableWriteModelOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientUpsertableWriteModelOptionsTest.java index 5992a508574..8fecf8d14fd 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientUpsertableWriteModelOptionsTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientUpsertableWriteModelOptionsTest.java @@ -16,7 +16,7 @@ package com.mongodb.client.model.bulk; -import com.mongodb.MongoBaseInterfaceAssertions; +import com.mongodb.testing.MongoBaseInterfaceAssertions; import org.junit.jupiter.api.Test; final class BaseClientUpsertableWriteModelOptionsTest { diff --git a/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientWriteModelOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientWriteModelOptionsTest.java index 66fec81632e..17b3803727a 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientWriteModelOptionsTest.java +++ b/driver-core/src/test/unit/com/mongodb/client/model/bulk/BaseClientWriteModelOptionsTest.java @@ -16,7 +16,7 @@ package com.mongodb.client.model.bulk; -import com.mongodb.MongoBaseInterfaceAssertions; +import com.mongodb.testing.MongoBaseInterfaceAssertions; import org.junit.jupiter.api.Test; final class BaseClientWriteModelOptionsTest { diff --git a/driver-core/src/test/unit/com/mongodb/testing/MongoAssertions.java b/driver-core/src/test/unit/com/mongodb/testing/MongoAssertions.java new file mode 100644 index 00000000000..8f1bbf8df67 --- /dev/null +++ b/driver-core/src/test/unit/com/mongodb/testing/MongoAssertions.java @@ -0,0 +1,43 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.testing; + +import org.junit.jupiter.api.function.Executable; +import org.opentest4j.AssertionFailedError; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +public final class MongoAssertions { + + private MongoAssertions() { + //NOP + } + + public static void assertCause( + final Class expectedCause, final String expectedMessageFragment, final Executable e) { + Throwable cause = assertThrows(Throwable.class, e); + while (cause.getCause() != null) { + cause = cause.getCause(); + } + if (!cause.getMessage().contains(expectedMessageFragment)) { + throw new AssertionFailedError("Unexpected message: " + cause.getMessage(), cause); + } + if (!expectedCause.isInstance(cause)) { + throw new AssertionFailedError("Unexpected cause: " + cause.getClass(), assertThrows(Throwable.class, e)); + } + } +} diff --git a/driver-core/src/test/unit/com/mongodb/MongoBaseInterfaceAssertions.java b/driver-core/src/test/unit/com/mongodb/testing/MongoBaseInterfaceAssertions.java similarity index 98% rename from driver-core/src/test/unit/com/mongodb/MongoBaseInterfaceAssertions.java rename to driver-core/src/test/unit/com/mongodb/testing/MongoBaseInterfaceAssertions.java index 93f784b0506..0c0fe913123 100644 --- a/driver-core/src/test/unit/com/mongodb/MongoBaseInterfaceAssertions.java +++ b/driver-core/src/test/unit/com/mongodb/testing/MongoBaseInterfaceAssertions.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.mongodb; +package com.mongodb.testing; import org.reflections.Reflections; diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/CollectionInfoRetriever.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/CollectionInfoRetriever.java index 08df35c00f0..786055b1886 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/CollectionInfoRetriever.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/CollectionInfoRetriever.java @@ -20,7 +20,7 @@ import com.mongodb.lang.Nullable; import com.mongodb.reactivestreams.client.MongoClient; import org.bson.BsonDocument; -import reactor.core.publisher.Mono; +import reactor.core.publisher.Flux; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.reactivestreams.client.internal.TimeoutHelper.databaseWithTimeoutDeferred; @@ -35,8 +35,8 @@ class CollectionInfoRetriever { this.client = notNull("client", client); } - public Mono filter(final String databaseName, final BsonDocument filter, @Nullable final Timeout operationTimeout) { + public Flux filter(final String databaseName, final BsonDocument filter, @Nullable final Timeout operationTimeout) { return databaseWithTimeoutDeferred(client.getDatabase(databaseName), TIMEOUT_ERROR_MESSAGE, operationTimeout) - .flatMap(database -> Mono.from(database.listCollections(BsonDocument.class).filter(filter).first())); + .flatMapMany(database -> Flux.from(database.listCollections(BsonDocument.class).filter(filter))); } } diff --git a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/Crypt.java b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/Crypt.java index 17d82e32c49..61ccaa320fe 100644 --- a/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/Crypt.java +++ b/driver-reactive-streams/src/main/com/mongodb/reactivestreams/client/internal/crypt/Crypt.java @@ -304,10 +304,8 @@ private void collInfo(final MongoCryptContext cryptContext, } else { collectionInfoRetriever.filter(databaseName, cryptContext.getMongoOperation(), operationTimeout) .contextWrite(sink.contextView()) - .doOnSuccess(result -> { - if (result != null) { - cryptContext.addMongoOperationResult(result); - } + .doOnNext(result -> cryptContext.addMongoOperationResult(result)) + .doOnComplete(() -> { cryptContext.completeMongoOperation(); executeStateMachineWithSink(cryptContext, databaseName, sink, operationTimeout); }) diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryption25LookupProseTests.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryption25LookupProseTests.java new file mode 100644 index 00000000000..6cbd9b60e0d --- /dev/null +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientSideEncryption25LookupProseTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.reactivestreams.client; + +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.reactivestreams.client.syncadapter.SyncClientEncryption; +import com.mongodb.reactivestreams.client.syncadapter.SyncMongoClient; +import com.mongodb.reactivestreams.client.vault.ClientEncryptions; + +public class ClientSideEncryption25LookupProseTests extends com.mongodb.client.ClientSideEncryption25LookupProseTests { + + @Override + protected MongoClient createMongoClient(final MongoClientSettings settings) { + return new SyncMongoClient(MongoClients.create(settings)); + } + + @Override + protected ClientEncryption createClientEncryption(final ClientEncryptionSettings settings) { + return new SyncClientEncryption(ClientEncryptions.create(settings)); + } + +} diff --git a/driver-sync/src/main/com/mongodb/client/internal/CollectionInfoRetriever.java b/driver-sync/src/main/com/mongodb/client/internal/CollectionInfoRetriever.java index 934a3dce486..9d02a1e8756 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/CollectionInfoRetriever.java +++ b/driver-sync/src/main/com/mongodb/client/internal/CollectionInfoRetriever.java @@ -21,6 +21,9 @@ import com.mongodb.lang.Nullable; import org.bson.BsonDocument; +import java.util.ArrayList; +import java.util.List; + import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.client.internal.TimeoutHelper.databaseWithTimeout; @@ -33,9 +36,10 @@ class CollectionInfoRetriever { this.client = notNull("client", client); } - @Nullable - public BsonDocument filter(final String databaseName, final BsonDocument filter, @Nullable final Timeout operationTimeout) { - return databaseWithTimeout(client.getDatabase(databaseName), TIMEOUT_ERROR_MESSAGE, - operationTimeout).listCollections(BsonDocument.class).filter(filter).first(); + public List filter(final String databaseName, final BsonDocument filter, @Nullable final Timeout operationTimeout) { + return databaseWithTimeout(client.getDatabase(databaseName), TIMEOUT_ERROR_MESSAGE, operationTimeout) + .listCollections(BsonDocument.class) + .filter(filter) + .into(new ArrayList<>()); } } diff --git a/driver-sync/src/main/com/mongodb/client/internal/Crypt.java b/driver-sync/src/main/com/mongodb/client/internal/Crypt.java index b910f0ab01c..15ba16e66da 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/Crypt.java +++ b/driver-sync/src/main/com/mongodb/client/internal/Crypt.java @@ -41,6 +41,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; +import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -308,9 +309,10 @@ private void fetchCredentials(final MongoCryptContext cryptContext) { private void collInfo(final MongoCryptContext cryptContext, final String databaseName, @Nullable final Timeout operationTimeout) { try { - BsonDocument collectionInfo = assertNotNull(collectionInfoRetriever).filter(databaseName, cryptContext.getMongoOperation(), operationTimeout); - if (collectionInfo != null) { - cryptContext.addMongoOperationResult(collectionInfo); + List results = assertNotNull(collectionInfoRetriever) + .filter(databaseName, cryptContext.getMongoOperation(), operationTimeout); + for (BsonDocument result : results) { + cryptContext.addMongoOperationResult(result); } cryptContext.completeMongoOperation(); } catch (Throwable t) { diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryption25LookupProseTests.java b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryption25LookupProseTests.java new file mode 100644 index 00000000000..f7c672b289b --- /dev/null +++ b/driver-sync/src/test/functional/com/mongodb/client/ClientSideEncryption25LookupProseTests.java @@ -0,0 +1,255 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.mongodb.client; + +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.WriteConcern; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.ValidationOptions; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; +import com.mongodb.crypt.capi.MongoCryptException; +import com.mongodb.fixture.EncryptionFixture; +import org.bson.BsonArray; +import org.bson.BsonDocument; +import org.bson.Document; +import org.bson.types.Binary; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.io.File; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +import static com.mongodb.ClusterFixture.isStandalone; +import static com.mongodb.ClusterFixture.serverVersionAtLeast; +import static com.mongodb.ClusterFixture.serverVersionLessThan; +import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; +import static com.mongodb.fixture.EncryptionFixture.getKmsProviders; +import static com.mongodb.testing.MongoAssertions.assertCause; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeFalse; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static util.JsonPoweredTestHelper.getTestDocument; + +/** + * + * 25. Test $lookup + */ +public class ClientSideEncryption25LookupProseTests { + private MongoClient client; + + protected MongoClient createMongoClient(final MongoClientSettings settings) { + return MongoClients.create(settings); + } + + protected ClientEncryption createClientEncryption(final ClientEncryptionSettings settings) { + return ClientEncryptions.create(settings); + } + + @BeforeEach + public void setUp() { + assumeFalse(isStandalone()); + assumeTrue(serverVersionAtLeast(7, 0)); + + // Create an encrypted MongoClient named `encryptedClient` configured with: + MongoNamespace dataKeysNamespace = new MongoNamespace("db.keyvault"); + Map> kmsProviders = getKmsProviders(EncryptionFixture.KmsProviderType.LOCAL); + MongoClient encryptedClient = createMongoClient(getMongoClientSettingsBuilder() + .autoEncryptionSettings( + AutoEncryptionSettings.builder() + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build()) + .build()); + // Use `encryptedClient` to drop `db.keyvault`. + MongoDatabase encryptedDb = encryptedClient.getDatabase("db"); + MongoCollection encryptedCollection = encryptedDb + .getCollection(dataKeysNamespace.getCollectionName(), BsonDocument.class) + .withWriteConcern(WriteConcern.MAJORITY); + encryptedCollection.drop(); + // Insert `` into `db.keyvault` with majority write concern. + encryptedCollection.insertOne(bsonDocumentFromPath("key-doc.json")); + + // Use `encryptedClient` to drop and create the following collections: + Arrays.asList("csfle", "csfle2", "qe", "qe2", "no_schema", "no_schema2").forEach(c -> { + encryptedDb.getCollection(c).drop(); + }); + // create + encryptedDb.createCollection("csfle", new CreateCollectionOptions() + .validationOptions(new ValidationOptions() + .validator(new BsonDocument("$jsonSchema", bsonDocumentFromPath("schema-csfle.json"))))); + encryptedDb.createCollection("csfle2", new CreateCollectionOptions() + .validationOptions(new ValidationOptions() + .validator(new BsonDocument("$jsonSchema", bsonDocumentFromPath("schema-csfle2.json"))))); + + encryptedDb.createCollection("qe", + new CreateCollectionOptions().encryptedFields(bsonDocumentFromPath("schema-qe.json"))); + encryptedDb.createCollection("qe2", + new CreateCollectionOptions().encryptedFields(bsonDocumentFromPath("schema-qe2.json"))); + + encryptedDb.createCollection("no_schema"); + encryptedDb.createCollection("no_schema2"); + + // Insert documents with `encryptedClient`: + Consumer insert = (name) -> { + encryptedDb.getCollection(name).insertOne(new Document(name, name)); + }; + insert.accept("csfle"); + insert.accept("csfle2"); + insert.accept("qe"); + insert.accept("qe2"); + insert.accept("no_schema"); + insert.accept("no_schema2"); + + // Create an unencrypted MongoClient named `unencryptedClient`. + MongoClient unencryptedClient = createMongoClient(getMongoClientSettingsBuilder().build()); + MongoDatabase unencryptedDb = unencryptedClient.getDatabase("db"); + + Consumer assertDocument = (name) -> { + List pipeline = Arrays.asList( + BsonDocument.parse("{\"$project\" : {\"_id\" : 0, \"__safeContent__\" : 0}}") + ); + Document decryptedDoc = encryptedDb.getCollection(name) + .aggregate(pipeline).first(); + assertEquals(decryptedDoc, new Document(name, name)); + Document encryptedDoc = unencryptedDb.getCollection(name) + .aggregate(pipeline).first(); + assertNotNull(encryptedDoc); + assertEquals(Binary.class, encryptedDoc.get(name).getClass()); + }; + + assertDocument.accept("csfle"); + assertDocument.accept("csfle2"); + assertDocument.accept("qe"); + assertDocument.accept("qe2"); + + unencryptedClient.close(); + encryptedClient.close(); + + client = createMongoClient(getMongoClientSettingsBuilder() + .autoEncryptionSettings( + AutoEncryptionSettings.builder() + .keyVaultNamespace(dataKeysNamespace.getFullName()) + .kmsProviders(kmsProviders) + .build()) + .build()); + } + + @AfterEach + @SuppressWarnings("try") + public void cleanUp() { + //noinspection EmptyTryBlock + try (MongoClient ignored = this.client) { + // just using try-with-resources to ensure they all get closed, even in the case of exceptions + } + } + + @ParameterizedTest + @CsvSource({ + "csfle, no_schema", + "qe, no_schema", + "no_schema, csfle", + "no_schema, qe", + "csfle, csfle2", + "qe, qe2", + "no_schema, no_schema2"}) + void testCase1Through7(final String from, final String to) { + assumeTrue(serverVersionAtLeast(8, 1)); + String mql = ("[\n" + + " {\"$match\" : {\"\" : \"\"}},\n" + + " {\n" + + " \"$lookup\" : {\n" + + " \"from\" : \"\",\n" + + " \"as\" : \"matched\",\n" + + " \"pipeline\" : [ {\"$match\" : {\"\" : \"\"}}, {\"$project\" : {\"_id\" : 0, \"__safeContent__\" : 0}} ]\n" + + " }\n" + + " },\n" + + " {\"$project\" : {\"_id\" : 0, \"__safeContent__\" : 0}}\n" + + "]").replace("", from).replace("", to); + + List pipeline = BsonArray.parse(mql).stream() + .map(stage -> stage.asDocument()) + .collect(Collectors.toList()); + assertEquals( + Document.parse("{\"\" : \"\", \"matched\" : [ {\"\" : \"\"} ]}" + .replace("", from).replace("", to)), + client.getDatabase("db").getCollection(from).aggregate(pipeline).first()); + } + + @Test + void testCase8() { + assumeTrue(serverVersionAtLeast(8, 1)); + List pipeline = BsonArray.parse("[\n" + + " {\"$match\" : {\"csfle\" : \"qe\"}},\n" + + " {\n" + + " \"$lookup\" : {\n" + + " \"from\" : \"qe\",\n" + + " \"as\" : \"matched\",\n" + + " \"pipeline\" : [ {\"$match\" : {\"qe\" : \"qe\"}}, {\"$project\" : {\"_id\" : 0}} ]\n" + + " }\n" + + " },\n" + + " {\"$project\" : {\"_id\" : 0}}\n" + + "]").stream().map(stage -> stage.asDocument()).collect(Collectors.toList()); + + assertCause( + MongoCryptException.class, + "not supported", + () -> client.getDatabase("db").getCollection("csfle").aggregate(pipeline).first()); + } + + @Test + void testCase9() { + assumeTrue(serverVersionLessThan(8, 1)); + List pipeline = BsonArray.parse("[\n" + + " {\"$match\" : {\"csfle\" : \"csfle\"}},\n" + + " {\n" + + " \"$lookup\" : {\n" + + " \"from\" : \"no_schema\",\n" + + " \"as\" : \"matched\",\n" + + " \"pipeline\" : [ {\"$match\" : {\"no_schema\" : \"no_schema\"}}, {\"$project\" : {\"_id\" : 0}} ]\n" + + " }\n" + + " },\n" + + " {\"$project\" : {\"_id\" : 0}}\n" + + "]").stream().map(stage -> stage.asDocument()).collect(Collectors.toList()); + assertCause( + RuntimeException.class, + "Upgrade", + () -> client.getDatabase("db").getCollection("csfle").aggregate(pipeline).first()); + } + + public static BsonDocument bsonDocumentFromPath(final String path) { + try { + return getTestDocument(new File(ClientSideEncryption25LookupProseTests.class + .getResource("/client-side-encryption-data/lookup/" + path).toURI())); + } catch (Exception e) { + fail("Unable to load resource", e); + return null; + } + } +} diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 85722fade52..b6a23a576ce 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -39,8 +39,6 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.opentest4j.AssertionFailedError; import java.io.IOException; import java.lang.reflect.Field; @@ -70,6 +68,7 @@ import static com.mongodb.MongoCredential.OidcCallbackResult; import static com.mongodb.MongoCredential.TOKEN_RESOURCE_KEY; import static com.mongodb.assertions.Assertions.assertNotNull; +import static com.mongodb.testing.MongoAssertions.assertCause; import static java.lang.System.getenv; import static java.util.Arrays.asList; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -923,20 +922,6 @@ private void performFind(final MongoClient mongoClient) { .first(); } - private static void assertCause( - final Class expectedCause, final String expectedMessageFragment, final Executable e) { - Throwable cause = assertThrows(Throwable.class, e); - while (cause.getCause() != null) { - cause = cause.getCause(); - } - if (!cause.getMessage().contains(expectedMessageFragment)) { - throw new AssertionFailedError("Unexpected message: " + cause.getMessage(), cause); - } - if (!expectedCause.isInstance(cause)) { - throw new AssertionFailedError("Unexpected cause: " + cause.getClass(), assertThrows(Throwable.class, e)); - } - } - protected void delayNextFind() { try (MongoClient client = createMongoClient(Fixture.getMongoClientSettings())) { diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts index 6c07a315185..72d7fd47292 100644 --- a/mongodb-crypt/build.gradle.kts +++ b/mongodb-crypt/build.gradle.kts @@ -60,7 +60,7 @@ val jnaLibsPath: String = System.getProperty("jnaLibsPath", "${jnaResourcesDir}$ val jnaResources: String = System.getProperty("jna.library.path", jnaLibsPath) // Download jnaLibs that match the git tag or revision to jnaResourcesBuildDir -val downloadRevision = "9a88ac5698e8e3ffcd6580b98c247f0126f26c40" // r1.11.0 +val downloadRevision = "1.13.0" val binariesArchiveName = "libmongocrypt-java.tar.gz" /** diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/CAPI.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/CAPI.java index b8e2cacc677..075bbe15c9c 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/CAPI.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/CAPI.java @@ -486,6 +486,14 @@ public interface mongocrypt_random_fn extends Callback { public static native void mongocrypt_setopt_bypass_query_analysis (mongocrypt_t crypt); + /** + * Opt-into enabling sending multiple collection info documents. + * + * @param crypt The @ref mongocrypt_t object to update + */ + public static native void + mongocrypt_setopt_enable_multiple_collinfo (mongocrypt_t crypt); + /** * Set the contention factor used for explicit encryption. * The contention factor is only used for indexed Queryable Encryption. diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptImpl.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptImpl.java index 37f2263da69..d365f7f7671 100644 --- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptImpl.java +++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoCryptImpl.java @@ -65,6 +65,7 @@ import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_bypass_query_analysis; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_crypto_hook_sign_rsaes_pkcs1_v1_5; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_crypto_hooks; +import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_enable_multiple_collinfo; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_encrypted_field_config_map; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_kms_provider_aws; import static com.mongodb.internal.crypt.capi.CAPI.mongocrypt_setopt_kms_provider_local; @@ -120,6 +121,8 @@ class MongoCryptImpl implements MongoCrypt { logCallback = new LogCallback(); + mongocrypt_setopt_enable_multiple_collinfo(wrapped); + configure(() -> mongocrypt_setopt_log_handler(wrapped, logCallback, null)); if (mongocrypt_is_crypto_available()) { From 0ca4d66ad11ef55ac563fc8caafd0d1fe5444d07 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov <10205+katcharov@users.noreply.github.com> Date: Thu, 27 Feb 2025 21:11:41 +0000 Subject: [PATCH 31/43] Version: bump 5.4.0-alpha0 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 4715dfb27ff..a516020089e 100644 --- a/build.gradle +++ b/build.gradle @@ -77,7 +77,7 @@ configure(coreProjects) { apply plugin: 'idea' group = 'org.mongodb' - version = '5.4.0-SNAPSHOT' + version = '5.4.0-alpha0' repositories { mavenLocal() From d29c080f7b77b2b46e208cae4b4d7204b93623d2 Mon Sep 17 00:00:00 2001 From: Maxim Katcharov <10205+katcharov@users.noreply.github.com> Date: Thu, 27 Feb 2025 21:11:41 +0000 Subject: [PATCH 32/43] Version: bump 5.4.0-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index a516020089e..4715dfb27ff 100644 --- a/build.gradle +++ b/build.gradle @@ -77,7 +77,7 @@ configure(coreProjects) { apply plugin: 'idea' group = 'org.mongodb' - version = '5.4.0-alpha0' + version = '5.4.0-SNAPSHOT' repositories { mavenLocal() From b0e19bcd1e3407a17c6f16cc3f6e7c5110abed6e Mon Sep 17 00:00:00 2001 From: Jeff Yemin Date: Tue, 4 Mar 2025 07:36:26 -0500 Subject: [PATCH 33/43] Update AWS SDK dependencies to latest (#1639) JAVA-5802 --- build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 4715dfb27ff..b979842f50c 100644 --- a/build.gradle +++ b/build.gradle @@ -53,8 +53,8 @@ ext { nettyVersion = '4.1.87.Final' snappyVersion = '1.1.10.3' zstdVersion = '1.5.5-3' - awsSdkV2Version = '2.18.9' - awsSdkV1Version = '1.12.337' + awsSdkV2Version = '2.30.31' + awsSdkV1Version = '1.12.782' projectReactorVersion = '2022.0.0' junitBomVersion = '5.10.2' logbackVersion = '1.3.14' From 5ed3c319110e4e97daf0d24fce346b6089ce978a Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin Date: Tue, 4 Mar 2025 13:19:05 -0800 Subject: [PATCH 34/43] Optimize BSON codec lookup. (#1632) - Use `BsonTypeCodecMap` for decoding and encoding in `BsonArray` to enable faster access. - Optimize `BsonTypeClassMap` by replacing `Map` with a plain array to eliminate redundant hash computations. JAVA-5339 --------- Co-authored-by: Ross Lawley --- .../main/org/bson/codecs/BsonArrayCodec.java | 19 ++-- .../org/bson/codecs/BsonDocumentCodec.java | 13 +-- .../org/bson/codecs/BsonTypeClassMap.java | 61 +++++------- .../org/bson/codecs/BsonTypeCodecMap.java | 2 +- .../main/org/bson/codecs/DocumentCodec.java | 2 +- .../IterableCodecProviderSpecification.groovy | 3 +- driver-benchmarks/build.gradle | 10 ++ .../jmh/codec/BsonArrayCodecBenchmark.java | 99 +++++++++++++++++++ .../jmh/codec/BsonDocumentBenchmark.java | 86 ++++++++++++++++ .../benchmark/jmh/codec/BsonUtils.java | 46 +++++++++ .../benchmark/jmh/codec/package-info.java | 27 +++++ 11 files changed, 314 insertions(+), 54 deletions(-) create mode 100644 driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonArrayCodecBenchmark.java create mode 100644 driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonDocumentBenchmark.java create mode 100644 driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/BsonUtils.java create mode 100644 driver-benchmarks/src/main/com/mongodb/benchmark/jmh/codec/package-info.java diff --git a/bson/src/main/org/bson/codecs/BsonArrayCodec.java b/bson/src/main/org/bson/codecs/BsonArrayCodec.java index 9b4bef5e4c5..6d16bb7d1b0 100644 --- a/bson/src/main/org/bson/codecs/BsonArrayCodec.java +++ b/bson/src/main/org/bson/codecs/BsonArrayCodec.java @@ -24,6 +24,7 @@ import org.bson.codecs.configuration.CodecRegistry; import static org.bson.assertions.Assertions.notNull; +import static org.bson.codecs.BsonValueCodecProvider.getBsonTypeClassMap; import static org.bson.codecs.configuration.CodecRegistries.fromProviders; /** @@ -34,8 +35,8 @@ public class BsonArrayCodec implements Codec { private static final CodecRegistry DEFAULT_REGISTRY = fromProviders(new BsonValueCodecProvider()); - - private final CodecRegistry codecRegistry; + private static final BsonTypeCodecMap DEFAULT_BSON_TYPE_CODEC_MAP = new BsonTypeCodecMap(getBsonTypeClassMap(), DEFAULT_REGISTRY); + private final BsonTypeCodecMap bsonTypeCodecMap; /** * Creates a new instance with a default codec registry that uses the {@link BsonValueCodecProvider}. @@ -43,7 +44,7 @@ public class BsonArrayCodec implements Codec { * @since 3.4 */ public BsonArrayCodec() { - this(DEFAULT_REGISTRY); + this(DEFAULT_BSON_TYPE_CODEC_MAP); } /** @@ -52,7 +53,11 @@ public BsonArrayCodec() { * @param codecRegistry the codec registry */ public BsonArrayCodec(final CodecRegistry codecRegistry) { - this.codecRegistry = notNull("codecRegistry", codecRegistry); + this(new BsonTypeCodecMap(getBsonTypeClassMap(), codecRegistry)); + } + + private BsonArrayCodec(final BsonTypeCodecMap bsonTypeCodecMap) { + this.bsonTypeCodecMap = notNull("bsonTypeCodecMap", bsonTypeCodecMap); } @Override @@ -72,7 +77,7 @@ public void encode(final BsonWriter writer, final BsonArray array, final Encoder writer.writeStartArray(); for (BsonValue value : array) { - Codec codec = codecRegistry.get(value.getClass()); + Codec codec = bsonTypeCodecMap.get(value.getBsonType()); encoderContext.encodeWithChildContext(codec, writer, value); } @@ -93,7 +98,7 @@ public Class getEncoderClass() { * @return the non-null value read from the reader */ protected BsonValue readValue(final BsonReader reader, final DecoderContext decoderContext) { - return codecRegistry.get(BsonValueCodecProvider.getClassForBsonType(reader.getCurrentBsonType())).decode(reader, decoderContext); + BsonType currentBsonType = reader.getCurrentBsonType(); + return (BsonValue) bsonTypeCodecMap.get(currentBsonType).decode(reader, decoderContext); } - } diff --git a/bson/src/main/org/bson/codecs/BsonDocumentCodec.java b/bson/src/main/org/bson/codecs/BsonDocumentCodec.java index 405fd78e117..75bd3b7a2b0 100644 --- a/bson/src/main/org/bson/codecs/BsonDocumentCodec.java +++ b/bson/src/main/org/bson/codecs/BsonDocumentCodec.java @@ -17,7 +17,6 @@ package org.bson.codecs; import org.bson.BsonDocument; -import org.bson.BsonElement; import org.bson.BsonObjectId; import org.bson.BsonReader; import org.bson.BsonType; @@ -26,8 +25,6 @@ import org.bson.codecs.configuration.CodecRegistry; import org.bson.types.ObjectId; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import static org.bson.assertions.Assertions.notNull; @@ -79,17 +76,15 @@ public CodecRegistry getCodecRegistry() { @Override public BsonDocument decode(final BsonReader reader, final DecoderContext decoderContext) { - List keyValuePairs = new ArrayList<>(); - + BsonDocument bsonDocument = new BsonDocument(); reader.readStartDocument(); while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) { String fieldName = reader.readName(); - keyValuePairs.add(new BsonElement(fieldName, readValue(reader, decoderContext))); + bsonDocument.append(fieldName, readValue(reader, decoderContext)); } reader.readEndDocument(); - - return new BsonDocument(keyValuePairs); + return bsonDocument; } /** @@ -135,7 +130,7 @@ private boolean skipField(final EncoderContext encoderContext, final String key) @SuppressWarnings({"unchecked", "rawtypes"}) private void writeValue(final BsonWriter writer, final EncoderContext encoderContext, final BsonValue value) { - Codec codec = codecRegistry.get(value.getClass()); + Codec codec = bsonTypeCodecMap.get(value.getBsonType()); encoderContext.encodeWithChildContext(codec, writer, value); } diff --git a/bson/src/main/org/bson/codecs/BsonTypeClassMap.java b/bson/src/main/org/bson/codecs/BsonTypeClassMap.java index 82144e9b4aa..32acaeb7f85 100644 --- a/bson/src/main/org/bson/codecs/BsonTypeClassMap.java +++ b/bson/src/main/org/bson/codecs/BsonTypeClassMap.java @@ -31,12 +31,11 @@ import org.bson.types.ObjectId; import org.bson.types.Symbol; +import java.util.Arrays; import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; /** *