Skip to content

Commit 1be03da

Browse files
committed
Add $unset and $geoNear stages, use Java tests
JAVA-4594
1 parent 5bf87c6 commit 1be03da

File tree

7 files changed

+423
-3
lines changed

7 files changed

+423
-3
lines changed

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,24 @@
2121
import com.mongodb.client.model.densify.DensifyRange;
2222
import com.mongodb.client.model.fill.FillOutputField;
2323
import com.mongodb.client.model.fill.FillOptions;
24+
import com.mongodb.client.model.geojson.Point;
2425
import com.mongodb.client.model.search.SearchOperator;
2526
import com.mongodb.client.model.search.SearchCollector;
2627
import com.mongodb.client.model.search.SearchOptions;
2728
import com.mongodb.lang.Nullable;
29+
import org.bson.BsonArray;
2830
import org.bson.BsonBoolean;
2931
import org.bson.BsonDocument;
3032
import org.bson.BsonDocumentWriter;
3133
import org.bson.BsonInt32;
3234
import org.bson.BsonString;
3335
import org.bson.BsonType;
3436
import org.bson.BsonValue;
37+
import org.bson.Document;
3538
import org.bson.codecs.configuration.CodecRegistry;
3639
import org.bson.conversions.Bson;
3740

41+
import java.util.Arrays;
3842
import java.util.List;
3943
import java.util.Map;
4044
import java.util.Objects;
@@ -920,6 +924,48 @@ public static Bson searchMeta(final SearchCollector collector, final SearchOptio
920924
return new SearchStage("$searchMeta", notNull("collector", collector), notNull("options", options));
921925
}
922926

927+
/**
928+
* Creates an $unset pipeline stage that removes/excludes fields from documents
929+
*
930+
* @param fields the fields to exclude. May use dot notation.
931+
* @return the $unset pipeline stage
932+
* @mongodb.driver.manual reference/operator/aggregation/project/ $unset
933+
* @mongodb.server.release 4.2
934+
* @since 4.8
935+
*/
936+
public static Bson unset(final String... fields) {
937+
if (fields.length == 1) {
938+
return new BsonDocument().append("$unset", new BsonString(fields[0]));
939+
}
940+
BsonArray array = new BsonArray();
941+
Arrays.stream(fields).map(BsonString::new).forEach(array::add);
942+
return new BsonDocument().append("$unset", array);
943+
}
944+
945+
/**
946+
* Creates a $geoNear pipeline stage that outputs documents in order of nearest to farthest from a specified point.
947+
*
948+
* @param near The point for which to find the closest documents.
949+
* @param distanceField The output field that contains the calculated distance.
950+
* To specify a field within an embedded document, use dot notation.
951+
* @param options {@link GeoNearOption}
952+
* @return the $geoNear pipeline stage
953+
* @mongodb.driver.manual reference/operator/aggregation/project/ $geoNear
954+
* @since 4.8
955+
*/
956+
public static Bson geoNear(
957+
final Point near,
958+
final String distanceField,
959+
final GeoNearOption... options) {
960+
Document d = new Document();
961+
d.append("near", near);
962+
d.append("distanceField", distanceField);
963+
for (GeoNearOption o : options) {
964+
d.append(o.getKey(), o.getValue());
965+
}
966+
return new Document("$geoNear", d);
967+
}
968+
923969
static void writeBucketOutput(final CodecRegistry codecRegistry, final BsonDocumentWriter writer,
924970
@Nullable final List<BsonField> output) {
925971
if (output != null) {
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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;
18+
19+
import org.bson.Document;
20+
21+
/**
22+
* Optional fields for the {@link Aggregates#geoNear} pipeline stage.
23+
*/
24+
public final class GeoNearOption {
25+
private final String key;
26+
private final Object value;
27+
28+
String getKey() {
29+
return key;
30+
}
31+
Object getValue() {
32+
return value;
33+
}
34+
35+
private GeoNearOption(final String key, final Object value) {
36+
this.key = key;
37+
this.value = value;
38+
}
39+
40+
/**
41+
* @param distanceMultiplier The factor to multiply all distances returned by the query.
42+
* @return the option
43+
*/
44+
public static GeoNearOption distanceMultiplier(final Number distanceMultiplier) {
45+
return new GeoNearOption("distanceMultiplier", distanceMultiplier);
46+
}
47+
48+
/**
49+
* This specifies the output field that identifies the location used to calculate the distance.
50+
* This option is useful when a location field contains multiple locations.
51+
* To specify a field within an embedded document, use dot notation.
52+
*
53+
* @param includeLocs the output field
54+
* @return the option
55+
*/
56+
public static GeoNearOption includeLocs(final String includeLocs) {
57+
return new GeoNearOption("includeLocs", includeLocs);
58+
}
59+
60+
/**
61+
* Specify the geospatial indexed field to use when calculating the distance.
62+
*
63+
* @param key the geospatial indexed field.
64+
* @return the option
65+
*/
66+
public static GeoNearOption key(final String key) {
67+
return new GeoNearOption("key", key);
68+
}
69+
70+
/**
71+
* The minimum distance from the center point that the documents can be.
72+
* MongoDB limits the results to those documents that fall outside the specified distance from the center point.
73+
*
74+
* @param minDistance the distance in meters for GeoJSON data.
75+
* @return the option
76+
*/
77+
public static GeoNearOption minDistance(final Number minDistance) {
78+
return new GeoNearOption("minDistance", minDistance);
79+
}
80+
81+
/**
82+
* The maximum distance from the center point that the documents can be.
83+
* MongoDB limits the results to those documents that fall within the specified distance from the center point.
84+
*
85+
* @param maxDistance the distance in meters for GeoJSON data.
86+
* @return the option
87+
*/
88+
public static GeoNearOption maxDistance(final Number maxDistance) {
89+
return new GeoNearOption("maxDistance", maxDistance);
90+
}
91+
92+
/**
93+
* Limits the results to the documents that match the query.
94+
* The query syntax is the usual MongoDB read operation query syntax.
95+
*
96+
* @param query the query
97+
* @return the option
98+
*/
99+
public static GeoNearOption query(final Document query) {
100+
return new GeoNearOption("query", query);
101+
}
102+
103+
/**
104+
* Determines how MongoDB calculates the distance between two points.
105+
* By default, when this option is not provided, MongoDB uses $near semantics:
106+
* spherical geometry for 2dsphere indexes and planar geometry for 2d indexes.
107+
* When provided, MongoDB uses $nearSphere semantics and calculates distances
108+
* using spherical geometry.
109+
*
110+
* @return the option
111+
*/
112+
public static GeoNearOption spherical() {
113+
return new GeoNearOption("spherical", true);
114+
}
115+
}

driver-core/src/test/functional/com/mongodb/OperationFunctionalSpecification.groovy

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,20 @@ import static com.mongodb.internal.operation.OperationUnitSpecification.getMaxWi
7575

7676
class OperationFunctionalSpecification extends Specification {
7777

78-
def setup() {
78+
void setup() {
79+
setupInternal()
80+
}
81+
82+
protected void setupInternal() {
7983
ServerHelper.checkPool(getPrimary())
8084
CollectionHelper.drop(getNamespace())
8185
}
8286

83-
def cleanup() {
87+
void cleanup() {
88+
cleanupInternal()
89+
}
90+
91+
protected void cleanupInternal() {
8492
CollectionHelper.drop(getNamespace())
8593
checkReferenceCountReachesTarget(getBinding(), 1)
8694
checkReferenceCountReachesTarget(getAsyncBinding(), 1)
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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;
18+
19+
import com.mongodb.OperationFunctionalSpecification;
20+
import com.mongodb.client.model.geojson.Point;
21+
import com.mongodb.client.model.geojson.Position;
22+
import org.bson.BsonArray;
23+
import org.bson.BsonDocument;
24+
import org.bson.BsonValue;
25+
import org.bson.Document;
26+
27+
import java.util.Collections;
28+
import java.util.List;
29+
import java.util.stream.Collectors;
30+
31+
import org.bson.codecs.DecoderContext;
32+
import org.bson.conversions.Bson;
33+
import org.junit.jupiter.api.AfterEach;
34+
import org.junit.jupiter.api.BeforeEach;
35+
import org.junit.jupiter.api.Test;
36+
37+
import static com.mongodb.ClusterFixture.serverVersionAtLeast;
38+
import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry;
39+
import static com.mongodb.client.model.GeoNearOption.distanceMultiplier;
40+
import static com.mongodb.client.model.GeoNearOption.includeLocs;
41+
import static com.mongodb.client.model.GeoNearOption.key;
42+
import static com.mongodb.client.model.GeoNearOption.maxDistance;
43+
import static com.mongodb.client.model.GeoNearOption.minDistance;
44+
import static com.mongodb.client.model.GeoNearOption.query;
45+
import static com.mongodb.client.model.GeoNearOption.spherical;
46+
import static com.mongodb.client.model.Aggregates.geoNear;
47+
import static com.mongodb.client.model.Aggregates.unset;
48+
import static org.junit.jupiter.api.Assertions.assertEquals;
49+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
50+
51+
public class AggregatesTest extends OperationFunctionalSpecification {
52+
53+
@BeforeEach
54+
public void beforeEach() {
55+
super.setupInternal();
56+
}
57+
58+
@AfterEach
59+
public void afterEach() {
60+
super.cleanupInternal();
61+
}
62+
63+
void insertAll(final String insertAll) {
64+
List<BsonDocument> documents = BsonArray.parse(insertAll).stream().map(BsonValue::asDocument).collect(Collectors.toList());
65+
getCollectionHelper().insertDocuments(documents);
66+
}
67+
68+
private List<Bson> assertPipeline(final String stageAsString, final Bson stage) {
69+
BsonDocument expectedStage = BsonDocument.parse(stageAsString);
70+
List<Bson> pipeline = Collections.singletonList(stage);
71+
assertEquals(expectedStage, pipeline.get(0).toBsonDocument(BsonDocument.class, getDefaultCodecRegistry()));
72+
return pipeline;
73+
}
74+
75+
private void assertResults(final List<Bson> pipeline, final String s) {
76+
List<Document> expectedResults = parseToList(s);
77+
List<Document> results = getCollectionHelper().aggregate(pipeline);
78+
assertEquals(expectedResults, results);
79+
}
80+
81+
private List<Document> parseToList(final String s) {
82+
return BsonArray.parse(s).stream().map(v -> toDocument(v.asDocument())).collect(Collectors.toList());
83+
}
84+
85+
public Document toDocument(final BsonDocument bsonDocument) {
86+
return getDefaultCodecRegistry().get(Document.class).decode(bsonDocument.asBsonReader(), DecoderContext.builder().build());
87+
}
88+
89+
@Test
90+
public void testUnset() {
91+
assumeTrue(serverVersionAtLeast(4, 2));
92+
insertAll("[\n"
93+
+ " { _id: 1, title: 'Antelope Antics', author: { last:'An', first: 'Auntie' } },\n"
94+
+ " { _id: 2, title: 'Bees Babble', author: { last:'Bumble', first: 'Bee' } }\n"
95+
+ "]");
96+
97+
assertPipeline(
98+
"{ $unset: ['title', 'author.first'] }",
99+
unset("title", "author.first"));
100+
101+
List<Bson> pipeline = assertPipeline(
102+
"{ $unset: 'author.first' }",
103+
unset("author.first"));
104+
105+
assertResults(pipeline, "[\n"
106+
+ " { _id: 1, title: 'Antelope Antics', author: { last:'An' } },\n"
107+
+ " { _id: 2, title: 'Bees Babble', author: { last:'Bumble' } }\n"
108+
+ "]");
109+
}
110+
111+
@Test
112+
public void testGeoNear() {
113+
insertAll("[\n"
114+
+ " {\n"
115+
+ " _id: 1,\n"
116+
+ " name: 'Central Park',\n"
117+
+ " location: { type: 'Point', coordinates: [ -73.97, 40.77 ] },\n"
118+
+ " category: 'Parks'\n"
119+
+ " },\n"
120+
+ " {\n"
121+
+ " _id: 2,\n"
122+
+ " name: 'Sara D. Roosevelt Park',\n"
123+
+ " location: { type: 'Point', coordinates: [ -73.9928, 40.7193 ] },\n"
124+
+ " category: 'Parks'\n"
125+
+ " },\n"
126+
+ " {\n"
127+
+ " _id: 3,\n"
128+
+ " name: 'Polo Grounds',\n"
129+
+ " location: { type: 'Point', coordinates: [ -73.9375, 40.8303 ] },\n"
130+
+ " category: 'Stadiums'\n"
131+
+ " }\n"
132+
+ "]");
133+
getCollectionHelper().createIndex(BsonDocument.parse("{ location: '2dsphere' }"));
134+
135+
List<Bson> pipeline = assertPipeline("{\n"
136+
+ " $geoNear: {\n"
137+
+ " near: { type: 'Point', coordinates: [ -73.99279 , 40.719296 ] },\n"
138+
+ " distanceField: 'dist.calculated',\n"
139+
+ " minDistance: 0,\n"
140+
+ " maxDistance: 2,\n"
141+
+ " query: { category: 'Parks' },\n"
142+
+ " includeLocs: 'dist.location',\n"
143+
+ " spherical: true,\n"
144+
+ " key: 'location',\n"
145+
+ " distanceMultiplier: 10.0\n"
146+
+ " }\n"
147+
+ "}",
148+
geoNear(
149+
new Point(new Position(-73.99279, 40.719296)),
150+
"dist.calculated",
151+
minDistance(0),
152+
maxDistance(2),
153+
query(new Document("category", "Parks")),
154+
includeLocs("dist.location"),
155+
spherical(),
156+
key("location"),
157+
distanceMultiplier(10.0)
158+
));
159+
160+
assertResults(pipeline, ""
161+
+ "[{\n"
162+
+ " '_id': 2,\n"
163+
+ " 'name' : 'Sara D. Roosevelt Park',\n"
164+
+ " 'category' : 'Parks',\n"
165+
+ " 'location' : {\n"
166+
+ " 'type' : 'Point',\n"
167+
+ " 'coordinates' : [ -73.9928, 40.7193 ]\n"
168+
+ " },\n"
169+
+ " 'dist' : {\n"
170+
+ " 'calculated' : 9.539931676365992,\n"
171+
+ " 'location' : {\n"
172+
+ " 'type' : 'Point',\n"
173+
+ " 'coordinates' : [ -73.9928, 40.7193 ]\n"
174+
+ " }\n"
175+
+ " }\n"
176+
+ "}]");
177+
}
178+
}

0 commit comments

Comments
 (0)