Adding pagination, filters, and sorting capabilities

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.

Getting ready

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.

How to do it...

  1. To the method handlers, we have added the parameters we want them to support. The following handler in 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);
    }
  2. In the corresponding service layer implementation, the 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:

    How to do it...
  3. We have also applied this pattern to dynamically retrieve indices with pagination even though it is almost the same method handler definition.
  4. We also applied the same pattern again to retrieve user activities (in 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);
    }
  5. Now we have adapted the AngularJS layer (detailed in the See also... section of this recipe), we have been able to entirely rewire our welcome page to use REST services with also an infinite scrolling for user activities:
    How to do it...
  6. To fully use the REST service's capabilities, there is now a new screen called INDICES BY MARKET accessible from the Prices and markets menu:
    How to do it...

    The table presented here is entirely autonomous since it features the fully angularized (AngularJS) and asynchronous pagination/sorting capabilities.

  7. The 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);
    }
  8. The 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());
    }
  9. The 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));
    }
        };
      }
    }
  10. Overall, the 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:
    How to do it...
  11. The following screenshot corresponds to the SEARCH BY MARKET form:
    How to do it...
  12. Finally, find the following new Risers and Fallers screen:
    How to do it...

How it works...

Again, this recipe is mostly about Spring Data and how to make Spring MVC support Spring Data for us.

Spring Data pagination support (you will love it!)

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.

Pagination and sorting in repositories

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);

Tip

Bear in mind that sorting options are also handled through Pageable. Incidentally, this is the way we sort in the application.

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).

Tip

This means that Spring Data first executed the query we wanted it to execute, and then executed transparently a count query without the pagination filters.

A Slice object is a super interface of Page, which does not carry the counts for numberOfElements and totalElements.

PagingAndSortingRepository<T,ID>

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);

The web part – PageableHandlerMethodArgumentResolver

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

page

The page we want to retrieve.

0

size

The size of the page we want to retrieve.

10

sort

The properties that should be sorted in the format property,property(,ASC|DESC).

We should use multiple sort parameters if we want to switch directions, for example: ?sort=firstname&sort=lastname,asc.

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
)

Tip

The page, size, and sort parameter names can be overridden by setting the appropriate PageableHandlerMethodArgumentResolver properties in the Spring configuration.

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).

A useful specification argument resolver

Before introducing this useful specification argument resolver, we must introduce the concept of specification.

The JPA2 criteria API and Spring Data JPA specifications

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.

SpecificationArgumentResolver

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":

http://blog.kaczmarzyk.net/2014/03/23/alternative-api-for-filtering-data-with-spring-mvc-and-spring-data

Also, find with the following address the project’s repository and its documentation:

https://github.com/tkaczmarzyk/specification-arg-resolver

Tip

As useful as it can be, do bear in mind that the number of users of this library is still much lower than the Spring community.

There's more...

We have been focusing on Spring MVC so far. However with the presented new screens, there are also changes at the front end (AngularJS).

Spring Data

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

Angular routes

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 template
  • homeMainController is the target controller

See also

You can read more about AngularJS routing at:

https://docs.angularjs.org/tutorial/step_07

Bootstrap pagination with the Angular UI

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.

Tip

To find out more about this project, visit its documentation at:

http://angular-ui.github.io/bootstrap

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset