10

I'm using 'admin-on-rest' UI for my Web-application and it has next restriction:

Note: The jsonServer REST client expects the API to include a X-Total-Count header in the response to GET_LIST calls. The value must be the total number of resources in the collection. This allows admin-on-rest to know how many pages of resources there are in total, and build the pagination controls.

I solved the problem by adding X-Total-Count header to my list-returning REST-endpoints manually like this: response.addHeader("X-Total-Count", String.valueOf(outputList.size()));

But I'm wondering: if there's some elegant way to do it automatically in Spring? I mean auto-add this header with proper value when some endpoint returns JSON-list?

Dmytro Titov
  • 2,802
  • 6
  • 38
  • 60
  • If you are trying to add the header for many requests, you can use a filter: https://stackoverflow.com/a/16191770/1646783 – jlars62 Jun 05 '17 at 18:42
  • @jlars62, I've been thinking about filters, but what is the proper way to distinguish endpoints which return single item in their JSON-response from endpoints which return multiple items in their JSON-response? I need that header only for last ones. – Dmytro Titov Jun 05 '17 at 18:54
  • @DmytroTitov using the PagingAndSortingRepository spring does most of work for you and the total is real (based on the query) and not just a count of the list returned :) Check my answer – Michail Michailidis Sep 21 '17 at 14:16

3 Answers3

9

Yes, there is! (If you are using spring 4.1 or above).

It's called ResponseBodyAdvice and it enables you to intercept calls (just before response is written and gives access to raw http response).

Basically what you need is to implement controller advice like this:

@ControllerAdvice
public class ResourceSizeAdvice implements ResponseBodyAdvice<Collection<?>> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        //Checks if this advice is applicable. 
        //In this case it applies to any endpoint which returns a collection.
        return Collection.class.isAssignableFrom(returnType.getParameterType()); 
    }

    @Override
    public Collection<?> beforeBodyWrite(Collection<?> body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        response.getHeaders().add("X-Total-Count", String.valueOf(body.size()));
        return body;
    }
}
Bohdan Levchenko
  • 3,411
  • 2
  • 24
  • 28
5

In case you don't want just the total number of the elements in the response but the total number of entities in the corresponding JPA method of PagingAndSortingRepository you can do something like that which is useful for paging applications :)

Inspired by Bohdan's answer ( https://stackoverflow.com/a/44376133/986160)

@ControllerAdvice
public class ResourceSizeAdvice implements ResponseBodyAdvice<Page<?>> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        //Checks if this advice is applicable.
        //In this case it applies to any endpoint which returns a page.
        return Page.class.isAssignableFrom(returnType.getParameterType());
    }

    @Override
    public Page<?> beforeBodyWrite(Page<?> page, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        serverHttpResponse.getHeaders().add("X-Total-Count",String.valueOf(page.getTotalElements()));
        return page;
    }

}

Make sure to call the Pageable version of the default methods like so it returns a Page and not a List:

repository.findAll(new PageRequest(0,100));

If you are not using Repositories then you have to do two queries: Select * from ... and Select count(*) from ... and return a Wrapper which has contents for the list of results plus total for the total coming from count. Then you can change the @ControllerAdvice class to expect your Wrapper and get the total from it and put it in the header

Michail Michailidis
  • 11,792
  • 6
  • 63
  • 106
  • Yeah, nice combo! :) – Bohdan Levchenko Sep 21 '17 at 11:29
  • 1
    @BohdanLevchenko thanks! Although I realized that it is more difficult to count when you have applied some filtering to the repository.. not sure how to make it work like that yet - if I find a way I will post it :) – Michail Michailidis Sep 21 '17 at 11:30
  • 1
    @BohdanLevchenko made it :) – Michail Michailidis Sep 21 '17 at 14:15
  • Neat! Clean and simple! – Bohdan Levchenko Sep 21 '17 at 14:35
  • Cool solution, thanks. But only for repo-based endpoints. Mine actually were fetching data not from the repo, but from some other service. So the collection-size estimation is probably the most general approach. But still: good solution :) – Dmytro Titov Sep 21 '17 at 15:25
  • :) if that is the case then you need to be doing a second query every-time to get the exact count and put the results in a specific wrapper that contains the contents plus the total.. Then you can change the @ControllerAdvice class to expect your `WrapperClass` and get the total from it and put it in the header – Michail Michailidis Sep 21 '17 at 15:46
  • btw if you return a Page Iterable from a Controller.. it is an object that has content with an array of elements, pagination details and total – Michail Michailidis Jan 08 '18 at 08:56
1

Because from Page you can get total elements. So I've added headers to ResponseEntity

@GetMapping(value = POSTS, headers = "Accept=application/json")
public ResponseEntity<?> getListPost(@RequestParam(required = false, defaultValue = "0") Integer page, @RequestParam(required = false, defaultValue = "25") Integer size) {
    // Create pageable
    Pageable pageable = new PageRequest(page, size);
    Page<Post> pagePost = postService.getPagePost(pageable);

    HttpHeaders headers = new HttpHeaders() {
        {
            add("Access-Control-Expose-Headers", "Content-Range");
            add("Content-Range", String.valueOf(pagePost.getTotalElements()));
        }
    };
    //        return new ResponseEntity<>(new CommonResponseBody("OK", 200, postList), HttpStatus.OK);
    return new ResponseEntity<>(new CommonResponseBody("OK", 200, new LinkedHashMap() {
        {
            put("data", pagePost.getContent());
        }
    }), headers, HttpStatus.OK);
}

getPagePost is service method which uses Page findAll(Pageable pageable) in Repository.

Note: Change Content-Range to X-Total-Count if it doesn't work for you.

Long Nguyen
  • 9,898
  • 5
  • 53
  • 52