/*
* Copyright 2016-2017 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
*
* http://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 java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.Output;
import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField;
import org.springframework.util.Assert;
import org.bson.Document;
/**
* Encapsulates the aggregation framework {@code $facet}-operation. <br />
* Facet of {@link AggregationOperation}s to be used in an {@link Aggregation}. Processes multiple
* {@link AggregationOperation} pipelines within a single stage on the same set of input documents. Each sub-pipeline
* has its own field in the output document where its results are stored as an array of documents.
* {@link FacetOperation} enables various aggregations on the same set of input documents, without needing to retrieve
* the input documents multiple times. <br />
* As of MongoDB 3.4, {@link FacetOperation} cannot be used with nested pipelines containing {@link GeoNearOperation},
* {@link OutOperation} and {@link FacetOperation}. <br />
* We recommend to use the static factory method {@link Aggregation#facet()} instead of creating instances of this class
* directly.
*
* @author Mark Paluch
* @author Christoph Strobl
* @since 1.10
* @see <a href="https://docs.mongodb.org/manual/reference/aggregation/facet/">MongoDB Aggregation Framework: $facet</a>
*/
public class FacetOperation implements FieldsExposingAggregationOperation {
/**
* Empty (initial) {@link FacetOperation}.
*/
public static final FacetOperation EMPTY = new FacetOperation();
private final Facets facets;
/**
* Creates a new {@link FacetOperation}.
*/
public FacetOperation() {
this(Facets.EMPTY);
}
private FacetOperation(Facets facets) {
this.facets = facets;
}
/**
* Creates a new {@link FacetOperationBuilder} to append a new facet using {@literal operations}. <br />
* {@link FacetOperationBuilder} takes a pipeline of {@link AggregationOperation} to categorize documents into a
* single facet.
*
* @param operations must not be {@literal null} or empty.
* @return
*/
public FacetOperationBuilder and(AggregationOperation... operations) {
Assert.notNull(operations, "AggregationOperations must not be null!");
Assert.notEmpty(operations, "AggregationOperations must not be empty!");
return new FacetOperationBuilder(facets, Arrays.asList(operations));
}
/* (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("$facet", facets.toDocument(context));
}
/* (non-Javadoc)
* @see org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation#getFields()
*/
@Override
public ExposedFields getFields() {
return facets.asExposedFields();
}
/**
* Builder for {@link FacetOperation} by adding existing and the new pipeline of {@link AggregationOperation} to the
* new {@link FacetOperation}.
*
* @author Mark Paluch
*/
public static class FacetOperationBuilder {
private final Facets current;
private final List<AggregationOperation> operations;
private FacetOperationBuilder(Facets current, List<AggregationOperation> operations) {
this.current = current;
this.operations = operations;
}
/**
* Creates a new {@link FacetOperation} that contains the configured pipeline of {@link AggregationOperation}
* exposed as {@literal fieldName} in the resulting facet document.
*
* @param fieldName must not be {@literal null} or empty.
* @return
*/
public FacetOperation as(String fieldName) {
Assert.hasText(fieldName, "FieldName must not be null or empty!");
return new FacetOperation(current.and(fieldName, operations));
}
}
/**
* Encapsulates multiple {@link Facet}s
*
* @author Mark Paluch
*/
private static class Facets {
private static final Facets EMPTY = new Facets(Collections.<Facet> emptyList());
private List<Facet> facets;
/**
* Creates a new {@link Facets} given {@link List} of {@link Facet}.
*
* @param facets
*/
private Facets(List<Facet> facets) {
this.facets = facets;
}
/**
* @return the {@link ExposedFields} derived from {@link Output}.
*/
ExposedFields asExposedFields() {
ExposedFields fields = ExposedFields.from();
for (Facet facet : facets) {
fields = fields.and(facet.getExposedField());
}
return fields;
}
protected Document toDocument(AggregationOperationContext context) {
Document document = new Document();
for (Facet facet : facets) {
document.put(facet.getExposedField().getName(), facet.toDocuments(context));
}
return document;
}
/**
* Adds a facet to this {@link Facets}.
*
* @param fieldName must not be {@literal null}.
* @param operations must not be {@literal null}.
* @return the new {@link Facets}.
*/
Facets and(String fieldName, List<AggregationOperation> operations) {
Assert.hasText(fieldName, "FieldName must not be null or empty!");
Assert.notNull(operations, "AggregationOperations must not be null!");
List<Facet> facets = new ArrayList<Facet>(this.facets.size() + 1);
facets.addAll(this.facets);
facets.add(new Facet(new ExposedField(fieldName, true), operations));
return new Facets(facets);
}
}
/**
* A single facet with a {@link ExposedField} and its {@link AggregationOperation} pipeline.
*
* @author Mark Paluch
*/
private static class Facet {
private final ExposedField exposedField;
private final List<AggregationOperation> operations;
/**
* Creates a new {@link Facet} given {@link ExposedField} and {@link AggregationOperation} pipeline.
*
* @param exposedField must not be {@literal null}.
* @param operations must not be {@literal null}.
*/
Facet(ExposedField exposedField, List<AggregationOperation> operations) {
Assert.notNull(exposedField, "ExposedField must not be null!");
Assert.notNull(operations, "AggregationOperations must not be null!");
this.exposedField = exposedField;
this.operations = operations;
}
ExposedField getExposedField() {
return exposedField;
}
protected List<Document> toDocuments(AggregationOperationContext context) {
return AggregationOperationRenderer.toDocument(operations, context);
}
}
}