diff --git a/.gitignore b/.gitignore
index 02e9bdf0e21..5581f51dc17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,3 +68,4 @@ driver-benchmarks/.factorypath
# bin build directories
**/bin
+docs/superpowers/
diff --git a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java
index f509f8b3ea3..e39deea3eec 100644
--- a/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java
+++ b/driver-core/src/main/com/mongodb/client/model/vault/EncryptOptions.java
@@ -34,6 +34,7 @@ public class EncryptOptions {
private String queryType;
private RangeOptions rangeOptions;
private TextOptions textOptions;
+ private StringOptions stringOptions;
/**
* Construct an instance with the given algorithm.
@@ -54,12 +55,12 @@ public EncryptOptions(final String algorithm) {
*
Indexed
* Unindexed
* Range
- * TextPreview
+ * String
*
*
- * The "TextPreview" algorithm is in preview and should be used for experimental workloads only.
- * These features are unstable and their security is not guaranteed until released as Generally Available (GA).
- * The GA version of these features may not be backwards compatible with the preview version.
+ * The "String" algorithm supports Queryable Encryption prefix, suffix, and substring string queries.
+ * Use the "String" algorithm with query types "prefix"/"suffix"/"substring" (server 9.0+) or the deprecated
+ * aliases "prefixPreview"/"suffixPreview"/"substringPreview" (server 8.2 to pre-9.0).
*
* @return the encryption algorithm
*/
@@ -149,8 +150,12 @@ public Long getContentionFactor() {
/**
* The QueryType.
*
- * Currently, we support only "equality", "range", "prefixPreview", "suffixPreview" or "substringPreview" queryType.
- * It is an error to set queryType when the algorithm is not "Indexed", "Range" or "TextPreview".
+ * Currently, we support only "equality", "range", "prefix", "suffix", "substring", "prefixPreview",
+ * "suffixPreview" or "substringPreview" queryType.
+ * The "prefix", "suffix", "substring", "prefixPreview", "suffixPreview" and "substringPreview" query types are
+ * only valid with the "String" algorithm. "prefixPreview"/"suffixPreview"/"substringPreview" are deprecated
+ * aliases supported for servers 8.2 to pre-9.0; use "prefix"/"suffix"/"substring" on server 9.0+.
+ * It is an error to set queryType when the algorithm is not "Indexed", "Range" or "String".
* @param queryType the query type
* @return this
* @since 4.7
@@ -164,7 +169,7 @@ public EncryptOptions queryType(@Nullable final String queryType) {
/**
* Gets the QueryType.
*
- * Currently, we support only "equality" or "range" queryType.
+ * See {@link #queryType(String)} for the supported query types.
* @see #queryType(String)
* @return the queryType or null
* @since 4.7
@@ -205,13 +210,15 @@ public RangeOptions getRangeOptions() {
/**
* The TextOptions
*
- * It is an error to set TextOptions when the algorithm is not "TextPreview".
+ *
It is an error to set TextOptions when the algorithm is not "String".
* @param textOptions the text options
* @return this
* @since 5.6
* @mongodb.server.release 8.2
* @mongodb.driver.manual /core/queryable-encryption/ queryable encryption
+ * @deprecated Use {@link #stringOptions(StringOptions)} instead.
*/
+ @Deprecated
@Alpha(Reason.SERVER)
public EncryptOptions textOptions(@Nullable final TextOptions textOptions) {
this.textOptions = textOptions;
@@ -225,13 +232,45 @@ public EncryptOptions textOptions(@Nullable final TextOptions textOptions) {
* @since 5.6
* @mongodb.server.release 8.2
* @mongodb.driver.manual /core/queryable-encryption/ queryable encryption
+ * @deprecated Use {@link #getStringOptions()} instead.
*/
+ @Deprecated
@Alpha(Reason.SERVER)
@Nullable
public TextOptions getTextOptions() {
return textOptions;
}
+ /**
+ * The StringOptions
+ *
+ *
It is an error to set StringOptions when the algorithm is not "String".
+ * @param stringOptions the string options
+ * @return this
+ * @since 5.9
+ * @mongodb.server.release 8.2
+ * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption
+ */
+ @Alpha(Reason.SERVER)
+ public EncryptOptions stringOptions(@Nullable final StringOptions stringOptions) {
+ this.stringOptions = stringOptions;
+ return this;
+ }
+
+ /**
+ * Gets the StringOptions
+ * @see #stringOptions(StringOptions)
+ * @return the string options or null if not set
+ * @since 5.9
+ * @mongodb.server.release 8.2
+ * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption
+ */
+ @Alpha(Reason.SERVER)
+ @Nullable
+ public StringOptions getStringOptions() {
+ return stringOptions;
+ }
+
@Override
public String toString() {
return "EncryptOptions{"
@@ -241,6 +280,8 @@ public String toString() {
+ ", contentionFactor=" + contentionFactor
+ ", queryType='" + queryType + '\''
+ ", rangeOptions=" + rangeOptions
+ + ", textOptions=" + textOptions
+ + ", stringOptions=" + stringOptions
+ '}';
}
}
diff --git a/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java
new file mode 100644
index 00000000000..98795335082
--- /dev/null
+++ b/driver-core/src/main/com/mongodb/client/model/vault/StringOptions.java
@@ -0,0 +1,186 @@
+/*
+ * 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.vault;
+
+import com.mongodb.annotations.Alpha;
+import com.mongodb.annotations.Reason;
+import com.mongodb.lang.Nullable;
+import org.bson.BsonDocument;
+
+/**
+ * String options for a Queryable Encryption field that supports string queries (prefix, suffix, and substring).
+ *
+ *
Note: StringOptions is in Alpha and subject to backwards breaking changes.
+ *
+ * @since 5.9
+ * @mongodb.server.release 8.2
+ * @mongodb.driver.manual /core/queryable-encryption/ queryable encryption
+ */
+@Alpha(Reason.SERVER)
+public class StringOptions {
+ private boolean caseSensitive;
+ private boolean diacriticSensitive;
+ @Nullable
+ private BsonDocument prefixOptions;
+ @Nullable
+ private BsonDocument suffixOptions;
+ @Nullable
+ private BsonDocument substringOptions;
+
+ /**
+ * Construct a new instance
+ */
+ public StringOptions() {
+ }
+
+ /**
+ * @return true if string indexes for this field are case sensitive.
+ */
+ public boolean getCaseSensitive() {
+ return caseSensitive;
+ }
+
+ /**
+ * Set case sensitivity
+ *
+ * @param caseSensitive true if string indexes are case sensitive
+ * @return this
+ */
+ public StringOptions caseSensitive(final boolean caseSensitive) {
+ this.caseSensitive = caseSensitive;
+ return this;
+ }
+
+ /**
+ * @return true if string indexes are diacritic sensitive
+ */
+ public boolean getDiacriticSensitive() {
+ return diacriticSensitive;
+ }
+
+ /**
+ * Set diacritic sensitivity
+ *
+ * @param diacriticSensitive true if string indexes are diacritic sensitive
+ * @return this
+ */
+ public StringOptions diacriticSensitive(final boolean diacriticSensitive) {
+ this.diacriticSensitive = diacriticSensitive;
+ return this;
+ }
+
+ /**
+ * Set the prefix options.
+ *
+ *
Expected to be a {@link BsonDocument} in the format of:
+ *
+ *
+ * {@code
+ * {
+ * // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
+ * strMinQueryLength: BsonInt32,
+ * // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
+ * strMaxQueryLength: BsonInt32
+ * }
+ * }
+ *
+ *
+ * @param prefixOptions the prefix options or null
+ * @return this
+ */
+ public StringOptions prefixOptions(@Nullable final BsonDocument prefixOptions) {
+ this.prefixOptions = prefixOptions;
+ return this;
+ }
+
+ /**
+ * @see #prefixOptions(BsonDocument)
+ * @return the prefix options document or null
+ */
+ @Nullable
+ public BsonDocument getPrefixOptions() {
+ return prefixOptions;
+ }
+
+ /**
+ * Set the suffix options.
+ *
+ * Expected to be a {@link BsonDocument} in the format of:
+ *
+ *
+ * {@code
+ * {
+ * // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
+ * strMinQueryLength: BsonInt32,
+ * // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
+ * strMaxQueryLength: BsonInt32
+ * }
+ * }
+ *
+ *
+ * @param suffixOptions the suffix options or null
+ * @return this
+ */
+ public StringOptions suffixOptions(@Nullable final BsonDocument suffixOptions) {
+ this.suffixOptions = suffixOptions;
+ return this;
+ }
+
+ /**
+ * @see #suffixOptions(BsonDocument)
+ * @return the suffix options document or null
+ */
+ @Nullable
+ public BsonDocument getSuffixOptions() {
+ return suffixOptions;
+ }
+
+ /**
+ * Set the substring options.
+ *
+ * Expected to be a {@link BsonDocument} in the format of:
+ *
+ *
+ * {@code
+ * {
+ * // strMaxLength is the maximum allowed length to insert. Inserting longer strings will error.
+ * strMaxLength: BsonInt32,
+ * // strMinQueryLength is the minimum allowed query length. Querying with a shorter string will error.
+ * strMinQueryLength: BsonInt32,
+ * // strMaxQueryLength is the maximum allowed query length. Querying with a longer string will error.
+ * strMaxQueryLength: BsonInt32
+ * }
+ * }
+ *
+ *
+ * @param substringOptions the substring options or null
+ * @return this
+ */
+ public StringOptions substringOptions(@Nullable final BsonDocument substringOptions) {
+ this.substringOptions = substringOptions;
+ return this;
+ }
+
+ /**
+ * @see #substringOptions(BsonDocument)
+ * @return the substring options document or null
+ */
+ @Nullable
+ public BsonDocument getSubstringOptions() {
+ return substringOptions;
+ }
+}
diff --git a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java
index 34dcd0d806d..a14ca54378e 100644
--- a/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java
+++ b/driver-core/src/main/com/mongodb/client/model/vault/TextOptions.java
@@ -29,11 +29,13 @@
* @since 5.6
* @mongodb.server.release 8.2
* @mongodb.driver.manual /core/queryable-encryption/ queryable encryption
+ * @deprecated Use {@link StringOptions} instead.
*/
+@Deprecated
@Alpha(Reason.SERVER)
public class TextOptions {
- private Boolean caseSensitive;
- private Boolean diacriticSensitive;
+ private boolean caseSensitive;
+ private boolean diacriticSensitive;
@Nullable
private BsonDocument prefixOptions;
@Nullable
diff --git a/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java b/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java
index 2b472668d98..7a8dab950f4 100644
--- a/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java
+++ b/driver-core/src/main/com/mongodb/internal/client/vault/EncryptOptionsHelper.java
@@ -17,8 +17,10 @@
import com.mongodb.client.model.vault.EncryptOptions;
import com.mongodb.client.model.vault.RangeOptions;
+import com.mongodb.client.model.vault.StringOptions;
import com.mongodb.client.model.vault.TextOptions;
import com.mongodb.internal.crypt.capi.MongoExplicitEncryptOptions;
+import com.mongodb.lang.Nullable;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
@@ -73,31 +75,51 @@ public static MongoExplicitEncryptOptions asMongoExplicitEncryptOptions(final En
encryptOptionsBuilder.rangeOptions(rangeOptionsBsonDocument);
}
- TextOptions textOptions = options.getTextOptions();
- if (textOptions != null) {
- BsonDocument textOptionsDocument = new BsonDocument();
- textOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(textOptions.getCaseSensitive()));
- textOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(textOptions.getDiacriticSensitive()));
+ StringOptions stringOptions = resolveStringOptions(options);
+ if (stringOptions != null) {
+ BsonDocument stringOptionsDocument = new BsonDocument();
+ stringOptionsDocument.put("caseSensitive", BsonBoolean.valueOf(stringOptions.getCaseSensitive()));
+ stringOptionsDocument.put("diacriticSensitive", BsonBoolean.valueOf(stringOptions.getDiacriticSensitive()));
- BsonDocument substringOptions = textOptions.getSubstringOptions();
+ BsonDocument substringOptions = stringOptions.getSubstringOptions();
if (substringOptions != null) {
- textOptionsDocument.put("substring", substringOptions);
+ stringOptionsDocument.put("substring", substringOptions);
}
- BsonDocument prefixOptions = textOptions.getPrefixOptions();
+ BsonDocument prefixOptions = stringOptions.getPrefixOptions();
if (prefixOptions != null) {
- textOptionsDocument.put("prefix", prefixOptions);
+ stringOptionsDocument.put("prefix", prefixOptions);
}
- BsonDocument suffixOptions = textOptions.getSuffixOptions();
+ BsonDocument suffixOptions = stringOptions.getSuffixOptions();
if (suffixOptions != null) {
- textOptionsDocument.put("suffix", suffixOptions);
+ stringOptionsDocument.put("suffix", suffixOptions);
}
- encryptOptionsBuilder.textOptions(textOptionsDocument);
+ encryptOptionsBuilder.textOptions(stringOptionsDocument);
}
return encryptOptionsBuilder.build();
}
+
+ @SuppressWarnings("deprecation")
+ @Nullable
+ private static StringOptions resolveStringOptions(final EncryptOptions options) {
+ StringOptions stringOptions = options.getStringOptions();
+ if (stringOptions != null) {
+ return stringOptions;
+ }
+ TextOptions textOptions = options.getTextOptions();
+ if (textOptions == null) {
+ return null;
+ }
+ return new StringOptions()
+ .caseSensitive(textOptions.getCaseSensitive())
+ .diacriticSensitive(textOptions.getDiacriticSensitive())
+ .prefixOptions(textOptions.getPrefixOptions())
+ .suffixOptions(textOptions.getSuffixOptions())
+ .substringOptions(textOptions.getSubstringOptions());
+ }
+
private EncryptOptionsHelper() {
}
}
diff --git a/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java
new file mode 100644
index 00000000000..2859d728e99
--- /dev/null
+++ b/driver-core/src/test/unit/com/mongodb/client/model/vault/EncryptOptionsTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.vault;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class EncryptOptionsTest {
+
+ @Test
+ void shouldStoreStringOptions() {
+ StringOptions stringOptions = new StringOptions().caseSensitive(true);
+ EncryptOptions options = new EncryptOptions("String").stringOptions(stringOptions);
+ assertSame(stringOptions, options.getStringOptions());
+ assertNull(options.getTextOptions());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void shouldStoreDeprecatedTextOptionsIndependently() {
+ TextOptions textOptions = new TextOptions().caseSensitive(true);
+ EncryptOptions options = new EncryptOptions("String").textOptions(textOptions);
+ assertSame(textOptions, options.getTextOptions());
+ assertNull(options.getStringOptions());
+ }
+
+ @Test
+ void toStringShouldIncludeStringOptions() {
+ EncryptOptions options = new EncryptOptions("String").stringOptions(new StringOptions());
+ assertTrue(options.toString().contains("stringOptions="));
+ }
+}
diff --git a/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java b/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java
new file mode 100644
index 00000000000..04f20239423
--- /dev/null
+++ b/driver-core/src/test/unit/com/mongodb/client/model/vault/StringOptionsTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.vault;
+
+import org.bson.BsonDocument;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class StringOptionsTest {
+
+ @Test
+ void shouldRoundTripAllProperties() {
+ BsonDocument prefix = BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}");
+ BsonDocument suffix = BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}");
+ BsonDocument substring = BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}");
+
+ StringOptions options = new StringOptions()
+ .caseSensitive(true)
+ .diacriticSensitive(true)
+ .prefixOptions(prefix)
+ .suffixOptions(suffix)
+ .substringOptions(substring);
+
+ assertTrue(options.getCaseSensitive());
+ assertTrue(options.getDiacriticSensitive());
+ assertEquals(prefix, options.getPrefixOptions());
+ assertEquals(suffix, options.getSuffixOptions());
+ assertEquals(substring, options.getSubstringOptions());
+ }
+
+ @Test
+ void shouldDefaultOptionDocumentsToNull() {
+ StringOptions options = new StringOptions();
+ assertNull(options.getPrefixOptions());
+ assertNull(options.getSuffixOptions());
+ assertNull(options.getSubstringOptions());
+ }
+
+ @Test
+ void shouldDefaultBooleanFlagsToFalse() {
+ StringOptions options = new StringOptions();
+ assertFalse(options.getCaseSensitive());
+ assertFalse(options.getDiacriticSensitive());
+ }
+}
diff --git a/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java b/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java
new file mode 100644
index 00000000000..65815e668a7
--- /dev/null
+++ b/driver-core/src/test/unit/com/mongodb/internal/client/vault/EncryptOptionsHelperTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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.client.vault;
+
+import com.mongodb.client.model.vault.EncryptOptions;
+import com.mongodb.client.model.vault.StringOptions;
+import com.mongodb.client.model.vault.TextOptions;
+import com.mongodb.internal.crypt.capi.MongoExplicitEncryptOptions;
+import org.bson.BsonDocument;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class EncryptOptionsHelperTest {
+
+ @Test
+ void shouldMapStringOptionsToTextOptionsDocument() {
+ EncryptOptions options = new EncryptOptions("String").stringOptions(new StringOptions()
+ .caseSensitive(true)
+ .diacriticSensitive(false)
+ .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")));
+
+ MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options);
+
+ assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: false, "
+ + "prefix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"),
+ result.getTextOptions());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void shouldFallBackToDeprecatedTextOptions() {
+ EncryptOptions options = new EncryptOptions("String").textOptions(new TextOptions()
+ .caseSensitive(true)
+ .diacriticSensitive(true)
+ .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")));
+
+ MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options);
+
+ assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: true, "
+ + "suffix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"),
+ result.getTextOptions());
+ }
+
+ @Test
+ void shouldLeaveTextOptionsNullWhenNeitherSet() {
+ MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(
+ new EncryptOptions("Indexed"));
+ assertNull(result.getTextOptions());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void shouldPreferStringOptionsOverDeprecatedTextOptionsWhenBothSet() {
+ EncryptOptions options = new EncryptOptions("String")
+ .stringOptions(new StringOptions()
+ .caseSensitive(true)
+ .diacriticSensitive(true)
+ .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")))
+ .textOptions(new TextOptions()
+ .caseSensitive(false)
+ .diacriticSensitive(false)
+ .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 5, strMinQueryLength: 1}")));
+
+ MongoExplicitEncryptOptions result = EncryptOptionsHelper.asMongoExplicitEncryptOptions(options);
+
+ // stringOptions wins: prefix present (from stringOptions), no suffix (from the ignored textOptions),
+ // and caseSensitive/diacriticSensitive reflect stringOptions (true), not textOptions (false).
+ assertEquals(BsonDocument.parse("{caseSensitive: true, diacriticSensitive: true, "
+ + "prefix: {strMaxQueryLength: 10, strMinQueryLength: 2}}"),
+ result.getTextOptions());
+ }
+}
diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionStringExplicitEncryptionTest.java
similarity index 87%
rename from driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java
rename to driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionStringExplicitEncryptionTest.java
index 2c7eda55aae..d53f4b2c331 100644
--- a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionTextExplicitEncryptionTest.java
+++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/ClientEncryptionStringExplicitEncryptionTest.java
@@ -18,14 +18,14 @@
import com.mongodb.ClientEncryptionSettings;
import com.mongodb.MongoClientSettings;
-import com.mongodb.client.AbstractClientEncryptionTextExplicitEncryptionTest;
+import com.mongodb.client.AbstractClientEncryptionStringExplicitEncryptionTest;
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 ClientEncryptionTextExplicitEncryptionTest extends AbstractClientEncryptionTextExplicitEncryptionTest {
+public class ClientEncryptionStringExplicitEncryptionTest extends AbstractClientEncryptionStringExplicitEncryptionTest {
@Override
protected MongoClient createMongoClient(final MongoClientSettings settings) {
return new SyncMongoClient(settings);
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java
new file mode 100644
index 00000000000..011cf8325fe
--- /dev/null
+++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionStringExplicitEncryptionTest.java
@@ -0,0 +1,413 @@
+/*
+ * 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.MongoException;
+import com.mongodb.MongoNamespace;
+import com.mongodb.WriteConcern;
+import com.mongodb.client.model.CreateCollectionOptions;
+import com.mongodb.client.model.DropCollectionOptions;
+import com.mongodb.client.model.vault.EncryptOptions;
+import com.mongodb.client.model.vault.StringOptions;
+import com.mongodb.client.vault.ClientEncryption;
+import com.mongodb.connection.ServerVersion;
+import com.mongodb.fixture.EncryptionFixture;
+import org.bson.BsonBinary;
+import org.bson.BsonDocument;
+import org.bson.BsonString;
+import org.bson.Document;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Map;
+
+import static com.mongodb.ClusterFixture.getDefaultDatabaseName;
+import static com.mongodb.ClusterFixture.getMongoCryptVersion;
+import static com.mongodb.ClusterFixture.hasEncryptionTestsEnabled;
+import static com.mongodb.ClusterFixture.isStandalone;
+import static com.mongodb.ClusterFixture.serverVersionAtLeast;
+import static com.mongodb.client.Fixture.getDefaultDatabase;
+import static com.mongodb.client.Fixture.getMongoClient;
+import static com.mongodb.client.Fixture.getMongoClientSettings;
+import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder;
+import static com.mongodb.fixture.EncryptionFixture.getKmsProviders;
+import static java.util.Arrays.asList;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+import static util.JsonPoweredTestHelper.getTestDocument;
+
+public abstract class AbstractClientEncryptionStringExplicitEncryptionTest {
+
+ private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 19, 1));
+ // GA "substring" (server 9.0+) additionally needs libmongocrypt 1.20+; the class-level 1.19.1 minimum below
+ // already covers prefix/suffix GA and the pre-9.0 preview query types.
+ private static final ServerVersion SUBSTRING_GA_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 20, 0));
+ private boolean gaSupported;
+ private boolean substringGaSupported;
+ private MongoClient explicitEncryptedClient;
+ private MongoClient autoEncryptedClient;
+ private MongoDatabase explicitEncryptedDatabase;
+ private MongoDatabase autoEncryptedDatabase;
+ private ClientEncryption clientEncryption;
+ private BsonBinary key1Id;
+
+ protected abstract MongoClient createMongoClient(MongoClientSettings settings);
+ protected abstract ClientEncryption createClientEncryption(ClientEncryptionSettings settings);
+
+ @BeforeEach
+ public void setUp() {
+ assumeTrue(hasEncryptionTestsEnabled(), "String explicit encryption tests disabled");
+ assumeTrue(getMongoCryptVersion().compareTo(REQUIRED_LIB_MONGOCRYPT_VERSION) >= 0, "Requires newer MongoCrypt version");
+ assumeTrue(serverVersionAtLeast(8, 2));
+ assumeFalse(isStandalone());
+
+ gaSupported = serverVersionAtLeast(9, 0);
+ substringGaSupported = gaSupported && getMongoCryptVersion().compareTo(SUBSTRING_GA_LIB_MONGOCRYPT_VERSION) >= 0;
+
+ MongoNamespace dataKeysNamespace = new MongoNamespace("keyvault.datakeys");
+ BsonDocument key1Document = bsonDocumentFromPath("keys/key1-document.json");
+
+ Map> kmsProviders = getKmsProviders(EncryptionFixture.KmsProviderType.LOCAL);
+
+ if (gaSupported) {
+ createEncryptedCollection("prefix-suffix", "encryptedFields-prefix-suffix.json");
+ createEncryptedCollection("prefix-suffix-ci-di", "encryptedFields-prefix-suffix-ci-di.json");
+ } else {
+ createEncryptedCollection("prefix-suffix-preview", "encryptedFields-prefix-suffix-preview.json");
+ createEncryptedCollection("substring-preview", "encryptedFields-substring-preview.json");
+ }
+ if (substringGaSupported) {
+ createEncryptedCollection("substring", "encryptedFields-substring.json");
+ createEncryptedCollection("substring-ci-di", "encryptedFields-substring-ci-di.json");
+ }
+
+ MongoCollection dataKeysCollection = getMongoClient()
+ .getDatabase(dataKeysNamespace.getDatabaseName())
+ .getCollection(dataKeysNamespace.getCollectionName(), BsonDocument.class)
+ .withWriteConcern(WriteConcern.MAJORITY);
+ dataKeysCollection.drop();
+ dataKeysCollection.insertOne(key1Document);
+ key1Id = key1Document.getBinary("_id");
+
+ clientEncryption = createClientEncryption(ClientEncryptionSettings.builder()
+ .keyVaultMongoClientSettings(getMongoClientSettings())
+ .keyVaultNamespace(dataKeysNamespace.getFullName())
+ .kmsProviders(kmsProviders)
+ .build());
+
+ explicitEncryptedClient = createMongoClient(getMongoClientSettingsBuilder()
+ .autoEncryptionSettings(AutoEncryptionSettings.builder()
+ .keyVaultNamespace(dataKeysNamespace.getFullName())
+ .kmsProviders(kmsProviders)
+ .bypassQueryAnalysis(true)
+ .build())
+ .build());
+ explicitEncryptedDatabase = explicitEncryptedClient.getDatabase(getDefaultDatabaseName())
+ .withWriteConcern(WriteConcern.MAJORITY);
+
+ autoEncryptedClient = createMongoClient(getMongoClientSettingsBuilder()
+ .autoEncryptionSettings(AutoEncryptionSettings.builder()
+ .keyVaultNamespace(dataKeysNamespace.getFullName())
+ .kmsProviders(kmsProviders)
+ .build())
+ .build());
+ autoEncryptedDatabase = autoEncryptedClient.getDatabase(getDefaultDatabaseName())
+ .withWriteConcern(WriteConcern.MAJORITY);
+
+ // Seed the prefix-suffix collection(s) with an encrypted "foobarbaz" document.
+ BsonBinary prefixSuffixSeed = clientEncryption.encrypt(new BsonString("foobarbaz"),
+ new EncryptOptions("String")
+ .keyId(key1Id)
+ .contentionFactor(0L)
+ .stringOptions(new StringOptions()
+ .caseSensitive(true)
+ .diacriticSensitive(true)
+ .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))
+ .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))));
+ if (gaSupported) {
+ explicitEncryptedDatabase.getCollection("prefix-suffix")
+ .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed));
+ } else {
+ explicitEncryptedDatabase.getCollection("prefix-suffix-preview")
+ .insertOne(new Document("_id", 0).append("encryptedText", prefixSuffixSeed));
+ }
+
+ // Seed the substring collection: GA "substring" on 9.0+/libmongocrypt 1.20+, preview on pre-9.0.
+ // Skipped on 9.0+ with libmongocrypt < 1.20, where neither path is available.
+ if (substringGaSupported || !gaSupported) {
+ BsonBinary substringSeed = clientEncryption.encrypt(new BsonString("foobarbaz"),
+ new EncryptOptions("String")
+ .keyId(key1Id)
+ .contentionFactor(0L)
+ .stringOptions(new StringOptions()
+ .caseSensitive(true)
+ .diacriticSensitive(true)
+ .substringOptions(BsonDocument.parse(
+ "{strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2}"))));
+ explicitEncryptedDatabase.getCollection(gaSupported ? "substring" : "substring-preview")
+ .insertOne(new Document("_id", 0).append("encryptedText", substringSeed));
+ }
+ }
+
+ @Test
+ @DisplayName("Case 1: can find a document by prefix")
+ public void test1CanFindADocumentByPrefix() {
+ String queryType = gaSupported ? "prefix" : "prefixPreview";
+ String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview";
+ BsonBinary encrypted = encryptForPrefix("foo", queryType, true, true);
+ Document result = explicitEncryptedDatabase.getCollection(collection)
+ .find(encStrStartsWith(encrypted)).first();
+ assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result);
+ }
+
+ @Test
+ @DisplayName("Case 2: can find a document by suffix")
+ public void test2CanFindADocumentBySuffix() {
+ String queryType = gaSupported ? "suffix" : "suffixPreview";
+ String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview";
+ BsonBinary encrypted = encryptForSuffix("baz", queryType, true, true);
+ Document result = explicitEncryptedDatabase.getCollection(collection)
+ .find(encStrEndsWith(encrypted)).first();
+ assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result);
+ }
+
+ @Test
+ @DisplayName("Case 3: assert no document found by prefix")
+ public void test3AssertNoDocumentFoundByPrefix() {
+ String queryType = gaSupported ? "prefix" : "prefixPreview";
+ String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview";
+ BsonBinary encrypted = encryptForPrefix("baz", queryType, true, true);
+ Document result = explicitEncryptedDatabase.getCollection(collection)
+ .find(encStrStartsWith(encrypted)).first();
+ assertNull(result);
+ }
+
+ @Test
+ @DisplayName("Case 4: assert no document found by suffix")
+ public void test4AssertNoDocumentFoundBySuffix() {
+ String queryType = gaSupported ? "suffix" : "suffixPreview";
+ String collection = gaSupported ? "prefix-suffix" : "prefix-suffix-preview";
+ BsonBinary encrypted = encryptForSuffix("foo", queryType, true, true);
+ Document result = explicitEncryptedDatabase.getCollection(collection)
+ .find(encStrEndsWith(encrypted)).first();
+ assertNull(result);
+ }
+
+ @Test
+ @DisplayName("Case 5: can find a document by substring")
+ public void test5CanFindADocumentBySubstring() {
+ // On 9.0+, GA substring needs libmongocrypt 1.20+; on pre-9.0 the preview path applies.
+ assumeTrue(substringGaSupported || !gaSupported);
+ String queryType = gaSupported ? "substring" : "substringPreview";
+ String collection = gaSupported ? "substring" : "substring-preview";
+ BsonBinary encrypted = encryptForSubstring("bar", queryType, true, true);
+ Document result = explicitEncryptedDatabase.getCollection(collection)
+ .find(encStrContains(encrypted)).first();
+ assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result);
+ }
+
+ @Test
+ @DisplayName("Case 6: assert no document found by substring")
+ public void test6AssertNoDocumentFoundBySubstring() {
+ // On 9.0+, GA substring needs libmongocrypt 1.20+; on pre-9.0 the preview path applies.
+ assumeTrue(substringGaSupported || !gaSupported);
+ String queryType = gaSupported ? "substring" : "substringPreview";
+ String collection = gaSupported ? "substring" : "substring-preview";
+ BsonBinary encrypted = encryptForSubstring("qux", queryType, true, true);
+ Document result = explicitEncryptedDatabase.getCollection(collection)
+ .find(encStrContains(encrypted)).first();
+ assertNull(result);
+ }
+
+ @Test
+ @DisplayName("Case 7: assert `contentionFactor` is required")
+ public void test7AssertContentionFactorIsRequired() {
+ assumeTrue(gaSupported);
+ EncryptOptions encryptOptions = new EncryptOptions("String")
+ .keyId(key1Id)
+ .queryType("prefix")
+ .stringOptions(new StringOptions()
+ .caseSensitive(true)
+ .diacriticSensitive(true)
+ .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}")));
+ MongoException exception = assertThrows(MongoException.class,
+ () -> clientEncryption.encrypt(new BsonString("foo"), encryptOptions));
+ assertTrue(exception.getMessage().contains("contention factor is required for string algorithm"));
+ }
+
+ @Test
+ @DisplayName("Case 8: can find an auto-encrypted case-insensitively indexed document by prefix and suffix")
+ public void test8AutoEncryptedCaseInsensitivePrefixAndSuffix() {
+ assumeTrue(gaSupported);
+ autoEncryptedDatabase.getCollection("prefix-suffix-ci-di")
+ .insertOne(new Document("encryptedText", "BingQiLin"));
+
+ BsonBinary prefix = encryptForPrefix("bing", "prefix", false, false);
+ Document byPrefix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di")
+ .find(encStrStartsWith(prefix)).first();
+ assertEncryptedTextEquals("BingQiLin", byPrefix);
+
+ BsonBinary suffix = encryptForSuffix("lin", "suffix", false, false);
+ Document bySuffix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di")
+ .find(encStrEndsWith(suffix)).first();
+ assertEncryptedTextEquals("BingQiLin", bySuffix);
+ }
+
+ @Test
+ @DisplayName("Case 9: can find an auto-encrypted diacritic-insensitively indexed document by prefix and suffix")
+ public void test9AutoEncryptedDiacriticInsensitivePrefixAndSuffix() {
+ assumeTrue(gaSupported);
+ autoEncryptedDatabase.getCollection("prefix-suffix-ci-di")
+ .insertOne(new Document("encryptedText", "cafébarbäz"));
+
+ BsonBinary prefix = encryptForPrefix("cafe", "prefix", false, false);
+ Document byPrefix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di")
+ .find(encStrStartsWith(prefix)).first();
+ assertEncryptedTextEquals("cafébarbäz", byPrefix);
+
+ BsonBinary suffix = encryptForSuffix("baz", "suffix", false, false);
+ Document bySuffix = explicitEncryptedDatabase.getCollection("prefix-suffix-ci-di")
+ .find(encStrEndsWith(suffix)).first();
+ assertEncryptedTextEquals("cafébarbäz", bySuffix);
+ }
+
+ @Test
+ @DisplayName("Case 10: can find an auto-encrypted case-insensitively indexed document by substring")
+ public void test10AutoEncryptedCaseInsensitiveSubstring() {
+ // GA substring auto-encryption requires server 9.0+ and libmongocrypt 1.20+, like the prefix/suffix ci-di
+ // cases 8/9. The substring-ci-di collection is created only under those conditions, so this case runs there.
+ // TODO JAVA-6244 the spec's "skip on 9.0.0+" for this case contradicts its own substring-ci-di 9.0+ setup
+ // requirement; running on 9.0+ pending upstream spec clarification (DRIVERS-3540).
+ assumeTrue(substringGaSupported);
+ autoEncryptedDatabase.getCollection("substring-ci-di")
+ .insertOne(new Document("encryptedText", "FooBarBaz"));
+
+ BsonBinary substring = encryptForSubstring("bar", "substring", false, false);
+ Document result = explicitEncryptedDatabase.getCollection("substring-ci-di")
+ .find(encStrContains(substring)).first();
+ assertEncryptedTextEquals("FooBarBaz", result);
+ }
+
+ @Test
+ @DisplayName("Case 11: can find an auto-encrypted diacritic-insensitively indexed document by substring")
+ public void test11AutoEncryptedDiacriticInsensitiveSubstring() {
+ // Runs on server 9.0+ with GA substring (libmongocrypt 1.20+), like Case 10 (see its note).
+ assumeTrue(substringGaSupported);
+ autoEncryptedDatabase.getCollection("substring-ci-di")
+ .insertOne(new Document("encryptedText", "foocafébaz"));
+
+ BsonBinary substring = encryptForSubstring("cafe", "substring", false, false);
+ Document result = explicitEncryptedDatabase.getCollection("substring-ci-di")
+ .find(encStrContains(substring)).first();
+ assertEncryptedTextEquals("foocafébaz", result);
+ }
+
+ @AfterEach
+ @SuppressWarnings("try")
+ public void cleanUp() {
+ getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY).drop();
+ try (ClientEncryption ignored = this.clientEncryption;
+ MongoClient ignored1 = this.explicitEncryptedClient;
+ MongoClient ignored2 = this.autoEncryptedClient
+ ) {
+ // just using try-with-resources to ensure they all get closed, even in the case of exceptions
+ }
+ }
+
+ private void createEncryptedCollection(final String name, final String encryptedFieldsFile) {
+ BsonDocument encryptedFields = bsonDocumentFromPath(encryptedFieldsFile);
+ MongoDatabase database = getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY);
+ database.getCollection(name).drop(new DropCollectionOptions().encryptedFields(encryptedFields));
+ database.createCollection(name, new CreateCollectionOptions().encryptedFields(encryptedFields));
+ }
+
+ private BsonBinary encryptForPrefix(final String value, final String queryType,
+ final boolean caseSensitive, final boolean diacriticSensitive) {
+ return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String")
+ .keyId(key1Id)
+ .contentionFactor(0L)
+ .queryType(queryType)
+ .stringOptions(new StringOptions()
+ .caseSensitive(caseSensitive)
+ .diacriticSensitive(diacriticSensitive)
+ .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))));
+ }
+
+ private BsonBinary encryptForSuffix(final String value, final String queryType,
+ final boolean caseSensitive, final boolean diacriticSensitive) {
+ return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String")
+ .keyId(key1Id)
+ .contentionFactor(0L)
+ .queryType(queryType)
+ .stringOptions(new StringOptions()
+ .caseSensitive(caseSensitive)
+ .diacriticSensitive(diacriticSensitive)
+ .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))));
+ }
+
+ private BsonBinary encryptForSubstring(final String value, final String queryType,
+ final boolean caseSensitive, final boolean diacriticSensitive) {
+ return clientEncryption.encrypt(new BsonString(value), new EncryptOptions("String")
+ .keyId(key1Id)
+ .contentionFactor(0L)
+ .queryType(queryType)
+ .stringOptions(new StringOptions()
+ .caseSensitive(caseSensitive)
+ .diacriticSensitive(diacriticSensitive)
+ .substringOptions(BsonDocument.parse(
+ "{strMaxLength: 10, strMaxQueryLength: 6, strMinQueryLength: 2}"))));
+ }
+
+ private static Document encStrStartsWith(final BsonBinary encrypted) {
+ return new Document("$expr", new Document("$encStrStartsWith",
+ new Document("input", "$encryptedText").append("prefix", encrypted)));
+ }
+
+ private static Document encStrEndsWith(final BsonBinary encrypted) {
+ return new Document("$expr", new Document("$encStrEndsWith",
+ new Document("input", "$encryptedText").append("suffix", encrypted)));
+ }
+
+ private static Document encStrContains(final BsonBinary encrypted) {
+ return new Document("$expr", new Document("$encStrContains",
+ new Document("input", "$encryptedText").append("substring", encrypted)));
+ }
+
+ private static void assertDocumentEquals(final Document expectedDocument, final Document actualDocument) {
+ actualDocument.remove("__safeContent__");
+ assertEquals(expectedDocument, actualDocument);
+ }
+
+ private static void assertEncryptedTextEquals(final String expectedText, final Document actualDocument) {
+ assertNotNull(actualDocument);
+ assertEquals(expectedText, actualDocument.getString("encryptedText"));
+ }
+
+ private static BsonDocument bsonDocumentFromPath(final String path) {
+ return getTestDocument("client-side-encryption/etc/data/" + path);
+ }
+}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java
deleted file mode 100644
index 1677e49a66f..00000000000
--- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientEncryptionTextExplicitEncryptionTest.java
+++ /dev/null
@@ -1,326 +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.
- */
-
-package com.mongodb.client;
-
-import com.mongodb.AutoEncryptionSettings;
-import com.mongodb.ClientEncryptionSettings;
-import com.mongodb.MongoClientSettings;
-import com.mongodb.MongoException;
-import com.mongodb.MongoNamespace;
-import com.mongodb.WriteConcern;
-import com.mongodb.client.model.CreateCollectionOptions;
-import com.mongodb.client.model.DropCollectionOptions;
-import com.mongodb.client.model.vault.EncryptOptions;
-import com.mongodb.client.model.vault.TextOptions;
-import com.mongodb.client.vault.ClientEncryption;
-import com.mongodb.connection.ServerVersion;
-import com.mongodb.fixture.EncryptionFixture;
-import org.bson.BsonBinary;
-import org.bson.BsonDocument;
-import org.bson.BsonString;
-import org.bson.Document;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-import java.util.Map;
-
-import static com.mongodb.ClusterFixture.getDefaultDatabaseName;
-import static com.mongodb.ClusterFixture.getMongoCryptVersion;
-import static com.mongodb.ClusterFixture.hasEncryptionTestsEnabled;
-import static com.mongodb.ClusterFixture.isStandalone;
-import static com.mongodb.ClusterFixture.serverVersionAtLeast;
-import static com.mongodb.client.Fixture.getDefaultDatabase;
-import static com.mongodb.client.Fixture.getMongoClient;
-import static com.mongodb.client.Fixture.getMongoClientSettings;
-import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder;
-import static com.mongodb.fixture.EncryptionFixture.getKmsProviders;
-import static java.util.Arrays.asList;
-import static org.junit.Assume.assumeTrue;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.junit.jupiter.api.Assumptions.assumeFalse;
-import static util.JsonPoweredTestHelper.getTestDocument;
-
-public abstract class AbstractClientEncryptionTextExplicitEncryptionTest {
-
- private static final ServerVersion REQUIRED_LIB_MONGOCRYPT_VERSION = new ServerVersion(asList(1, 15, 1));
- private MongoClient encryptedClient;
- private MongoDatabase encryptedDatabase;
- private ClientEncryption clientEncryption;
- private BsonBinary key1Id;
-
- protected abstract MongoClient createMongoClient(MongoClientSettings settings);
- protected abstract ClientEncryption createClientEncryption(ClientEncryptionSettings settings);
-
-
- @BeforeEach
- public void setUp() {
- assumeTrue("Text explicit encryption tests disabled", hasEncryptionTestsEnabled());
- assumeTrue("Requires newer MongoCrypt version", getMongoCryptVersion().compareTo(REQUIRED_LIB_MONGOCRYPT_VERSION) >= 0);
- assumeTrue(serverVersionAtLeast(8, 2));
- // TODO-JAVA-6168 update prose tests for post 9.0
- assumeTrue(!serverVersionAtLeast(9, 0));
- assumeFalse(isStandalone());
-
- MongoNamespace dataKeysNamespace = new MongoNamespace("keyvault.datakeys");
- BsonDocument encryptedFieldsPrefixSuffix = bsonDocumentFromPath("encryptedFields-prefix-suffix.json");
- BsonDocument encryptedFieldsSubstring = bsonDocumentFromPath("encryptedFields-substring.json");
- BsonDocument key1Document = bsonDocumentFromPath("keys/key1-document.json");
-
- MongoDatabase database = getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY);
- database.getCollection("prefix-suffix")
- .drop(new DropCollectionOptions().encryptedFields(encryptedFieldsPrefixSuffix));
- database.createCollection("prefix-suffix",
- new CreateCollectionOptions().encryptedFields(encryptedFieldsPrefixSuffix));
-
- database.getCollection("substring")
- .drop(new DropCollectionOptions().encryptedFields(encryptedFieldsSubstring));
- database.createCollection("substring",
- new CreateCollectionOptions().encryptedFields(encryptedFieldsSubstring));
-
- MongoCollection dataKeysCollection = getMongoClient()
- .getDatabase(dataKeysNamespace.getDatabaseName())
- .getCollection(dataKeysNamespace.getCollectionName(), BsonDocument.class)
- .withWriteConcern(WriteConcern.MAJORITY);
-
- dataKeysCollection.drop();
- dataKeysCollection.insertOne(key1Document);
- key1Id = key1Document.getBinary("_id");
-
- Map> kmsProviders = getKmsProviders(EncryptionFixture.KmsProviderType.LOCAL);
-
- clientEncryption = createClientEncryption(ClientEncryptionSettings.builder()
- .keyVaultMongoClientSettings(getMongoClientSettings())
- .keyVaultNamespace(dataKeysNamespace.getFullName())
- .kmsProviders(kmsProviders)
- .build());
-
- encryptedClient = createMongoClient(getMongoClientSettingsBuilder()
- .autoEncryptionSettings(
- AutoEncryptionSettings.builder()
- .keyVaultNamespace(dataKeysNamespace.getFullName())
- .kmsProviders(kmsProviders)
- .bypassQueryAnalysis(true)
- .build())
- .build());
-
- encryptedDatabase = encryptedClient.getDatabase(getDefaultDatabaseName()).withWriteConcern(WriteConcern.MAJORITY);
-
- EncryptOptions prefixSuffixEncryptOptions = new EncryptOptions("TextPreview")
- .keyId(key1Id)
- .contentionFactor(0L)
- .textOptions(new TextOptions()
- .caseSensitive(true)
- .diacriticSensitive(true)
- .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))
- .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))
- );
-
- BsonBinary foobarbaz = clientEncryption.encrypt(new BsonString("foobarbaz"), prefixSuffixEncryptOptions);
-
- encryptedDatabase
- .getCollection("prefix-suffix")
- .insertOne(new Document("_id", 0).append("encryptedText", foobarbaz));
-
- EncryptOptions substringEncryptOptions = new EncryptOptions("TextPreview")
- .keyId(key1Id)
- .contentionFactor(0L)
- .textOptions(new TextOptions()
- .caseSensitive(true)
- .diacriticSensitive(true)
- .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}"))
- );
- foobarbaz = clientEncryption.encrypt(new BsonString("foobarbaz"), substringEncryptOptions);
-
- encryptedDatabase
- .getCollection("substring")
- .insertOne(new Document("_id", 0).append("encryptedText", foobarbaz));
- }
-
- @Test
- @DisplayName("Case 1: can find a document by prefix")
- public void test1CanFindADocumentByPrefix() {
- EncryptOptions encryptOptions = new EncryptOptions("TextPreview")
- .keyId(key1Id)
- .contentionFactor(0L)
- .queryType("prefixPreview")
- .textOptions(new TextOptions()
- .caseSensitive(true)
- .diacriticSensitive(true)
- .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))
- );
-
- BsonBinary encrypted = clientEncryption.encrypt(new BsonString("foo"), encryptOptions);
- Document result = encryptedDatabase.getCollection("prefix-suffix")
- .find(new Document("$expr",
- new Document("$encStrStartsWith",
- new Document("input", "$encryptedText").append("prefix", encrypted)))).first();
-
- assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result);
- }
-
- @Test
- @DisplayName("Case 2: can find a document by suffix")
- public void test2CanFindADocumentBySuffix() {
- EncryptOptions encryptOptions = new EncryptOptions("TextPreview")
- .keyId(key1Id)
- .contentionFactor(0L)
- .queryType("suffixPreview")
- .textOptions(new TextOptions()
- .caseSensitive(true)
- .diacriticSensitive(true)
- .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))
- );
-
- BsonBinary encrypted = clientEncryption.encrypt(new BsonString("baz"), encryptOptions);
- Document result = encryptedDatabase.getCollection("prefix-suffix")
- .find(new Document("$expr",
- new Document("$encStrEndsWith",
- new Document("input", "$encryptedText").append("suffix", encrypted)))).first();
-
- assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result);
- }
-
- @Test
- @DisplayName("Case 3: assert no document found by prefix")
- public void test3AssertNoDocumentFoundByPrefix() {
- EncryptOptions encryptOptions = new EncryptOptions("TextPreview")
- .keyId(key1Id)
- .contentionFactor(0L)
- .queryType("prefixPreview")
- .textOptions(new TextOptions()
- .caseSensitive(true)
- .diacriticSensitive(true)
- .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))
- );
-
- BsonBinary encrypted = clientEncryption.encrypt(new BsonString("baz"), encryptOptions);
- Document result = encryptedDatabase.getCollection("prefix-suffix")
- .find(new Document("$expr",
- new Document("$encStrStartsWith",
- new Document("input", "$encryptedText").append("prefix", encrypted)))).first();
-
- assertNull(result);
- }
-
- @Test
- @DisplayName("Case 4: assert no document found by suffix")
- public void test4AssertNoDocumentFoundByPrefix() {
- EncryptOptions encryptOptions = new EncryptOptions("TextPreview")
- .keyId(key1Id)
- .contentionFactor(0L)
- .queryType("suffixPreview")
- .textOptions(new TextOptions()
- .caseSensitive(true)
- .diacriticSensitive(true)
- .suffixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))
- );
-
- BsonBinary encrypted = clientEncryption.encrypt(new BsonString("foo"), encryptOptions);
- Document result = encryptedDatabase.getCollection("prefix-suffix")
- .find(new Document("$expr",
- new Document("$encStrEndsWith",
- new Document("input", "$encryptedText").append("suffix", encrypted)))).first();
-
- assertNull(result);
- }
-
- @Test
- @DisplayName("Case 5: can find a document by substring")
- public void test5CanFindADocumentBySubstring() {
- EncryptOptions encryptOptions = new EncryptOptions("TextPreview")
- .keyId(key1Id)
- .contentionFactor(0L)
- .queryType("substringPreview")
- .textOptions(new TextOptions()
- .caseSensitive(true)
- .diacriticSensitive(true)
- .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}"))
- );
-
- BsonBinary encrypted = clientEncryption.encrypt(new BsonString("bar"), encryptOptions);
- Document result = encryptedDatabase.getCollection("substring")
- .find(new Document("$expr",
- new Document("$encStrContains",
- new Document("input", "$encryptedText").append("substring", encrypted)))).first();
-
- assertDocumentEquals(Document.parse("{ \"_id\": 0, \"encryptedText\": \"foobarbaz\" }"), result);
- }
-
- @Test
- @DisplayName("Case 6: assert no document found by substring")
- public void test6AssertNoDocumentFoundBySubstring() {
- EncryptOptions encryptOptions = new EncryptOptions("TextPreview")
- .keyId(key1Id)
- .contentionFactor(0L)
- .queryType("substringPreview")
- .textOptions(new TextOptions()
- .caseSensitive(true)
- .diacriticSensitive(true)
- .substringOptions(BsonDocument.parse("{strMaxLength: 10, strMaxQueryLength: 10, strMinQueryLength: 2}"))
- );
-
- BsonBinary encrypted = clientEncryption.encrypt(new BsonString("qux"), encryptOptions);
- Document result = encryptedDatabase.getCollection("substring")
- .find(new Document("$expr",
- new Document("$encStrContains",
- new Document("input", "$encryptedText").append("substring", encrypted)))).first();
-
- assertNull(result);
- }
-
- @Test
- @DisplayName("Case 7: assert `contentionFactor` is required")
- public void test7AssertContentionFactorIsRequired() {
- EncryptOptions encryptOptions = new EncryptOptions("TextPreview")
- .keyId(key1Id)
- .queryType("prefixPreview")
- .textOptions(new TextOptions()
- .caseSensitive(true)
- .diacriticSensitive(true)
- .prefixOptions(BsonDocument.parse("{strMaxQueryLength: 10, strMinQueryLength: 2}"))
- );
- MongoException exception = assertThrows(MongoException.class, () -> clientEncryption.encrypt(new BsonString("foo"), encryptOptions));
- assertTrue(exception.getMessage().contains("contention factor is required for textPreview algorithm"));
- }
-
-
- @AfterEach
- @SuppressWarnings("try")
- public void cleanUp() {
- //noinspection EmptyTryBlock
- getDefaultDatabase().withWriteConcern(WriteConcern.MAJORITY).drop();
- try (ClientEncryption ignored = this.clientEncryption;
- MongoClient ignored1 = this.encryptedClient
- ) {
- // just using try-with-resources to ensure they all get closed, even in the case of exceptions
- }
- }
-
- private static void assertDocumentEquals(final Document expectedDocument, final Document actualDocument) {
- actualDocument.remove("__safeContent__");
- assertEquals(expectedDocument, actualDocument);
- }
-
- private static BsonDocument bsonDocumentFromPath(final String path) {
- return getTestDocument("client-side-encryption/etc/data/" + path);
- }
-}
diff --git a/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java b/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionStringExplicitEncryptionTest.java
similarity index 90%
rename from driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java
rename to driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionStringExplicitEncryptionTest.java
index 23bd9ec135d..e0614a0d450 100644
--- a/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionTextExplicitEncryptionTest.java
+++ b/driver-sync/src/test/functional/com/mongodb/client/ClientEncryptionStringExplicitEncryptionTest.java
@@ -21,7 +21,7 @@
import com.mongodb.client.vault.ClientEncryption;
import com.mongodb.client.vault.ClientEncryptions;
-public class ClientEncryptionTextExplicitEncryptionTest extends AbstractClientEncryptionTextExplicitEncryptionTest {
+public class ClientEncryptionStringExplicitEncryptionTest extends AbstractClientEncryptionStringExplicitEncryptionTest {
@Override
protected MongoClient createMongoClient(final MongoClientSettings settings) {
return MongoClients.create(settings);
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 7b432acd050..15f62c58ee1 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
@@ -63,9 +63,6 @@ public static void applyCustomizations(final TestDef def) {
// Client side encryption (QE)
def.skipJira("https://jira.mongodb.org/browse/JAVA-5675 Support QE with Client.bulkWrite")
.file("client-side-encryption/tests/unified", "client bulkWrite with queryable encryption");
- def.skipJira("https://jira.mongodb.org/browse/JAVA-6244 QE GA \"substring\" query type is not yet "
- + "implemented (DRIVERS-3540)")
- .file("client-side-encryption/tests/unified", "QE-Text-substring");
// client-side-operation-timeout (CSOT)
// The expected change stream timeout-refresh behaviour is unspecified on server 9.0+ (DRIVERS-3006), so the
diff --git a/mongodb-crypt/build.gradle.kts b/mongodb-crypt/build.gradle.kts
index 6a46f2dfc9a..208034beaad 100644
--- a/mongodb-crypt/build.gradle.kts
+++ b/mongodb-crypt/build.gradle.kts
@@ -17,6 +17,7 @@ import ProjectExtensions.configureJarManifest
import ProjectExtensions.configureMavenPublication
import de.undercouch.gradle.tasks.download.Download
import java.io.ByteArrayOutputStream
+import java.io.File
import javax.inject.Inject
import org.gradle.api.GradleException
import org.gradle.process.ExecOperations
@@ -66,7 +67,7 @@ val jnaResources: String = System.getProperty("jna.library.path", jnaLibsPath)
// Download the libmongocrypt per-platform tarballs (and their signatures) to jnaDownloadsDir.
// To upgrade: change downloadRevision, run `./gradlew clean downloadJnaLibs`, and verify the build.
-val downloadRevision = "1.18.1"
+val downloadRevision = "1.20.0"
val downloadUrlBase = "https://github.com/mongodb/libmongocrypt/releases/download/$downloadRevision"
/**
@@ -75,7 +76,7 @@ val downloadUrlBase = "https://github.com/mongodb/libmongocrypt/releases/downloa
* layout differ per platform, so both must be tracked explicitly.
*
* libmongocrypt's signature assets replace the `.tar.gz` suffix with `.asc` (e.g.
- * `libmongocrypt-linux-x86_64-glibc_2_7-nocrypto-1.18.1.asc`).
+ * `libmongocrypt-linux-x86_64-glibc_2_7-nocrypto-.asc`).
*/
data class CryptBinary(val jnaPlatform: String, val tarball: String, val libPathInTarball: String) {
val signature: String = tarball.removeSuffix(".tar.gz") + ".asc"
@@ -137,8 +138,9 @@ tasks.register("downloadCryptLibs") {
* Per DRIVERS-3441, drivers that bundle libmongocrypt must verify GPG signatures of
* release tarballs against the official MongoDB libmongocrypt signing key.
*
- * The keyring is kept under `build/` so this task does not touch the developer's
- * system GPG keyring and so `./gradlew clean` resets the trust state.
+ * The task uses a scratch keyring (not the developer's system GPG keyring), which {@code verify()}
+ * recreates on every run so the trust state is always reset. It lives under the system temp dir
+ * rather than {@code build/} — see the {@code gnupgHome} assignment in {@code verifyCryptLibs} for why.
*/
val skipCryptVerify = providers.gradleProperty("skipCryptVerify").map { it.toBoolean() }.orElse(false)
@@ -275,7 +277,16 @@ tasks.register("verifyCryptLibs") {
publicKey.set(file("$jnaDownloadsDir/$libmongocryptPublicKeyFile"))
skipVerify.set(skipCryptVerify)
expectedFingerprint.set("F2F5BF4ABF517E039AFCADAA81F1404DEBACA586")
- gnupgHome.set(layout.buildDirectory.dir("jnaLibs/gnupg"))
+ // Keep the scratch GPG keyring under the system temp dir, not the module build dir. gpg derives
+ // its agent socket path from the homedir, and macOS caps AF_UNIX socket paths (sun_path) at 104
+ // bytes. A deeply-nested checkout pushes "/build/jnaLibs/gnupg/S.gpg-agent" past that
+ // limit, so on a fresh keyring gpg fails ("can't connect to the gpg-agent: File name too long")
+ // and exits non-zero even though the import/verify would otherwise succeed. A short,
+ // checkout-independent homedir avoids this; the hash suffix isolates builds of different
+ // checkouts. verify() recreates this directory on every run, so a stable name is safe.
+ val cryptGpgHomeName =
+ "crypt-gpg-" + layout.buildDirectory.get().asFile.absolutePath.hashCode().toUInt().toString(16)
+ gnupgHome.set(layout.dir(providers.provider { File(System.getProperty("java.io.tmpdir"), cryptGpgHomeName) }))
verificationStamp.set(layout.buildDirectory.file("jnaLibs/verified.stamp"))
/* Bypass entirely when the caller has supplied a local libmongocrypt directory. */
diff --git a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java
index c08608ca595..8b37549ca37 100644
--- a/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java
+++ b/mongodb-crypt/src/main/com/mongodb/internal/crypt/capi/MongoExplicitEncryptOptions.java
@@ -87,7 +87,7 @@ public Builder algorithm(final String algorithm) {
/**
* The contention factor.
*
- * Only applies when algorithm is "Indexed", "Range", or "TextPreview".
+ * Only applies when algorithm is "Indexed", "Range", or "String".
* @param contentionFactor the contention factor
* @return this
* @since 1.5
@@ -100,7 +100,7 @@ public Builder contentionFactor(final Long contentionFactor) {
/**
* The QueryType.
*
- * Only applies when algorithm is "Indexed", "Range", or "TextPreview".
+ * Only applies when algorithm is "Indexed", "Range", or "String".
*
* @param queryType the query type
* @return this
@@ -126,11 +126,12 @@ public Builder rangeOptions(final BsonDocument rangeOptions) {
}
/**
- * The Text Options.
+ * The String Options.
*
- * Only applies when algorithm is "TextPreview".
+ * Only applies when algorithm is "String". The method name mirrors the libmongocrypt {@code textOptions}
+ * BSON field, which is unchanged.
*
- * @param textOptions the text options
+ * @param textOptions the string options, as a BSON document
* @return this
* @since 5.6
*/