How to correctly use PagedResourcesAssembler from Spring Data?
Asked Answered
K

3

45

I'm using Spring 4.0.0.RELEASE, Spring Data Commons 1.7.0.M1, Spring Hateoas 0.8.0.RELEASE

My resource is a simple POJO:

public class UserResource extends ResourceSupport { ... }

My resource assembler converts User objects to UserResource objects:

@Component
public class UserResourceAssembler extends ResourceAssemblerSupport<User, UserResource> { 
    public UserResourceAssembler() {
        super(UserController.class, UserResource.class);
    }

    @Override
    public UserResource toResource(User entity) {
        // map User to UserResource
    }
}

Inside my UserController I want to retrieve Page<User> from my service and then convert it to PagedResources<UserResource> using PagedResourcesAssembler, like displayed here: https://mcmap.net/q/298524/-spring-mvc-3-return-a-spring-data-page-as-json

@RequestMapping(value="", method=RequestMethod.GET)
PagedResources<UserResource> get(@PageableDefault Pageable p, PagedResourcesAssembler assembler) {
    Page<User> u = service.get(p)
    return assembler.toResource(u);
}

This doesn't call UserResourceAssembler and simply the contents of User are returned instead of my custom UserResource.

Returning a single resource works:

@Autowired
UserResourceAssembler assembler;

@RequestMapping(value="{id}", method=RequestMethod.GET)
UserResource getById(@PathVariable ObjectId id) throws NotFoundException {
    return assembler.toResource(service.getById(id));
}

The PagedResourcesAssembler wants some generic argument, but then I can't use T toResource(T), because I don't want to convert my Page<User> to PagedResources<User>, especially because User is a POJO and no Resource.

So the question is: How does it work?

EDIT:

My WebMvcConfigurationSupport:

@Configuration
@ComponentScan
@EnableHypermediaSupport
public class WebMvcConfig extends WebMvcConfigurationSupport {
    @Override
    protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(pageableResolver());
        argumentResolvers.add(sortResolver());
        argumentResolvers.add(pagedResourcesAssemblerArgumentResolver());
    }

    @Bean
    public HateoasPageableHandlerMethodArgumentResolver pageableResolver() {
        return new HateoasPageableHandlerMethodArgumentResolver(sortResolver());
    }

    @Bean
    public HateoasSortHandlerMethodArgumentResolver sortResolver() {
        return new HateoasSortHandlerMethodArgumentResolver();
    }

    @Bean
    public PagedResourcesAssembler<?> pagedResourcesAssembler() {
        return new PagedResourcesAssembler<Object>(pageableResolver(), null);
    }

    @Bean
    public PagedResourcesAssemblerArgumentResolver pagedResourcesAssemblerArgumentResolver() {
        return new PagedResourcesAssemblerArgumentResolver(pageableResolver(), null);
    }

    /* ... */
}

SOLUTION:

@Autowired
UserResourceAssembler assembler;

@RequestMapping(value="", method=RequestMethod.GET)
PagedResources<UserResource> get(@PageableDefault Pageable p, PagedResourcesAssembler pagedAssembler) {
    Page<User> u = service.get(p)
    return pagedAssembler.toResource(u, assembler);
}
Klansman answered 25/1, 2014 at 3:54 Comment(0)
M
83

You seem to have already found out about the proper way to use but I'd like to go into some of the details here a bit for others to find as well. I went into similar detail about PagedResourceAssembler in this answer.

Representation models

Spring HATEOAS ships with a variety of base classes for representation models that make it easy to create representations equipped with links. There are three types of classes provided out of the box:

  • Resource - an item resource. Effectively to wrap around some DTO or entity that captures a single item and enriches it with links.
  • Resources - a collection resource, that can be a collection of somethings but usually are a collection of Resource instances.
  • PagedResources - an extension of Resources that captures additional pagination information like the number of total pages etc.

All of these classes derive from ResourceSupport, which is a basic container for Link instances.

Resource assemblers

A ResourceAssembler is now the mitigating component to convert your domain objects or DTOs into such resource instances. The important part here is, that it turns one source object into one target object.

So the PagedResourcesAssembler will take a Spring Data Page instance and transform it into a PagedResources instance by evaluating the Page and creating the necessary PageMetadata as well as the prev and next links to navigate the pages. By default - and this is probably the interesting part here - it will use a plain SimplePagedResourceAssembler (an inner class of PRA) to transform the individual elements of the page into nested Resource instances.

To allow to customize this, PRA has additional toResource(…) methods that take a delegate ResourceAssembler to process the individual items. So you end up with something like this:

 class UserResource extends ResourceSupport { … }

 class UserResourceAssembler extends ResourceAssemblerSupport<User, UserResource> { … }

And the client code now looking something like this:

 PagedResourcesAssembler<User> parAssembler = … // obtain via DI
 UserResourceAssembler userResourceAssembler = … // obtain via DI

 Page<User> users = userRepository.findAll(new PageRequest(0, 10));

 // Tell PAR to use the user assembler for individual items.
 PagedResources<UserResource> pagedUserResource = parAssembler.toResource(
   users, userResourceAssembler);

Outlook

As of the upcoming Spring Data Commons 1.7 RC1 (and Spring HATEOAS 0.9 transitively) the prev and next links will be generated as RFC6540 compliant URI templates to expose the pagination request parameters configured in the HandlerMethodArgumentResolvers for Pageable and Sort.

The configuration you've shown above can be simplified by annotating the config class with @EnableSpringDataWebSupport which would let you get rid off all the explicit bean declarations.

Mernamero answered 26/1, 2014 at 10:29 Comment(11)
Hi, could you please tell how to use those generics right with PagedResourcesAssembler? I only can use it as raw type. It says PagedResources<R> toResource(Page<T>, ResourceAssemblerSupport<T, R>). Eclipse won't accept this: PagedResources<UserResource> p = parAssembler.toResource(userPage, userResourceAssembler).Klansman
I've added the generics for the PagedResourcesAssembler in the answer above. If you provide a custom UserResourceAssembler which is a ResourceAssembler<User, UserResource>, you need to get a PagedResourcesAssembler<User> injected to eventually create PagedResources<UserResource>. I just added a test case to the codebase giving a working example of that.Mernamero
Facing the same issue, on Spring 4.0.6.RELEASE and 0.16.0.RELEASE the toResource accepts only one argument, the adminResourcePages, and no way to pass in the custom adminResourceAssembler. Here is the method signature: toResource(Page<AdminResource>, Link) Am I using the wrong API here ?Hesler
I've got it now: Page<EventAdmin> eventAdminPages = new PageImpl<EventAdmin>(searchedAdminsEvent.getEventAdmins(), pageable, searchedAdminsEvent.getTotalElements()); PagedResources<AdminResource> adminPageResources = pagedResourcesAssembler.toResource(eventAdminPages, adminResourceAssembler);Hesler
In this new way of paging the resources, is there any use left for overriding toResources ? (Note the trailing 's'). Cheers.Hesler
Any way to have the links of a sub resource ? Inside my main resource, the admin resource does not have its links, since the toResource method of the main resource assembler only provides the links for the main resource.Hesler
I posted a question to be clearer. #25602902Hesler
Could you please show us your toResource method? Regards, NehaSonora
Am I correct in my assertion that PagedResourcesAssembler takes a JPA entity and the @Controller method signature takes a PagedResourcesAssembler<JPAEntity>? When I'm thinking about my implementation, this feels like a leaky abstraction. I feel that the abstraction would be more cohesive if PagedResourcesAssembler took a <Resource of JPAEntity>. That way our persistence layer deals with JPA entities and our service layer maps those to <? extends ResourceSupport>. I generally don't expect JPA entities to be referenced above the service layer. Am I using the API incorrectly?Ferdinand
@OliverGierke is there a ResourcesAssembler to handle collection of resourced that are not needed to be paged?Emileeemili
When using Spring Data Rest, I don't need to create a Resource nor a ResourceAssembler class, this leads me to think that SDR might have a generic implementation of these classes. Is there a way to use it?Impasse
D
0

I wanted to convert list of Resources to page. but when giving it PagedResourcesAssembler it was eating up the internal links.

This will get your List paged.

 public class JobExecutionInfoResource extends ResourceSupport {
    private final JobExecutionInfo jobExecution;

    public JobExecutionInfoResource(final JobExecutionInfo jobExecution) {
        this.jobExecution = jobExecution;        
        add(ControllerLinkBuilder.linkTo(methodOn(JobsMonitorController.class).get(jobExecution.getId())).withSelfRel()); // add your own links.          
    }

    public JobExecutionInfo getJobExecution() {
        return jobExecution;
    }
}

Paged resource Providing ResourceAssembler telling Paged resource to use it, which does nothing simply return's it back as it is already a resource list that is passed.

    private final PagedResourcesAssembler<JobExecutionInfoResource> jobExecutionInfoResourcePagedResourcesAssembler;
    public static final PageRequest DEFAULT_PAGE_REQUEST = new PageRequest(0, 20);
    public static final ResourceAssembler<JobExecutionInfoResource, JobExecutionInfoResource> SIMPLE_ASSEMBLER = entity -> entity;

@GetMapping("/{clientCode}/{propertyCode}/summary")
    public PagedResources<JobExecutionInfoResource> getJobsSummary(@PathVariable String clientCode, @PathVariable String propertyCode,
                                                                   @RequestParam(required = false) String exitStatus,
                                                                   @RequestParam(required = false) String jobName,
                                                                   Pageable pageRequest) {
        List<JobExecutionInfoResource> listOfResources = // your code to generate the list of resource;
        int totalCount = 10// some code to get total count;
        Link selfLink = linkTo(methodOn(JobsMonitorController.class).getJobsSummary(clientCode, propertyCode, exitStatus, jobName, DEFAULT_PAGE_REQUEST)).withSelfRel();
        Page<JobExecutionInfoResource> page = new PageImpl<>(jobExecutions, pageRequest, totalCount);
        return jobExecutionInfoResourcePagedResourcesAssembler.toResource(page, SIMPLE_ASSEMBLER, selfLink);
    }
Date answered 8/3, 2019 at 10:37 Comment(0)
H
-8

ALTERNATIVE WAY

Another way is use the Range HTTP header (read more in RFC 7233). You can define HTTP header this way:

Range: resources=20-41

That means, you want to get resource from 20 to 41 (including). This way allows consuments of API receive exactly defined resources.

It is just alternative way. Range is often used with another units (like bytes etc.)

RECOMMENDED WAY

If you wanna work with pagination and have really applicable API (hypermedia / HATEOAS included) then I recommend add Page and PageSize to your URL. Example:

http://host.loc/articles?Page=1&PageSize=20

Then, you can read this data in your BaseApiController and create some QueryFilter object in all your requests:

{
    var requestHelper = new RequestHelper(Request);

    int page = requestHelper.GetValueFromQueryString<int>("page");
    int pageSize = requestHelper.GetValueFromQueryString<int>("pagesize");

    var filter = new QueryFilter
    {
        Page = page != 0 ? page : DefaultPageNumber,
        PageSize = pageSize != 0 ? pageSize : DefaultPageSize
    };

    return filter;
}

Your api should returns some special collection with information about number of items.

public class ApiCollection<T>
{
    public ApiCollection()
    {
        Data = new List<T>();
    }

    public ApiCollection(int? totalItems, int? totalPages)
    {
        Data = new List<T>();
        TotalItems = totalItems;
        TotalPages = totalPages;
    }

    public IEnumerable<T> Data { get; set; }

    public int? TotalItems { get; set; }
    public int? TotalPages { get; set; }
}

Your model classes can inherit some class with pagination support:

public abstract class ApiEntity
{
    public List<ApiLink> Links { get; set; }
}

public class ApiLink
{
    public ApiLink(string rel, string href)
    {
        Rel = rel;
        Href = href;
    }

    public string Href { get; set; }

    public string Rel { get; set; }
}
Heeley answered 21/10, 2014 at 8:46 Comment(3)
You answer has got nothing to do with Springs PagedResourcesAssembler . Model classes should never inherit any API stuff. ... Regarding Range: Not recommendable at all! The generated URL is not idempotent, but GET requests should be. This means, I can call the same URL with different Range headers and get different results. --- You can't copy and paste the URL and get the same result, because you will always loose the Range information.Klansman
1) agree, I just showed alternative way... Range is commonly used for streaming api'sHeeley
2) agree and my example doesn't contain any data model classes, there are just API model classesHeeley

© 2022 - 2024 — McMap. All rights reserved.