【サーブレット/JSP】フォームとエラーメッセージの表示を覚える

サーブレット/JSPで、フォームの表示と、入力に対してのエラーメッセージを表示する方法を覚えるための自分用メモです。

# 使用環境
PC：MacBook Air 2018
OS：macOS Sonoma 14.3.1
IDE：Eclipse 2023
Webコンテナ：Tomcat 10.1.18

# フォームとは？
フォームとは、HTMLのformタグで記述してWebページに表示できる、入力欄や送信ボタンです。
サーブレット/JSPでは、JSPでフォームを表示してサーブレットに入力データを送ることで、データの処理やJSPでの結果の表示が行えます。

## フォーム（JSPファイル）を表示するサーブレットを作成する
まず、フォームを記述したJSPファイル（registrationForm.jsp）を表示するために、サーブレットのdoGetメソッドに以下の記述をします。
```java:RegistrationFormServlet.java
package servlet;

import java.io.IOException;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public class RegistrationFormServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // JSPファイルを指定してフォワード
        RequestDispatcher dispatcher = request.getRequestDispatcher("WEB-INF/jsp/registrationForm.jsp");
        dispatcher.forward(request, response);
    }

}
```

## JSPファイルにフォームの記述をする
次に、JSPファイルを新規作成して以下のようにフォームの記述をします。
```jsp:registrationForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>会員登録</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header id="header">
        <div class="flex-item wrapper">
            <h1><a href="IndexServlet">SampleSite</a></h1>
        </div>
    </header>
    <main>
        <section id="registration-form">
            <h2>会員登録</h2>
            <form action="RegistrationFormServlet" method="post">
                <div class="row">
                    <label for="name" class="label-title">名前</label>
                    <input type="text" name="name" id="name" class="text-box">
                </div>
                <div class="row">
                    <label for="gender" class="label-title">性別</label>
                    <div>
                        <input type="checkbox" name="gender" id="man" value="男性">
                        <label for="man">男性</label>
                    </div>
                    <div>
                        <input type="checkbox" name="gender" id="woman" value="女性">
                        <label for="woman">女性</label>
                    </div>
                    <div>
                        <input type="checkbox" name="gender" id="none" value="選択しない">
                        <label for="none">選択しない</label>
                    </div>
                </div>
                <div class="row">
                    <label class="label-title">生年月日</label>
                    <div>
                        <select name="year" id="year">
                            <option value="">選択してください</option>
                            <option value="1900">1900</option>
                            <!-- 以下省略 -->
                        </select>
                        <label for="year">年</label>
                    </div>
                    <div>
                        <select name="month" id="month">
                            <option value="">選択してください</option>
                            <option value="1">1</option>
                            <!-- 以下省略 -->
                        </select>
                        <label for="month">月</label>
                    </div>
                    <div>
                        <select name="day">
                            <option value="">選択してください</option>
                            <option value="1">1</option>
                            <!-- 以下省略 -->
                        </select>
                        <label for="day">日</label>
                    </div>
                </div>
                <div class="row">
                    <label for="email" class="label-title">メールアドレス</label>
                    <input type="email" name="email" id="email" class="text-box">
                </div>
                <div class="row">
                    <label for="password" class="label-title">パスワード</label>
                    <input type="password" name="password" id="password" class="text-box">
                </div>
                <input type="submit" value="登録">
            </form>
        </section>
    </main>
</body>
</html>
```
今回は、名前、性別、生年月日、メールアドレス、パスワードといった簡単なアカウント情報を入力（登録）するフォームにしました。

送信ボタンを押すと、入力した情報（リクエストパラメータ）の送信先（サーブレットクラス）とリクエストメソッド（doGet or doPost）が指定され、アプリケーションサーバーに格納されます。

## サーブレットクラスのdoPostメソッドでリクエストパラメータを受け取る
サーブレットクラスにdoPostメソッドを記述して、リクエストパラメータを受け取ります。
```java:RegistrationFormServlet.java
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 文字コードを指定してリクエストパラメータを受け取る
        request.setCharacterEncoding("UTF-8");
        String name = request.getParameter("name");
        String gender = request.getParameter("gender");
        String year = request.getParameter("year");
        String month = request.getParameter("month");
        String day = request.getParameter("day");
        String email = request.getParameter("email");
        String password = request.getParameter("password");
    }
```

## 入力チェックをする
サーブレットから引数を受け取って、入力内容をチェック、問題があればエラーメッセージを追加するクラスを作成します。
```java:CheckRegistrationForm.java
package model;

public class CheckRegistrationForm {
	private String errorMessage;
	
	public CheckRegistrationForm() {
		errorMessage = ""; // エラーメッセージ文頭のnullを消すため
	}
	
	public String checkAll(String name, String gender, String year, String month, String day, String email, String password) {
		checkName(name);
		checkGender(gender);
		checkYear(year);
		checkMonth(month);
		checkDay(day);
		checkEmail(email);
		checkPassword(password);
		return errorMessage;
	}
    // 文字列を受け取って空かどうか判定するメソッド
    private void checkNull(String target, String targetName) {
        if(target == null || target.isEmpty()) {
            errorMessage += targetName + "が入力されていません。<br>";
        }
    }
	private void checkName(String name) {
        checkNull(name, "名前");
		// 以下チェック条件省略
	}
	private void checkGender(String gender) {
        checkNull(gender, "性別");
		// 以下チェック条件省略
	}
	private void checkYear(String year) {
        checkNull(year, "生年月日(年)");
		// 以下チェック条件省略
	}
	private void checkMonth(String month) {
        checkNull(month, "生年月日(月)");
		// 以下チェック条件省略
	}
	private void checkDay(String day) {
        checkNull(day, "生年月日(日)");
		// 以下チェック条件省略
	}
	private void checkEmail(String email) {
        checkNull(email, "メールアドレス");
		// 以下チェック条件省略
	}
	private void checkPassword(String password) {
        checkNull(password, "パスワード");
		// 以下チェック条件省略
	}
}
```
今回は、未入力に対するチェック条件のみ記述しました。
作成したら、サーブレットのdoPostメソッドから以下のような記述で引数を渡します。
```java:RegistrationFormServlet.java
import model.CheckRegistrationForm;
```
```java:RegistrationFormServlet.java
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 文字コードを指定してリクエストパラメータを受け取る
        request.setCharacterEncoding("UTF-8");
        String name = request.getParameter("name");
        String gender = request.getParameter("gender");
        String year = request.getParameter("year");
        String month = request.getParameter("month");
        String day = request.getParameter("day");
        String email = request.getParameter("email");
        String password = request.getParameter("password");
        
        // リクエストパラメータの値を引数で渡してエラーメッセージを受け取る
        CheckRegistrationForm checkRegistrationForm = new CheckRegistrationForm();
        String errorMessage = checkRegistrationForm.checkAll(name, gender, year, month, day, email, password);
    }
```

## 入力確認画面を表示、または入力画面を再度表示する
入力内容をセッションスコープに保存するため、Entity（JavaBeansの仕様に基づいて、データベースのテーブルをそのまま抽出したクラス）が必要なので別途作成します。
```java:RegistrationFormServlet.java
import bean.MemberBean;
```
```java:MemberBean.java
package bean;

import java.io.Serializable;

public class MemberBean implements Serializable {
    private String id;
	private String name;
	private String gender;
	private String year;
	private String month;
	private String day;
	private String email;
	private String password;
	
	public MemberBean() {
	}
    public MemberBean(String id, String name, String gender, String year, String month, String day, String email, String password) {
        this.id = id;
		this.name = name;
		this.gender = gender;
		this.year = year;
		this.month = month;
		this.day = day;
		this.email = email;
		this.password = password;
	}
	public MemberBean(String name, String gender, String year, String month, String day, String email, String password) {
		this.name = name;
		this.gender = gender;
		this.year = year;
		this.month = month;
		this.day = day;
		this.email = email;
		this.password = password;
	}

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getGender() {
		return gender;
	}
	public void setGender(String gender) {
		this.gender = gender;
	}
	public String getYear() {
		return year;
	}
	public void setYear(String year) {
		this.year = year;
	}
	public String getMonth() {
		return month;
	}
	public void setMonth(String month) {
		this.month = month;
	}
	public String getDay() {
		return day;
	}
	public void setDay(String day) {
		this.day = day;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public String getPassword() {
		return password;
	}
	public void setPasword(String password) {
		this.password = password;
	}
}
```
次に、エラーメッセージがなければ入力確認画面、エラーメッセージがあれば入力画面を再度表示するif文をサーブレットのdoPostメソッドに記述します。
```java:RegistrationFormServlet.java
        if(errorMessage.isEmpty()) {
        	// セッションスコープにBeanのインスタンスを保存する
        	MemberBean memberBean = new MemberBean(name, gender, year, month, day, email, password);
        	HttpSession session = request.getSession();
        	session.setAttribute("memberBean", memberBean);
            
            // JSPファイルを指定してフォワード
            RequestDispatcher dispatcher = request.getRequestDispatcher("WEB-INF/jsp/confirmRegistrationForm.jsp");
            dispatcher.forward(request, response);
        } else {
        	// リクエストスコープにエラーメッセージのインスタンスを保存する
        	request.setAttribute("errorMessage", errorMessage);
            
            // JSPファイルを指定してフォワード
        	RequestDispatcher dispatcher = request.getRequestDispatcher("WEB-INF/jsp/registrationForm.jsp");
            dispatcher.forward(request, response);
        }
```
また、Entityのインスタンスを受け取って、入力内容を表示するJSPファイルも必要なので、作成します。
```jsp:confirmRegistrationForm.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page import="bean.MemberBean" %>
<%
MemberBean memberBean = (MemberBean)session.getAttribute("memberBean");
%>
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>入力内容確認</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header id="header">
        <div class="flex-item wrapper">
            <h1><a href="IndexServlet">SampleSite</a></h1>
        </div>
    </header>
    <main>
        <section id="confirm-registration">
            <h2>入力内容確認</h2>
            <p>入力内容を確認の上、登録ボタンをクリックしてください。</p>
            <div class="row">
                <h3>名前</h3>
                <p><%= memberBean.getName() %></p>
            </div>
            <div class="row">
                <h3>性別</h3>
                <p><%= memberBean.getGender() %></p>
            </div>
            <div class="row">
                <h3>生年月日</h3>
                <p><%= memberBean.getYear() %>年<%= memberBean.getMonth() %>月<%= memberBean.getDay() %>日</p>
            </div>
            <div class="row">
                <h3>メールアドレス</h3>
                <p><%= memberBean.getEmail() %></p>
            </div>
            <div class="row">
                <h3>パスワード</h3>
                <p><%= memberBean.getPassword() %></p>
            </div>
            <div class="flex-button">
                <div class="white-button"><a href="RegistrationFormServlet">戻る</a></div>
                <div class="black-button"><a href="">登録</a></div>
            </div>
        </section>
    </main>
</body>
</html>
```
最後に、入力画面のJSPファイルに、エラーメッセージのインスタンスを受け取って表示する記述を追加します。
```jsp:registrationForm.jsp
<body>
    <main>
        <p class="red">${errorMessage}</p>
        <!-- 以下省略 -->
    </main>
</body>
 ```
（この際、EL式にしないと最初に入力画面を表示した際にnullが表示されてしまいます。）

# Eclipseから実行してみる
Eclipseで制作した場合は、サーブレットを右クリック→「実行」からTomCatで実行します。
入力確認画面に遷移、またはエラーメッセージが表示されればOKです。
![member_2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806658/88a0b332-be5c-714a-ffa5-d135c51d92c3.jpeg)
![member_3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806658/dd68b2ca-e84a-9a51-8162-9ea65233f9ad.jpeg)
![member_4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3806658/bf29d883-765d-47c5-677f-0630017cd933.jpeg)

# 次回の学習に向けて
まず、バックエンドに関しては詳細なバリデーションの実装とJDBCを使ったデータベースとの連携を学んで、より実用的なフォームを目指します。
フロントエンドに関しては、CSSによる綺麗で使いやすいレイアウトを目指します。