DATAMONGO-2623 - Polishing.

Avoid nullable method arguments and add assertions. Introduce build() method to AccumulatorFinalizeBuilder to build Accumulator without specifying a finalize function.

Original pull request: #887.
This commit is contained in:
Mark Paluch
2020-10-07 09:50:23 +02:00
parent 0ef852a8fc
commit 217be64a77
3 changed files with 95 additions and 45 deletions

View File

@@ -29,8 +29,11 @@ import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Support class for {@link AggregationExpression} implementations.
*
* @author Christoph Strobl
* @author Matt Morrissette
* @author Mark Paluch
* @since 1.10
*/
abstract class AbstractAggregationExpression implements AggregationExpression {
@@ -49,7 +52,6 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
return toDocument(this.value, context);
}
@SuppressWarnings("unchecked")
public Document toDocument(Object value, AggregationOperationContext context) {
return new Document(getMongoMethod(), unpack(value, context));
}
@@ -101,17 +103,19 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
return value;
}
@SuppressWarnings("unchecked")
protected List<Object> append(Object value, Expand expandList) {
if (this.value instanceof List) {
List<Object> clone = new ArrayList<Object>((List) this.value);
List<Object> clone = new ArrayList<>((List<Object>) this.value);
if (value instanceof Collection && Expand.EXPAND_VALUES.equals(expandList)) {
clone.addAll((Collection<?>) value);
} else {
clone.add(value);
}
return clone;
}
@@ -129,22 +133,23 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
return append(value, Expand.EXPAND_VALUES);
}
@SuppressWarnings("unchecked")
protected java.util.Map<String, Object> append(String key, Object value) {
@SuppressWarnings({ "unchecked", "rawtypes" })
protected Map<String, Object> append(String key, Object value) {
Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map!");
java.util.Map<String, Object> clone = new LinkedHashMap<>((java.util.Map) this.value);
Map<String, Object> clone = new LinkedHashMap<>((java.util.Map) this.value);
clone.put(key, value);
return clone;
}
protected java.util.Map<String, Object> remove(String key) {
@SuppressWarnings({ "unchecked", "rawtypes" })
protected Map<String, Object> remove(String key) {
Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map!");
java.util.Map<String, Object> clone = new LinkedHashMap<>((java.util.Map) this.value);
Map<String, Object> clone = new LinkedHashMap<>((java.util.Map) this.value);
clone.remove(key);
return clone;
}
@@ -158,14 +163,15 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
* @return
* @since 3.1
*/
protected java.util.Map<String, Object> appendAt(int index, String key, Object value) {
@SuppressWarnings({ "unchecked" })
protected Map<String, Object> appendAt(int index, String key, Object value) {
Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map!");
java.util.LinkedHashMap<String, Object> clone = new java.util.LinkedHashMap<>();
Map<String, Object> clone = new LinkedHashMap<>();
int i = 0;
for (Map.Entry<String, Object> entry : ((java.util.Map<String, Object>) this.value).entrySet()) {
for (Map.Entry<String, Object> entry : ((Map<String, Object>) this.value).entrySet()) {
if (i == index) {
clone.put(key, value);
@@ -182,14 +188,17 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
}
@SuppressWarnings({ "rawtypes" })
protected List<Object> values() {
if (value instanceof List) {
return new ArrayList<Object>((List) value);
}
if (value instanceof java.util.Map) {
return new ArrayList<Object>(((java.util.Map) value).values());
}
return new ArrayList<>(Collections.singletonList(value));
}
@@ -219,7 +228,7 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map!");
return (T) ((java.util.Map<String, Object>) this.value).get(key);
return (T) ((Map<String, Object>) this.value).get(key);
}
/**
@@ -229,11 +238,11 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
* @return
*/
@SuppressWarnings("unchecked")
protected java.util.Map<String, Object> argumentMap() {
protected Map<String, Object> argumentMap() {
Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map!");
return Collections.unmodifiableMap((java.util.Map) value);
return Collections.unmodifiableMap((java.util.Map<String, Object>) value);
}
/**
@@ -250,7 +259,7 @@ abstract class AbstractAggregationExpression implements AggregationExpression {
return false;
}
return ((java.util.Map<String, Object>) this.value).containsKey(key);
return ((Map<String, Object>) this.value).containsKey(key);
}
protected abstract String getMongoMethod();

View File

@@ -36,6 +36,7 @@ import org.springframework.util.CollectionUtils;
* <a href="https://docs.mongodb.com/master/reference/configuration-options/#security.javascriptEnabled">enabled</a>.
*
* @author Christoph Strobl
* @author Mark Paluch
* @since 3.1
*/
public class ScriptOperators {
@@ -83,7 +84,6 @@ public class ScriptOperators {
*
* @see <a href="https://docs.mongodb.com/master/reference/operator/aggregation/function/">MongoDB Documentation:
* $function</a>
* @since 3.1
*/
public static class Function extends AbstractAggregationExpression {
@@ -99,6 +99,8 @@ public class ScriptOperators {
*/
public static Function function(String body) {
Assert.notNull(body, "Function body must not be null!");
Map<String, Object> function = new LinkedHashMap<>(2);
function.put(Fields.BODY.toString(), body);
function.put(Fields.ARGS.toString(), Collections.emptyList());
@@ -126,6 +128,7 @@ public class ScriptOperators {
public Function args(List<Object> args) {
Assert.notNull(args, "Args must not be null! Use an empty list instead.");
return new Function(appendAt(1, Fields.ARGS.toString(), args));
}
@@ -137,7 +140,8 @@ public class ScriptOperators {
*/
public Function lang(String lang) {
Assert.hasText(lang, "Lang must not be null nor emtpy! The default would be 'js'.");
Assert.hasText(lang, "Lang must not be null nor empty! The default would be 'js'.");
return new Function(appendAt(2, Fields.LANG.toString(), lang));
}
@@ -198,7 +202,6 @@ public class ScriptOperators {
*
* @see <a href="https://docs.mongodb.com/master/reference/operator/aggregation/accumulator/">MongoDB Documentation:
* $accumulator</a>
* @since 3.1
*/
public static class Accumulator extends AbstractAggregationExpression {
@@ -293,10 +296,10 @@ public class ScriptOperators {
/**
* Define the optional {@code initArgs} for the {@link AccumulatorInitBuilder#init(String)} function.
*
* @param args can be {@literal null}.
* @param args must not be {@literal null}.
* @return this.
*/
AccumulatorAccumulateBuilder initArgs(@Nullable List<Object> args);
AccumulatorAccumulateBuilder initArgs(List<Object> args);
}
public interface AccumulatorAccumulateBuilder {
@@ -355,10 +358,10 @@ public class ScriptOperators {
* Define additional {@code accumulateArgs} for the {@link AccumulatorAccumulateBuilder#accumulate(String)}
* function.
*
* @param args can be {@literal null}.
* @param args must not be {@literal null}.
* @return this.
*/
AccumulatorMergeBuilder accumulateArgs(@Nullable List<Object> args);
AccumulatorMergeBuilder accumulateArgs(List<Object> args);
}
public interface AccumulatorMergeBuilder {
@@ -398,9 +401,16 @@ public class ScriptOperators {
* @return new instance of {@link Accumulator}.
*/
Accumulator finalize(String function);
/**
* Build the {@link Accumulator} object without specifying a {@link #finalize(String) finalize function}.
*
* @return new instance of {@link Accumulator}.
*/
Accumulator build();
}
public static class AccumulatorBuilder
static class AccumulatorBuilder
implements AccumulatorInitBuilder, AccumulatorInitArgsBuilder, AccumulatorAccumulateBuilder,
AccumulatorAccumulateArgsBuilder, AccumulatorMergeBuilder, AccumulatorFinalizeBuilder {
@@ -426,6 +436,7 @@ public class ScriptOperators {
* @param function must not be {@literal null}.
* @return this.
*/
@Override
public AccumulatorBuilder init(String function) {
this.initFunction = function;
@@ -435,12 +446,15 @@ public class ScriptOperators {
/**
* Define the optional {@code initArgs} for the {@link #init(String)} function.
*
* @param args can be {@literal null}.
* @param function must not be {@literal null}.
* @return this.
*/
public AccumulatorBuilder initArgs(@Nullable List<Object> args) {
@Override
public AccumulatorBuilder initArgs(List<Object> args) {
this.initArgs = args != null ? new ArrayList<>(args) : Collections.emptyList();
Assert.notNull(args, "Args must not be null");
this.initArgs = new ArrayList<>(args);
return this;
}
@@ -458,8 +472,11 @@ public class ScriptOperators {
* @param function must not be {@literal null}.
* @return this.
*/
@Override
public AccumulatorBuilder accumulate(String function) {
Assert.notNull(function, "Accumulate function must not be null");
this.accumulateFunction = function;
return this;
}
@@ -467,12 +484,15 @@ public class ScriptOperators {
/**
* Define additional {@code accumulateArgs} for the {@link #accumulate(String)} function.
*
* @param args can be {@literal null}.
* @param args must not be {@literal null}.
* @return this.
*/
public AccumulatorBuilder accumulateArgs(@Nullable List<Object> args) {
@Override
public AccumulatorBuilder accumulateArgs(List<Object> args) {
this.accumulateArgs = args != null ? new ArrayList<>(args) : Collections.emptyList();
Assert.notNull(args, "Args must not be null");
this.accumulateArgs = new ArrayList<>(args);
return this;
}
@@ -491,8 +511,11 @@ public class ScriptOperators {
* @param function must not be {@literal null}.
* @return this.
*/
@Override
public AccumulatorBuilder merge(String function) {
Assert.notNull(function, "Merge function must not be null");
this.mergeFunction = function;
return this;
}
@@ -505,6 +528,8 @@ public class ScriptOperators {
*/
public AccumulatorBuilder lang(String lang) {
Assert.hasText(lang, "Lang must not be null nor empty! The default would be 'js'.");
this.lang = lang;
return this;
}
@@ -523,10 +548,26 @@ public class ScriptOperators {
* @param function must not be {@literal null}.
* @return new instance of {@link Accumulator}.
*/
@Override
public Accumulator finalize(String function) {
Assert.notNull(function, "Finalize function must not be null");
this.finalizeFunction = function;
Map<String, Object> args = createArgumentMap();
args.put(Fields.FINALIZE.toString(), finalizeFunction);
return new Accumulator(args);
}
@Override
public Accumulator build() {
return new Accumulator(createArgumentMap());
}
private Map<String, Object> createArgumentMap() {
Map<String, Object> args = new LinkedHashMap<>();
args.put(Fields.INIT.toString(), initFunction);
if (!CollectionUtils.isEmpty(initArgs)) {
@@ -537,12 +578,10 @@ public class ScriptOperators {
args.put(Fields.ACCUMULATE_ARGS.toString(), accumulateArgs);
}
args.put(Fields.MERGE.toString(), mergeFunction);
args.put(Fields.FINALIZE.toString(), finalizeFunction);
args.put(Fields.LANG.toString(), lang);
return new Accumulator(args);
return args;
}
}
}
}

View File

@@ -24,6 +24,8 @@ import org.bson.Document;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link ScriptOperators}.
*
* @author Christoph Strobl
*/
class ScriptOperatorsUnitTests {
@@ -32,20 +34,6 @@ class ScriptOperatorsUnitTests {
private static final Document EMPTY_ARGS_FUNCTION_DOCUMENT = new Document("body", FUNCTION_BODY)
.append("args", Collections.emptyList()).append("lang", "js");
@Test // DATAMONGO-2623
void functionWithoutArgsShouldBeRenderedCorrectly() {
assertThat(function(FUNCTION_BODY).toDocument(Aggregation.DEFAULT_CONTEXT))
.isEqualTo($function(EMPTY_ARGS_FUNCTION_DOCUMENT));
}
@Test // DATAMONGO-2623
void functionWithArgsShouldBeRenderedCorrectly() {
assertThat(function(FUNCTION_BODY).args("$name").toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo(
$function(new Document(EMPTY_ARGS_FUNCTION_DOCUMENT).append("args", Collections.singletonList("$name"))));
}
private static final String INIT_FUNCTION = "function() { return { count: 0, sum: 0 } }";
private static final String ACC_FUNCTION = "function(state, numCopies) { return { count: state.count + 1, sum: state.sum + numCopies } }";
private static final String MERGE_FUNCTION = "function(state1, state2) { return { count: state1.count + state2.count, sum: state1.sum + state2.sum } }";
@@ -64,6 +52,20 @@ class ScriptOperatorsUnitTests {
" }" + //
" }");
@Test // DATAMONGO-2623
void functionWithoutArgsShouldBeRenderedCorrectly() {
assertThat(function(FUNCTION_BODY).toDocument(Aggregation.DEFAULT_CONTEXT))
.isEqualTo($function(EMPTY_ARGS_FUNCTION_DOCUMENT));
}
@Test // DATAMONGO-2623
void functionWithArgsShouldBeRenderedCorrectly() {
assertThat(function(FUNCTION_BODY).args("$name").toDocument(Aggregation.DEFAULT_CONTEXT)).isEqualTo(
$function(new Document(EMPTY_ARGS_FUNCTION_DOCUMENT).append("args", Collections.singletonList("$name"))));
}
@Test // DATAMONGO-2623
void accumulatorWithStringInput() {