16
16
package org .springframework .data .mongodb .core ;
17
17
18
18
import java .util .ArrayList ;
19
+ import java .util .Collections ;
19
20
import java .util .List ;
20
21
import java .util .Map ;
21
22
import java .util .function .IntFunction ;
@@ -45,32 +46,110 @@ class ScrollUtils {
45
46
* @param idPropertyName
46
47
* @return
47
48
*/
48
- static KeySetScrollQuery createKeysetPaginationQuery (Query query , String idPropertyName ) {
49
+ static KeysetScrollQuery createKeysetPaginationQuery (Query query , String idPropertyName ) {
49
50
50
51
KeysetScrollPosition keyset = query .getKeyset ();
51
- Map <String , Object > keysetValues = keyset .getKeys ();
52
- Document queryObject = query .getQueryObject ();
52
+ KeysetScrollDirector director = KeysetScrollDirector .of (keyset .getDirection ());
53
+ Document sortObject = director .getSortObject (idPropertyName , query );
54
+ Document fieldsObject = director .getFieldsObject (query .getFieldsObject (), sortObject );
55
+ Document queryObject = director .createQuery (keyset , query .getQueryObject (), sortObject );
53
56
54
- Document sortObject = query . isSorted () ? query . getSortObject () : new Document ( );
55
- sortObject . put ( idPropertyName , 1 );
57
+ return new KeysetScrollQuery ( queryObject , fieldsObject , sortObject );
58
+ }
56
59
57
- // make sure we can extract the keyset
58
- Document fieldsObject = query .getFieldsObject ();
59
- if (!fieldsObject .isEmpty ()) {
60
- for (String field : sortObject .keySet ()) {
61
- fieldsObject .put (field , 1 );
62
- }
60
+ static <T > Window <T > createWindow (Query query , List <T > result , Class <?> sourceType , EntityOperations operations ) {
61
+
62
+ Document sortObject = query .getSortObject ();
63
+ KeysetScrollPosition keyset = query .getKeyset ();
64
+ KeysetScrollDirector director = KeysetScrollDirector .of (keyset .getDirection ());
65
+
66
+ director .postPostProcessResults (result );
67
+
68
+ IntFunction <KeysetScrollPosition > positionFunction = value -> {
69
+
70
+ T last = result .get (value );
71
+ Entity <T > entity = operations .forEntity (last );
72
+
73
+ Map <String , Object > keys = entity .extractKeys (sortObject , sourceType );
74
+ return KeysetScrollPosition .of (keys );
75
+ };
76
+
77
+ return createWindow (result , query .getLimit (), positionFunction );
78
+ }
79
+
80
+ static <T > Window <T > createWindow (List <T > result , int limit , IntFunction <? extends ScrollPosition > positionFunction ) {
81
+ return Window .from (getSubList (result , limit ), positionFunction , hasMoreElements (result , limit ));
82
+ }
83
+
84
+ static boolean hasMoreElements (List <?> result , int limit ) {
85
+ return !result .isEmpty () && result .size () > limit ;
86
+ }
87
+
88
+ static <T > List <T > getSubList (List <T > result , int limit ) {
89
+
90
+ if (limit > 0 && result .size () > limit ) {
91
+ return result .subList (0 , limit );
63
92
}
64
93
65
- List <Document > or = (List <Document >) queryObject .getOrDefault ("$or" , new ArrayList <>());
66
- List <String > sortKeys = new ArrayList <>(sortObject .keySet ());
94
+ return result ;
95
+ }
96
+
97
+ record KeysetScrollQuery (Document query , Document fields , Document sort ) {
67
98
68
- if (!keysetValues .isEmpty () && !keysetValues .keySet ().containsAll (sortKeys )) {
69
- throw new IllegalStateException ("KeysetScrollPosition does not contain all keyset values" );
99
+ }
100
+
101
+ /**
102
+ * Director for keyset scrolling.
103
+ */
104
+ static class KeysetScrollDirector {
105
+
106
+ private static final KeysetScrollDirector forward = new KeysetScrollDirector ();
107
+ private static final KeysetScrollDirector reverse = new ReverseKeysetScrollDirector ();
108
+
109
+ /**
110
+ * Factory method to obtain the right {@link KeysetScrollDirector}.
111
+ *
112
+ * @param direction
113
+ * @return
114
+ */
115
+ public static KeysetScrollDirector of (KeysetScrollPosition .Direction direction ) {
116
+ return direction == Direction .Forward ? forward : reverse ;
117
+ }
118
+
119
+ public Document getSortObject (String idPropertyName , Query query ) {
120
+
121
+ Document sortObject = query .isSorted () ? query .getSortObject () : new Document ();
122
+ sortObject .put (idPropertyName , 1 );
123
+
124
+ return sortObject ;
70
125
}
71
126
72
- // first query doesn't come with a keyset
73
- if (!keysetValues .isEmpty ()) {
127
+ public Document getFieldsObject (Document fieldsObject , Document sortObject ) {
128
+
129
+ // make sure we can extract the keyset
130
+ if (!fieldsObject .isEmpty ()) {
131
+ for (String field : sortObject .keySet ()) {
132
+ fieldsObject .put (field , 1 );
133
+ }
134
+ }
135
+
136
+ return fieldsObject ;
137
+ }
138
+
139
+ public Document createQuery (KeysetScrollPosition keyset , Document queryObject , Document sortObject ) {
140
+
141
+ Map <String , Object > keysetValues = keyset .getKeys ();
142
+ List <Document > or = (List <Document >) queryObject .getOrDefault ("$or" , new ArrayList <>());
143
+ List <String > sortKeys = new ArrayList <>(sortObject .keySet ());
144
+
145
+ // first query doesn't come with a keyset
146
+ if (keysetValues .isEmpty ()) {
147
+ return queryObject ;
148
+ }
149
+
150
+ if (!keysetValues .keySet ().containsAll (sortKeys )) {
151
+ throw new IllegalStateException ("KeysetScrollPosition does not contain all keyset values" );
152
+ }
74
153
75
154
// build matrix query for keyset paging that contains sort^2 queries
76
155
// reflecting a query that follows sort order semantics starting from the last returned keyset
@@ -89,7 +168,7 @@ static KeySetScrollQuery createKeysetPaginationQuery(Query query, String idPrope
89
168
throw new IllegalStateException (
90
169
"Cannot resume from KeysetScrollPosition. Offending key: '%s' is 'null'" .formatted (sortSegment ));
91
170
}
92
- sortConstraint .put (sortSegment , new Document (getComparator (sortOrder , keyset . getDirection () ), o ));
171
+ sortConstraint .put (sortSegment , new Document (getComparator (sortOrder ), o ));
93
172
break ;
94
173
}
95
174
@@ -100,61 +179,60 @@ static KeySetScrollQuery createKeysetPaginationQuery(Query query, String idPrope
100
179
or .add (sortConstraint );
101
180
}
102
181
}
103
- }
104
182
105
- if (!or .isEmpty ()) {
106
- queryObject .put ("$or" , or );
107
- }
183
+ if (!or .isEmpty ()) {
184
+ queryObject .put ("$or" , or );
185
+ }
108
186
109
- return new KeySetScrollQuery ( queryObject , fieldsObject , sortObject ) ;
110
- }
187
+ return queryObject ;
188
+ }
111
189
112
- private static String getComparator ( int sortOrder , Direction direction ) {
190
+ public < T > void postPostProcessResults ( List < T > result ) {
113
191
114
- // use gte/lte to include the object at the cursor/keyset so that
115
- // we can include it in the result to check whether there is a next object.
116
- // It needs to be filtered out later on.
117
- if (direction == Direction .Backward ) {
118
- return sortOrder == 0 ? "$gte" : "$lte" ;
119
192
}
120
193
121
- return sortOrder == 1 ? "$gt" : "$lt" ;
194
+ protected String getComparator (int sortOrder ) {
195
+ return sortOrder == 1 ? "$gt" : "$lt" ;
196
+ }
122
197
}
123
198
124
- static <T > Window <T > createWindow (Document sortObject , int limit , List <T > result , Class <?> sourceType ,
125
- EntityOperations operations ) {
126
-
127
- IntFunction <KeysetScrollPosition > positionFunction = value -> {
128
-
129
- T last = result .get (value );
130
- Entity <T > entity = operations .forEntity (last );
131
-
132
- Map <String , Object > keys = entity .extractKeys (sortObject , sourceType );
133
- return KeysetScrollPosition .of (keys );
134
- };
199
+ /**
200
+ * Reverse scrolling director variant applying {@link KeysetScrollPosition.Direction#Backward}. In reverse scrolling,
201
+ * we need to flip directions for the actual query so that we do not get everything from the top position and apply
202
+ * the limit but rather flip the sort direction, apply the limit and then reverse the result to restore the actual
203
+ * sort order.
204
+ */
205
+ private static class ReverseKeysetScrollDirector extends KeysetScrollDirector {
135
206
136
- return createWindow ( result , limit , positionFunction );
137
- }
207
+ @ Override
208
+ public Document getSortObject ( String idPropertyName , Query query ) {
138
209
139
- static <T > Window <T > createWindow (List <T > result , int limit , IntFunction <? extends ScrollPosition > positionFunction ) {
140
- return Window .from (getSubList (result , limit ), positionFunction , hasMoreElements (result , limit ));
141
- }
210
+ Document sortObject = super .getSortObject (idPropertyName , query );
142
211
143
- static boolean hasMoreElements (List <?> result , int limit ) {
144
- return !result .isEmpty () && result .size () > limit ;
145
- }
212
+ // flip sort direction for backward scrolling
146
213
147
- static <T > List <T > getSubList (List <T > result , int limit ) {
214
+ for (String field : sortObject .keySet ()) {
215
+ sortObject .put (field , sortObject .getInteger (field ) == 1 ? -1 : 1 );
216
+ }
148
217
149
- if (limit > 0 && result .size () > limit ) {
150
- return result .subList (0 , limit );
218
+ return sortObject ;
151
219
}
152
220
153
- return result ;
154
- }
221
+ @ Override
222
+ protected String getComparator ( int sortOrder ) {
155
223
156
- record KeySetScrollQuery (Document query , Document fields , Document sort ) {
224
+ // use gte/lte to include the object at the cursor/keyset so that
225
+ // we can include it in the result to check whether there is a next object.
226
+ // It needs to be filtered out later on.
227
+ return sortOrder == 1 ? "$gte" : "$lte" ;
228
+ }
157
229
230
+ @ Override
231
+ public <T > void postPostProcessResults (List <T > result ) {
232
+ // flip direction of the result list as we need to accomodate for the flipped sort order for proper offset
233
+ // querying.
234
+ Collections .reverse (result );
235
+ }
158
236
}
159
237
160
238
}
0 commit comments