読者です 読者をやめる 読者になる 読者になる

かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その33 )( 貸出申請画面の作成4 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その32 )( 貸出申請画面の作成3 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 貸出申請画面の作成
      • 最後に表示した貸出申請の ID を Cookie に保存し、次回ログイン時に前回表示していた貸出申請画面を表示する機能の実装

参照したサイト・書籍

  1. Class CookieGenerator
    http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/util/CookieGenerator.html

  2. TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.0.1.RELEASE documentation - 4.4.1.4.9. Cookieから値を取得する
    http://terasolunaorg.github.io/guideline/5.0.1.RELEASE/ja/ImplementationAtEachLayer/ApplicationLayer.html#cookie

  3. TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.0.1.RELEASE documentation - 4.4.1.4.10. Cookieに値を書き込む
    http://terasolunaorg.github.io/guideline/5.0.1.RELEASE/ja/ImplementationAtEachLayer/ApplicationLayer.html#controller-method-argument-cookiewrite-label

  4. What are the differences between Helper and Utility classes?
    http://stackoverflow.com/questions/12192050/what-are-the-differences-between-helper-and-utility-classes

    • helper クラスと utility クラスを使い分けるルールのようなものがあるのか調べた時に参照しました。

目次

  1. Spring Boot に Cookie 用のクラスがあるのか?
  2. Cookie 用クラス CookieLastLendingAppId の作成
  3. ユーティリティクラス CookieUtils の作成
  4. LendingappController クラスの変更
  5. UrlAfterLoginHelper クラスの変更
  6. RoleAwareAuthenticationSuccessHandler クラスの変更
  7. 動作確認
  8. 次回は。。。
  9. メモ書き
    1. helper クラスと utility クラスを使い分けるルールのようなものがあるのか?

手順

Spring Boot に Cookie 用のクラスがあるのか?

Spring Framework に org.springframework.web.util.CookieGenerator というクラスが用意されており、このクラスを継承して Cookie 用のクラスを作成すればよさそうです。ただし CookieGenerator の Javadoc には使用例として org.springframework.web.servlet.i18n.CookieLocaleResolver, org.springframework.web.servlet.theme.CookieThemeResolver が記載されているのですが、Google で検索するとほとんど使用例が見つかりませんでした。CookieGenerator の実装を見た感じでは普通に使えそうに思えるのですが、使われていないのでしょうか?

Spring Framework に用意されているのであれば利用したいので、今回は CookieGenerator クラスを継承して Cookie 用クラスを作成することにします。以下の仕様で実装します。

  • 貸出申請画面を表示した時に未申請の状態であれば URLパラメータに指定された貸出申請ID を LastLendingAppId Cookie に保存します。
  • 「申請」ボタンを押して申請完了したら LastLendingAppId Cookie を削除します。
  • URL を指定した場合ではなく普通にログイン画面を表示してログインした時に LastLendingAppId Cookie が存在したら貸出申請画面を表示するようにします。
  • LastLendingAppId Cookie の保存期間は3日間にします。

Cookie 用クラス CookieLastLendingAppId の作成

  1. src/main/java/ksbysample/webapp/lending の下に cookie パッケージを作成します。

  2. src/main/java/ksbysample/webapp/lending/cookie の下に CookieLastLendingAppId.java を作成します。作成後、リンク先の内容 に変更します。

ユーティリティクラス CookieUtils の作成

CookieGenerator の継承クラスに @Component アノテーションを付加したくないのと、CookieGenerator の継承クラスを使用するのに都度インスタンスを生成する処理を記述するのを避けたいので、ユーティリティクラスを作成します。

  1. src/main/java/ksbysample/webapp/lending/util の下に cookie パッケージを作成します。

  2. src/main/java/ksbysample/webapp/lending/util/cookie の下に CookieUtils.java を作成します。作成後、リンク先の内容 に変更します。

LendingappController クラスの変更

  1. src/main/java/ksbysample/webapp/lending/web/lendingapp の下の LendingappController.javaリンク先の内容 に変更します。

UrlAfterLoginHelper クラスの変更

  1. src/main/java/ksbysample/webapp/lending/config の下の Constant.javaリンク先の内容 に変更します。

  2. src/main/java/ksbysample/webapp/lending/helper/url の下の UrlAfterLoginHelper.javaリンク先の内容 に変更します。

RoleAwareAuthenticationSuccessHandler クラスの変更

  1. src/main/java/ksbysample/webapp/lending/security の下の RoleAwareAuthenticationSuccessHandler.javaリンク先の内容 に変更します。

動作確認

動作確認します。データは Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その31 )( 貸出申請画面の作成2 ) で作成した貸出申請ID = 105 のデータを使用します。lending_app.status の値が 3 になっている場合には 2 に変更しておきます。

また今回は Cookie の作成状況を確認したいので IE ではなく Chrome を使用します。

  1. Gradle projects View から bootRun タスクを実行して Tomcat を起動します。

  2. 最初に貸出申請画面にアクセスした時に未申請の場合には LastLendingAppId Cookie が作成されることを確認します。

  3. ブラウザを起動し http://localhost:8080/lendingapp?lendingAppId=105 へアクセスします。最初はログイン画面が表示されますので ID に "tanaka.taro@sample.com"、Password に "taro" を入力して、「次回から自動的にログインする」をチェックせずに「ログイン」ボタンをクリックします。

    f:id:ksby:20151129044806p:plain

    貸出申請画面が表示され、LastLendingAppId Cookie が有効期限付きで作成されていることが確認できます。

    f:id:ksby:20151129045051p:plain

  4. 次にログアウトしてから単にログイン画面からログインした場合に最後にアクセスした貸出申請画面が表示されることを確認します。

  5. 画面右上の「ログアウト」メニューをクリックしてログアウトします。その後ログインします。

    f:id:ksby:20151129045535p:plain

    ログイン後に http://localhost:8080/lendingapp?lendingAppId=105 の URL にリダイレクトし、貸出申請画面が表示されることを確認します。

    f:id:ksby:20151129045750p:plain

  6. 「申請」ボタンをクリックして申請完了したら LastLendingAppId Cookie が削除されることを確認します。

  7. 上の画面の状態からいくつかで「申請する」を選択して申請理由を記入した後、「申請」ボタンをクリックします。

    LastLendingAppId Cookie が削除されていることを確認します。

    f:id:ksby:20151129051041p:plain

  8. 最後に LastLendingAppId Cookie がない状態でログインした場合には貸出申請画面へ遷移しないことを確認します。

  9. 画面右上の「ログアウト」メニューをクリックしてログアウトします。その後ログインします。

    f:id:ksby:20151129051345p:plain

    "tanaka.taro@sample.com" でログインすると ROLE_ADMIN が付与されているので検索対象図書館登録画面が表示されることが確認できます。

    f:id:ksby:20151129051602p:plain

  10. Ctrl+F2 を押して Tomcat を停止します。

  11. 一旦 commit します。

次回は。。。

「一時保存」ボタンクリック時の処理を実装する予定です。

メモ書き

helper クラスと utility クラスを使い分けるルールのようなものがあるのか?

今回も CookieUtils クラスを作成する時に helper クラスにするか utility クラスにするか迷ったので調べてみました。ついでに自分なりの基準を1度決めておきたいと思います。

自分が見つけた Web ページでは What are the differences between Helper and Utility classes? の回答が一番分かりやすそうに思えました。回答では特に基準はないとも書かれていましたが、このページの内容を元に今後は以下の基準で作成することにしたいと思います。

  • utility クラス
    • ステートレスで、メソッドは全て static メソッドのみで構成するクラスとする。
    • @Component アノテーションが必要になる場合には helper クラスにする。utility クラスにはしない。
  • helper クラス
    • 以下のいずれかの条件に該当する場合には utility クラスではなく helper クラスとする。
      • @Component アノテーションが必要。
      • ステートフルである ( 状態を保持する必要がある )。ただしこの場合には @Component アノテーションは付加しないこと。
      • システム固有のクラスに依存する必要がある。

上の基準だと VelocityUtils は helper クラスにするのが妥当なので、後で変更したいと思います。

ソースコード

CookieLastLendingAppId.java

package ksbysample.webapp.lending.cookie;

import org.springframework.web.util.CookieGenerator;

public class CookieLastLendingAppId extends CookieGenerator {

    public static final String COOKIE_NAME = "LastLendingAppId";

    private static final Integer COOKIE_MAX_AGE = 24 * 60 * 60 * 3; // 3日間

    public CookieLastLendingAppId() {
        setCookieName(COOKIE_NAME);
        setCookieMaxAge(COOKIE_MAX_AGE);
    }
    
}

CookieUtils.java

package ksbysample.webapp.lending.util.cookie;

import org.apache.commons.lang3.StringUtils;
import org.springframework.web.util.CookieGenerator;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;
import java.util.Optional;

public class CookieUtils {

    public static <T extends CookieGenerator> void addCookie(Class<T> clazz, HttpServletResponse response, String cookieValue) {
        try {
            T cookieGenerator = clazz.newInstance();
            cookieGenerator.addCookie(response, cookieValue);
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static <T extends CookieGenerator> void removeCookie(Class<T> clazz, HttpServletResponse response) {
        try {
            T cookieGenerator = clazz.newInstance();
            cookieGenerator.removeCookie(response);
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public static String getCookieValue(String cookieName, HttpServletRequest request) {
        Optional<String> result = Optional.empty();
        if (request != null) {
            Cookie[] cookies = request.getCookies();
            result = Arrays.asList(cookies).stream()
                    .filter(cookie -> StringUtils.equals(cookie.getName(), cookieName))
                    .map(cookie -> cookie.getValue())
                    .findFirst();
        }
        return result.orElse(null);
    }

}

LendingappController.java

package ksbysample.webapp.lending.web.lendingapp;

import ksbysample.webapp.lending.cookie.CookieLastLendingAppId;
import ksbysample.webapp.lending.entity.LendingApp;
import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.exception.WebApplicationRuntimeException;
import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper;
import ksbysample.webapp.lending.util.cookie.CookieUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.http.HttpServletResponse;
import java.util.List;

import static ksbysample.webapp.lending.values.LendingAppStatusValues.UNAPPLIED;

@Controller
@RequestMapping("/lendingapp")
public class LendingappController {

    @Autowired
    private LendingappService lendingappService;

    @Autowired
    private MessagesPropertiesHelper messagesPropertiesHelper;

    @Autowired
    private LendingappFormValidator lendingappFormValidator;

    @InitBinder(value = "lendingappForm")
    public void initBinder(WebDataBinder binder) {
        binder.addValidators(lendingappFormValidator);
    }

    @RequestMapping
    public String index(@Validated LendingappParamForm lendingappParamForm
            , BindingResult bindingResultForLendingappParamForm
            , LendingappForm lendingappForm
            , HttpServletResponse response) {
        if (bindingResultForLendingappParamForm.hasErrors()) {
            throw new WebApplicationRuntimeException(
                    messagesPropertiesHelper.getMessage("LendingappForm.lendingAppId.emptyerr", null));
        }

        // 画面に表示するデータを取得する
        setDispData(lendingappParamForm.getLendingAppId(), lendingappForm);

        // 未申請の場合には LastLendingAppId Cookie に貸出申請ID をセットする
        if (StringUtils.equals(lendingappForm.getLendingApp().getStatus(), UNAPPLIED.getValue())) {
            CookieUtils.addCookie(CookieLastLendingAppId.class
                    , response, String.valueOf(lendingappParamForm.getLendingAppId()));
        }

        return "lendingapp/lendingapp";
    }

    @RequestMapping(value = "/apply", method = RequestMethod.POST)
    public String apply(@Validated LendingappForm lendingappForm
            , BindingResult bindingResult
            , HttpServletResponse response) {
        if (bindingResult.hasErrors()) {
            return "lendingapp/lendingapp";
        }

        // 入力された内容で申請する
        lendingappService.apply(lendingappForm);

        // 画面に表示するデータを取得する
        setDispData(lendingappForm.getLendingApp().getLendingAppId(), lendingappForm);

        // LastLendingAppId Cookie を削除する
        CookieUtils.removeCookie(CookieLastLendingAppId.class, response);
        
        return "lendingapp/lendingapp";
    }

    @RequestMapping(value = "/temporarySave", method = RequestMethod.POST)
    public String temporarySave() {
        return "lendingapp/lendingapp";
    }

    private void setDispData(Long lendingAppId, LendingappForm lendingappForm) {
        LendingApp lendingApp = lendingappService.getLendingApp(lendingAppId);
        List<LendingBook> lendingBookList = lendingappService.getLendingBookList(lendingAppId);
        lendingappForm.setLendingApp(lendingApp);
        lendingappForm.setLendingBookList(lendingBookList);
    }

}
  • index メソッドに、未申請の場合には LastLendingAppId Cookie に貸出申請ID をセットする処理を追加します。
  • apply メソッドに、LastLendingAppId Cookie を削除する処理を追加します。

Constant.java

package ksbysample.webapp.lending.config;

public class Constant {

    /* 
     * RabbitMQ Queue一覧
     */
    public static final String QUEUE_NAME_INQUIRING_STATUSOFBOOK = "InquiringStatusOfBookQueue";

    /* 
     * URL一覧
     */
    public static final String URL_ADMIN_LIBRARY = "/admin/library";
    public static final String URL_LENDINGAPP = "/lendingapp";

    /* 
     * ログイン後ページのURL
     */
    public static final String URL_AFTER_LOGIN_FOR_ROLE_ADMIN = URL_ADMIN_LIBRARY;

}
  • 定数 URL_LENDINGAPP を追加します。

UrlAfterLoginHelper.java

package ksbysample.webapp.lending.helper.url;

import ksbysample.webapp.lending.config.Constant;
import ksbysample.webapp.lending.config.WebSecurityConfig;
import ksbysample.webapp.lending.cookie.CookieLastLendingAppId;
import ksbysample.webapp.lending.util.cookie.CookieUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import javax.servlet.http.HttpServletRequest;

public class UrlAfterLoginHelper {
    
    public static String getUrlAfterLogin(Authentication authentication) {
        return getUrlAfterLogin(authentication, null);
    }

    public static String getUrlAfterLogin(Authentication authentication, HttpServletRequest request) {
        String targetUrl = WebSecurityConfig.DEFAULT_SUCCESS_URL;

        // 特定の権限を持っている場合には対応するURLへリダイレクトする
        GrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN");
        if (authentication.getAuthorities().contains(roleAdmin)) {
            // 管理権限 ( ROLE_ADMIN ) を持っている場合には検索対象図書館登録画面へ遷移させる
            targetUrl = Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN;
        }

        // LastLendingAppId Cookie に貸出申請ID をセットされている場合には貸出申請画面へリダイレクトさせる
        String cookieLastLendingAppId = CookieUtils.getCookieValue(CookieLastLendingAppId.COOKIE_NAME, request);
        if (StringUtils.isNotBlank(cookieLastLendingAppId)) {
            targetUrl = String.format("%s?lendingAppId=%s", Constant.URL_LENDINGAPP, cookieLastLendingAppId);
        }
            
        return targetUrl;
    }

}
  • public static String getUrlAfterLogin(Authentication authentication, HttpServletRequest request) メソッドを追加します。public static String getUrlAfterLogin(Authentication authentication) メソッドの処理をこちらへ移動した後、LastLendingAppId Cookie に貸出申請ID をセットされている場合には貸出申請画面へリダイレクトさせる処理を追加します。
  • public static String getUrlAfterLogin(Authentication authentication) メソッドpublic static String getUrlAfterLogin(Authentication authentication, HttpServletRequest request) を第2引数は null で呼び出すように変更します。

RoleAwareAuthenticationSuccessHandler.java

package ksbysample.webapp.lending.security;

import ksbysample.webapp.lending.cookie.CookieLastLendingAppId;
import ksbysample.webapp.lending.helper.url.UrlAfterLoginHelper;
import ksbysample.webapp.lending.util.cookie.CookieUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class RoleAwareAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws ServletException, IOException {
        // ログイン画面以外のURLを指定してアクセスされていた場合には、処理を SavedRequestAwareAuthenticationSuccessHandler へ委譲する
        SavedRequest savedRequest = requestCache.getRequest(request, response);
        if (savedRequest != null) {
            super.onAuthenticationSuccess(request, response, authentication);
            return;
        }
        
        String targetUrl = UrlAfterLoginHelper.getUrlAfterLogin(authentication, request);
        clearAuthenticationAttributes(request);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
    
}
  • UrlAfterLoginHelper.getUrlAfterLogin の第2引数に request を追加します。

履歴

2015/11/29
初版発行。
2015/12/03
* 「helper クラスと utility クラスを使い分けるルールのようなものがあるのか?」の内容をもう少し判断しやすい基準に変更しました。