javarestheaderquarkusmicroprofile

Reuse Eclipse Microprofile @Header definition in REST API declaration


I'm struggling on how to reuse an @Header annotation within my API specification using Quarkus and Eclipse Microprofile. I have a lot of methods which return a paginated list of objects and I want to deliver the information about the

as X-... Header, this currently looks like

@GET
@Path("/obj1")
@Operation(description = "get filtered list of obj1")
@APIResponse(
  responseCode = "200",
  headers = { @Header(name = "X-Length"),@Header(name = "X-Offset") ,@Header(name = "X-Limit")},
  content = @Content(
    mediaType = MediaType.APPLICATION_JSON,
    schema = @Schema(implementation = Obj1.class, type = SchemaType.ARRAY)
  )
)

Since the headers are always the same for every method returning a list, I don't want to copy & paste them on every method, since I'm also currently not sure about the naming.

Is it possible, and if so how, to declare this array once and reuse it like

paginationHeaders = { @Header(name = "X-Length"),@Header(name = "X-Offset") ,@Header(name = "X-Limit")}
...
@APIResponse(...
headers = paginationHeaders

Is there maybe another standardized way to return pagination information (but not in the response object) using Quarkus?

I tried searching via Google, browsing the Quarkus docs and defining a custom annotation.


Solution

  • At first I'd like to mention that passing pagination information to HTTP headers is not the best idea. For example firewalls or proxy servers can modify (even remove) HTTP headers. I'm going to show my preferred solution later.

    Add pagination headers to response

    However, reusing header constants is not fully supported neither OpenAPI nor Quarkus the MicroProfile OpenAPI Specification (and Smallrye OpenAPI implementation) supports to modify OpenAPI documentation programatically.

    A REST resource

    @Path("/hello")
    public class ExampleResource {
    
        @Operation(
                operationId = "listExamples",
                summary = "List examples"
        )
        @APIResponse(
                headers = @Header(name = "X-Paginated"),
                responseCode = "200",
                description = "Successful operation"
        )
        @APIResponse(
                responseCode = "418",
                description = "In case of Armageddon"
        )
    
        @GET
        @Produces(MediaType.TEXT_PLAIN)
        public List<String> filter() {
            return List.of("Hello from RESTEasy Reactive");
        }
    
        @DELETE
        public void remove() {
            // Just to haven an other endpoint
        }
    }
    

    The listExamples operation has possible two responses, but the only one should contain pagination response headers. I've introduced a meta header X-Paginated to mark which response has to be modified.

    By implementing a custom OpenAPI filter it is possible to change OpenAPI definition programatically.

    The following filter will change each occurrence of X-Paginated header to X-Length, X-Offset and X-Limit triple.

    public class PaginatedHeaderOASFilter implements OASFilter {
    
        @Override
        public APIResponse filterAPIResponse(APIResponse apiResponse) {
            return Optional.of(apiResponse)
                    .filter(isPaginated)
                    .map(mapResponse)
                    .orElse(apiResponse);
        }
    
        private static final Predicate<APIResponse> isPaginated = response -> Optional
                .ofNullable(response.getHeaders())
                .map(map -> map.containsKey("X-Paginated"))
                .orElse(false);
    
        private static final UnaryOperator<APIResponse> mapResponse = response -> {
            response.removeHeader("X-Paginated");
            return response
                    .addHeader("X-Length", OASFactory.createHeader()
                            .description("Description of X-Length")
                            .schema(OASFactory.createSchema().type(Schema.SchemaType.INTEGER))
                    )
                    .addHeader("X-Offset", OASFactory.createHeader().description("Description of X-Offset"))
                    .addHeader("X-Limit", OASFactory.createHeader().description("Description of X-Limit"));
        };
    }
    

    Note: Don't forget to register custom OASFilter(s) in application configuration file e.g.:

    mp.openapi.filter=io.github.zforgo.PaginatedHeaderOASFilter
    

    Add pagination properties to response

    As I started I prefer to pass pagination properties in response body. Luckily it can be reusable, because Smallrye supports generic response types.

    Pagination holds the pagination properties

    public class Pagination {
    
        protected long totalCount;
        protected Integer pageCount;
        protected Integer pageIndex;
    
        protected Integer pageSize;
    
        public Pagination(long totalCount, Integer pageCount, Integer pageIndex) {
            this.totalCount = totalCount;
            this.pageCount = pageCount;
            this.pageIndex = pageIndex;
        }
    
        // getters / setters / others
    }
    

    FilterResult contains the result list along with pagination

    public class FilterResult<T> {
        private final Pagination pagination;
        private final List<T> items;
    
        public FilterResult(List<T> items, Pagination pagination) {
            this.items = items;
            this.pagination = pagination;
        }
    
        @JsonCreator
        public FilterResult(List<T> items, long totalCount, Integer pageCount, Integer pageIndex) {
            this(items, new Pagination(totalCount, pageCount, pageIndex));
        }
    
        // getters / setters / others
    }
    

    Now, an enpoint like

    @GET
    @Path("/orders")
    @Produces(MediaType.APPLICATION_JSON)
    public FilterResult<OrderDto> filterOrders() {
        return null;
    }
    

    will be generated in OpenAPI documentation as:

    paths:
      /orders:
        get:
          responses:
            "200":
              description: OK
              content:
                application/json:
                  schema:
                    $ref: '#/components/schemas/FilterResultOrderDto'
    components:
      schemas:
        FilterResultOrderDto:
          type: object
          properties:
            pagination:
              $ref: '#/components/schemas/Pagination'
            items:
              type: array
              items:
                $ref: '#/components/schemas/OrderDto'
        OrderDto:
          type: object
          properties:
            billingAddress:
              type: string
        Pagination:
          type: object
          properties:
            totalCount:
              format: int64
              type: integer
            pageCount:
              format: int32
              type: integer
            pageIndex:
              format: int32
              type: integer
            pageSize:
              format: int32
              type: integer