Now we have introduced the basis for a REST configuration of Spring MVC, we will improve our REST services by adding pagination, filtering, and sorting capabilities.
Pagination is a concept developed in the Spring Data project. To add pagination, we will introduce the Pageable
interface for wrapper implementations populated from the request. These are further on recognized and handled by Spring Data.
The Page
interface and specifically the PageImpl
instances can be produced by Spring Data to format its results. We will use them, as they are perfectly suited to REST rendering.
Finally, we will detail two data-binding tools used here to abstract filtering and pagination from our controllers' logic.
IndexController
now offers pagination and sorting:import org.springframework.data.domain.PageRequest;
@RequestMapping(value="/{market}", method=GET)
public Page<IndexOverviewDTO> getIndicesPerMarket(
@PathVariable MarketCode market,
@PageableDefault(size=10, page=0, sort={"dailyLatestValue"}, direction=Direction.DESC) Pageable pageable){
return marketService. getLastDayIndicesOverview(market, pageable);
}
pageable
instance is passed to the Spring Data JPA abstracted implementation:@Override public Page<IndexOverviewDTO> getLastDayIndicesOverview(Pageable pageable) { Page<Index> indices = indexProductRepository.findAll(pageable); List<IndexOverviewDTO> result = new LinkedList<>(); for (Index index : indices) { result.add(IndexOverviewDTO.build(index)); } return new PageImpl<>(result, pageable, indices.getTotalElements()); }
That's pretty much all about the pagination and sorting pattern! All the boilerplate code is transparent. It allows us to magically retrieve a resource wrapped in a page element that carries the tools that the front end may need for pagination. For our specific method handler, calling the URL:
http://localhost:8080/api/indices/US.json?size=2&page=0&sort=dailyLatestValue
,asc
results in the following JSON response:
CommunityController
):@RequestMapping(value="/activity", method=GET) @ResponseStatus(HttpStatus.OK) public Page<UserActivityDTO> getPublicActivities( @PageableDefault(size=10, page=0, sort={"quote.date"},direction=Direction.DESC) Pageable pageable){ return communityService.getPublicActivity(pageable); }
The table presented here is entirely autonomous since it features the fully angularized (AngularJS) and asynchronous pagination/sorting capabilities.
StockProductController
object, in its search()
method handler, has implemented the pagination and sorting pattern, but also a filtering feature that allows the user to operate LIKE
SQL operators combined with AND
restrictions:@RequestMapping(method=GET) @ResponseStatus(HttpStatus.OK) public Page<ProductOverviewDTO> search( @And(value = { @Spec(params = "mkt", path="market.code",spec = EqualEnum.class)}, and = { @Or({ @Spec(params="cn", path="code", spec=LikeIgnoreCase.class), @Spec(params="cn", path="name", spec=LikeIgnoreCase.class)})} ) Specification<StockProduct> spec, @RequestParam(value="mkt", required=false) MarketCodeParam market, @RequestParam(value="sw", defaultValue="") String startWith, @RequestParam(value="cn", defaultValue="") String contain, @PageableDefault(size=10, page=0, sort={"dailyLatestValue"}, direction=Direction.DESC) Pageable pageable){ return productService.getProductsOverview(startWith, spec, pageable); }
productService
implementation, in its getProductsOverview
method (as shown), refers to a created nameStartsWith
method:@Override public Page<ProductOverviewDTO> getProductsOverview(String startWith, Specification<T> spec, Pageable pageable) { if(StringUtils.isNotBlank(startWith)){ spec = Specifications.where(spec).and(new ProductSpecifications<T>().nameStartsWith(startWith); } Page<T> products = productRepository.findAll(spec, pageable); List<ProductOverviewDTO> result = new LinkedList<>(); for (T product : products) { result.add(ProductOverviewDTO.build(product)); } return new PageImpl<>(result, pageable, products.getTotalElements()); }
nameStartsWith
method is a specification factory located in the core module inside the ProductSpecifications
class:public class ProductSpecifications<T extends Product> { public Specification<T> nameStartsWith(final String searchTerm) { return new Specification<T>() { private String startWithPattern(final String searchTerm) { StringBuilder pattern = new StringBuilder(); pattern.append(searchTerm.toLowerCase()); pattern.append("%"); return pattern.toString(); } @Override public Predicate toPredicate(Root<T> root,CriteriaQuery<?> query, CriteriaBuilder cb) { return cb.like(cb.lower(root.<String>get("name")), startWithPattern(searchTerm)); } }; } }
search()
REST service is extensively used over three new screens related to stocks retrieval. These screens are accessible through the Prices and markets menu. Here is the new ALL PRICES SEARCH form:Again, this recipe is mostly about Spring Data and how to make Spring MVC support Spring Data for us.
We already looked at some of the benefits of the Spring Data repository abstraction in the previous chapter.
In this section, we will see how Spring Data supports the pagination concepts in its abstracted repositories. A very beneficial extension of that, is offered to Spring MVC with a specific argument-resolver to prevent any custom adaption logic.
You can notice the use of Pageable arguments in the methods of our repository interfaces. For example below is the IndexRepositoryJpa
repository:
public interface IndexRepositoryJpa extends JpaRepository<Index, String>{ List<Index> findByMarket(Market market); Page<Index> findByMarket(Market market, Pageable pageable); List<Index> findAll(); Page<Index> findAll(Pageable pageable); Index findByCode(MarketCode code); }
Spring Data recognizes the org.springframework.data.domain.Pageable
Type as the method argument. It also recognizes the org.springframework.data.domain.Sort
Type when a full Pageable
instance is not necessary. It applies pagination and sorting to our queries dynamically.
You can see more examples here (taken from the Spring reference document):
Page<User> findByLastname(String lastname, Pageable pageable); Slice<User> findByLastname(String lastname, Pageable pageable); List<User> findByLastname(String lastname, Sort sort); List<User> findByLastname(String lastname, Pageable pageable);
From these extra examples, you can see that Spring Data can return a Page
(org.springframework.data.domain.Page)
, a Slice
(org.springframework.data.domain.Slice)
or simply a List
.
But here is the amazing part: a Page
object contains everything we need to build powerful pagination tools at the front end! Earlier, we saw the json
response provided with one Page
of elements.
With With the following request: http://localhost:8080/api/indices/US.json?size=2&page=0&sort=dailyLatestValue,asc
, we have asked for the first page and received a Page
object telling us whether or not this page is the first or the last one (firstPage: true/false
, lastPage: true/false
), the number of elements within the page (numberOfElements: 2
), the total number of pages, and the total number of elements (totalPages: 2
, totalElements: 3
).
A Slice
object is a super interface of Page
, which does not carry the counts for numberOfElements
and totalElements
.
If a repository does not already extend JpaRepository<T,ID>
, we can make it extend PagingAndSortingRepository<T,ID>
, which is an extension of CrudRepository<T,ID>
. It will provide extra methods to retrieve Entities using the pagination and sorting abstraction. These methods are:
Iterable<T> findAll(Sort sort);
Page<T> findAll(Pageable pageable);
As introduced earlier, we have added the org.springframework.data.web.PageableHandlerMethodArgumentResolver
bean to our RequestMappingHandlerAdapter
as a customArgumentResolver
. Doing so has allowed us to rely on the Spring data binding to transparently prepopulate a Pageable
instance available as a method handler argument (highlighted in bold in the 1st step of this recipe).
Here is some more information about the request parameters we can use for the binding:
Parameter name |
Purpose / usage |
Default values |
---|---|---|
|
The page we want to retrieve. |
0 |
|
The size of the page we want to retrieve. |
10 |
|
The properties that should be sorted in the format We should use multiple |
The default sort direction is ascending. |
As implemented in our first step, default values can be customized in cases where specific parameters are missing. This is achieved with the @PageableDefault
annotation:
@PageableDefault( size=10, page=0, sort={"dailyLatestValue"}, direction=Direction.DESC )
If for some reason we don't make use of PageableHandlerMethodArgumentResolver
, we can still catch our own request parameters (for pagination) and build a PageRequest
instance from them (for example, org.springframework.data.domain.PageRequest
is a Pageable
implementation).
Before introducing this useful specification argument resolver, we must introduce the concept of specification.
The Spring Data reference document tells us that JPA 2 has introduced a criteria API that can be used to build queries programmatically. When writing criteria
, we actually define the where clause of a query for a domain class.
The Spring Data JPA takes the concept of specification from Eric Evans's book Domain Driven Design, following the same semantics and providing an API to define such specifications using the JPA criteria API.
To support specifications, we can extend our repository interface with the JpaSpecificationExecutor
interface, as we did in our ProductRepository
interface:
@Repository public interface ProductRepository<T extends Product> extends JpaRepository<T, String>, JpaSpecificationExecutor<T> { Page<T> findByMarket(Market marketEntity, Pageable pageable); Page<T> findByNameStartingWith(String param, Pageable pageable); Page<T> findByNameStartingWith(String param, Specification<T> spec, Pageable pageable); }
In our example, the findByNameStartingWith
method retrieves all the products of a specific Type (StockProduct
) that have a name starting with the param
argument and that match the spec
specification.
As we said earlier, this CustomArgumentResolver
is not bound to an official Spring project (yet). Its use can fit some use cases such as local search engines to complement Spring Data dynamic queries, pagination, and sorting features.
In the same way we build a Pageable
instance from specific parameters, this argument resolver also allows us to transparently build a Specification
instance from specific parameters.
It uses @Spec
annotations to define where
clauses such as like
, equal
, likeIgnoreCase
, in
, and so on. These @Spec
annotations can then be combined with each other to form groups of AND
and OR
clauses with the help of @And
and @Or
annotations. A perfect use case is to develop our search features as a complement to the pagination and sorting function.
You should read the following article which is an introduction to the project. This article is entitled "an alternative API for filtering data with Spring MVC & Spring Data JPA":
Also, find with the following address the project’s repository and its documentation:
We have been focusing on Spring MVC so far. However with the presented new screens, there are also changes at the front end (AngularJS).
To find out more about Spring Data capabilities, check out the official reference document:
http://docs.spring.io/spring-data/jpa/docs/1.8.0.M1/reference/html
If you navigate between the Home and Prices and Market menus, you will see that the whole page is never entirely refreshed. All the content is loaded asynchronously.
To achieve this, we used the AngularJS routing. The global_routes.js
file has been created for this purpose:
cloudStreetMarketApp.config(function($locationProvider, $routeProvider) { $locationProvider.html5Mode(true); $routeProvider .when('/portal/index', { templateUrl: '/portal/html/home.html', controller: 'homeMainController' }) .when('/portal/indices-:name', { templateUrl: '/portal/html/indices-by-market.html', controller: 'indicesByMarketTableController' }) .when('/portal/stock-search', { templateUrl: '/portal/html/stock-search.html', controller: 'stockSearchMainController' }) .when('/portal/stock-search-by-market', { templateUrl: '/portal/html/stock-search-by-market.html', controller: 'stockSearchByMarketMainController' }) .when('/portal/stocks-risers-fallers', { templateUrl: '/portal/html/stocks-risers-fallers.html', controller: 'stocksRisersFallersMainController' }) .otherwise({ redirectTo: '/' }); });
Here, we defined a mapping table between routes (URL paths that the application queries, as part of the navigation through the href
tags) and HTML templates (which are available on the server as public static resources). We have created an html
directory for these templates.
Then, AngularJS asynchronously loads a template each time we request a specific URL path. As often, AngularJS operates transclusions do to this (it basically drops and replace entire DOM sections). Since templates are just templates, they need to be bound to controllers, which operate other AJAX requests through our factories, pull data from our REST API, and render the expected content.
In the previous example:
/portal/index
is a route, that is, a requested path/portal/html/home.html
is the mapped templatehomeMainController
is the target controllerYou can read more about AngularJS routing at:
https://docs.angularjs.org/tutorial/step_07
We have used the pagination component of the UI Bootstrap project (http://angular-ui.github.io/bootstrap) from the AngularUI team (http://angular-ui.github.io). This project provides a Boostrap
component operated with and for AngularJS.
In the case of pagination, we obtain a Bootstrap
component (perfectly integrated with the Bootstrap stylesheet) driven by specific AngularJS directives.
One of our pagination components can be
found in the stock-search.html
template:
<pagination page="paginationCurrentPage" ng-model="paginationCurrentPage" items-per-page="pageSize" total-items="paginationTotalItems" ng-change="setPage(paginationCurrentPage)"> </pagination>
The page
, ng-model
, items-per-page
, total-items
, and ng-change
directives use variables (paginationCurrentPage
, pageSize
and paginationTotalItems
), which are attached to the stockSearchController
scope.