/* * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.data.web; import static org.springframework.web.util.UriComponentsBuilder.*; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; import org.springframework.core.MethodParameter; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.hateoas.Link; import org.springframework.hateoas.PagedResources; import org.springframework.hateoas.PagedResources.PageMetadata; import org.springframework.hateoas.Resource; import org.springframework.hateoas.ResourceAssembler; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.core.EmbeddedWrapper; import org.springframework.hateoas.core.EmbeddedWrappers; import org.springframework.util.Assert; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** * {@link ResourceAssembler} to easily convert {@link Page} instances into {@link PagedResources}. * * @since 1.6 * @author Oliver Gierke * @author Nick Williams */ public class PagedResourcesAssembler<T> implements ResourceAssembler<Page<T>, PagedResources<Resource<T>>> { private final HateoasPageableHandlerMethodArgumentResolver pageableResolver; private final Optional<UriComponents> baseUri; private final EmbeddedWrappers wrappers = new EmbeddedWrappers(false); private boolean forceFirstAndLastRels = false; /** * Creates a new {@link PagedResourcesAssembler} using the given {@link PageableHandlerMethodArgumentResolver} and * base URI. If the former is {@literal null}, a default one will be created. If the latter is {@literal null}, calls * to {@link #toResource(Page)} will use the current request's URI to build the relevant previous and next links. * * @param resolver * @param baseUri */ public PagedResourcesAssembler(HateoasPageableHandlerMethodArgumentResolver resolver, UriComponents baseUri) { this.pageableResolver = resolver == null ? new HateoasPageableHandlerMethodArgumentResolver() : resolver; this.baseUri = Optional.ofNullable(baseUri); } /** * Configures whether to always add {@code first} and {@code last} links to the {@link PagedResources} created. * Defaults to {@literal false} which means that {@code first} and {@code last} links only appear in conjunction with * {@code prev} and {@code next} links. * * @param forceFirstAndLastRels whether to always add {@code first} and {@code last} links to the * {@link PagedResources} created. * @since 1.11 */ public void setForceFirstAndLastRels(boolean forceFirstAndLastRels) { this.forceFirstAndLastRels = forceFirstAndLastRels; } /* * (non-Javadoc) * @see org.springframework.hateoas.ResourceAssembler#toResource(java.lang.Object) */ @Override public PagedResources<Resource<T>> toResource(Page<T> entity) { return toResource(entity, it -> new Resource<>(it)); } /** * Creates a new {@link PagedResources} by converting the given {@link Page} into a {@link PageMetadata} instance and * wrapping the contained elements into {@link Resource} instances. Will add pagination links based on the given the * self link. * * @param page must not be {@literal null}. * @param selfLink must not be {@literal null}. * @return */ public PagedResources<Resource<T>> toResource(Page<T> page, Link selfLink) { return toResource(page, it -> new Resource<>(it), selfLink); } /** * Creates a new {@link PagedResources} by converting the given {@link Page} into a {@link PageMetadata} instance and * using the given {@link ResourceAssembler} to turn elements of the {@link Page} into resources. * * @param page must not be {@literal null}. * @param assembler must not be {@literal null}. * @return */ public <R extends ResourceSupport> PagedResources<R> toResource(Page<T> page, ResourceAssembler<T, R> assembler) { return createResource(page, assembler, Optional.empty()); } /** * Creates a new {@link PagedResources} by converting the given {@link Page} into a {@link PageMetadata} instance and * using the given {@link ResourceAssembler} to turn elements of the {@link Page} into resources. Will add pagination * links based on the given the self link. * * @param page must not be {@literal null}. * @param assembler must not be {@literal null}. * @param link must not be {@literal null}. * @return */ public <R extends ResourceSupport> PagedResources<R> toResource(Page<T> page, ResourceAssembler<T, R> assembler, Link link) { Assert.notNull(link, "Link must not be null!"); return createResource(page, assembler, Optional.of(link)); } /** * Creates a {@link PagedResources} with an empt collection {@link EmbeddedWrapper} for the given domain type. * * @param page must not be {@literal null}, content must be empty. * @param type must not be {@literal null}. * @return * @since 2.0 */ public PagedResources<?> toEmptyResource(Page<?> page, Class<?> type) { return toEmptyResource(page, type, Optional.empty()); } /** * Creates a {@link PagedResources} with an empt collection {@link EmbeddedWrapper} for the given domain type. * * @param page must not be {@literal null}, content must be empty. * @param type must not be {@literal null}. * @param link must not be {@literal null}. * @return * @since 1.11 */ public PagedResources<?> toEmptyResource(Page<?> page, Class<?> type, Link link) { return toEmptyResource(page, type, Optional.of(link)); } private PagedResources<?> toEmptyResource(Page<?> page, Class<?> type, Optional<Link> link) { Assert.notNull(page, "Page must must not be null!"); Assert.isTrue(!page.hasContent(), "Page must not have any content!"); Assert.notNull(type, "Type must not be null!"); Assert.notNull(link, "Link must not be null!"); PageMetadata metadata = asPageMetadata(page); EmbeddedWrapper wrapper = wrappers.emptyCollectionOf(type); List<EmbeddedWrapper> embedded = Collections.singletonList(wrapper); return addPaginationLinks(new PagedResources<>(embedded, metadata), page, link); } /** * Creates the {@link PagedResources} to be equipped with pagination links downstream. * * @param resources the original page's elements mapped into {@link ResourceSupport} instances. * @param metadata the calculated {@link PageMetadata}, must not be {@literal null}. * @param page the original page handed to the assembler, must not be {@literal null}. * @return must not be {@literal null}. */ protected <R extends ResourceSupport, S> PagedResources<R> createPagedResource(List<R> resources, PageMetadata metadata, Page<S> page) { Assert.notNull(resources, "Content resources must not be null!"); Assert.notNull(metadata, "PageMetadata must not be null!"); Assert.notNull(page, "Page must not be null!"); return new PagedResources<>(resources, metadata); } private <S, R extends ResourceSupport> PagedResources<R> createResource(Page<S> page, ResourceAssembler<S, R> assembler, Optional<Link> link) { Assert.notNull(page, "Page must not be null!"); Assert.notNull(assembler, "ResourceAssembler must not be null!"); List<R> resources = new ArrayList<>(page.getNumberOfElements()); for (S element : page) { resources.add(assembler.toResource(element)); } PagedResources<R> resource = createPagedResource(resources, asPageMetadata(page), page); return addPaginationLinks(resource, page, link); } private <R> PagedResources<R> addPaginationLinks(PagedResources<R> resources, Page<?> page, Optional<Link> link) { UriTemplate base = getUriTemplate(link); boolean isNavigable = page.hasPrevious() || page.hasNext(); if (isNavigable || forceFirstAndLastRels) { resources.add(createLink(base, PageRequest.of(0, page.getSize(), page.getSort()), Link.REL_FIRST)); } if (page.hasPrevious()) { resources.add(createLink(base, page.previousPageable(), Link.REL_PREVIOUS)); } Link selfLink = link.map(it -> it.withSelfRel())// .orElseGet(() -> createLink(base, page.getPageable(), Link.REL_SELF)); resources.add(selfLink); if (page.hasNext()) { resources.add(createLink(base, page.nextPageable(), Link.REL_NEXT)); } if (isNavigable || forceFirstAndLastRels) { int lastIndex = page.getTotalPages() == 0 ? 0 : page.getTotalPages() - 1; resources.add(createLink(base, PageRequest.of(lastIndex, page.getSize(), page.getSort()), Link.REL_LAST)); } return resources; } /** * Returns a default URI string either from the one configured on assembler creatino or by looking it up from the * current request. * * @return */ private UriTemplate getUriTemplate(Optional<Link> baseLink) { return new UriTemplate(baseLink.map(Link::getHref).orElseGet(this::baseUriOrCurrentRequest)); } /** * Creates a {@link Link} with the given rel that will be based on the given {@link UriTemplate} but enriched with the * values of the given {@link Pageable} (if not {@literal null}). * * @param base must not be {@literal null}. * @param pageable can be {@literal null} * @param rel must not be {@literal null} or empty. * @return */ private Link createLink(UriTemplate base, Pageable pageable, String rel) { UriComponentsBuilder builder = fromUri(base.expand()); pageableResolver.enhance(builder, getMethodParameter(), pageable); return new Link(new UriTemplate(builder.build().toString()), rel); } /** * Return the {@link MethodParameter} to be used to potentially qualify the paging and sorting request parameters to. * Default implementations returns {@literal null}, which means the parameters will not be qualified. * * @return * @since 1.7 */ protected MethodParameter getMethodParameter() { return null; } /** * Creates a new {@link PageMetadata} instance from the given {@link Page}. * * @param page must not be {@literal null}. * @return */ private static <T> PageMetadata asPageMetadata(Page<T> page) { Assert.notNull(page, "Page must not be null!"); return new PageMetadata(page.getSize(), page.getNumber(), page.getTotalElements(), page.getTotalPages()); } private String baseUriOrCurrentRequest() { return baseUri.map(Object::toString).orElseGet(PagedResourcesAssembler::currentRequest); } private static String currentRequest() { return ServletUriComponentsBuilder.fromCurrentRequest().build().toString(); } }