ZetCode

Java Servlet pagination

last modified August 24, 2023

Java servlet pagination tutorial shows how to do pagination using Java servlets. In the example, Bootstrap is used for the UI.

Pagination

Pagination is the process of dividing content into several pages. The user has a navigation interface for accessing these pages with specific page links. The navigation often includes previous/next and first/last links. Pagination is used when there is lots of data in the database or there are many comments to be shown in one page.

Java Servlet

Servlet is a Java class which responds to a particular type of network request - most commonly an HTTP request. Java servlets are used to create web applications. They run in servlet containers such as Tomcat or Jetty. Modern-day Java web development uses frameworks that are built on top of servlets.

Bootstrap

Bootstrap is a UI library from Twitter for creating responsive, mobile-first web applications.

Java Servlet pagination example

In the following web application, we load data from MySQL database and display it in the table. There is a navigation system to go over all data from the database table. Before the data is displayed in the table, the user has an option to choose how many rows the table will display. The web application is deployed on Jetty server.

Note: Some functionality including data validation or database connection pooling has been omitted to make the application more accessible.

In addition to fetching data from the database table, we also need to know the number of all rows in the database table, the number of records per page, and the number of pages to display in the navigation. The number of all rows in the database is figured out by an SQL statement. The number of records per page is selected by user in an HTML form. Finally, the number of pages in the pagination is computed from the other two values.

countries_mysql.sql
CREATE TABLE countries(id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(255), population INT);

INSERT INTO countries(name, population) VALUES('China', 1382050000);
INSERT INTO countries(name, population) VALUES('India', 1313210000);
INSERT INTO countries(name, population) VALUES('USA', 324666000);
INSERT INTO countries(name, population) VALUES('Indonesia', 260581000);
INSERT INTO countries(name, population) VALUES('Brazil', 207221000);
INSERT INTO countries(name, population) VALUES('Pakistan', 196626000);
INSERT INTO countries(name, population) VALUES('Nigeria', 186988000);
INSERT INTO countries(name, population) VALUES('Bangladesh', 162099000);
INSERT INTO countries(name, population) VALUES('Nigeria', 186988000);
INSERT INTO countries(name, population) VALUES('Russia', 146838000);
INSERT INTO countries(name, population) VALUES('Japan', 126830000);
INSERT INTO countries(name, population) VALUES('Mexico', 122273000);
INSERT INTO countries(name, population) VALUES('Philippines', 103738000);
INSERT INTO countries(name, population) VALUES('Ethiopia', 101853000);
INSERT INTO countries(name, population) VALUES('Vietnam', 92700000);
INSERT INTO countries(name, population) VALUES('Egypt', 92641000);
INSERT INTO countries(name, population) VALUES('Germany', 82800000);
INSERT INTO countries(name, population) VALUES('the Congo', 82243000);
INSERT INTO countries(name, population) VALUES('Iran', 82800000);
INSERT INTO countries(name, population) VALUES('Turkey', 79814000);
INSERT INTO countries(name, population) VALUES('Thailand', 68147000);
INSERT INTO countries(name, population) VALUES('France', 66984000);
INSERT INTO countries(name, population) VALUES('United Kingdom', 60589000);
INSERT INTO countries(name, population) VALUES('South Africa', 55908000);
INSERT INTO countries(name, population) VALUES('Myanmar', 51446000);
INSERT INTO countries(name, population) VALUES('South Korea', 68147000);
INSERT INTO countries(name, population) VALUES('Colombia', 49129000);
INSERT INTO countries(name, population) VALUES('Kenya', 47251000);
INSERT INTO countries(name, population) VALUES('Spain', 46812000);
INSERT INTO countries(name, population) VALUES('Argentina', 43850000);
INSERT INTO countries(name, population) VALUES('Ukraine', 42603000);
INSERT INTO countries(name, population) VALUES('Sudan', 41176000);
INSERT INTO countries(name, population) VALUES('Algeria', 40400000);
INSERT INTO countries(name, population) VALUES('Poland', 38439000);

This SQL script creates the countries table in MySQL.

pom.xml
src
├── main
│   ├── java
│   │   └── com
│   │       └── zetcode
│   │           ├── model
│   │           │   └── Country.java
│   │           ├── service
│   │           │   ├── CountryService.java
│   │           │   └── ICountryService.java
│   │           └── web
│   │               └── ReadCountries.java
│   ├── resources
│   └── webapp
│       ├── index.html
│       └── listCountries.jsp
└── test
    └── java

This is the project structure.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.zetcode</groupId>
    <artifactId>PaginationEx</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>13</maven.compiler.source>
        <maven.compiler.target>13</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>5.2.3.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.20</version>
        </dependency>

        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>jstl</artifactId>
            <version>1.2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.0</version>
            </plugin>

            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.4.30.v20200611</version>
            </plugin>
        </plugins>
    </build>

</project>

This is the Maven POM file. The javax.servlet-api artifact is for servlets. The spring-jdbc dependency is used for the JdbcTemplate library, which simplifies database programming in Java. The mysql-connector-java is a MySQL driver for Java language. The jstl dependency provides some additional functionality to JSP pages.

The maven-war-plugin is responsible for collecting all artifact dependencies, classes and resources of the web application and packaging them into a web application archive (WAR). The jetty-maven-plugin is a useful Maven plugin for rapid development and testing. It creates a web application, starts a Jetty web server, and deploys the application on the server.

com/zetcode/model/Country.java
package com.zetcode.model;

import java.util.Objects;

public class Country {

    private String name;
    private int population;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPopulation() {
        return population;
    }

    public void setPopulation(int population) {
        this.population = population;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Country country = (Country) o;
        return population == country.population &&
                Objects.equals(name, country.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, population);
    }
}

The Country bean holds one row from the countries database table.

com/zetcode/web/ReadCountries.java
package com.zetcode.web;

import com.zetcode.model.Country;
import com.zetcode.service.CountryService;
import java.io.IOException;
import java.util.List;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(name = "ReadCountries", urlPatterns = {"/ReadCountries"})
public class ReadCountries extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("text/html;charset=UTF-8");

        int currentPage = Integer.parseInt(request.getParameter("currentPage"));
        int recordsPerPage = Integer.parseInt(request.getParameter("recordsPerPage"));

        var countryService = new CountryService();

        List<Country> countries = countryService.findCountries(currentPage,
                recordsPerPage);

        request.setAttribute("countries", countries);

        int rows = countryService.getNumberOfRows();

        int nOfPages = rows / recordsPerPage;

        if (nOfPages % recordsPerPage > 0) {

            nOfPages++;
        }

        request.setAttribute("noOfPages", nOfPages);
        request.setAttribute("currentPage", currentPage);
        request.setAttribute("recordsPerPage", recordsPerPage);

        RequestDispatcher dispatcher = request.getRequestDispatcher("listCountries.jsp");
        dispatcher.forward(request, response);
    }
}

The ReadCountries servlet determines how much data will be retrieved from the request attributes and reads the specified amount of rows from the database table.

@WebServlet(name = "ReadCountries", urlPatterns = {"/ReadCountries"})

The Java class is decorated with the @WebServlet annotation. It is mapped to the ReadCountries URL pattern.

response.setContentType("text/html;charset=UTF-8");

The servlet will output data in HTML and the encoding of the data is set to UTF-8.

int currentPage = Integer.parseInt(request.getParameter("currentPage"));
int recordsPerPage = Integer.parseInt(request.getParameter("recordsPerPage"));

From the request we get two important values: the current page and the number of records per page. (The validation of the values is skipped.)

var countryService = new CountryService();

List<Country> countries = countryService.findCountries(currentPage,
        recordsPerPage);

request.setAttribute("countries", countries);

CountryService is a service class for connecting to the database and reading data. The list of countries is retrieved and set as an attribute to the request. It will be used later by the target JSP page.

int rows = countryService.getNumberOfRows();

int nOfPages = rows / recordsPerPage;

if (nOfPages % recordsPerPage > 0) {
    nOfPages++;
}

We get the number of all rows from the database table with the getNumberOfRows service method. We calculate the number of pages in the navigation.

request.setAttribute("noOfPages", nOfPages);
request.setAttribute("currentPage", currentPage);
request.setAttribute("recordsPerPage", recordsPerPage);

The number of pages, the current page, and the number of records per page are values that we need to build the pagination.

var dispatcher = request.getRequestDispatcher("listCountries.jsp");
dispatcher.forward(request, response);

The processing is forwarded to the listCountries.jsp page.

com/zetcode/service/ICountryService.java
package com.zetcode.service;

import com.zetcode.model.Country;
import java.util.List;

public interface ICountryService  {

    List<Country> findCountries(int currentPage, int numOfRecords);
    Integer getNumberOfRows();
}

The ICountryService contains two contract methods: findCountries and getNumberOfRows.

com/zetcode/service/CountryService.java
package com.zetcode.service;

import com.zetcode.model.Country;
import java.sql.SQLException;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;

public class CountryService implements ICountryService {

    @Override
    public List<Country> findCountries(int currentPage, int recordsPerPage)  {

        List<Country> countries = null;

        int start = currentPage * recordsPerPage - recordsPerPage;

        try {
            String sql = "SELECT * FROM countries LIMIT ?, ?";

            var ds = new SimpleDriverDataSource();
            ds.setDriver(new com.mysql.jdbc.Driver());
            ds.setUrl("jdbc:mysql://localhost:3306/testdb?serverTimezone=UTC");
            ds.setUsername("user7");
            ds.setPassword("7user");

            var jtm = new JdbcTemplate(ds);
            countries = jtm.query(sql, new Object[] {start, recordsPerPage},
                    new BeanPropertyRowMapper<>(Country.class));

        } catch (SQLException ex) {
            Logger.getLogger(CountryService.class.getName()).log(Level.SEVERE,
                    null, ex);
        }

        return countries;
    }

    @Override
    public Integer getNumberOfRows() {

        Integer numOfRows = 0;

        try {
            String sql = "SELECT COUNT(id) FROM countries";

            var ds = new SimpleDriverDataSource();
            ds.setDriver(new com.mysql.jdbc.Driver());
            ds.setUrl("jdbc:mysql://localhost:3306/testdb?serverTimezone=UTC");
            ds.setUsername("user7");
            ds.setPassword("7user");

            var jtm = new JdbcTemplate(ds);
            numOfRows = jtm.queryForObject(sql, Integer.class);

        } catch (SQLException ex) {
            Logger.getLogger(CountryService.class.getName()).log(Level.SEVERE,
                    null, ex);
        }

        return numOfRows;
    }
}

The CountryService contains the implementation of the two contract methods.

String sql = "SELECT * FROM countries LIMIT ?, ?";

The SQL LIMIT clause is used to fetch the amount of rows for the current page.

var jtm = new JdbcTemplate(ds);
countries = jtm.query(sql, new Object[] {start, recordsPerPage},
        new BeanPropertyRowMapper(Country.class));

JdbcTemplate is used to execute the SQL statement. The rows are automatically mapped to the Country bean with the help of the BeanPropertyRowMapper.

String sql = "SELECT COUNT(id) FROM countries";

With this SQL statement, we get the number of rows from the database table.

webapp/index.html
<!DOCTYPE html>
<html>
<head>
    <title>Home page</title>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
          integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" 
          crossorigin="anonymous">
</head>

<body>

<main class="m-3">

    <h1>Show countries</h1>

    <form action="ReadCountries">

        <input type="hidden" name="currentPage" value="1">

        <div class="form-group col-md-4">

            <label for="records">Select records per page:</label>

            <select class="form-control" id="records" name="recordsPerPage">
                <option value="5">5</option>
                <option value="10" selected>10</option>
                <option value="15">15</option>
            </select>

        </div>

        <button type="submit" class="btn btn-primary">Submit</button>

    </form>
</main>

</body>
</html>

This is the home page. It contains an HTML form to select the number of records per page with the select tag. The form uses style classes from the Bootstrap library. Upon submitting the form, the processing is sent to the ReadCountries servlet.

<input type="hidden" name="currentPage" value="1">

The form contains a hidden input tag which sets the currentPage parameter to 1.

<select class="form-control" id="records" name="recordsPerPage">
    <option value="5">5</option>
    <option value="10" selected>10</option>
    <option value="15">15</option>
</select>

The select tags allows to choose 5, 10, or 15 records per page.

<button type="submit" class="btn btn-primary">Submit</button>

The Submit button executes the form.

webapp/listCountries.jsp
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Countries</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css"
          integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk"
          crossorigin="anonymous">
</head>

<body>

<main class="m-3">
    <div class="row col-md-6">
        <table class="table table-striped table-bordered table-sm">
            <tr>
                <th>Name</th>
                <th>Population</th>
            </tr>

            <c:forEach items="${countries}" var="country">
                <tr>
                    <td>${country.getName()}</td>
                    <td>${country.getPopulation()}</td>
                </tr>
            </c:forEach>
        </table>
    </div>

    <nav aria-label="Navigation for countries">
        <ul class="pagination">
            <c:if test="${currentPage != 1}">
                <li class="page-item"><a class="page-link"
                    href="ReadCountries?recordsPerPage=${recordsPerPage}¤tPage=${currentPage-1}">Previous</a>
                </li>
            </c:if>

            <c:forEach begin="1" end="${noOfPages}" var="i">
                <c:choose>
                    <c:when test="${currentPage eq i}">
                        <li class="page-item active"><a class="page-link">
                                ${i} <span class="sr-only">(current)</span></a>
                        </li>
                    </c:when>
                    <c:otherwise>
                        <li class="page-item"><a class="page-link"
                            href="ReadCountries?recordsPerPage=${recordsPerPage}¤tPage=${i}">${i}</a>
                        </li>
                    </c:otherwise>
                </c:choose>
            </c:forEach>

            <c:if test="${currentPage lt noOfPages}">
                <li class="page-item"><a class="page-link"
                    href="ReadCountries?recordsPerPage=${recordsPerPage}¤tPage=${currentPage+1}">Next</a>
                </li>
            </c:if>
        </ul>
    </nav>
</main>

</body>
</html>

The listCountries.jsp displays the data in the table and the pagination system. The Bootstrap is used to make the UI responsive and look good.

<table class="table table-striped table-bordered table-sm">

The table, table-striped, table-bordered, table-sm are all Bootstrap classes.

<c:forEach items="${countries}" var="country">
    <tr>
        <td>${country.getName()}</td>
        <td>${country.getPopulation()}</td>
    </tr>
</c:forEach>

With the JSTL's forEach tag, we display all data for the current page.

<c:if test="${currentPage != 1}">
    <li class="page-item"><a class="page-link"
        href="ReadCountries?recordsPerPage=${recordsPerPage}&currentPage=${currentPage-1}">Previous</a>
    </li>
</c:if>

With the c:if tag we only show the Previous link when there is one. In the link, we pass the recordsPerPage and currentPage values to the request object.

<c:forEach begin="1" end="${noOfPages}" var="i">
    <c:choose>
        <c:when test="${currentPage eq i}">
            <li class="page-item active"><a class="page-link">
                    ${i} <span class="sr-only">(current)</span></a>
            </li>
        </c:when>
        <c:otherwise>
            <li class="page-item"><a class="page-link"
                href="ReadCountries?recordsPerPage=${recordsPerPage}&currentPage=${i}">${i}</a>
            </li>
        </c:otherwise>
    </c:choose>
</c:forEach>

With the forEach tag, we display all the page links.

$ mvn jetty:run

We run the Jetty server and navigate to localhost:8080.

Java Servlet Pagination
Figure: Java Servlet Pagination

The example shows a table filled with data and the pagination system. The currently selected page is highlighted.

In this article we have shown how to create a pagination system in a web application with a Java Servlet.

Author

My name is Jan Bodnar and I am a passionate programmer with many years of programming experience. I have been writing programming articles since 2007. So far, I have written over 1400 articles and 8 e-books. I have over eight years of experience in teaching programming.

List all Java Servlet tutorials.