ExceptionController.kt

package com.distasilucas.cryptobalancetracker.controller

import com.distasilucas.cryptobalancetracker.constants.INVALID_VALUE_FOR
import com.distasilucas.cryptobalancetracker.constants.UNKNOWN_ERROR
import com.distasilucas.cryptobalancetracker.exception.ApiException
import com.distasilucas.cryptobalancetracker.exception.TooManyRequestsException
import com.distasilucas.cryptobalancetracker.service.CoingeckoCryptoNotFoundException
import com.distasilucas.cryptobalancetracker.service.DuplicatedCryptoPlatFormException
import com.distasilucas.cryptobalancetracker.service.DuplicatedGoalException
import com.distasilucas.cryptobalancetracker.service.DuplicatedPlatformException
import com.distasilucas.cryptobalancetracker.service.DuplicatedPriceTargetException
import com.distasilucas.cryptobalancetracker.service.GoalNotFoundException
import com.distasilucas.cryptobalancetracker.service.InsufficientBalanceException
import com.distasilucas.cryptobalancetracker.service.PlatformNotFoundException
import com.distasilucas.cryptobalancetracker.service.PriceTargetNotFoundException
import com.distasilucas.cryptobalancetracker.service.UserCryptoNotFoundException
import com.distasilucas.cryptobalancetracker.service.UsernameNotFoundException
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.validation.ConstraintViolationException
import org.springframework.http.HttpStatus
import org.springframework.http.ProblemDetail
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.HttpMessageNotReadableException
import org.springframework.security.access.AccessDeniedException
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.MissingServletRequestParameterException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.context.request.ServletWebRequest
import org.springframework.web.context.request.WebRequest
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException
import java.net.URI

@RestControllerAdvice
class ExceptionController {

  private val logger = KotlinLogging.logger { }
  private val NOT_FOUND_STATUS = HttpStatus.NOT_FOUND
  private val BAD_REQUEST_STATUS = HttpStatus.BAD_REQUEST

  @ExceptionHandler(PlatformNotFoundException::class)
  fun handlePlatformNotFoundException(
    exception: PlatformNotFoundException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A PlatformNotFoundException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      NOT_FOUND_STATUS.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(NOT_FOUND_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(CoingeckoCryptoNotFoundException::class)
  fun handleCoingeckoCryptoNotFoundException(
    exception: CoingeckoCryptoNotFoundException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A CoingeckoCryptoNotFoundException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      NOT_FOUND_STATUS.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(NOT_FOUND_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(UserCryptoNotFoundException::class)
  fun handleUserCryptoNotFoundException(
    exception: UserCryptoNotFoundException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A UserCryptoNotFoundException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      NOT_FOUND_STATUS.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(NOT_FOUND_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(GoalNotFoundException::class)
  fun handleGoalNotFoundException(
    exception: GoalNotFoundException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A GoalNotFoundException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      NOT_FOUND_STATUS.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(NOT_FOUND_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(PriceTargetNotFoundException::class)
  fun handlePriceTargetNotFoundException(
    exception: PriceTargetNotFoundException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A PriceTargetNotFoundException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      NOT_FOUND_STATUS.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(NOT_FOUND_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(DuplicatedPriceTargetException::class)
  fun handleDuplicatedPriceTargetException(
    exception: DuplicatedPriceTargetException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A DuplicatedPriceTargetException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      BAD_REQUEST_STATUS.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(BAD_REQUEST_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(DuplicatedPlatformException::class)
  fun handleDuplicatedPlatformException(
    exception: DuplicatedPlatformException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A DuplicatedPlatformException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      BAD_REQUEST_STATUS.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(BAD_REQUEST_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(DuplicatedCryptoPlatFormException::class)
  fun handleDuplicatedCryptoPlatFormException(
    exception: DuplicatedCryptoPlatFormException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A DuplicatedCryptoPlatFormException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      BAD_REQUEST_STATUS.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(listOf(problemDetail))
  }

  @ExceptionHandler(DuplicatedGoalException::class)
  fun handleDuplicatedGoalException(
    exception: DuplicatedGoalException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A DuplicatedGoalException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      BAD_REQUEST_STATUS.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(BAD_REQUEST_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(InsufficientBalanceException::class)
  fun handleInsufficientBalanceException(
    exception: InsufficientBalanceException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "An InsufficientBalanceException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      HttpStatus.BAD_REQUEST.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(listOf(problemDetail))
  }

  @ExceptionHandler(UsernameNotFoundException::class)
  fun handleUsernameNotFoundException(
    exception: UsernameNotFoundException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "An UsernameNotFoundException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      HttpStatus.NOT_FOUND.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(listOf(problemDetail))
  }

  @ExceptionHandler(TooManyRequestsException::class)
  fun handleTooManyRequestsException(
    exception: TooManyRequestsException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.warn { "A TooManyRequestsException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail =
      HttpStatus.TOO_MANY_REQUESTS.withDetailsAndURI(exception.message!!, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body(listOf(problemDetail))
  }

  @ExceptionHandler(MethodArgumentNotValidException::class)
  fun handleMethodArgumentNotValidException(
    exception: MethodArgumentNotValidException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A MethodArgumentNotValidException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetails = exception.allErrors.map {
      HttpStatus.BAD_REQUEST.withDetailsAndURI(
        it.defaultMessage ?: UNKNOWN_ERROR,
        URI.create(request.requestURL.toString())
      )
    }

    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(problemDetails)
  }

  @ExceptionHandler(HttpMessageNotReadableException::class)
  fun handleHttpMessageNotReadableException(
    exception: HttpMessageNotReadableException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A HttpMessageNotReadableException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val problemDetail = ProblemDetail.forStatus(BAD_REQUEST_STATUS)
    problemDetail.type = URI.create(request.requestURL.toString())

    return ResponseEntity.status(BAD_REQUEST_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(ConstraintViolationException::class)
  fun handleConstraintViolationException(
    exception: ConstraintViolationException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A ConstraintViolationException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val constraintViolations = exception.constraintViolations.toList()

    val problemDetails = constraintViolations.map {
      BAD_REQUEST_STATUS.withDetailsAndURI(it.message, URI.create(request.requestURL.toString()))
    }

    return ResponseEntity.status(BAD_REQUEST_STATUS).body(problemDetails)
  }

  @ExceptionHandler(MissingServletRequestParameterException::class)
  fun handleMissingServletRequestParameterException(
    exception: MissingServletRequestParameterException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A MissingServletRequestParameterException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val detail = exception.body.detail ?: exception.message
    val problemDetail = BAD_REQUEST_STATUS.withDetailsAndURI(detail, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(BAD_REQUEST_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(AccessDeniedException::class)
  fun handleAccessDeniedException(
    exception: AccessDeniedException,
    webRequest: WebRequest
  ): ResponseEntity<ProblemDetail> {
    logger.info { "An AccessDeniedException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val detail = exception.message ?: "Forbidden"
    val problemDetail = HttpStatus.FORBIDDEN.withDetailsAndURI(detail, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(HttpStatus.FORBIDDEN).body(problemDetail)
  }

  @ExceptionHandler(MethodArgumentTypeMismatchException::class)
  fun handleMethodArgumentTypeMismatchException(
    exception: MethodArgumentTypeMismatchException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.info { "A MethodArgumentTypeMismatchException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val name = exception.name
    val availableValues = exception.requiredType?.enumConstants?.contentToString()
    val message = if (availableValues != null) {
      INVALID_VALUE_FOR.format(exception.value, name, availableValues)
    } else {
      "Invalid value ${exception.value} for $name"
    }
    val problemDetail = BAD_REQUEST_STATUS.withDetailsAndURI(message, URI.create(request.requestURL.toString()))

    return ResponseEntity.status(BAD_REQUEST_STATUS).body(listOf(problemDetail))
  }

  @ExceptionHandler(ApiException::class)
  fun handleApiException(
    exception: ApiException,
    webRequest: WebRequest
  ): ResponseEntity<List<ProblemDetail>> {
    logger.warn { "An ApiException occurred $exception" }

    val request = (webRequest as ServletWebRequest).request
    val httpStatusCode = exception.httpStatusCode

    val problemDetail = ProblemDetail.forStatusAndDetail(httpStatusCode, exception.message)
    problemDetail.type = URI.create(request.requestURL.toString())

    return ResponseEntity.status(httpStatusCode).body(listOf(problemDetail))
  }

  @ExceptionHandler(Exception::class)
  fun handleException(
    exception: Exception
  ): ResponseEntity<List<ProblemDetail>> {
    logger.error { "An unhandled Exception occurred $exception" }

    val problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, UNKNOWN_ERROR)

    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(listOf(problemDetail))
  }

  private fun HttpStatus.withDetailsAndURI(
    detail: String,
    type: URI
  ): ProblemDetail {
    val problemDetail = ProblemDetail.forStatusAndDetail(this, detail)
    problemDetail.type = type

    return problemDetail
  }
}