Skip to content

Commit 93355a1

Browse files
authored
Implement string expressions (#1036)
JAVA-4801
1 parent 275824f commit 93355a1

File tree

6 files changed

+251
-8
lines changed

6 files changed

+251
-8
lines changed

driver-core/src/main/com/mongodb/client/model/expressions/Expressions.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
import java.time.Instant;
3333
import java.util.ArrayList;
3434
import java.util.Arrays;
35-
import java.util.Date;
3635
import java.util.List;
3736
import java.util.stream.Collectors;
3837

@@ -84,6 +83,7 @@ public static DateExpression of(final Instant of) {
8483
* @return the string expression
8584
*/
8685
public static StringExpression of(final String of) {
86+
Assertions.notNull("String", of);
8787
return new MqlExpression<>((codecRegistry) -> new BsonString(of));
8888
}
8989

driver-core/src/main/com/mongodb/client/model/expressions/MqlExpression.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,41 @@ public T reduce(final T initialValue, final BinaryOperator<T> in) {
180180
.append("in", extractBsonValue(cr, in.apply(varThis, varValue)))).apply(cr));
181181
}
182182

183+
184+
/** @see StringExpression */
185+
186+
@Override
187+
public StringExpression toLower() {
188+
return new MqlExpression<>(ast("$toLower"));
189+
}
190+
191+
@Override
192+
public StringExpression toUpper() {
193+
return new MqlExpression<>(ast("$toUpper"));
194+
}
195+
196+
@Override
197+
public StringExpression concat(final StringExpression concat) {
198+
return new MqlExpression<>(ast("$concat", concat));
199+
}
200+
201+
@Override
202+
public IntegerExpression strLen() {
203+
return new MqlExpression<>(ast("$strLenCP"));
204+
}
205+
206+
@Override
207+
public IntegerExpression strLenBytes() {
208+
return new MqlExpression<>(ast("$strLenBytes"));
209+
}
210+
211+
@Override
212+
public StringExpression substr(final IntegerExpression start, final IntegerExpression length) {
213+
return new MqlExpression<>(ast("$substrCP", start, length));
214+
}
215+
216+
@Override
217+
public StringExpression substrBytes(final IntegerExpression start, final IntegerExpression length) {
218+
return new MqlExpression<>(ast("$substrBytes", start, length));
219+
}
183220
}

driver-core/src/main/com/mongodb/client/model/expressions/StringExpression.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,32 @@
1616

1717
package com.mongodb.client.model.expressions;
1818

19+
import static com.mongodb.client.model.expressions.Expressions.of;
20+
1921
/**
2022
* Expresses a string value.
2123
*/
2224
public interface StringExpression extends Expression {
2325

26+
StringExpression toLower();
27+
28+
StringExpression toUpper();
29+
30+
StringExpression concat(StringExpression concat);
31+
32+
IntegerExpression strLen();
33+
34+
IntegerExpression strLenBytes();
35+
36+
StringExpression substr(IntegerExpression start, IntegerExpression length);
37+
38+
default StringExpression substr(int start, int length) {
39+
return this.substr(of(start), of(length));
40+
}
41+
42+
StringExpression substrBytes(IntegerExpression start, IntegerExpression length);
43+
44+
default StringExpression substrBytes(int start, int length) {
45+
return this.substrBytes(of(start), of(length));
46+
}
2447
}

driver-core/src/test/functional/com/mongodb/client/model/expressions/AbstractExpressionsFunctionalTest.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.mongodb.client.model.Field;
2020
import com.mongodb.client.model.OperationTest;
21+
import com.mongodb.lang.Nullable;
2122
import org.bson.BsonArray;
2223
import org.bson.BsonDocument;
2324
import org.bson.BsonReader;
@@ -53,8 +54,16 @@ public void tearDown() {
5354
getCollectionHelper().drop();
5455
}
5556

56-
protected void assertExpression(final Object expectedResult, final Expression expression, final String expectedMql) {
57-
assertEval(expectedResult, expression);
57+
protected void assertExpression(final Object expected, final Expression expression) {
58+
assertExpression(expected, expression, null);
59+
}
60+
61+
protected void assertExpression(@Nullable final Object expected, final Expression expression, @Nullable final String expectedMql) {
62+
assertEval(expected, expression);
63+
64+
if (expectedMql == null) {
65+
return;
66+
}
5867

5968
BsonValue expressionValue = ((MqlExpression<?>) expression).toBsonValue(fromProviders(new BsonValueCodecProvider()));
6069
BsonValue bsonValue = new BsonDocumentFragmentCodec().readValue(
@@ -63,9 +72,17 @@ protected void assertExpression(final Object expectedResult, final Expression ex
6372
assertEquals(bsonValue, expressionValue, expressionValue.toString().replace("\"", "'"));
6473
}
6574

66-
private void assertEval(final Object expected, final Expression toEvaluate) {
75+
private void assertEval(@Nullable final Object expected, final Expression toEvaluate) {
6776
BsonValue evaluated = evaluate(toEvaluate);
68-
assertEquals(new Document("val", expected).toBsonDocument().get("val"), evaluated);
77+
BsonValue expected1 = toBsonValue(expected);
78+
assertEquals(expected1, evaluated);
79+
}
80+
81+
protected BsonValue toBsonValue(@Nullable final Object value) {
82+
if (value instanceof BsonValue) {
83+
return (BsonValue) value;
84+
}
85+
return new Document("val", value).toBsonDocument().get("val");
6986
}
7087

7188
protected BsonValue evaluate(final Expression toEvaluate) {
@@ -87,8 +104,7 @@ protected BsonValue evaluate(final Expression toEvaluate) {
87104
} else {
88105
results = getCollectionHelper().aggregate(stages);
89106
}
90-
BsonValue evaluated = results.get(0).get("val");
91-
return evaluated;
107+
return results.get(0).get("val");
92108
}
93109

94110
private static class BsonDocumentFragmentCodec extends BsonDocumentCodec {

driver-core/src/test/functional/com/mongodb/client/model/expressions/ComparisonExpressionsFunctionalTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@
2424

2525
import java.time.Instant;
2626
import java.util.Arrays;
27-
import java.util.Date;
2827
import java.util.List;
2928

3029
import static com.mongodb.client.model.expressions.Expressions.of;
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.mongodb.client.model.expressions;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.Arrays;
23+
24+
import static com.mongodb.client.model.expressions.Expressions.of;
25+
import static org.junit.jupiter.api.Assertions.assertThrows;
26+
27+
@SuppressWarnings({"ConstantConditions"})
28+
class StringExpressionsFunctionalTest extends AbstractExpressionsFunctionalTest {
29+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/#string-expression-operators
30+
31+
private final String jalapeno = "jalape\u00F1o";
32+
private final String sushi = "\u5BFF\u53F8";
33+
private final String fish = "\uD83D\uDC1F";
34+
35+
@Test
36+
public void literalsTest() {
37+
assertExpression("", of(""), "''");
38+
assertExpression("abc", of("abc"), "'abc'");
39+
assertThrows(IllegalArgumentException.class, () -> of((String) null));
40+
assertExpression(fish, of(fish), "'" + fish + "'");
41+
}
42+
43+
@Test
44+
public void concatTest() {
45+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/concat/
46+
assertExpression(
47+
"abc".concat("de"),
48+
of("abc").concat(of("de")),
49+
"{'$concat': ['abc', 'de']}");
50+
}
51+
52+
@Test
53+
public void toLowerTest() {
54+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/toLower/
55+
assertExpression(
56+
"ABC".toLowerCase(),
57+
of("ABC").toLower(),
58+
"{'$toLower': 'ABC'}");
59+
}
60+
61+
@Test
62+
public void toUpperTest() {
63+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/toUpper/ (?)
64+
assertExpression(
65+
"abc".toUpperCase(),
66+
of("abc").toUpper(),
67+
"{'$toUpper': 'abc'}");
68+
}
69+
70+
@Test
71+
public void strLenTest() {
72+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenCP/ (?)
73+
assertExpression(
74+
"abc".codePointCount(0, 3),
75+
of("abc").strLen(),
76+
"{'$strLenCP': 'abc'}");
77+
78+
// unicode
79+
assertExpression(
80+
jalapeno.codePointCount(0, jalapeno.length()),
81+
of(jalapeno).strLen(),
82+
"{'$strLenCP': '" + jalapeno + "'}");
83+
assertExpression(
84+
sushi.codePointCount(0, sushi.length()),
85+
of(sushi).strLen(),
86+
"{'$strLenCP': '" + sushi + "'}");
87+
assertExpression(
88+
fish.codePointCount(0, fish.length()),
89+
of(fish).strLen(),
90+
"{'$strLenCP': '" + fish + "'}");
91+
}
92+
93+
@Test
94+
public void strLenBytesTest() {
95+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/strLenBytes/ (?)
96+
assertExpression(
97+
"abc".getBytes(StandardCharsets.UTF_8).length,
98+
of("abc").strLenBytes(),
99+
"{'$strLenBytes': 'abc'}");
100+
101+
// unicode
102+
assertExpression(
103+
jalapeno.getBytes(StandardCharsets.UTF_8).length,
104+
of(jalapeno).strLenBytes(),
105+
"{'$strLenBytes': '" + jalapeno + "'}");
106+
assertExpression(
107+
sushi.getBytes(StandardCharsets.UTF_8).length,
108+
of(sushi).strLenBytes(),
109+
"{'$strLenBytes': '" + sushi + "'}");
110+
assertExpression(
111+
fish.getBytes(StandardCharsets.UTF_8).length,
112+
of(fish).strLenBytes(),
113+
"{'$strLenBytes': '" + fish + "'}");
114+
115+
// comparison
116+
assertExpression(8, of(jalapeno).strLen());
117+
assertExpression(9, of(jalapeno).strLenBytes());
118+
assertExpression(2, of(sushi).strLen());
119+
assertExpression(6, of(sushi).strLenBytes());
120+
assertExpression(1, of(fish).strLen());
121+
assertExpression(4, of(fish).strLenBytes());
122+
}
123+
124+
@Test
125+
public void substrTest() {
126+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/substr/
127+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrCP/ (?)
128+
// substr is deprecated, an alias for bytes
129+
assertExpression(
130+
"abc".substring(1, 1 + 1),
131+
of("abc").substr(of(1), of(1)),
132+
"{'$substrCP': ['abc', 1, 1]}");
133+
134+
// unicode
135+
assertExpression(
136+
jalapeno.substring(5, 5 + 3),
137+
of(jalapeno).substr(of(5), of(3)),
138+
"{'$substrCP': ['" + jalapeno + "', 5, 3]}");
139+
assertExpression(
140+
"e\u00F1o",
141+
of(jalapeno).substr(of(5), of(3)));
142+
143+
// bounds; convenience
144+
assertExpression("abc", of("abc").substr(0, 99));
145+
assertExpression("ab", of("abc").substr(0, 2));
146+
assertExpression("b", of("abc").substr(1, 1));
147+
assertExpression("", of("abc").substr(1, 0));
148+
}
149+
150+
@Test
151+
public void substrBytesTest() {
152+
// https://www.mongodb.com/docs/manual/reference/operator/aggregation/substrBytes/ (?)
153+
assertExpression(
154+
"b",
155+
of("abc").substrBytes(of(1), of(1)),
156+
"{'$substrBytes': ['abc', 1, 1]}");
157+
158+
// unicode
159+
byte[] bytes = Arrays.copyOfRange(sushi.getBytes(StandardCharsets.UTF_8), 0, 3);
160+
String expected = new String(bytes, StandardCharsets.UTF_8);
161+
assertExpression(expected,
162+
of(sushi).substrBytes(of(0), of(3)));
163+
// server returns "starting index is a UTF-8 continuation byte" error when substrBytes(1, 1)
164+
165+
// convenience
166+
assertExpression("b", of("abc").substrBytes(1, 1));
167+
}
168+
}

0 commit comments

Comments
 (0)