10

I want to implement a custom ResponseBodyAdvice, which simply looks for Page<?> and then adds the number of total elements to the response headers.

@ControllerAdvice
public class PageResponseAdvice implements ResponseBodyAdvice<Object> {

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return Page.class.isAssignableFrom(returnType.getParameterType());
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        ((Page<?>) body).getTotalElements();
        ...
    }

}

The questions are:

  • Why is body (within the beforeBodyWrite method) of type MappingJacksonValue?

  • Is there a better way to achieve this? / Am I using the wrong interceptor?

I don't want to take care of wrapper classes, though I just want the plain unmodified Page object within the beforeBodyWrite method.


EDIT:

I now just extend AbstractMappingJacksonResponseBodyAdvice. This works well, but doesn't feel right. Maybe someone still got a better idea.

Here's the code for Page -> Content-Range Header:

@ControllerAdvice
public class PageResponseAdvice extends AbstractMappingJacksonResponseBodyAdvice {
    @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        return super.supports(returnType, converterType) && Page.class.isAssignableFrom(returnType.getParameterType());
    }

    @Override protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) {
        Page<?> page = ((Page<?>) bodyContainer.getValue());
        Long from = null;
        Long to = null;
        if(page.getTotalElements() > 0 && page.getNumberOfElements() > 0) {
            from = Integer.valueOf(page.getNumber()).longValue()*page.getSize();
            to = from + page.getNumberOfElements() - 1;
        }
        response.getHeaders().add(
                HttpHeaders.CONTENT_RANGE,
                ContentRangeEncoder.encode(
                        "items",
                        from,
                        to,
                        page.getTotalElements()
                )
        );
        response.getHeaders().add(
                HttpHeaders.ACCEPT_RANGES,
                "items"
        );
    }
}

(And if anyone is interested. Here's the ContentRangeEncoder):

public class ContentRangeEncoder {
    private static final Pattern TYPE_PATTERN = Pattern.compile("[a-zA-Z0-9]+");
    private static final Predicate<String> TYPE_PATTERN_PREDICATE = TYPE_PATTERN.asPredicate();

    public static <T extends Number & Comparable<T>> String encode(String unit, T from, T to, T length) {
        StringBuilder sb = new StringBuilder();
        if(unit != null) {
            Assert.isTrue(TYPE_PATTERN_PREDICATE.test(unit));
            sb.append(unit).append(" ");
        }

        if(from == null && to == null) {
            sb.append("*");
        } else {
            Assert.notNull(from);
            Assert.notNull(to);
            Assert.isTrue(from.compareTo(to) <= 0);
            sb.append(from).append("-").append(to);
        }
        sb.append("/");
        if(length == null) {
            sb.append("*");
        } else {
            Assert.isTrue(to == null || length.compareTo(to) > 0);
            sb.append(length);
        }
        return sb.toString();
    }
}
Benjamin M
  • 23,599
  • 32
  • 121
  • 201

1 Answers1

0

As to why the body is of type MappingJacksonValue, it depends on your configuration and your particular handler method's return value.

As a side note, WebMvcConfigurationSupportadds JsonViewResponseBodyAdvice to the RequestMappingHandlerAdapter, and if this ResponseBodyAdvice bean is invoked before yours, that would explain why the body is a MappingJacksonValue. The body is passed around from one ResponseBodyAdvice bean to the next, each returning the body in whatever "form" it sees fit.

Here's an extract of 'AbstractMappingJacksonResponseBodyAdvice', superclass of JsonViewResponseBodyAdvice to see what happens to the body:

public final Object beforeBodyWrite(@Nullable Object body,
    MethodParameter returnType, ..., ..., ..., ...) {

    //...

    MappingJacksonValue container = getOrCreateContainer(body);

    //...

    return container;
}
NatFar
  • 2,090
  • 1
  • 12
  • 29