1717
1818import java .time .Duration ;
1919import java .util .Map ;
20+ import java .util .function .Function ;
2021import java .util .function .IntUnaryOperator ;
2122import java .util .function .LongUnaryOperator ;
2223
2829import org .springframework .data .mongodb .core .aggregation .Aggregation ;
2930import org .springframework .data .mongodb .core .aggregation .AggregationOptions ;
3031import org .springframework .data .mongodb .core .aggregation .AggregationPipeline ;
32+ import org .springframework .data .mongodb .core .aggregation .AggregationResults ;
33+ import org .springframework .data .mongodb .core .aggregation .TypedAggregation ;
3134import org .springframework .data .mongodb .core .convert .MongoConverter ;
3235import org .springframework .data .mongodb .core .mapping .FieldName ;
36+ import org .springframework .data .mongodb .core .mapping .MongoSimpleTypes ;
3337import org .springframework .data .mongodb .core .query .Collation ;
3438import org .springframework .data .mongodb .core .query .Meta ;
3539import org .springframework .data .mongodb .core .query .Query ;
40+ import org .springframework .data .repository .query .ResultProcessor ;
41+ import org .springframework .data .repository .query .ReturnedType ;
42+ import org .springframework .data .util .ReflectionUtils ;
43+ import org .springframework .data .util .TypeInformation ;
3644import org .springframework .lang .Nullable ;
3745import org .springframework .util .ClassUtils ;
3846import org .springframework .util .ObjectUtils ;
@@ -116,13 +124,15 @@ static AggregationOptions.Builder applyHint(AggregationOptions.Builder builder,
116124 }
117125
118126 /**
119- * If present apply the preference from the {@link org.springframework.data.mongodb.repository.ReadPreference} annotation.
127+ * If present apply the preference from the {@link org.springframework.data.mongodb.repository.ReadPreference}
128+ * annotation.
120129 *
121130 * @param builder must not be {@literal null}.
122131 * @return never {@literal null}.
123132 * @since 4.2
124133 */
125- static AggregationOptions .Builder applyReadPreference (AggregationOptions .Builder builder , MongoQueryMethod queryMethod ) {
134+ static AggregationOptions .Builder applyReadPreference (AggregationOptions .Builder builder ,
135+ MongoQueryMethod queryMethod ) {
126136
127137 if (!queryMethod .hasAnnotatedReadPreference ()) {
128138 return builder ;
@@ -131,6 +141,93 @@ static AggregationOptions.Builder applyReadPreference(AggregationOptions.Builder
131141 return builder .readPreference (ReadPreference .valueOf (queryMethod .getAnnotatedReadPreference ()));
132142 }
133143
144+ static AggregationOptions computeOptions (MongoQueryMethod method , ConvertingParameterAccessor accessor ,
145+ AggregationPipeline pipeline , ValueExpressionEvaluator evaluator ) {
146+
147+ AggregationOptions .Builder builder = Aggregation .newAggregationOptions ();
148+
149+ AggregationUtils .applyCollation (builder , method .getAnnotatedCollation (), accessor , evaluator );
150+ AggregationUtils .applyMeta (builder , method );
151+ AggregationUtils .applyHint (builder , method );
152+ AggregationUtils .applyReadPreference (builder , method );
153+
154+ TypeInformation <?> returnType = method .getReturnType ();
155+ if (returnType .getComponentType () != null ) {
156+ returnType = returnType .getRequiredComponentType ();
157+ }
158+ if (ReflectionUtils .isVoid (returnType .getType ()) && pipeline .isOutOrMerge ()) {
159+ builder .skipOutput ();
160+ }
161+
162+ return builder .build ();
163+ }
164+
165+ /**
166+ * Prepares the AggregationPipeline including type discovery and calling {@link AggregationCallback} to run the
167+ * aggregation.
168+ */
169+ @ Nullable
170+ static <T > T doAggregate (AggregationPipeline pipeline , MongoQueryMethod method , ResultProcessor processor ,
171+ ConvertingParameterAccessor accessor ,
172+ Function <MongoParameterAccessor , ValueExpressionEvaluator > evaluatorFunction , AggregationCallback <T > callback ) {
173+
174+ Class <?> sourceType = method .getDomainClass ();
175+ ReturnedType returnedType = processor .getReturnedType ();
176+ // 🙈Interface Projections do not happen on the Aggregation level but through our repository infrastructure.
177+ // Non-projections and raw results (AggregationResults<…>) are handled here. Interface projections read a Document
178+ // and DTO projections read the returned type.
179+ // We also support simple return types (String) that are read from a Document
180+ TypeInformation <?> returnType = method .getReturnType ();
181+ Class <?> returnElementType = (returnType .getComponentType () != null ? returnType .getRequiredComponentType ()
182+ : returnType ).getType ();
183+ Class <?> entityType ;
184+
185+ boolean isRawAggregationResult = ClassUtils .isAssignable (AggregationResults .class , method .getReturnedObjectType ());
186+
187+ if (returnElementType .equals (Document .class )) {
188+ entityType = sourceType ;
189+ } else {
190+ entityType = returnElementType ;
191+ }
192+
193+ AggregationUtils .appendSortIfPresent (pipeline , accessor , entityType );
194+
195+ if (method .isSliceQuery ()) {
196+ AggregationUtils .appendLimitAndOffsetIfPresent (pipeline , accessor , LongUnaryOperator .identity (),
197+ limit -> limit + 1 );
198+ } else {
199+ AggregationUtils .appendLimitAndOffsetIfPresent (pipeline , accessor );
200+ }
201+
202+ AggregationOptions options = AggregationUtils .computeOptions (method , accessor , pipeline ,
203+ evaluatorFunction .apply (accessor ));
204+ TypedAggregation <?> aggregation = new TypedAggregation <>(sourceType , pipeline .getOperations (), options );
205+
206+ boolean isSimpleReturnType = MongoSimpleTypes .HOLDER .isSimpleType (returnElementType );
207+ Class <?> typeToRead ;
208+
209+ if (isSimpleReturnType ) {
210+ typeToRead = Document .class ;
211+ } else if (isRawAggregationResult ) {
212+ typeToRead = returnElementType ;
213+ } else {
214+
215+ if (returnedType .isProjecting ()) {
216+ typeToRead = returnedType .getReturnedType ().isInterface () ? Document .class : returnedType .getReturnedType ();
217+ } else {
218+ typeToRead = entityType ;
219+ }
220+ }
221+
222+ return callback .doAggregate (aggregation , sourceType , typeToRead , returnElementType , isSimpleReturnType ,
223+ isRawAggregationResult );
224+ }
225+
226+ static AggregationPipeline computePipeline (AbstractMongoQuery mongoQuery , MongoQueryMethod method ,
227+ ConvertingParameterAccessor accessor ) {
228+ return new AggregationPipeline (mongoQuery .parseAggregationPipeline (method .getAnnotatedAggregation (), accessor ));
229+ }
230+
134231 /**
135232 * Append {@code $sort} aggregation stage if {@link ConvertingParameterAccessor#getSort()} is present.
136233 *
@@ -139,7 +236,7 @@ static AggregationOptions.Builder applyReadPreference(AggregationOptions.Builder
139236 * @param targetType
140237 */
141238 static void appendSortIfPresent (AggregationPipeline aggregationPipeline , ConvertingParameterAccessor accessor ,
142- Class <?> targetType ) {
239+ @ Nullable Class <?> targetType ) {
143240
144241 if (accessor .getSort ().isUnsorted ()) {
145242 return ;
@@ -254,4 +351,26 @@ private static <T> T getPotentiallyConvertedSimpleTypeValue(MongoConverter conve
254351
255352 return converter .getConversionService ().convert (value , targetType );
256353 }
354+
355+ /**
356+ * Interface to invoke an aggregation along with source, intermediate, and target types.
357+ *
358+ * @param <T>
359+ */
360+ interface AggregationCallback <T > {
361+
362+ /**
363+ * @param aggregation
364+ * @param domainType
365+ * @param typeToRead
366+ * @param elementType
367+ * @param simpleType whether the aggregation returns {@link Document} or a
368+ * {@link org.springframework.data.mapping.model.SimpleTypeHolder simple type}.
369+ * @param rawResult whether the aggregation returns {@link AggregationResults}.
370+ * @return
371+ */
372+ @ Nullable
373+ T doAggregate (TypedAggregation <?> aggregation , Class <?> domainType , Class <?> typeToRead , Class <?> elementType ,
374+ boolean simpleType , boolean rawResult );
375+ }
257376}
0 commit comments