From 581961e79a51695385eeafebad24ee0ebd94b78b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 19 Mar 2020 10:21:17 +0100 Subject: [PATCH] DATAMONGO-931 - Add support for $redact aggregation. We now support $redact via Aggregation.redact. Aggregation.redact(ConditionalOperators.when(Criteria.where("level").is(5)) .then(RedactOperation.PRUNE) .otherwise(RedactOperation.DESCEND)); Original pull request: #844. --- .../mongodb/core/aggregation/Aggregation.java | 20 ++ .../core/aggregation/RedactOperation.java | 239 ++++++++++++++++++ .../aggregation/RedactOperationUnitTests.java | 94 +++++++ 3 files changed, 353 insertions(+) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java index efcb26f24..8a29c0dfc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Aggregation.java @@ -657,6 +657,26 @@ public class Aggregation { return new CountOperationBuilder(); } + /** + * Creates a new {@link RedactOperation} that can restrict the content of a document based on information stored + * within the document itself. + * + *
+	 * 
+	 * Aggregation.redact(ConditionalOperators.when(Criteria.where("level").is(5)) //
+	 * 		.then(RedactOperation.PRUNE) //
+	 * 		.otherwise(RedactOperation.DESCEND));
+	 * 
+ * + * @param condition Any {@link AggregationExpression} that resolves to {@literal $$DESCEND}, {@literal $$PRUNE}, or + * {@literal $$KEEP}. Must not be {@literal null}. + * @return new instance of {@link RedactOperation}. Never {@literal null}. + * @since 3.0 + */ + public static RedactOperation redact(AggregationExpression condition) { + return new RedactOperation(condition); + } + /** * Creates a new {@link Fields} instance for the given field names. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java new file mode 100644 index 000000000..0ef9914b1 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java @@ -0,0 +1,239 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.aggregation; + +import org.bson.Document; +import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond.ThenBuilder; +import org.springframework.data.mongodb.core.query.CriteriaDefinition; +import org.springframework.util.Assert; + +/** + * {@link RedactOperation} allows to restrict the content of a {@link Document} based on information stored within + * itself. + * + *
+ * RedactOperation.builder() //
+ * 		.when(Criteria.where("level").is(5)) //
+ * 		.thenPrune() //
+ * 		.otherwiseDescend() //
+ * 		.build();
+ * 
+ * + * @author Christoph Strobl + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/redact/ + * @since 3.0 + */ +public class RedactOperation implements AggregationOperation { + + /** + * Return fields at the current document level. Exclude embedded ones. + */ + public static final String DESCEND = "$$DESCEND"; + + /** + * Return/Keep all fields at the current document/embedded level. + */ + public static final String KEEP = "$$KEEP"; + + /** + * Exclude all fields at this current document/embedded level. + */ + public static final String PRUNE = "$$PRUNE"; + + private final AggregationExpression condition; + + /** + * Create new {@link RedactOperation}. + * + * @param condition Any {@link AggregationExpression} that resolves to {@literal $$DESCEND}, {@literal $$PRUNE}, or + * {@literal $$KEEP}. Must not be {@literal null}. + */ + public RedactOperation(AggregationExpression condition) { + + Assert.notNull(condition, "Condition must not be null!"); + this.condition = condition; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.springframework.data.mongodb.core.aggregation.AggregationOperationContext) + */ + @Override + public Document toDocument(AggregationOperationContext context) { + return new Document("$redact", condition.toDocument(context)); + } + + /** + * Obtain a new instance of {@link RedactOperationBuilder} to specify condition and outcome of the {@literal $redact} + * operation. + * + * @return new instance of {@link RedactOperationBuilder}. + */ + public static RedactOperationBuilder builder() { + return new RedactOperationBuilder(); + } + + /** + * Builder to create new instance of {@link RedactOperation}. + * + * @author Christoph Strobl + */ + public static class RedactOperationBuilder { + + private Object when; + private Object then; + private Object otherwise; + + /** + * Specify the evaluation condition. + * + * @param criteria must not be {@literal null}. + * @return this. + */ + public RedactOperationBuilder when(CriteriaDefinition criteria) { + + this.when = criteria; + return this; + } + + /** + * Specify the evaluation condition. + * + * @param condition must not be {@literal null}. + * @return this. + */ + public RedactOperationBuilder when(AggregationExpression condition) { + + this.when = condition; + return this; + } + + /** + * Specify the evaluation condition. + * + * @param condition must not be {@literal null}. + * @return this. + */ + public RedactOperationBuilder when(Document condition) { + + this.when = condition; + return this; + } + + /** + * Return fields at the current document level and exclude embedded ones if the condition is met. + * + * @return this. + */ + public RedactOperationBuilder thenDescend() { + return then(DESCEND); + } + + /** + * Return/Keep all fields at the current document/embedded level if the condition is met. + * + * @return this. + */ + public RedactOperationBuilder thenKeep() { + return then(KEEP); + } + + /** + * Exclude all fields at this current document/embedded level if the condition is met. + * + * @return this. + */ + public RedactOperationBuilder thenPrune() { + return then(PRUNE); + } + + /** + * Define the outcome (anything that resolves to {@literal $$DESCEND}, {@literal $$PRUNE}, or {@literal $$KEEP}) + * when the condition is met. + * + * @param then must not be {@literal null}. + * @return this. + */ + public RedactOperationBuilder then(Object then) { + + this.then = then; + return this; + } + + /** + * Return fields at the current document level and exclude embedded ones if the condition is not met. + * + * @return this. + */ + public RedactOperationBuilder otherwiseDescend() { + return otherwise(DESCEND); + } + + /** + * Return/Keep all fields at the current document/embedded level if the condition is not met. + * + * @return this. + */ + public RedactOperationBuilder otherwiseKeep() { + return otherwise(KEEP); + } + + /** + * Exclude all fields at this current document/embedded level if the condition is not met. + * + * @return this. + */ + public RedactOperationBuilder otherwisePrune() { + return otherwise(PRUNE); + } + + /** + * Define the outcome (anything that resolves to {@literal $$DESCEND}, {@literal $$PRUNE}, or {@literal $$KEEP}) + * when the condition is not met. + * + * @param otherwise must not be {@literal null}. + * @return this. + */ + public RedactOperationBuilder otherwise(Object otherwise) { + this.otherwise = otherwise; + return this; + } + + /** + * @return new instance of {@link RedactOperation}. + */ + public RedactOperation build() { + return new RedactOperation(when().then(then).otherwise(otherwise)); + } + + private ThenBuilder when() { + + if (when instanceof CriteriaDefinition) { + return ConditionalOperators.Cond.when((CriteriaDefinition) when); + } + if (when instanceof AggregationExpression) { + return ConditionalOperators.Cond.when((AggregationExpression) when); + } + if (when instanceof Document) { + return ConditionalOperators.Cond.when((Document) when); + } + + throw new IllegalArgumentException(String.format( + "Invalid Condition. Expected CriteriaDefinition, AggregationExpression or Document but was %s.", when)); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java new file mode 100644 index 000000000..93b422468 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2020 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.aggregation; + +import static org.assertj.core.api.Assertions.*; + +import lombok.Data; + +import java.util.Arrays; + +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.convert.QueryMapper; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + */ +class RedactOperationUnitTests { + + Document expected = new Document("$redact", + new Document("$cond", new Document("if", new Document("$eq", Arrays.asList("$level", 5))) + .append("then", "$$PRUNE").append("else", "$$DESCEND"))); + Document expectedMapped = new Document("$redact", + new Document("$cond", new Document("if", new Document("$eq", Arrays.asList("$le_v_el", 5))) + .append("then", "$$PRUNE").append("else", "$$DESCEND"))); + + @Test // DATAMONGO-931 + void errorsOnNullExpression() { + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> new RedactOperation(null)); + } + + @Test // DATAMONGO-931 + void mapsAggregationExpressionCorrectly() { + + assertThat(new RedactOperation(ConditionalOperators.when(Criteria.where("level").is(5)) // + .then(RedactOperation.PRUNE) // + .otherwise(RedactOperation.DESCEND)).toDocument(contextFor(null))).isEqualTo(expected); + } + + @Test // DATAMONGO-931 + void mapsAggregationExpressionViaBuilderCorrectly() { + + assertThat(RedactOperation.builder().when(Criteria.where("level").is(5)) // + .thenPrune() // + .otherwiseDescend().build().toDocument(contextFor(null))).isEqualTo(expected); + } + + @Test // DATAMONGO-931 + void mapsTypedAggregationExpressionCorrectly() { + + assertThat(new RedactOperation(ConditionalOperators.when(Criteria.where("level").is(5)) // + .then(RedactOperation.PRUNE) // + .otherwise(RedactOperation.DESCEND)).toDocument(contextFor(DomainType.class))).isEqualTo(expectedMapped); + } + + @Data + static class DomainType { + + @Field("le_v_el") String level; + } + + private static AggregationOperationContext contextFor(@Nullable Class type) { + + if (type == null) { + return Aggregation.DEFAULT_CONTEXT; + } + + MappingMongoConverter mongoConverter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, + new MongoMappingContext()); + mongoConverter.afterPropertiesSet(); + + return new TypeBasedAggregationOperationContext(type, mongoConverter.getMappingContext(), + new QueryMapper(mongoConverter)).continueOnMissingFieldReference(); + } +}