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 .web ;
17
+
18
+ import static org .springframework .web .util .UriComponentsBuilder .fromUri ;
19
+
20
+ import java .util .ArrayList ;
21
+ import java .util .Collections ;
22
+ import java .util .List ;
23
+ import java .util .Optional ;
24
+
25
+ import org .springframework .core .MethodParameter ;
26
+ import org .springframework .data .domain .PageRequest ;
27
+ import org .springframework .data .domain .Pageable ;
28
+ import org .springframework .data .domain .Slice ;
29
+ import org .springframework .hateoas .*;
30
+ import org .springframework .hateoas .SlicedModel .SliceMetadata ;
31
+ import org .springframework .hateoas .server .RepresentationModelAssembler ;
32
+ import org .springframework .hateoas .server .core .EmbeddedWrapper ;
33
+ import org .springframework .hateoas .server .core .EmbeddedWrappers ;
34
+ import org .springframework .lang .Nullable ;
35
+ import org .springframework .util .Assert ;
36
+ import org .springframework .web .servlet .support .ServletUriComponentsBuilder ;
37
+ import org .springframework .web .util .UriComponents ;
38
+ import org .springframework .web .util .UriComponentsBuilder ;
39
+
40
+ /**
41
+ * {@link RepresentationModelAssembler} to easily convert {@link Slice} instances into
42
+ * {@link SlicedModel}.
43
+ *
44
+ * @author Michael Schout
45
+ */
46
+ public class SlicedResourcesAssembler <T >
47
+ implements RepresentationModelAssembler <Slice <T >, SlicedModel <EntityModel <T >>> {
48
+
49
+ private final HateoasPageableHandlerMethodArgumentResolver pageableResolver ;
50
+
51
+ private final Optional <UriComponents > baseUri ;
52
+ private final EmbeddedWrappers wrappers = new EmbeddedWrappers (false );
53
+
54
+ private boolean forceFirstRel = false ;
55
+
56
+ /**
57
+ * Creates a new {@link SlicedResourcesAssembler} using the given
58
+ * {@link PageableHandlerMethodArgumentResolver} and base URI. If the former is
59
+ * {@literal null}, a default one will be created. If the latter is {@literal null}, calls
60
+ * to {@link #toModel(Slice)} will use the current request's URI to build the relevant
61
+ * previous and next links.
62
+ *
63
+ * @param resolver can be {@literal null}.
64
+ * @param baseUri can be {@literal null}.
65
+ */
66
+ public SlicedResourcesAssembler (@ Nullable HateoasPageableHandlerMethodArgumentResolver resolver ,
67
+ @ Nullable UriComponents baseUri ) {
68
+ this .pageableResolver = resolver == null ? new HateoasPageableHandlerMethodArgumentResolver () : resolver ;
69
+ this .baseUri = Optional .ofNullable (baseUri );
70
+ }
71
+
72
+ private static String currentRequest () {
73
+ return ServletUriComponentsBuilder .fromCurrentRequest ().build ().toString ();
74
+ }
75
+
76
+ /**
77
+ * Configures whether to always add {@code first} links to the {@link SlicedModel} *
78
+ * created. Defaults to {@literal false} which means that {@code first} links onlys appear
79
+ * in conjunction with {@code prev} and {@code next} links.
80
+ *
81
+ * @param forceFirstRel whether to always add {@code first} links to the
82
+ * {@link SlicedModel} created.
83
+ */
84
+ public void setForceFirstRel (boolean forceFirstRel ) {
85
+ this .forceFirstRel = forceFirstRel ;
86
+ }
87
+
88
+ @ Override
89
+ public SlicedModel <EntityModel <T >> toModel (Slice <T > entity ) {
90
+ return toModel (entity , EntityModel ::of );
91
+ }
92
+
93
+ /**
94
+ * Creates a new {@link SlicedModel} by converting the given {@link Slice} into a
95
+ * {@link SliceMetadata} instance and wrapping the contained elements into *
96
+ * {@link SlicedModel} instances. Will add pagination links based on the given self link.
97
+ *
98
+ * @param slice must not be {@literal null}.
99
+ * @param selfLink must not be {@literal null}.
100
+ * @return
101
+ */
102
+ public SlicedModel <EntityModel <T >> toModel (Slice <T > slice , Link selfLink ) {
103
+ return toModel (slice , EntityModel ::of , selfLink );
104
+ }
105
+
106
+ /**
107
+ * Creates a new {@link SlicedModel} by converting the given {@link Slice} into a
108
+ * {@link SliceMetadata} instance and using the given {@link SlicedModel} to turn elements
109
+ * of the {@link Slice} into resources.
110
+ *
111
+ * @param slice must not be {@literal null}.
112
+ * @param assembler must not be {@literal null}.
113
+ * @return
114
+ */
115
+ public <R extends RepresentationModel <?>> SlicedModel <R > toModel (Slice <T > slice ,
116
+ RepresentationModelAssembler <T , R > assembler ) {
117
+ return createModel (slice , assembler , Optional .empty ());
118
+ }
119
+
120
+ /**
121
+ * Creates a new {@link SlicedModel} by converting the given {@link Slice} into a
122
+ * {@link SliceMetadata} instance and using the given {@link SlicedModel} to turn elements
123
+ * of the {@link Slice} into resources. Will add pagination links based on the given the
124
+ * self link.
125
+ *
126
+ * @param slice must not be {@literal null}.
127
+ * @param assembler must not be {@literal null}.
128
+ * @param link must not be {@literal null}.
129
+ * @return
130
+ */
131
+ public <R extends RepresentationModel <?>> SlicedModel <R > toModel (Slice <T > slice ,
132
+ RepresentationModelAssembler <T , R > assembler , Link link ) {
133
+ return createModel (slice , assembler , Optional .of (link ));
134
+ }
135
+
136
+ /**
137
+ * Creates a {@link SlicedModel} with an empty collection {@link EmbeddedWrapper} for the
138
+ * given domain type.
139
+ *
140
+ * @param slice must not be {@literal null}, content must be empty.
141
+ * @param type must not be {@literal null}.
142
+ * @return
143
+ */
144
+ public SlicedModel <?> toEmptyModel (Slice <?> slice , Class <?> type ) {
145
+ return toEmptyModel (slice , type , Optional .empty ());
146
+ }
147
+
148
+ /**
149
+ * Creates a {@link SlicedModel} with an empty collection {@link EmbeddedWrapper} for the
150
+ * given domain type.
151
+ *
152
+ * @param slice must not be {@literal null}, content must be empty.
153
+ * @param type must not be {@literal null}.
154
+ * @param link must not be {@literal null}.
155
+ * @return
156
+ */
157
+ public SlicedModel <?> toEmptyModel (Slice <?> slice , Class <?> type , Link link ) {
158
+ return toEmptyModel (slice , type , Optional .of (link ));
159
+ }
160
+
161
+ public SlicedModel <?> toEmptyModel (Slice <?> slice , Class <?> type , Optional <Link > link ) {
162
+ Assert .notNull (slice , "Slice must not be null" );
163
+ Assert .isTrue (!slice .hasContent (), "Slice must not have any content" );
164
+ Assert .notNull (type , "Type must not be null" );
165
+ Assert .notNull (link , "Link must not be null" );
166
+
167
+ SliceMetadata metadata = asSliceMetadata (slice );
168
+
169
+ EmbeddedWrapper wrapper = wrappers .emptyCollectionOf (type );
170
+ List <EmbeddedWrapper > embedded = Collections .singletonList (wrapper );
171
+
172
+ return addPaginationLinks (SlicedModel .of (embedded , metadata ), slice , link );
173
+ }
174
+
175
+ /**
176
+ * Creates the {@link SlicedModel} to be equipped with pagination links downstream.
177
+ *
178
+ * @param resources the original slices's elements mapped into {@link RepresentationModel}
179
+ * instances.
180
+ * @param metadata the calculated {@link SliceMetadata}, must not be {@literal null}.
181
+ * @param slice the original page handed to the assembler, must not be {@literal null}.
182
+ * @return must not be {@literal null}.
183
+ */
184
+ protected <R extends RepresentationModel <?>, S > SlicedModel <R > createSlicedModel (List <R > resources ,
185
+ SliceMetadata metadata , Slice <S > slice ) {
186
+ Assert .notNull (resources , "Content resources must not be null" );
187
+ Assert .notNull (metadata , "SliceMetadata must not be null" );
188
+ Assert .notNull (slice , "Slice must not be null" );
189
+
190
+ return SlicedModel .of (resources , metadata );
191
+ }
192
+
193
+ private <S , R extends RepresentationModel <?>> SlicedModel <R > createModel (Slice <S > slice ,
194
+ RepresentationModelAssembler <S , R > assembler , Optional <Link > link ) {
195
+ Assert .notNull (slice , "Slice must not be null" );
196
+ Assert .notNull (assembler , "ResourceAssembler must not be null" );
197
+
198
+ List <R > resources = new ArrayList <>(slice .getNumberOfElements ());
199
+
200
+ for (S element : slice ) {
201
+ resources .add (assembler .toModel (element ));
202
+ }
203
+
204
+ SlicedModel <R > resource = createSlicedModel (resources , asSliceMetadata (slice ), slice );
205
+
206
+ return addPaginationLinks (resource , slice , link );
207
+ }
208
+
209
+ private <R > SlicedModel <R > addPaginationLinks (SlicedModel <R > resources , Slice <?> slice , Optional <Link > link ) {
210
+ UriTemplate base = getUriTemplate (link );
211
+
212
+ boolean isNavigable = slice .hasPrevious () || slice .hasNext ();
213
+
214
+ if (isNavigable || forceFirstRel ) {
215
+ resources .add (
216
+ createLink (base , PageRequest .of (0 , slice .getSize (), slice .getSort ()), IanaLinkRelations .FIRST ));
217
+ }
218
+
219
+ Link selfLink = link .map (Link ::withSelfRel )
220
+ .orElseGet (() -> createLink (base , slice .getPageable (), IanaLinkRelations .SELF ));
221
+
222
+ resources .add (selfLink );
223
+
224
+ if (slice .hasPrevious ()) {
225
+ resources .add (createLink (base , slice .previousPageable (), IanaLinkRelations .PREV ));
226
+ }
227
+
228
+ if (slice .hasNext ()) {
229
+ resources .add (createLink (base , slice .nextPageable (), IanaLinkRelations .NEXT ));
230
+ }
231
+
232
+ return resources ;
233
+ }
234
+
235
+ /**
236
+ * Returns a default URI string either from the one configured on then assembler or by
237
+ * looking it up from the current request.
238
+ *
239
+ * @return
240
+ */
241
+ private UriTemplate getUriTemplate (Optional <Link > baseLink ) {
242
+ return UriTemplate .of (baseLink .map (Link ::getHref ).orElseGet (this ::baseUriOrCurrentRequest ));
243
+ }
244
+
245
+ /**
246
+ * Creates a {@link Link} with the given {@link LinkRelation} that will be based on the
247
+ * given {@link UriTemplate} but enriched with the values of the given {@link Pageable}
248
+ * (if not {@literal null}).
249
+ *
250
+ * @param base must not be {@literal null}.
251
+ * @param pageable can be {@literal null}
252
+ * @param relation must not be {@literal null}.
253
+ * @return
254
+ */
255
+ private Link createLink (UriTemplate base , Pageable pageable , LinkRelation relation ) {
256
+ UriComponentsBuilder builder = fromUri (base .expand ());
257
+ pageableResolver .enhance (builder , getMethodParameter (), pageable );
258
+
259
+ return Link .of (UriTemplate .of (builder .build ().toString ()), relation );
260
+ }
261
+
262
+ /**
263
+ * Return the {@link MethodParameter} to be used to potentially qualify the paging and
264
+ * sorting request parameters to. Default implementations returns {@literal null}, which
265
+ * means the parameters will not be qualified.
266
+ *
267
+ * @return
268
+ */
269
+ @ Nullable
270
+ protected MethodParameter getMethodParameter () {
271
+ return null ;
272
+ }
273
+
274
+ /**
275
+ * Creates a new {@link SliceMetadata} instance from the given {@link Slice}.
276
+ *
277
+ * @param slice must not be {@literal null}.
278
+ * @return
279
+ */
280
+ private SliceMetadata asSliceMetadata (Slice <?> slice ) {
281
+ Assert .notNull (slice , "Slice must not be null" );
282
+
283
+ int number = pageableResolver .isOneIndexedParameters () ? slice .getNumber () + 1 : slice .getNumber ();
284
+
285
+ return new SliceMetadata (slice .getSize (), number );
286
+ }
287
+
288
+ private String baseUriOrCurrentRequest () {
289
+ return baseUri .map (Object ::toString ).orElseGet (SlicedResourcesAssembler ::currentRequest );
290
+ }
291
+ }
0 commit comments