Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve exception handler #84

Merged
merged 6 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,4 @@ interface BucketService {
fun saveToImportDir(bucketFile: BucketFile)
fun saveTextToBucket(bucketFile: BucketFile, messages: Sequence<String>)
fun loadBucketAsZip(path: String): Resource
}

class BucketServiceException(message: String?, throwable: Throwable?): RuntimeException(message, throwable)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package dev.marcal.chatvault.model

import dev.marcal.chatvault.in_out_boundary.output.exceptions.AmbiguousDateException
import dev.marcal.chatvault.in_out_boundary.output.exceptions.MessageParserException
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeFormatterBuilder
Expand Down Expand Up @@ -50,7 +52,7 @@ class MessageParser(pattern: String? = null) {
return LocalDateTime.parse(textToParse, firstComesTheMonthFormatter)
} else {
if (lastUsed == null) {
throw RuntimeException("there is ambiguity in the date, it is not possible to know which value is the day and which is the month $text")
throw AmbiguousDateException("There is ambiguity in the date, it is not possible to know which value is the day and which is the month $text")
} else {
return LocalDateTime.parse(textToParse, lastUsed)
}
Expand Down Expand Up @@ -98,7 +100,7 @@ class MessageParser(pattern: String? = null) {
val content = result.groupValues[3].trim()
Triple(date, null, removePrefix(content))

} ?: throw IllegalStateException("unexpected situation for the line $firstLine")
} ?: throw MessageParserException("Parse text fail. Unexpected situation for the line $firstLine")
}

private fun removePrefix(content: String): String {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package dev.marcal.chatvault.in_out_boundary.output.exceptions
open class BucketServiceException(message: String?, throwable: Throwable?): RuntimeException(message, throwable)

class BucketFileNotFoundException(message: String?, throwable: Throwable?): BucketServiceException(message, throwable)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dev.marcal.chatvault.in_out_boundary.output.exceptions

class ChatImporterException(message: String? = null, throwable: Throwable? = null) : RuntimeException(message, throwable)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package dev.marcal.chatvault.in_out_boundary.output.exceptions

class ChatNotFoundException(message: String? = null, throwable: Throwable? = null): RuntimeException(message, throwable)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.marcal.chatvault.in_out_boundary.output.exceptions

open class MessageParserException(message: String) : RuntimeException(message)

class AmbiguousDateException(message: String) : MessageParserException(message)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package dev.marcal.chatvault.app_service.bucket_service

import dev.marcal.chatvault.in_out_boundary.output.exceptions.AttachmentFinderException
import dev.marcal.chatvault.in_out_boundary.output.exceptions.AttachmentNotFoundException
import dev.marcal.chatvault.in_out_boundary.output.exceptions.BucketFileNotFoundException
import dev.marcal.chatvault.in_out_boundary.output.exceptions.BucketServiceException
import dev.marcal.chatvault.model.Bucket
import dev.marcal.chatvault.model.BucketFile
import jakarta.annotation.PostConstruct
Expand Down Expand Up @@ -66,13 +68,20 @@ class BucketServiceImpl(
}

override fun saveTextToBucket(bucketFile: BucketFile, messages: Sequence<String>) {
val file = bucketFile.file(bucketRootPath)
BufferedWriter(FileWriter(file)).use { writer ->
messages.forEach { messageLine ->
writer.write(messageLine)
writer.newLine()
try {
val file = bucketFile.file(bucketRootPath)
BufferedWriter(FileWriter(file)).use { writer ->
messages.forEach { messageLine ->
writer.write(messageLine)
writer.newLine()
}
}
} catch (ex: FileNotFoundException) {
throw BucketFileNotFoundException("File to save ${bucketFile.fileName}. Bucket chat was not found", ex)
} catch (ex: Exception) {
throw BucketServiceException("File to save ${bucketFile.fileName}. Unexpected error", ex)
}

}

override fun loadBucketAsZip(path: String): Resource {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package dev.marcal.chatvault.web

import dev.marcal.chatvault.in_out_boundary.input.AttachmentCriteriaInput
import dev.marcal.chatvault.in_out_boundary.input.FileTypeInputEnum
import dev.marcal.chatvault.in_out_boundary.output.AttachmentInfoOutput
import dev.marcal.chatvault.in_out_boundary.output.ChatLastMessageOutput
import dev.marcal.chatvault.in_out_boundary.output.MessageOutput
Expand All @@ -14,19 +13,14 @@ import org.springframework.data.web.SortDefault
import org.springframework.http.*
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.util.HtmlUtils
import java.util.*
import java.util.concurrent.TimeUnit

@RestController
@RequestMapping("/api/chats")
class ChatController(
private val chatLister: ChatLister,
private val messageFinderByChatId: MessageFinderByChatId,
private val chatFileImporter: ChatFileImporter,
private val attachmentFinder: AttachmentFinder,
private val bucketDiskImporter: BucketDiskImporter,
private val chatFileExporter: ChatFileExporter,
private val chatNameUpdater: ChatNameUpdater,
private val attachmentInfoFinderByChatId: AttachmentInfoFinderByChatId,
private val profileImageManager: ProfileImageManager
Expand All @@ -51,14 +45,6 @@ class ChatController(
)
}

@PostMapping("{chatId}/messages/import")
fun importFileByChatId(
@PathVariable chatId: Long,
@RequestParam("file") file: MultipartFile
): ResponseEntity<String> {
return importChat(chatId = chatId, file = file)
}

@PatchMapping("{chatId}/chatName/{chatName}")
fun update(
@PathVariable chatId: Long,
Expand All @@ -68,72 +54,6 @@ class ChatController(
return ResponseEntity.noContent().build()
}

@PostMapping("import/{chatName}")
fun importChatByChatName(
@PathVariable chatName: String,
@RequestParam("file") file: MultipartFile
): ResponseEntity<String> {
return importChat(chatName = chatName, file = file)
}

@GetMapping("{chatId}/export")
fun importChatByChatName(
@PathVariable chatId: Long,
): ResponseEntity<Resource> {
val resource = chatFileExporter.execute(chatId)
val headers = HttpHeaders()
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=${UUID.randomUUID()}.zip")
headers.contentType = MediaType.APPLICATION_OCTET_STREAM
return ResponseEntity.ok()
.headers(headers)
.body(resource)
}


fun importChat(chatId: Long? = null, chatName: String? = null, file: MultipartFile): ResponseEntity<String> {
if (file.isEmpty) {
return ResponseEntity.badRequest().body("The file is empty")
}

val fileType = when (file.contentType) {
null -> return ResponseEntity.badRequest().body("media type is required.")
"text/plain" -> FileTypeInputEnum.TEXT
"application/zip" -> FileTypeInputEnum.ZIP
else -> return ResponseEntity.badRequest()
.body("media type not supported ${HtmlUtils.htmlEscape(file.contentType!!)}.")
}

importChat(chatId, file, fileType, chatName)
return ResponseEntity.ok("The file was imported")
}

private fun importChat(
chatId: Long?,
file: MultipartFile,
fileType: FileTypeInputEnum,
chatName: String?
) {
chatId?.let {
chatFileImporter.execute(
chatId = it,
inputStream = file.inputStream,
fileType = fileType
)
} ?: run {
chatFileImporter.execute(
chatName = chatName,
inputStream = file.inputStream,
fileType = fileType
)
}
}

@PostMapping("disk-import")
fun executeDiskImport() {
bucketDiskImporter.execute()
}


@GetMapping("{chatId}/messages/{messageId}/attachment")
fun downloadAttachment(
@PathVariable("chatId") chatId: Long,
Expand All @@ -159,8 +79,6 @@ class ChatController(
fun downloadAttachment(
@PathVariable("chatId") chatId: Long
): ResponseEntity<Sequence<AttachmentInfoOutput>> {
attachmentInfoFinderByChatId.execute(chatId)

val cacheControl = CacheControl.maxAge(5, TimeUnit.MINUTES)

return ResponseEntity.ok()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package dev.marcal.chatvault.web

import dev.marcal.chatvault.in_out_boundary.input.FileTypeInputEnum
import dev.marcal.chatvault.service.BucketDiskImporter
import dev.marcal.chatvault.service.ChatFileExporter
import dev.marcal.chatvault.service.ChatFileImporter
import org.springframework.core.io.Resource
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile
import org.springframework.web.server.ResponseStatusException
import org.springframework.web.util.HtmlUtils
import java.util.*

@RestController
@RequestMapping("/api/chats")
class ChatImportExportController(
private val chatFileImporter: ChatFileImporter,
private val bucketDiskImporter: BucketDiskImporter,
private val chatFileExporter: ChatFileExporter,
) {

@PostMapping("disk-import")
fun executeDiskImport() {
bucketDiskImporter.execute()
}


@PostMapping("{chatId}/messages/import")
fun importFileByChatId(
@PathVariable chatId: Long,
@RequestParam("file") file: MultipartFile
): ResponseEntity<String> {
return importChat(chatId = chatId, file = file)
}

@PostMapping("import/{chatName}")
fun importChatByChatName(
@PathVariable chatName: String,
@RequestParam("file") file: MultipartFile
): ResponseEntity<String> {
return importChat(chatName = chatName, file = file)
}

@GetMapping("{chatId}/export")
fun importChatByChatName(
@PathVariable chatId: Long,
): ResponseEntity<Resource> {
val resource = chatFileExporter.execute(chatId)
val headers = HttpHeaders()
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=${UUID.randomUUID()}.zip")
headers.contentType = MediaType.APPLICATION_OCTET_STREAM
return ResponseEntity.ok()
.headers(headers)
.body(resource)
}

fun importChat(chatId: Long? = null, chatName: String? = null, file: MultipartFile): ResponseEntity<String> {
if (file.isEmpty) {
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "This file cannot be imported. It is empty.")
}

val fileType = getFileType(file)

importChat(chatId, file, fileType, chatName)
return ResponseEntity.noContent().build()
}


private fun getFileType(file: MultipartFile): FileTypeInputEnum {
val fileType = when (file.contentType) {
null -> throw ResponseStatusException(
HttpStatus.BAD_REQUEST,
"This file cannot be imported. Media type is required."
)

"text/plain" -> FileTypeInputEnum.TEXT
"application/zip" -> FileTypeInputEnum.ZIP
else -> throw ResponseStatusException(
HttpStatus.UNSUPPORTED_MEDIA_TYPE,
"This file cannot be imported. Media type not supported ${HtmlUtils.htmlEscape(file.contentType!!)}."
)
}
return fileType
}

private fun importChat(
chatId: Long?,
file: MultipartFile,
fileType: FileTypeInputEnum,
chatName: String?
) {
chatId?.let {
chatFileImporter.execute(
chatId = it,
inputStream = file.inputStream,
fileType = fileType
)
} ?: run {
chatFileImporter.execute(
chatName = chatName,
inputStream = file.inputStream,
fileType = fileType
)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package dev.marcal.chatvault.web.config

import dev.marcal.chatvault.in_out_boundary.output.exceptions.*
import org.slf4j.LoggerFactory
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.context.request.WebRequest
import org.springframework.web.server.ResponseStatusException
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler

@ControllerAdvice
class WebControllerAdvice : ResponseEntityExceptionHandler() {

private val log = LoggerFactory.getLogger(this.javaClass)

@ExceptionHandler(value = [AttachmentNotFoundException::class, AttachmentFinderException::class, ChatNotFoundException::class, BucketFileNotFoundException::class])
fun handleNotFound(
ex: RuntimeException, request: WebRequest
): ResponseStatusException {
return ResponseStatusException(
HttpStatus.NOT_FOUND, ex.message ?: "Resource was not found", ex
)
}

@ExceptionHandler(value = [MessageParserException::class])
fun handleUnprocessableEntity(
ex: RuntimeException, request: WebRequest
): ResponseStatusException {
log.error("The request failed due to an unprocessable entity error at", ex)
return ResponseStatusException(
HttpStatus.UNPROCESSABLE_ENTITY, ex.message
)
}

@ExceptionHandler(value = [IllegalArgumentException::class, IllegalStateException::class])
fun handleConflict(
ex: RuntimeException, request: WebRequest
): ResponseStatusException {
log.error("Invalid state or requirements at", ex)
return ResponseStatusException(
HttpStatus.FAILED_DEPENDENCY, ex.message ?: "Invalid state or requirements", ex
)
}

@ExceptionHandler(value = [ChatImporterException::class, BucketServiceException::class])
fun handleInternalServerError(ex: RuntimeException, request: WebRequest): ResponseStatusException {
log.error("The request failed due to an unexpected error at", ex)
return ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR,
ex.message ?: "The request failed due to an unexpected error. See the server logs for more details.",
ex
)
}

}
Loading