From 1805a86d28d2ca10ffab9b878b95f8d8f11ef763 Mon Sep 17 00:00:00 2001 From: Denis Halitsky Date: Sun, 17 Dec 2023 14:00:05 +0300 Subject: [PATCH] Java 21 Improved Controller exceptions handling Added REST endpoints versioning --- .github/workflows/maven.yml | 6 +- .../com/koldyr/genealogy/dto/ErrorResponse.kt | 6 + pom.xml | 4 +- webapp/family.http | 26 ++-- webapp/lineage.http | 16 +-- webapp/person.http | 26 ++-- .../com/koldyr/genealogy/GenealogyConfig.kt | 10 +- .../com/koldyr/genealogy/SecurityConfig.kt | 4 +- .../genealogy/controllers/BaseController.kt | 37 +----- .../genealogy/controllers/FamilyController.kt | 10 +- .../controllers/LineageController.kt | 6 +- .../genealogy/controllers/PersonController.kt | 6 +- .../genealogy/controllers/UserController.kt | 4 +- .../genealogy/util/GlobalExceptionHandler.kt | 118 ++++++++++++++++++ webapp/src/main/resources/application.yaml | 3 + .../controllers/BaseControllerTest.kt | 12 +- .../controllers/FamilyControllerTest.kt | 4 +- .../controllers/UserControllerTest.kt | 12 +- 18 files changed, 198 insertions(+), 112 deletions(-) create mode 100644 webapp/src/main/kotlin/com/koldyr/genealogy/util/GlobalExceptionHandler.kt diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 96398ac..e472c6b 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -10,11 +10,11 @@ jobs: steps: - name: Checkout Sources uses: actions/checkout@v2 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v2 with: - java-version: '17' - distribution: 'adopt' + java-version: '21' s + distribution: 'zulu' cache: 'maven' - name: Build All run: mvn -B package -DskipTests --file pom.xml diff --git a/model/src/main/kotlin/com/koldyr/genealogy/dto/ErrorResponse.kt b/model/src/main/kotlin/com/koldyr/genealogy/dto/ErrorResponse.kt index f00f78e..257e02a 100644 --- a/model/src/main/kotlin/com/koldyr/genealogy/dto/ErrorResponse.kt +++ b/model/src/main/kotlin/com/koldyr/genealogy/dto/ErrorResponse.kt @@ -16,3 +16,9 @@ data class ErrorResponse( var exception: String? = null, var trace: String? = null ) + + +data class ErrorDetails( + var pointer: String? = null, + var error: String? = null, +) diff --git a/pom.xml b/pom.xml index d50916b..d592f91 100644 --- a/pom.xml +++ b/pom.xml @@ -13,8 +13,8 @@ UTF-8 - 17 - 17 + 21 + 21 1.9.20 3.2.0 diff --git a/webapp/family.http b/webapp/family.http index 102c764..71249c4 100644 --- a/webapp/family.http +++ b/webapp/family.http @@ -1,5 +1,5 @@ ### Login -POST http://{{server}}/api/v1/user/login +POST http://{{server}}/api/user/v1/login Content-Type: application/json Accept: */* @@ -9,17 +9,17 @@ Accept: */* } ### Get Families -GET http://{{server}}/api/v1/lineage/1/families +GET http://{{server}}/api/lineage/v1/1/families Authorization: Bearer {{authToken}} Accept: application/json ### Get Family -GET http://{{server}}/api/v1/lineage/1/families/1 +GET http://{{server}}/api/lineage/v1/1/families/1 Authorization: Bearer {{authToken}} Accept: application/json ### Create Family -POST http://{{server}}/api/v1/lineage/1/families +POST http://{{server}}/api/lineage/v1/1/families Authorization: Bearer {{authToken}} Content-Type: application/json Accept: application/json @@ -30,7 +30,7 @@ Accept: application/json } ### Update Family -POST http://{{server}}/api/v1/lineage/1/families/1 +POST http://{{server}}/api/lineage/v1/1/families/1 Authorization: Bearer {{authToken}} Content-Type: application/json Accept: application/json @@ -41,11 +41,11 @@ Accept: application/json } ### Delete Family -DELETE http://{{server}}/api/v1/lineage/1/families/2 +DELETE http://{{server}}/api/lineage/v1/1/families/2 Authorization: Bearer {{authToken}} ### Create Child in Family -POST http://{{server}}/api/v1/lineage/1/families/1/children +POST http://{{server}}/api/lineage/v1/1/families/1/children Authorization: Bearer {{authToken}} Content-Type: application/json Accept: application/json @@ -63,22 +63,22 @@ Accept: application/json } ### Add Child to Family -PATCH http://{{server}}/api/v1/lineage/1/families/1/children/5 +PATCH http://{{server}}/api/lineage/v1/1/families/1/children/5 Authorization: Bearer {{authToken}} Accept: application/json ### Delete Child from Family -DELETE http://{{server}}/api/v1/lineage/1/families/1/children/5 +DELETE http://{{server}}/api/lineage/v1/1/families/1/children/5 Authorization: Bearer {{authToken}} Accept: application/json ### -GET http://{{server}}/api/v1/lineage/1/families/1/children +GET http://{{server}}/api/lineage/v1/1/families/1/children Authorization: Bearer {{authToken}} Accept: application/json ### Create Family Event -POST http://{{server}}/api/v1/lineage/1/families/1/events +POST http://{{server}}/api/lineage/v1/1/families/1/events Authorization: Bearer {{authToken}} Content-Type: application/json Accept: application/json @@ -91,11 +91,11 @@ Accept: application/json } ### Get Family Events -GET http://{{server}}/api/v1/lineage/1/families/1/events +GET http://{{server}}/api/lineage/v1/1/families/1/events Authorization: Bearer {{authToken}} Accept: application/json ### Delete Family Event -DELETE http://{{server}}/api/v1/lineage/1/families/1/events/6 +DELETE http://{{server}}/api/lineage/v1/1/families/1/events/6 Authorization: Bearer {{authToken}} Accept: application/json diff --git a/webapp/lineage.http b/webapp/lineage.http index 6d17115..b991ad2 100644 --- a/webapp/lineage.http +++ b/webapp/lineage.http @@ -1,5 +1,5 @@ ### User login -POST http://{{server}}/api/v1/user/login +POST http://{{server}}/api/user/v1/login Content-Type: application/json { @@ -8,7 +8,7 @@ Content-Type: application/json } ### Import Lineage -POST http://{{server}}/api/v1/lineage/import +POST http://{{server}}/api/lineage/v1/import Authorization: Bearer {{authToken}} Content-Type: text/ged Lineage-Name: Test Lineage GED @@ -16,17 +16,17 @@ Lineage-Name: Test Lineage GED < ../test-lineage.ged ### Get all Lineages -GET http://{{server}}/api/v1/lineage +GET http://{{server}}/api/lineage/v1 Authorization: Bearer {{authToken}} Accept: application/json ### Get Lineage -GET http://{{server}}/api/v1/lineage/2 +GET http://{{server}}/api/lineage/v1/2 Authorization: Bearer {{authToken}} Accept: application/json ### Create new Lineage -POST http://{{server}}/api/v1/lineage +POST http://{{server}}/api/lineage/v1 Authorization: Bearer {{authToken}} Content-Type: application/json Accept: application/json @@ -37,11 +37,11 @@ Accept: application/json } ### Delete Lineage -DELETE http://{{server}}/api/v1/lineage/4 +DELETE http://{{server}}/api/lineage/v1/4 Authorization: Bearer {{authToken}} ### Update Lineage -PUT http://{{server}}/api/v1/lineage/import +PUT http://{{server}}/api/lineage/v1/import Authorization: Bearer {{authToken}} Content-Type: application/json @@ -51,6 +51,6 @@ Content-Type: application/json } ### Export Lineage -GET http://{{server}}/api/v1/lineage/2/export +GET http://{{server}}/api/lineage/v1/2/export Authorization: Bearer {{authToken}} Accept: text/ged diff --git a/webapp/person.http b/webapp/person.http index 91bb1f0..bd6db33 100644 --- a/webapp/person.http +++ b/webapp/person.http @@ -1,5 +1,5 @@ ### User registration -POST http://{{server}}/api/v1/user/registration +POST http://{{server}}/api/user/v1/registration Content-Type: application/json { @@ -10,7 +10,7 @@ Content-Type: application/json } ### User login -POST http://{{server}}/api/v1/user/login +POST http://{{server}}/api/user/v1/login Content-Type: application/json { @@ -19,16 +19,16 @@ Content-Type: application/json } ### Get All Persons -GET http://{{server}}/api/v1/lineage/1/persons +GET http://{{server}}/api/lineage/v1/1/persons Authorization: Bearer {{authToken}} ### Get Person By ID -GET http://{{server}}/api/v1/lineage/1/persons/1 +GET http://{{server}}/api/lineage/v1/1/persons/11 Authorization: Bearer {{authToken}} Accept: application/json ### Create Person -POST http://{{server}}/api/v1/lineage/1/persons +POST http://{{server}}/api/lineage/v1/1/persons Authorization: Bearer {{authToken}} Content-Type: application/json Accept: application/json @@ -46,7 +46,7 @@ Accept: application/json } ### Update Person -PUT http://{{server}}/api/v1/lineage/1/persons/1 +PUT http://{{server}}/api/lineage/v1/1/persons/1 Authorization: Bearer {{authToken}} Content-Type: application/json Accept: application/json @@ -64,10 +64,10 @@ Accept: application/json } ### Delete Person -DELETE http://{{server}}/api/v1/lineage/1/persons/1 +DELETE http://{{server}}/api/lineage/v1/1/persons/11 ### Create Person Event -POST http://{{server}}/api/v1/lineage/1/persons/1/events +POST http://{{server}}/api/lineage/v1/1/persons/1/events Authorization: Bearer {{authToken}} Content-Type: application/json Accept: application/json @@ -80,28 +80,28 @@ Accept: application/json } ### Get Person Events -GET http://{{server}}/api/v1/lineage/1/persons/1/events +GET http://{{server}}/api/lineage/v1/1/persons/1/events Authorization: Bearer {{authToken}} Accept: application/json ### Delete Person Event -DELETE http://{{server}}/api/v1/lineage/1/persons/1/events/1 +DELETE http://{{server}}/api/lineage/v1/1/persons/1/events/1 Authorization: Bearer {{authToken}} Accept: application/json ### Create Person Photo -POST http://{{server}}/api/v1/lineage/1/persons/1/photo +POST http://{{server}}/api/lineage/v1/1/persons/1/photo Authorization: Bearer {{authToken}} Content-Type: image/jpeg < C:\Dist\avatar0.jpg ### Get Person Photo -GET http://{{server}}/api/v1/lineage/1/persons/1/photo +GET http://{{server}}/api/lineage/v1/1/persons/1/photo Authorization: Bearer {{authToken}} ### Person Search -POST http://{{server}}/api/v1/lineage/1/persons/search +POST http://{{server}}/api/lineage/v1/1/persons/search Authorization: Bearer {{authToken}} Content-Type: application/json Accept: application/json diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/GenealogyConfig.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/GenealogyConfig.kt index 8cb907a..95f3d1e 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/GenealogyConfig.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/GenealogyConfig.kt @@ -1,5 +1,7 @@ package com.koldyr.genealogy +import ma.glasnost.orika.MapperFacade +import ma.glasnost.orika.impl.DefaultMapperFactory import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.EnableAspectJAutoProxy @@ -10,11 +12,8 @@ import org.springframework.http.HttpMethod.HEAD import org.springframework.http.HttpMethod.PATCH import org.springframework.http.HttpMethod.POST import org.springframework.http.HttpMethod.PUT -import org.springframework.web.servlet.HandlerExceptionResolver import org.springframework.web.servlet.config.annotation.CorsRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer -import ma.glasnost.orika.MapperFacade -import ma.glasnost.orika.impl.DefaultMapperFactory import com.koldyr.genealogy.dto.FamilyDTO import com.koldyr.genealogy.mapper.FamilyEventConverter import com.koldyr.genealogy.mapper.PersonConverter @@ -24,7 +23,6 @@ import com.koldyr.genealogy.persistence.FamilyEventRepository import com.koldyr.genealogy.persistence.PersonRepository import com.koldyr.genealogy.persistence.UserRepository import com.koldyr.genealogy.services.AuthenticationUserDetailsService -import com.koldyr.genealogy.util.InternalExceptionResolver /** * Description of class GenealogyConfig @@ -62,10 +60,6 @@ class GenealogyConfig { fun corsConfigurer(): WebMvcConfigurer { return object : WebMvcConfigurer { - override fun extendHandlerExceptionResolvers(resolvers: MutableList) { - resolvers.add(InternalExceptionResolver()) - } - override fun addCorsMappings(registry: CorsRegistry) { registry.addMapping("/**") .allowedOrigins("*") diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/SecurityConfig.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/SecurityConfig.kt index 1229a02..704d4a3 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/SecurityConfig.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/SecurityConfig.kt @@ -59,8 +59,8 @@ class SecurityConfig { .headers { it.disable() } .authorizeHttpRequests { it.requestMatchers("/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs", "/v3/api-docs/**", "/error/**", "/favicon.ico").permitAll() - .requestMatchers(POST, "/api/v1/user/**").permitAll() - .requestMatchers("/api/v1/**").authenticated() + .requestMatchers(POST, "/api/user/v1/**").permitAll() + .requestMatchers("/api/lineage/v1/**").authenticated() } .oauth2ResourceServer { it.jwt { } } .build() diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/BaseController.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/BaseController.kt index 58cd528..ec84bc4 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/BaseController.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/BaseController.kt @@ -1,20 +1,11 @@ package com.koldyr.genealogy.controllers -import java.time.Instant import io.swagger.v3.oas.annotations.media.Content import io.swagger.v3.oas.annotations.media.Schema import io.swagger.v3.oas.annotations.responses.ApiResponse -import jakarta.persistence.EntityNotFoundException -import jakarta.servlet.http.HttpServletRequest import org.slf4j.LoggerFactory -import org.springframework.http.HttpStatus -import org.springframework.http.HttpStatus.BAD_REQUEST -import org.springframework.http.HttpStatus.NOT_FOUND import org.springframework.http.MediaType.APPLICATION_JSON_VALUE -import org.springframework.http.ResponseEntity import org.springframework.validation.annotation.Validated -import org.springframework.web.bind.MethodArgumentNotValidException -import org.springframework.web.bind.annotation.ExceptionHandler import com.koldyr.genealogy.dto.ErrorResponse /** @@ -29,31 +20,5 @@ import com.koldyr.genealogy.dto.ErrorResponse content = [Content(schema = Schema(implementation = ErrorResponse::class), mediaType = APPLICATION_JSON_VALUE)] ) class BaseController { - - private val logger = LoggerFactory.getLogger(javaClass) - - @ExceptionHandler(MethodArgumentNotValidException::class) - fun handleException(request: HttpServletRequest, ex: MethodArgumentNotValidException): ResponseEntity> { - val error = ex.allErrors.first() - val message = "${error.objectName}.${error.defaultMessage}" - val model = buildModel(BAD_REQUEST, request.requestURI, message) - return ResponseEntity.badRequest().body(model) - } - - @ExceptionHandler(EntityNotFoundException::class) - fun handleException(request: HttpServletRequest, ex: EntityNotFoundException): ResponseEntity> { - val message = ex.message!! - val model = buildModel(NOT_FOUND, request.requestURI, message) - return ResponseEntity.status(NOT_FOUND).body(model) - } - - private fun buildModel(status: HttpStatus, uri: String, message: String): Map { - return mapOf( - "timestamp" to Instant.now().toString(), - "status" to status.value(), - "error" to status.reasonPhrase, - "message" to message, - "path" to uri - ) - } + protected val logger = LoggerFactory.getLogger(javaClass) } diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/FamilyController.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/FamilyController.kt index 0ec47d8..b7f7777 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/FamilyController.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/FamilyController.kt @@ -37,7 +37,7 @@ import com.koldyr.genealogy.services.FamilyService * @created: 2021-09-25 */ @RestController -@RequestMapping("/api/v1/lineage") +@RequestMapping("/api/lineage/v1") @Tags(value = [Tag(name = "FamilyController")]) class FamilyController( private val familyService: FamilyService @@ -66,7 +66,7 @@ class FamilyController( fun create(@PathVariable("lineageId") lineageId: Long, @RequestBody family: FamilyDTO): ResponseEntity { val familyId = familyService.create(lineageId, family) - val uri = URI.create("/api/v1/lineage/$lineageId/families/${familyId}") + val uri = URI.create("/api/lineage/v1/$lineageId/families/${familyId}") return created(uri).build() } @@ -120,7 +120,7 @@ class FamilyController( @RequestBody @Valid event: FamilyEvent): ResponseEntity { val eventId = familyService.createEvent(familyId, event) - val uri = URI.create("/api/v1/lineage/$lineageId/families/$familyId/events/$eventId") + val uri = URI.create("/api/lineage/v1/$lineageId/families/$familyId/events/$eventId") return created(uri).build() } @@ -163,7 +163,7 @@ class FamilyController( @RequestBody @Valid child: Person): ResponseEntity { val childId = familyService.createChild(familyId, child) - val uri = URI.create("/api/v1/lineage/$lineageId/persons/$childId") + val uri = URI.create("/api/lineage/v1/$lineageId/persons/$childId") return created(uri).build() } @@ -182,7 +182,7 @@ class FamilyController( @PathVariable("childId") childId: Long): ResponseEntity { familyService.addChild(familyId, childId) - val uri = URI.create("/api/v1/lineage/$lineageId/persons/$childId") + val uri = URI.create("/api/lineage/v1/$lineageId/persons/$childId") return created(uri).build() } diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/LineageController.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/LineageController.kt index a6b11ed..bcc0dc3 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/LineageController.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/LineageController.kt @@ -40,7 +40,7 @@ const val TEXT_GED = "text/ged" * @created 2022-06-23 */ @RestController -@RequestMapping("/api/v1/lineage") +@RequestMapping("/api/lineage/v1") @Tags(value = [Tag(name = "LineageController")]) class LineageController( private val lineageService: LineageService @@ -69,7 +69,7 @@ class LineageController( fun create(@RequestBody @Valid lineage: LineageDTO): ResponseEntity { val lineageId = lineageService.create(lineage) - val uri = URI.create("/api/v1/lineage/${lineageId}") + val uri = URI.create("/api/lineage/v1/${lineageId}") return created(uri).build() } @@ -123,7 +123,7 @@ class LineageController( ): ResponseEntity { val lineageId = lineageService.importLineage(dataType, lineage, name, note) - val uri = URI.create("/api/v1/lineage/${lineageId}") + val uri = URI.create("/api/lineage/v1/${lineageId}") return created(uri).build() } diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/PersonController.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/PersonController.kt index 15078cf..9083744 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/PersonController.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/PersonController.kt @@ -49,7 +49,7 @@ const val IMAGE_JPG_VALUE = "image/jpg" * @created: 2021-09-25 */ @RestController -@RequestMapping("/api/v1/lineage") +@RequestMapping("/api/lineage/v1") @Tags(value = [Tag(name = "PersonController")]) class PersonController( private val personService: PersonService @@ -90,7 +90,7 @@ class PersonController( person.lineageId = lineageId val personId = personService.create(person) - val uri = URI.create("/api/v1/lineage/$lineageId/persons/$personId") + val uri = URI.create("/api/lineage/v1/$lineageId/persons/$personId") return created(uri).build() } @@ -143,7 +143,7 @@ class PersonController( @RequestBody @Valid event: PersonEvent): ResponseEntity { val eventId = personService.createEvent(personId, event) - val uri = URI.create("/api/v1/lineage/$lineageId/persons/$personId/events/$eventId") + val uri = URI.create("/api/lineage/v1/$lineageId/persons/$personId/events/$eventId") return created(uri).build() } diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/UserController.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/UserController.kt index 015e476..d3d5edc 100644 --- a/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/UserController.kt +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/controllers/UserController.kt @@ -18,7 +18,7 @@ import com.koldyr.genealogy.model.User import com.koldyr.genealogy.services.UserService @RestController -@RequestMapping("/api/v1/user") +@RequestMapping("/api/user/v1") @Tags(value = [Tag(name = "UserController")]) class UserController( private val userService: UserService @@ -28,7 +28,7 @@ class UserController( fun create(@RequestBody @Valid user: User): ResponseEntity { userService.create(user) - val uri = URI.create("/api/v1/user/login") + val uri = URI.create("/api/user/v1/login") return created(uri).build() } diff --git a/webapp/src/main/kotlin/com/koldyr/genealogy/util/GlobalExceptionHandler.kt b/webapp/src/main/kotlin/com/koldyr/genealogy/util/GlobalExceptionHandler.kt new file mode 100644 index 0000000..a042c2f --- /dev/null +++ b/webapp/src/main/kotlin/com/koldyr/genealogy/util/GlobalExceptionHandler.kt @@ -0,0 +1,118 @@ +package com.koldyr.genealogy.util + +import java.time.Instant +import jakarta.persistence.EntityNotFoundException +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.dao.DataAccessException +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatus.BAD_REQUEST +import org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR +import org.springframework.http.HttpStatus.NOT_FOUND +import org.springframework.http.HttpStatusCode +import org.springframework.http.ResponseEntity +import org.springframework.http.ResponseEntity.badRequest +import org.springframework.security.core.AuthenticationException +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.ResponseStatus +import org.springframework.web.context.request.ServletWebRequest +import org.springframework.web.context.request.WebRequest +import org.springframework.web.method.annotation.HandlerMethodValidationException +import org.springframework.web.server.ResponseStatusException +import org.springframework.web.server.ServerErrorException +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler +import com.koldyr.genealogy.dto.ErrorDetails +import com.koldyr.genealogy.export.UnsupportedExportFormatException + +val LOGGER: Logger = LoggerFactory.getLogger(GlobalExceptionHandler::class.java) + +/** + * Description of class GlobalExceptionHandler + * + * @author d.halitski@gmail.com + * @created: 2023-12-10 + */ +@ControllerAdvice +class GlobalExceptionHandler : ResponseEntityExceptionHandler() { + + public override fun handleMethodArgumentNotValid( + ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest): ResponseEntity { + LOGGER.error(ex.message, ex) + + val errors = ex.allErrors.map { error -> + "${error.objectName}.${error.defaultMessage}" + } + val uri = (request as ServletWebRequest).request.requestURI + val model = buildModel(BAD_REQUEST, uri, errors) + return badRequest().body(model) + } + + public override fun handleHandlerMethodValidationException( + ex: HandlerMethodValidationException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest): ResponseEntity { + LOGGER.error(ex.message, ex) + + val errors = ArrayList() + for (violation in ex.allValidationResults) { + val parameterName = violation.methodParameter.parameterName + violation.resolvableErrors.forEach { error -> + errors.add(ErrorDetails(parameterName, error.defaultMessage)) + } + } + + val uri = (request as ServletWebRequest).request.requestURI + val model = buildModel(BAD_REQUEST, uri, errors) + return badRequest().body(model) + } + + @ExceptionHandler(EntityNotFoundException::class) + fun handleEntityExceptions(ex: EntityNotFoundException, request: WebRequest): ResponseEntity { + val uri = (request as ServletWebRequest).request.requestURI + val message = ex.message!! + val model = buildModel(NOT_FOUND, uri, message) + return ResponseEntity.status(NOT_FOUND).body(model) + } + + @ExceptionHandler(*arrayOf( + org.springframework.security.access.AccessDeniedException::class, + AuthenticationException::class, + ResponseStatusException::class + )) + fun handleIgnoredExceptions(ex: Exception, request: WebRequest): Any? { + throw ex + } + + @ExceptionHandler(Throwable::class) + @ResponseStatus(INTERNAL_SERVER_ERROR) + fun handleAllExceptions(ex: Throwable, request: WebRequest): Map { + LOGGER.error(ex.message, ex) + + val message = when (ex) { + is UnsupportedExportFormatException, is ServerErrorException -> { + ex.message!! + } + is DataAccessException -> { + ex.javaClass.simpleName + } + else -> { + ex.toString() + } + } + + val uri = (request as ServletWebRequest).request.requestURI + val model = buildModel(INTERNAL_SERVER_ERROR, uri, message) + return model + } + + private fun buildModel(status: HttpStatus, uri: String, message: Any): Map { + return mapOf( + "timestamp" to Instant.now().toString(), + "status" to status.value(), + "error" to status.reasonPhrase, + "message" to message, + "path" to uri + ) + } +} diff --git a/webapp/src/main/resources/application.yaml b/webapp/src/main/resources/application.yaml index 8f7bad9..c2825cf 100644 --- a/webapp/src/main/resources/application.yaml +++ b/webapp/src/main/resources/application.yaml @@ -4,6 +4,9 @@ server: include-message: always include-stacktrace: on_param spring: + threads: + virtual: + enabled: true datasource: url: jdbc:postgresql://localhost:5432/genealogy username: xxx diff --git a/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/BaseControllerTest.kt b/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/BaseControllerTest.kt index 7827f68..bd7c96d 100644 --- a/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/BaseControllerTest.kt +++ b/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/BaseControllerTest.kt @@ -73,7 +73,7 @@ abstract class BaseControllerTest { @Value("\${spring.test.username}") lateinit var testUser: String @Value("\${spring.test.password}") lateinit var testPassword: String - protected val baseUrl = "/api/v1/lineage" + protected val baseUrl = "/api/lineage/v1" protected fun createPersonModel(gender: Gender): Person { val person = Person() @@ -128,14 +128,14 @@ abstract class BaseControllerTest { } protected fun register(user: User) { - mockMvc.post("/api/v1/user/registration") { + mockMvc.post("/api/user/v1/registration") { content = mapper.writeValueAsString(user) contentType = APPLICATION_JSON accept = APPLICATION_JSON } .andExpect { status { isCreated() } - header { string(LOCATION, matchesRegex("/api/v1/user/login")) } + header { string(LOCATION, matchesRegex("/api/user/v1/login")) } } } @@ -159,7 +159,7 @@ abstract class BaseControllerTest { } protected fun login(credentials: Credentials): String { - return mockMvc.post("/api/v1/user/login") { + return mockMvc.post("/api/user/v1/login") { content = mapper.writeValueAsString(credentials) contentType = APPLICATION_JSON accept = APPLICATION_JSON @@ -236,14 +236,14 @@ abstract class BaseControllerTest { protected fun createLineAge(): Long { val lineage = LineageDTO("Koldyrs", "Test lineage") - val location = mockMvc.post("/api/v1/lineage") { + val location = mockMvc.post("/api/lineage/v1") { header(AUTHORIZATION, getBearerToken()) content = mapper.writeValueAsString(lineage) contentType = APPLICATION_JSON } .andExpect { status { isCreated() } - header { string(LOCATION, matchesRegex("/api/v1/lineage/\\d+$")) } + header { string(LOCATION, matchesRegex("/api/lineage/v1/\\d+$")) } } .andReturn().response.getHeader(LOCATION) return getLastIdFromLocation(location) diff --git a/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/FamilyControllerTest.kt b/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/FamilyControllerTest.kt index e59e713..31b61fa 100644 --- a/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/FamilyControllerTest.kt +++ b/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/FamilyControllerTest.kt @@ -121,8 +121,8 @@ class FamilyControllerTest : BaseControllerTest() { @Test fun deleteFamily() { val children = listOf( - createPerson(Gender.FEMALE).id!!, - createPerson(Gender.FEMALE).id!! + createPerson(Gender.FEMALE).id!!, + createPerson(Gender.FEMALE).id!! ) val familyDto = createFamily(children) diff --git a/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/UserControllerTest.kt b/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/UserControllerTest.kt index 95fa49e..c036667 100644 --- a/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/UserControllerTest.kt +++ b/webapp/src/test/kotlin/com/koldyr/genealogy/controllers/UserControllerTest.kt @@ -1,7 +1,7 @@ package com.koldyr.genealogy.controllers -import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.Matchers.containsString import org.junit.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.http.HttpHeaders.AUTHORIZATION @@ -21,7 +21,7 @@ class UserControllerTest : BaseControllerTest() { @Test fun registerExistingUser() { - mockMvc.post("/api/v1/user/registration") { + mockMvc.post("/api/user/v1/registration") { content = mapper.writeValueAsString(createUser()) contentType = APPLICATION_JSON accept = APPLICATION_JSON @@ -40,7 +40,7 @@ class UserControllerTest : BaseControllerTest() { val credentials = Credentials().apply { username = "lemming@koldyr.com" } - mockMvc.post("/api/v1/user/registration") { + mockMvc.post("/api/user/v1/registration") { content = mapper.writeValueAsString(credentials) contentType = APPLICATION_JSON accept = APPLICATION_JSON @@ -61,7 +61,7 @@ class UserControllerTest : BaseControllerTest() { password = "1112" } - mockMvc.post("/api/v1/user/login") { + mockMvc.post("/api/user/v1/login") { content = mapper.writeValueAsString(credentials) contentType = APPLICATION_JSON accept = APPLICATION_JSON @@ -82,7 +82,7 @@ class UserControllerTest : BaseControllerTest() { password = "koldyr" } - mockMvc.post("/api/v1/user/login") { + mockMvc.post("/api/user/v1/login") { content = mapper.writeValueAsString(credentials) contentType = APPLICATION_JSON accept = APPLICATION_JSON @@ -158,7 +158,7 @@ class UserControllerTest : BaseControllerTest() { val adminToken = login(credentials) val lineage = LineageDTO("Koldyrs", "Test lineage") - mockMvc.post("/api/v1/lineage") { + mockMvc.post("/api/lineage/v1") { header(AUTHORIZATION, adminToken) content = mapper.writeValueAsString(lineage) contentType = APPLICATION_JSON