Skip to content

Commit 16a35e0

Browse files
christophstroblmp911de
authored andcommitted
Add support for $densify aggregation stage.
See #4139 Original pull request: #4182.
1 parent 79f05c3 commit 16a35e0

File tree

2 files changed

+527
-0
lines changed

2 files changed

+527
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.aggregation;
17+
18+
import java.time.temporal.ChronoUnit;
19+
import java.util.Arrays;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.Locale;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.function.Consumer;
25+
import java.util.stream.Collectors;
26+
27+
import org.bson.Document;
28+
import org.springframework.lang.Nullable;
29+
import org.springframework.util.Assert;
30+
import org.springframework.util.ObjectUtils;
31+
32+
/***
33+
* Encapsulates the aggregation framework {@code $densify}-operation.
34+
*
35+
* @author Christoph Strobl
36+
* @since 4.0
37+
*/
38+
public class DensifyOperation implements AggregationOperation {
39+
40+
private @Nullable Field field;
41+
private @Nullable List<?> partitionBy;
42+
private @Nullable Range range;
43+
44+
protected DensifyOperation(@Nullable Field field, @Nullable List<?> partitionBy, @Nullable Range range) {
45+
46+
this.field = field;
47+
this.partitionBy = partitionBy;
48+
this.range = range;
49+
}
50+
51+
/**
52+
* Obtain a builder to create the {@link DensifyOperation}.
53+
*
54+
* @return new instance of {@link DensifyOperationBuilder}.
55+
*/
56+
public static DensifyOperationBuilder builder() {
57+
return new DensifyOperationBuilder();
58+
}
59+
60+
@Override
61+
public Document toDocument(AggregationOperationContext context) {
62+
63+
Document densify = new Document();
64+
densify.put("field", context.getReference(field).getRaw());
65+
if (!ObjectUtils.isEmpty(partitionBy)) {
66+
densify.put("partitionByFields", partitionBy.stream().map(it -> {
67+
if (it instanceof Field field) {
68+
return context.getReference(field).getRaw();
69+
}
70+
if (it instanceof AggregationExpression expression) {
71+
return expression.toDocument(context);
72+
}
73+
return it;
74+
}).collect(Collectors.toList()));
75+
}
76+
densify.put("range", range.toDocument(context));
77+
return new Document("$densify", densify);
78+
}
79+
80+
/**
81+
* The {@link Range} specifies how the data is densified.
82+
*/
83+
public interface Range {
84+
85+
/**
86+
* Add documents spanning the range of values within the given lower (inclusive) and upper (exclusive) bound.
87+
*
88+
* @param lower must not be {@literal null}.
89+
* @param upper must not be {@literal null}.
90+
* @return new instance of {@link DensifyRange}.
91+
*/
92+
static DensifyRange bounded(Object lower, Object upper) {
93+
return new BoundedRange(lower, upper, DensifyUnits.NONE);
94+
}
95+
96+
/**
97+
* Add documents spanning the full value range.
98+
*
99+
* @return new instance of {@link DensifyRange}.
100+
*/
101+
static DensifyRange full() {
102+
103+
return new DensifyRange(DensifyUnits.NONE) {
104+
105+
@Override
106+
Object getBounds(AggregationOperationContext ctx) {
107+
return "full";
108+
}
109+
};
110+
}
111+
112+
/**
113+
* Add documents spanning the full value range for each partition.
114+
*
115+
* @return new instance of {@link DensifyRange}.
116+
*/
117+
static DensifyRange partition() {
118+
return new DensifyRange(DensifyUnits.NONE) {
119+
120+
@Override
121+
Object getBounds(AggregationOperationContext ctx) {
122+
return "partition";
123+
}
124+
};
125+
}
126+
127+
/**
128+
* Obtain the document representation of the window in a default {@link AggregationOperationContext context}.
129+
*
130+
* @return never {@literal null}.
131+
*/
132+
default Document toDocument() {
133+
return toDocument(Aggregation.DEFAULT_CONTEXT);
134+
}
135+
136+
/**
137+
* Obtain the document representation of the window in the given {@link AggregationOperationContext context}.
138+
*
139+
* @return never {@literal null}.
140+
*/
141+
Document toDocument(AggregationOperationContext ctx);
142+
}
143+
144+
/**
145+
* Base {@link Range} implementation.
146+
*
147+
* @author Christoph Strobl
148+
*/
149+
public static abstract class DensifyRange implements Range {
150+
151+
private @Nullable DensifyUnit unit;
152+
private Number step;
153+
154+
public DensifyRange(DensifyUnit unit) {
155+
this.unit = unit;
156+
}
157+
158+
@Override
159+
public Document toDocument(AggregationOperationContext ctx) {
160+
161+
Document range = new Document("step", step);
162+
if (unit != null && !DensifyUnits.NONE.equals(unit)) {
163+
range.put("unit", unit.name().toLowerCase(Locale.US));
164+
}
165+
range.put("bounds", getBounds(ctx));
166+
return range;
167+
}
168+
169+
/**
170+
* Set the increment for the value.
171+
*
172+
* @param step must not be {@literal null}.
173+
* @return this.
174+
*/
175+
public DensifyRange incrementBy(Number step) {
176+
this.step = step;
177+
return this;
178+
}
179+
180+
/**
181+
* Set the increment for the value.
182+
*
183+
* @param step must not be {@literal null}.
184+
* @return this.
185+
*/
186+
public DensifyRange incrementBy(Number step, DensifyUnit unit) {
187+
this.step = step;
188+
return unit(unit);
189+
}
190+
191+
/**
192+
* Set the {@link DensifyUnit unit} for the step field.
193+
*
194+
* @param unit
195+
* @return this.
196+
*/
197+
public DensifyRange unit(DensifyUnit unit) {
198+
199+
this.unit = unit;
200+
return this;
201+
}
202+
203+
abstract Object getBounds(AggregationOperationContext ctx);
204+
}
205+
206+
/**
207+
* {@link Range} implementation holding lower and upper bound values.
208+
*
209+
* @author Christoph Strobl
210+
*/
211+
public static class BoundedRange extends DensifyRange {
212+
213+
private List<Object> bounds;
214+
215+
protected BoundedRange(Object lower, Object upper, DensifyUnit unit) {
216+
217+
super(unit);
218+
this.bounds = Arrays.asList(lower, upper);
219+
}
220+
221+
@Override
222+
List<Object> getBounds(AggregationOperationContext ctx) {
223+
return bounds.stream().map(it -> {
224+
if (it instanceof AggregationExpression expression) {
225+
return expression.toDocument(ctx);
226+
}
227+
return it;
228+
}).collect(Collectors.toList());
229+
}
230+
}
231+
232+
/**
233+
* The actual time unit to apply to a {@link Range}.
234+
*/
235+
public interface DensifyUnit {
236+
237+
String name();
238+
239+
/**
240+
* Converts the given time unit into a {@link DensifyUnit}. Supported units are: days, hours, minutes, seconds, and
241+
* milliseconds.
242+
*
243+
* @param timeUnit the time unit to convert, must not be {@literal null}.
244+
* @return
245+
* @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion.
246+
*/
247+
static DensifyUnit from(TimeUnit timeUnit) {
248+
249+
Assert.notNull(timeUnit, "TimeUnit must not be null");
250+
251+
switch (timeUnit) {
252+
case DAYS:
253+
return DensifyUnits.DAY;
254+
case HOURS:
255+
return DensifyUnits.HOUR;
256+
case MINUTES:
257+
return DensifyUnits.MINUTE;
258+
case SECONDS:
259+
return DensifyUnits.SECOND;
260+
case MILLISECONDS:
261+
return DensifyUnits.MILLISECOND;
262+
}
263+
264+
throw new IllegalArgumentException(String.format("Cannot create DensifyUnit from %s", timeUnit));
265+
}
266+
267+
/**
268+
* Converts the given chrono unit into a {@link DensifyUnit}. Supported units are: years, weeks, months, days,
269+
* hours, minutes, seconds, and millis.
270+
*
271+
* @param chronoUnit the chrono unit to convert, must not be {@literal null}.
272+
* @return
273+
* @throws IllegalArgumentException if the {@link TimeUnit} is {@literal null} or not supported for conversion.
274+
*/
275+
static DensifyUnits from(ChronoUnit chronoUnit) {
276+
277+
switch (chronoUnit) {
278+
case YEARS:
279+
return DensifyUnits.YEAR;
280+
case WEEKS:
281+
return DensifyUnits.WEEK;
282+
case MONTHS:
283+
return DensifyUnits.MONTH;
284+
case DAYS:
285+
return DensifyUnits.DAY;
286+
case HOURS:
287+
return DensifyUnits.HOUR;
288+
case MINUTES:
289+
return DensifyUnits.MINUTE;
290+
case SECONDS:
291+
return DensifyUnits.SECOND;
292+
case MILLIS:
293+
return DensifyUnits.MILLISECOND;
294+
}
295+
296+
throw new IllegalArgumentException(String.format("Cannot create DensifyUnit from %s", chronoUnit));
297+
}
298+
}
299+
300+
/**
301+
* Quick access to available {@link DensifyUnit units}.
302+
*/
303+
public enum DensifyUnits implements DensifyUnit {
304+
NONE, YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND, MILLISECOND
305+
}
306+
307+
public static class DensifyOperationBuilder {
308+
309+
DensifyOperation target;
310+
311+
public DensifyOperationBuilder() {
312+
this.target = new DensifyOperation(null, Collections.emptyList(), null);
313+
}
314+
315+
/**
316+
* Set the field to densify.
317+
*
318+
* @param fieldname must not be {@literal null}.
319+
* @return this.
320+
*/
321+
public DensifyOperationBuilder densify(String fieldname) {
322+
this.target.field = Fields.field(fieldname);
323+
return this;
324+
}
325+
326+
/**
327+
* Set the fields used for grouping documents.
328+
*
329+
* @param fields must not be {@literal null}.
330+
* @return this.
331+
*/
332+
public DensifyOperationBuilder partitionBy(String... fields) {
333+
target.partitionBy = Fields.fields(fields).asList();
334+
return this;
335+
}
336+
337+
/**
338+
* Set the operational range.
339+
*
340+
* @param range must not be {@literal null}.
341+
* @return this.
342+
*/
343+
public DensifyOperationBuilder range(Range range) {
344+
345+
target.range = range;
346+
return this;
347+
}
348+
349+
/**
350+
* Operate on full range.
351+
*
352+
* @param consumer
353+
* @return this.
354+
*/
355+
public DensifyOperationBuilder fullRange(Consumer<DensifyRange> consumer) {
356+
357+
DensifyRange range = Range.full();
358+
consumer.accept(range);
359+
360+
return range(range);
361+
}
362+
363+
/**
364+
* Operate on full range.
365+
*
366+
* @param consumer
367+
* @return this.
368+
*/
369+
public DensifyOperationBuilder partitionRange(Consumer<DensifyRange> consumer) {
370+
371+
DensifyRange range = Range.partition();
372+
consumer.accept(range);
373+
374+
return range(range);
375+
}
376+
377+
DensifyOperation build() {
378+
return new DensifyOperation(target.field, target.partitionBy, target.range);
379+
}
380+
}
381+
}

0 commit comments

Comments
 (0)