SharedRealworldExceptionHandlers.kt

package com.example.realworldkotlinspringbootjdbc.presentation.shared

import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.GenericErrorModel
import com.example.realworldkotlinspringbootjdbc.openapi.generated.model.GenericErrorModelErrors
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException

/**
 * 共通の例外ハンドラー
 *
 * 扱う項目(AOP系)
 * - 認証失敗
 * - セッションEncodeのエラー
 * - 技術的(異常系)例外
 *
 * 保守がしづらくなるので、基本的に増えないことが望ましい
 * ファイルも(あえて)分けない
 *   - 仮に分けるとしたら専用のパッケージを切る
 *
 * 命名ルール
 * on${例外名}
 */
@RestControllerAdvice
class SharedRealworldExceptionHandlers {
    /**
     * RequestParamの型変換に失敗した場合のエラーハンドリング
     *
     *  - クエリパラーメータの型が合わない場合、ここでエラーハンドリングされる
     *
     *  例
     *  - Intに変換できるStringを要求しているのに、"1"等ではなく、"ああ"等でリクエストされた時
     */
    @ExceptionHandler(value = [MethodArgumentTypeMismatchException::class])
    fun onMethodArgumentTypeMismatchException(e: MethodArgumentTypeMismatchException): ResponseEntity<GenericErrorModel> {
        return ResponseEntity(
            GenericErrorModel(
                GenericErrorModelErrors(
                    body = listOf(
                        "クエリパラメータ: ${e.name}が不正です(${e.requiredType}への型変換に失敗しました)"
                    )
                )
            ),
            HttpStatus.UNPROCESSABLE_ENTITY
        )
    }

    /**
     * JSONのパースやMappingに失敗した場合のエラーハンドリング
     *
     * - リクエストボディが{ "foo": "hello }や""等、JSONのパースに失敗する場合、ここでエラーハンドリングされる
     * - リクエストボディがrequire = true等のフィールドを満たしていない場合、ここでエラーハンドリングされる
     *   - 例: @field:JsonProperty("user", required = true) val user: String
     *
     * 参考
     * - [Additional Handling of Jackson Parsing and Mapping Errors](https://dougbreaux.github.io/2020/10/11/Additional-Jackson-Parse-Errors.html)
     */
    @ExceptionHandler(value = [HttpMessageNotReadableException::class])
    fun onBindException(e: HttpMessageNotReadableException): ResponseEntity<GenericErrorModel> =
        when (e.cause) {
            is JsonParseException -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("リクエストボディが読み取れませんでした(Jsonとして間違っている可能性があります)"))),
                HttpStatus.valueOf(400)
            )

            is MismatchedInputException -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("リクエストボディが読み取れませんでした(要求しているJSONの形ではない可能性があります)"))),
                HttpStatus.valueOf(400)
            )

            is JsonMappingException -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("リクエストボディが読み取れませんでした(マッピングに失敗した可能性があります)"))),
                HttpStatus.valueOf(400)
            )

            else -> ResponseEntity(
                GenericErrorModel(GenericErrorModelErrors(body = listOf("リクエストボディが読み取れませんでした"))),
                HttpStatus.valueOf(400)
            )
        }

    /**
     * セッションのエンコード時に失敗した時のエラーハンドリング
     *
     * @return 基本的に500
     */
    @ExceptionHandler(value = [RealworldSessionEncodeErrorException::class])
    fun onRealworldSessionEncodeErrorException(): ResponseEntity<GenericErrorModel> =
        ResponseEntity(
            GenericErrorModel(GenericErrorModelErrors(body = listOf("想定外のエラーが起きました"))),
            HttpStatus.valueOf(500)
        )

    /**
     * セッション認証時に失敗した時のエラーハンドリング
     *
     * @return 基本的に401(Response Bodyは{})
     */
    @ExceptionHandler(value = [RealworldAuthenticationUseCaseUnauthorizedException::class])
    fun onRealworldAuthenticationUseCaseUnauthorizedException(): ResponseEntity<String> =
        ResponseEntity(
            "{}",
            HttpStatus.valueOf(401)
        )
}