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.
This commit is contained in:
Christoph Strobl
2020-03-19 10:21:17 +01:00
committed by Mark Paluch
parent 0e0d726457
commit 581961e79a
3 changed files with 353 additions and 0 deletions

View File

@@ -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.
*
* <pre class="code">
*
* Aggregation.redact(ConditionalOperators.when(Criteria.where("level").is(5)) //
* .then(RedactOperation.PRUNE) //
* .otherwise(RedactOperation.DESCEND));
* </pre>
*
* @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.
*

View File

@@ -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.
*
* <pre class="code">
* RedactOperation.builder() //
* .when(Criteria.where("level").is(5)) //
* .thenPrune() //
* .otherwiseDescend() //
* .build();
* </pre>
*
* @author Christoph Strobl
* @see <a href=
* "https://docs.mongodb.com/manual/reference/operator/aggregation/redact/">https://docs.mongodb.com/manual/reference/operator/aggregation/redact/</a>
* @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));
}
}
}

View File

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