Spring HATEOAS - Adding Pagination Links To RESTful API

Joe • updated : August 17, 2020

Previously, we discussed how to add links to a REST API using Spring HATEOAS. This is a continuation of the previous article. In this section, we will look at how to add pagination links to API resources.

Spring data provides the org.springframework.data.web.PagedResourcesAssembler class which is an implementation of the RepresentationModelAssembler, PagedModel>> interface.

Considering a service that returns a pageable result of enzymes;

Page< EnzymeEntry> enzymes = enzymeService.getEnzymes(pageable);

The PagedResourcesAssembler< EnzymeEntry> class is handy in converting our Page<EnzymeEntry> to a PagedModel< EnzymeModel>.

The overloaded toModel( … ) method creates a new PagedModel< EnzymeModel > by converting the given Page< EnzymeEntry  > into a PageMetadata instance and wrapping the contained elements into Page< EnzymeModel >  instances with pagination links.

Step. 1 – Implement the RepresentationModelAssembler

@Component
public class PaginationModelAssembler implements RepresentationModelAssembler<EnzymeEntry, EnzymeModel> {

    private static final int LIMIT = 10;
    private static final String ENZYME = "enzyme";
    private static final String ASSOCIATED_PROTEINS = "associated Proteins";

    @Override
    public EnzymeModel toModel(EnzymeEntry enzyme) {

        Class<EnzymeController> controllerClass = EnzymeController.class;
        EnzymeModel model = buildEnzymeModel(enzyme);
        model.add(linkTo(methodOn(controllerClass).findEnzymeByEcNumber(enzyme.getEc())).withRel(ENZYME));
        model.add(linkTo(methodOn(controllerClass).findAssociatedProteinsByEcNumber(enzyme.getEc(), LIMIT)).withRel(ASSOCIATED_PROTEINS));
        return model;
    }

    private EnzymeModel buildEnzymeModel(EnzymeEntry enzyme) {
        return EnzymeModel.builder()
                .ecNumber(enzyme.getEc())
                .enzymeName(enzyme.getEnzymeName())
                .enzymeFamily(enzyme.getEnzymeFamily())
                .alternativeNames(enzyme.getAltNames())
                .catalyticActivities(enzyme.getCatalyticActivities())
                .cofactors(enzyme.getIntenzCofactors())
                .associatedProteins(ProteinUtil.toCollectionModel(enzyme.getProteinGroupEntry()))
                .build();
    }

}

Step. 2 – Usage in Controller method

@RequestMapping(value = "/enzymes", produces = {MediaType.APPLICATION_JSON_VALUE, MediaTypes.HAL_JSON_VALUE})
@RestController
public class EnzymeController {

    private final EnzymeService enzymeService;
    private final PagedResourcesAssembler<EnzymeEntry> pagedResourcesAssembler;
    private final PaginationModelAssembler paginationModelAssembler;

    @Autowired
    public EnzymeController(EnzymeService enzymeService, PagedResourcesAssembler<EnzymeEntry> pagedResourcesAssembler, PaginationModelAssembler paginationModelAssembler) {
        this.enzymeService = enzymeService;
        this.pagedResourcesAssembler = pagedResourcesAssembler;
        this.paginationModelAssembler = paginationModelAssembler;
    }

    @GetMapping(value = "/")
    public PagedModel<EnzymeModel> enzymes(@Parameter(description = "page number") @RequestParam(value = "page", defaultValue = "0", name = "page") int page, @Parameter(description = " result limit") @RequestParam(value = "size", defaultValue = "10") int size) {
        Pageable pageable = PageRequest.of(page, size, Sort.Direction.DESC, "ecNumber");
        Page<EnzymeEntry> enzymes = enzymeService.getEnzymeEntries(pageable);
        return pagedResourcesAssembler.toModel(enzymes, paginationModelAssembler);
    }
}

Step. 3 – HAL Output with pagination links

curl -X GET "http://localhost:8080/computingfacts/rest/enzymes/?page=1&size=2" -H "accept: application/hal+json"
{
  "_embedded": {
    "enzymes": [
      {
        "enzymeName": "Hydrolases",
        "ecNumber": "3.-.-.-",
        "enzymeFamily": "Hydrolases",
        "alternativeNames": [],
        "catalyticActivities": [],
        "cofactors": [],
        "associatedProteins": {
          "_embedded": {
            "proteins": [
              {
                "accession": "P96655",
                "proteinName": "UPF0173 protein YddR",
                "organismName": "Bacillus subtilis (strain 168)"
              },
              {
                "accession": "O67893",
                "proteinName": "Uncharacterized protein aq_2135",
                "organismName": "Aquifex aeolicus (strain VF5)"
              }
            ]
          }
        },
        "_links": {
          "enzyme": {
            "href": "http://localhost:8080/computingfacts/rest/enzymes/3.-.-.-"
          },
          "associated Proteins": {
            "href": "http://localhost:8080/computingfacts/rest/enzymes/3.-.-.-/proteins?limit=10"
          }
        }
      },
      {
        "enzymeName": "Glycine N-methyltransferase",
        "ecNumber": "2.1.1.20",
        "enzymeFamily": "Transferases",
        "alternativeNames": [],
        "catalyticActivities": [],
        "cofactors": [],
        "associatedProteins": {
          "_embedded": {
            "proteins": [
              {
                "accession": "Q14749",
                "proteinName": "Glycine N-methyltransferase",
                "organismName": "Human"
              },
              {
                "accession": "A0A0W8FGQ6",
                "proteinName": "Glycine n-methyltransferase",
                "organismName": "hydrocarbon metagenome"
              }
            ]
          }
        },
        "_links": {
          "enzyme": {
            "href": "http://localhost:8080/computingfacts/rest/enzymes/2.1.1.20"
          },
          "associated Proteins": {
            "href": "http://localhost:8080/computingfacts/rest/enzymes/2.1.1.20/proteins?limit=10"
          }
        }
      }
    ]
  },
  "_links": {
    "first": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/?page=0&size=2&sort=ecNumber,desc"
    },
    "prev": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/?page=0&size=2&sort=ecNumber,desc"
    },
    "self": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/?page=1&size=2&sort=ecNumber,desc"
    },
    "next": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/?page=2&size=2&sort=ecNumber,desc"
    },
    "last": {
      "href": "http://localhost:8080/computingfacts/rest/enzymes/?page=3407&size=2&sort=ecNumber,desc"
    }
  },
  "page": {
    "size": 2,
    "totalElements": 6815,
    "totalPages": 3408,
    "number": 1
  }
}

In addition to the Page information, the enzyme result is equipped with pagination links showing prev, next, last etc.

The example code is available on GitHub. Please, drop me an email or leave a comment here. 😊

References

Spring HATEOAS -  https://spring.io/projects/spring-hateoas

Spring Data doc -  https://docs.spring.io/spring-data/data-commons/docs/current/api/org/springframework/data/web/PagedResourcesAssembler.html

Similar Posts ..

Subscribe to our monthly newsletter. No spam, we promise !

Guest