いよいよ英語辞書アプリの最終回だ。今回は検索結果が複数ページまたがるときにナビゲーションするページネーションを作成する。通常はフレームワークやプラグインを使ってページネーションを作ることが多いが一度は自分でコーディングしておくとよいだろう。

viewの変更

1.ページネーションはviewの中で生成するのはややヘビーなので、生成はcontrollerでやってviewはそれを読み込むだけにする。以下のようにmain.jspを変更する。
●main.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" import="model.*,java.util.*"%>
<%
String searchWord=(String)request.getAttribute("searchWord");
searchWord=searchWord ==null? "":searchWord;
String mode=(String)request.getAttribute("mode");
mode=mode == null? "":mode;
List<Word> list=(List<Word>)request.getAttribute("list");
Integer total=(Integer)request.getAttribute("total");
Integer limit=(Integer)request.getAttribute("limit");
Integer pageNo=(Integer)request.getAttribute("pageNo");
String pagination=(String)request.getAttribute("pagination");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>EJWord</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<style>
.container{
min-height:calc(100vh - 70px);
}
form{
margin:20px auto;
}
input,select{
margin-right:5px;
}
.pager{
text-align:left;
}
.paginationBox{
text-align:center;
}
footer{
height:40px;
background:#347ab7;
color:white;
text-align:center;
line-height:40px;
margin-top:30px;
}
</style>
</head>
<body>
<div class="container">
<form action="/ejword/main" method="get" class="form-inline">
<input type="text" name="searchWord" value="<%=searchWord%>" class="form-control" placeholder="検索語を入力" required>
<select name="mode" class="form-control">
<option value="startsWith"<%if(mode.equals("statsWith")) out.print(" selected"); %>>で始まる</option>
<option value="contains"<%if(mode.equals("contains")) out.print(" selected"); %>>含む</option>
<option value="endsWith"<%if(mode.equals("endsWith")) out.print(" selected"); %>>で終わる</option>
<option value="match"<%if(mode.equals("match")) out.print(" selected"); %>>一致する</option>
</select>
<button type="submit" class="btn btn-primary">検索</button>
</form>
<% if(list !=null && list.size() > 0){ %>
<%-- 件数表示部分作成 --%>

<% if(total <= limit){ %>
<p>全<%=total %>件</p>
<%}else{ %>
<%--ページ番号を利用して何件から何件を表示しているのかを表示する --%>
		<p>全<%=total %>件中 <%=(pageNo-1)*limit+1 %>~<%=pageNo*limit > total? total:pageNo*limit %>件を表示</p>
<%--ページ番号が1より大きかったら前へのリンクを表示 --%>
		<ul class="pager">
		<%if(pageNo > 1) {%>
		  <li><a href="/ejword/main?searchWord=<%=searchWord %>&mode=<%=mode %>&page=<%=pageNo-1%>"><span aria-hidden="true">&larr;</span>前へ</a></li>

		<%} %>
		<%--件数が全件数に届かないときは次へのリンクを表示 --%>
		<%if(pageNo*limit < total) {%>
		<li><a href="/ejword/main?searchWord=<%=searchWord %>&mode=<%=mode %>&page=<%=pageNo+1%>">次へ<span aria-hidden="true">&rarr;</a></li>

		<%} %>
		</ul>
<%} %>

<table class="table table-bordered table-striped">
<% for(Word w:list){ %>
<tr><th><%=w.getTitle() %></th><td><%=w.getBody() %></td></tr>
<%} %>
</table>
<%} %>
<%if(pagination != null){ %>
 <%=pagination %>
 <%} %>
</div><!-- container -->
<footer>
&copy; 2017 Joytas.net
</footer>
</body>
</html>

Paginationの作成

2.それではいよいよPaginationを作成していこう。見た目は今回Bootstrapを使うのでまずは雛形を探しに行く。
https://getbootstrap.com/docs/3.3/components/#pagination
component->paginationとリンクをたどるとそこにサンプルソースがあるのでこれをまず雛形とする。

<nav aria-label="Page navigation">
  <ul class="pagination">
    <li>
      <a href="#" aria-label="Previous">
        <span aria-hidden="true">&laquo;</span>
      </a>
    </li>
    <li><a href="#">1</a></li>
    <li><a href="#">2</a></li>
    <li><a href="#">3</a></li>
    <li><a href="#">4</a></li>
    <li><a href="#">5</a></li>
    <li>
      <a href="#" aria-label="Next">
        <span aria-hidden="true">&raquo;</span>
      </a>
    </li>
  </ul>
</nav>

3.ulを使ってマークアップすれば良いようだ。controllerで実際に記述していこう。Main.javaに以下のような修正を加える。
変更箇所:52行目以降。
●controller.Main.java

package controller;

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;

import dao.WordDAO;
import model.Word;

@WebServlet("/main")
public class Main extends HttpServlet {
	private static final long serialVersionUID = 1L;
	private static final int LIMIT = 20;

	protected void doGet(HttpServletRequest request, HttpServletResponse response)
			throws ServletException, IOException {
		request.setCharacterEncoding("UTF-8");
		String searchWord = (String) request.getParameter("searchWord");
		if (searchWord != null) {
			String mode = (String) request.getParameter("mode");
			if(mode ==null){
				mode="startsWith";
			}
			String page=(String)request.getParameter("page");
			int pageNo=page==null? 1:Integer.parseInt(page);
			WordDAO dao = new WordDAO();
			int total = dao.getCount(searchWord, mode);
			List<Word> list = dao.getListBySearchWord(searchWord, mode, LIMIT,(pageNo-1)*LIMIT);
			request.setAttribute("total", total);
			request.setAttribute("limit", LIMIT);
			request.setAttribute("searchWord", searchWord);
			request.setAttribute("mode", mode);
			request.setAttribute("list", list);
			request.setAttribute("pageNo",pageNo);
			if(total > LIMIT){
				//ページ数
				int pageCount=total%LIMIT == 0 ? total/LIMIT : total/LIMIT +1;
				String link="";
				StringBuilder sb=new StringBuilder();
				sb.append("<div class='paginationBox'>\n");
				sb.append("<ul class='pagination'>\n");
				//ページ数が20ページで収まるか?
				if(pageCount<20){
					for(int i=1;i<=pageCount;i++){
						link="/ejword/main?searchWord="+searchWord+"&mode="+mode+"&page="+i;
						sb.append("<li class='"+(pageNo==i? "active":"") +"'><a href='"+link+"'>"+i+"</a></li>\n");
					}
				}else{
					//大量にページがある場合先頭へのリンクを追加する
					link="/ejword/main?searchWord="+searchWord+"&mode="+mode+"&page="+1;
					sb.append("<li class='"+(pageNo==1? "disabled":"") +"'><a href='"+link+"' aria-label='Start'><span aria-hidden='true'>&laquo;</span></a></li>\n");
					//現在ページから前後5件を表示
					for(int i=pageNo-5;i<=pageNo+5;i++){
						if(i<1 || i>pageCount){continue;}
						link="/ejword/main?searchWord="+searchWord+"&mode="+mode+"&page="+i;
						sb.append("<li class='"+(pageNo==i? "active":"") +"'><a href='"+link+"'>"+i+"</a></li>\n");
					}
					//最後へのリンクを追加する
					link="/ejword/main?searchWord="+searchWord+"&mode="+mode+"&page="+pageCount;
					sb.append("<li class='"+(pageNo==total/LIMIT+1? "disabled":"") +"'><a href='"+link+"' aria-label='End'><span aria-hidden='true'>&raquo;</span></a></li>\n");			
				}	
				sb.append("</ul>\n");
				sb.append("</div>\n");
				request.setAttribute("pagination", sb.toString());	
			}	
		
		}

		RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/view/main.jsp");
		rd.forward(request, response);
	}

}


[ポイント解説]
今回は雛形にあった大外のnav要素は使わずに代わりにdivを使った。このdivにスタイルをあてることによってページネーション自体をセンタリングしている。今回検索語によっては大量にページが発生する場合があるので今回は20ページ以上発生する場合とそうでない場合でページネーションを分けている。こういった細かいことをしたい場合はフレームワークやプラグインを使うよりも自分でスクラッチしてしまった方が早い場合もある。基本StringBuilderで文字列を連結してるのだが末尾に改行文字を入れている。これはブラウザでソースコードを見たときにきれいに表示させるためのものだ。

動作確認

検索結果が膨大なページ数になっても問題なく表示できている。
そのほかフッターをつけたり少しスタイルをいれた。

以上で英語アプリ作成は終了だ。データベース登録から、データの読み込み表示部分にフォーカスしたお題だったがウェブアプリケーションを作る上で大事な部分が多く入っていた。
是非自分の手を動かして作ってみてもらいたい。