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();
+ }
+}