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