Skip to content

Commit 14623a3

Browse files
Thomas Darimontodrotbohm
authored andcommitted
DATAMONGO-586 - Add initial support for arithmetic expressions.
ProjectionOperationBuilder now implements AggregationOperation in order to be able support aliased as well as non alias projection operation expressions. Added test case for arithmetic operations to AggregationTests. Added Product domain class to be able to demonstrate some meaningful arithmetic operations. Applied changes from code review. Added internal private remove method to ProjectionOperation to allow previous operation to support aliasing.
1 parent 6dcaa31 commit 14623a3

File tree

3 files changed

+192
-11
lines changed

3 files changed

+192
-11
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java

Lines changed: 118 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
2424
import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference;
2525
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.FieldProjection;
26+
import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.OperationProjection;
2627
import org.springframework.util.Assert;
2728

2829
import com.mongodb.BasicDBObject;
@@ -72,7 +73,7 @@ protected ProjectionOperation and(Projection projection) {
7273
* @return
7374
*/
7475
public ProjectionOperationBuilder and(String name) {
75-
return new ProjectionOperationBuilder(name, this);
76+
return new ProjectionOperationBuilder(name, this, null);
7677
}
7778

7879
/**
@@ -125,6 +126,16 @@ protected ExposedFields getFields() {
125126
return fields;
126127
}
127128

129+
/**
130+
* Removes the given projectionOperation from the list of current projection operations. Needed in order to support
131+
* aliasing.
132+
*
133+
* @param projectionOperation
134+
*/
135+
private void remove(OperationProjection projectionOperation) {
136+
this.projections.remove(projectionOperation);
137+
}
138+
128139
/*
129140
* (non-Javadoc)
130141
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
@@ -146,25 +157,28 @@ public DBObject toDBObject(AggregationOperationContext context) {
146157
*
147158
* @author Oliver Gierke
148159
*/
149-
public static class ProjectionOperationBuilder {
160+
public static class ProjectionOperationBuilder implements AggregationOperation {
150161

151162
private final String name;
152163
private final ProjectionOperation operation;
164+
private final OperationProjection previousProjection;
153165

154166
/**
155167
* Creates a new {@link ProjectionOperationBuilder} for the field with the given name on top of the given
156168
* {@link ProjectionOperation}.
157169
*
158170
* @param name must not be {@literal null} or empty.
159171
* @param operation must not be {@literal null}.
172+
* @param previousProjection the previous operation projection, may be {@literal null}.
160173
*/
161-
public ProjectionOperationBuilder(String name, ProjectionOperation operation) {
174+
public ProjectionOperationBuilder(String name, ProjectionOperation operation, OperationProjection previousProjection) {
162175

163176
Assert.hasText(name, "Field name must not be null or empty!");
164177
Assert.notNull(operation, "ProjectionOperation must not be null!");
165178

166179
this.name = name;
167180
this.operation = operation;
181+
this.previousProjection = previousProjection;
168182
}
169183

170184
/**
@@ -189,14 +203,88 @@ public ProjectionOperation nested(Fields fields) {
189203
return this.operation.and(new NestedFieldProjection(name, fields));
190204
}
191205

192-
public ProjectionOperation plus(Number number) {
206+
/**
207+
* Allows to specify an alias for the previous projection operation.
208+
*
209+
* @param string
210+
* @return
211+
*/
212+
public ProjectionOperation as(String alias) {
213+
214+
Assert.notNull(this.previousProjection, "previousProjection must not be null!");
215+
this.operation.remove(this.previousProjection);
216+
return this.operation.and(previousProjection.withAlias(alias));
217+
}
218+
219+
/**
220+
* Generates an {@code $add} expression that adds the given number to the previously mentioned field.
221+
*
222+
* @param number
223+
* @return
224+
*/
225+
public ProjectionOperationBuilder plus(Number number) {
226+
193227
Assert.notNull(number, "Number must not be null!");
194228
return project("add", number);
195229
}
196230

197-
public ProjectionOperation minus(Number number) {
231+
/**
232+
* Generates an {@code $subtract} expression that subtracts the given number to the previously mentioned field.
233+
*
234+
* @param number
235+
* @return
236+
*/
237+
public ProjectionOperationBuilder minus(Number number) {
238+
198239
Assert.notNull(number, "Number must not be null!");
199-
return project("substract", number);
240+
return project("subtract", number);
241+
}
242+
243+
/**
244+
* Generates an {@code $multiply} expression that multiplies the given number with the previously mentioned field.
245+
*
246+
* @param number
247+
* @return
248+
*/
249+
public ProjectionOperationBuilder multiply(Number number) {
250+
251+
Assert.notNull(number, "Number must not be null!");
252+
return project("multiply", number);
253+
}
254+
255+
/**
256+
* Generates an {@code $divide} expression that divides the previously mentioned field by the given number.
257+
*
258+
* @param number
259+
* @return
260+
*/
261+
public ProjectionOperationBuilder divide(Number number) {
262+
263+
Assert.notNull(number, "Number must not be null!");
264+
Assert.isTrue(Math.abs(number.intValue()) != 0, "Number must not be zero!");
265+
return project("divide", number);
266+
}
267+
268+
/**
269+
* Generates an {@code $mod} expression that divides the previously mentioned field by the given number and returns
270+
* the remainder.
271+
*
272+
* @param number
273+
* @return
274+
*/
275+
public ProjectionOperationBuilder mod(Number number) {
276+
277+
Assert.notNull(number, "Number must not be null!");
278+
Assert.isTrue(Math.abs(number.intValue()) != 0, "Number must not be zero!");
279+
return project("mod", number);
280+
}
281+
282+
/* (non-Javadoc)
283+
* @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDBObject(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext)
284+
*/
285+
@Override
286+
public DBObject toDBObject(AggregationOperationContext context) {
287+
return this.operation.toDBObject(context);
200288
}
201289

202290
/**
@@ -206,8 +294,9 @@ public ProjectionOperation minus(Number number) {
206294
* @param values the values to be set for the projection operation.
207295
* @return
208296
*/
209-
public ProjectionOperation project(String operation, Object... values) {
210-
return this.operation.and(new OperationProjection(name, operation, values));
297+
public ProjectionOperationBuilder project(String operation, Object... values) {
298+
OperationProjection projectionOperation = new OperationProjection(name, operation, name, values);
299+
return new ProjectionOperationBuilder(name, this.operation.and(projectionOperation), projectionOperation);
211300
}
212301

213302
/**
@@ -308,21 +397,27 @@ static class OperationProjection extends Projection {
308397
private final String operation;
309398
private final List<Object> values;
310399

400+
private final String aliasName;
401+
311402
/**
312403
* Creates a new {@link OperationProjection} for the given field.
313404
*
314405
* @param name the name of the field to add the operation projection for, must not be {@literal null} or empty.
406+
* @param aliasName the name of the field alias to write that will hold the operation projection, must not be
407+
* {@literal null} or empty.
315408
* @param operation the actual operation key, must not be {@literal null} or empty.
316409
* @param values the values to pass into the operation, must not be {@literal null}.
317410
*/
318-
public OperationProjection(String name, String operation, Object... values) {
411+
public OperationProjection(String name, String operation, String aliasName, Object[] values) {
319412

320413
super(Fields.field(name));
321414

415+
Assert.hasText(aliasName, "aliasName must not be null or empty!");
322416
Assert.hasText(operation, "Operation must not be null or empty!");
323417
Assert.notNull(values, "Values must not be null!");
324418

325419
this.name = name;
420+
this.aliasName = aliasName;
326421
this.operation = operation;
327422
this.values = Arrays.asList(values);
328423
}
@@ -335,21 +430,32 @@ public OperationProjection(String name, String operation, Object... values) {
335430
public DBObject toDBObject(AggregationOperationContext context) {
336431

337432
List<Object> values = buildReferences(context);
338-
DBObject inner = new BasicDBObject(operation, values.size() == 1 ? values.get(0) : values.toArray());
433+
DBObject inner = new BasicDBObject("$" + operation, values.toArray());
339434

340-
return new BasicDBObject(name, inner);
435+
return new BasicDBObject(this.aliasName, inner);
341436
}
342437

343438
private List<Object> buildReferences(AggregationOperationContext context) {
344439

345440
List<Object> result = new ArrayList<Object>(values.size());
441+
result.add(context.getReference(this.name).toString());
346442

347443
for (Object element : values) {
348444
result.add(element instanceof Field ? context.getReference((Field) element).toString() : element);
349445
}
350446

351447
return result;
352448
}
449+
450+
/**
451+
* Creates a new instance of this {@link OperationProjection} with the given alias.
452+
*
453+
* @param aliasName the aliasName to set
454+
* @return
455+
*/
456+
public OperationProjection withAlias(String aliasName) {
457+
return new OperationProjection(name, operation, aliasName, values.toArray());
458+
}
353459
}
354460

355461
static class NestedFieldProjection extends Projection {
@@ -398,4 +504,5 @@ public ExposedField getField() {
398504

399505
public abstract DBObject toDBObject(AggregationOperationContext context);
400506
}
507+
401508
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ public void cleanUp() {
8282

8383
private void cleanDb() {
8484
mongoTemplate.dropCollection(INPUT_COLLECTION);
85+
mongoTemplate.dropCollection(Product.class);
8586
mongoTemplate.dropCollection(UserWithLikes.class);
8687
}
8788

@@ -421,6 +422,40 @@ public void returnFiveMostCommonLikesAggregationFrameworkExample() {
421422
assertLikeStats(result.getMappedResults().get(4), "e", 3);
422423
}
423424

425+
@Test
426+
public void arithmenticOperatorsInProjectionExample() {
427+
428+
double taxRate = 0.19;
429+
double netPrice = 1.99;
430+
double discountRate = 0.05;
431+
int spaceUnits = 3;
432+
String productId = "P1";
433+
String productName = "A";
434+
mongoTemplate.insert(new Product(productId, productName, netPrice, spaceUnits, discountRate, taxRate));
435+
436+
TypedAggregation<Product> agg = newAggregation(Product.class, //
437+
project("name", "netPrice") //
438+
.and("netPrice").plus(1).as("netPricePlus1") //
439+
.and("netPrice").minus(1).as("netPriceMinus1") //
440+
.and("netPrice").multiply(2).as("netPriceMul2") //
441+
.and("netPrice").divide(1.19).as("netPriceDiv119") //
442+
.and("spaceUnits").mod(2).as("spaceUnitsMod2") //
443+
);
444+
445+
AggregationResults<DBObject> result = mongoTemplate.aggregate(agg, DBObject.class);
446+
List<DBObject> resultList = result.getMappedResults();
447+
448+
assertThat(resultList, is(notNullValue()));
449+
assertThat((String) resultList.get(0).get("_id"), is(productId));
450+
assertThat((String) resultList.get(0).get("name"), is(productName));
451+
assertThat((Double) resultList.get(0).get("netPricePlus1"), is(netPrice + 1));
452+
assertThat((Double) resultList.get(0).get("netPriceMinus1"), is(netPrice - 1));
453+
assertThat((Double) resultList.get(0).get("netPriceMul2"), is(netPrice * 2));
454+
assertThat((Double) resultList.get(0).get("netPriceDiv119"), is(netPrice / 1.19));
455+
assertThat((Integer) resultList.get(0).get("spaceUnitsMod2"), is(spaceUnits % 2));
456+
457+
}
458+
424459
private void assertLikeStats(LikeStats like, String id, long count) {
425460

426461
assertThat(like, is(notNullValue()));
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2013 the original author or authors.
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+
package org.springframework.data.mongodb.core.aggregation;
17+
18+
/**
19+
* @author Thomas Darimont
20+
*/
21+
public class Product {
22+
String id;
23+
String name;
24+
double netPrice;
25+
int spaceUnits;
26+
double discountRate;
27+
double taxRate;
28+
29+
public Product() {}
30+
31+
public Product(String id, String name, double netPrice, int spaceUnits, double discountRate, double taxRate) {
32+
this.id = id;
33+
this.name = name;
34+
this.netPrice = netPrice;
35+
this.spaceUnits = spaceUnits;
36+
this.discountRate = discountRate;
37+
this.taxRate = taxRate;
38+
}
39+
}

0 commit comments

Comments
 (0)