/* * 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); } } }