From 0f649f983ce821f77761a5862d5512d7853bdf7e Mon Sep 17 00:00:00 2001 From: FyloZ Date: Tue, 27 Apr 2021 10:58:42 -0400 Subject: [PATCH] Ajustement de MaterialService pour utiliser FileService --- .../colorrecipesexplorer/model/Material.kt | 213 ++++++++++-------- .../rest/MaterialController.kt | 104 +++++---- .../colorrecipesexplorer/rest/RestUtils.kt | 22 +- .../rest/files/FileController.kt | 4 + .../service/MaterialService.kt | 89 ++++---- .../service/files/SimdutService.kt | 70 ------ .../service/MaterialServiceTest.kt | 76 ++----- .../service/files/SimdutServiceTest.kt | 140 ------------ 8 files changed, 265 insertions(+), 453 deletions(-) delete mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutService.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutServiceTest.kt diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 3b060a1..b337627 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -1,11 +1,13 @@ package dev.fyloz.colorrecipesexplorer.model +import com.fasterxml.jackson.annotation.JsonIgnore import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize import org.springframework.web.multipart.MultipartFile +import java.net.URI import javax.persistence.* import javax.validation.constraints.Min import javax.validation.constraints.NotBlank @@ -21,106 +23,135 @@ private const val MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE = "Un produit est requ private const val MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE = "Une quantité est requises" private const val MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE = "La quantité doit être supérieure ou égale à 0" +const val SIMDUT_FILES_PATH = "pdf/simdut" + @Entity @Table(name = "material") data class Material( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - override val id: Long?, + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + override val id: Long?, - @Column(unique = true) - override var name: String, + @Column(unique = true) + override var name: String, - @Column(name = "inventory_quantity") - var inventoryQuantity: Float, + @Column(name = "inventory_quantity") + var inventoryQuantity: Float, - @Column(name = "mix_type") - val isMixType: Boolean, + @Column(name = "mix_type") + val isMixType: Boolean, - @ManyToOne - @JoinColumn(name = "material_type_id") - var materialType: MaterialType? -) : NamedModel + @ManyToOne + @JoinColumn(name = "material_type_id") + var materialType: MaterialType? +) : NamedModel { + val simdutFilePath + @JsonIgnore + @Transient + get() = "$SIMDUT_FILES_PATH/$name.pdf" +} open class MaterialSaveDto( - @field:NotBlank(message = MATERIAL_NAME_NULL_MESSAGE) - val name: String, + @field:NotBlank(message = MATERIAL_NAME_NULL_MESSAGE) + val name: String, - @field:NotNull(message = MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE) - @field:Min(value = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE) - val inventoryQuantity: Float, + @field:NotNull(message = MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE) + @field:Min(value = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE) + val inventoryQuantity: Float, - @field:NotNull(message = MATERIAL_TYPE_NULL_MESSAGE) - val materialTypeId: Long, + @field:NotNull(message = MATERIAL_TYPE_NULL_MESSAGE) + val materialTypeId: Long, - val simdutFile: MultipartFile? = null + val simdutFile: MultipartFile? = null ) : EntityDto open class MaterialUpdateDto( - @field:NotNull(message = MATERIAL_ID_NULL_MESSAGE) - val id: Long, + @field:NotNull(message = MATERIAL_ID_NULL_MESSAGE) + val id: Long, - @field:NullOrNotBlank(message = MATERIAL_NAME_NULL_MESSAGE) - val name: String?, + @field:NullOrNotBlank(message = MATERIAL_NAME_NULL_MESSAGE) + val name: String?, - @field:NullOrSize(min = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE) - val inventoryQuantity: Float?, + @field:NullOrSize(min = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE) + val inventoryQuantity: Float?, - val materialTypeId: Long?, + val materialTypeId: Long?, - val simdutFile: MultipartFile? = null + val simdutFile: MultipartFile? = null ) : EntityDto -data class MaterialQuantityDto( - @field:NotNull(message = MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE) - val material: Long, +data class MaterialOutputDto( + val id: Long, + val name: String, + val inventoryQuantity: Float, + val isMixType: Boolean, + val materialType: MaterialType, + val simdutUrl: String? +) - @field:NotNull(message = MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE) - @field:Min(value = 0, message = MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE) - val quantity: Float +data class MaterialQuantityDto( + @field:NotNull(message = MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE) + val material: Long, + + @field:NotNull(message = MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE) + @field:Min(value = 0, message = MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE) + val quantity: Float ) // === DSL === fun material( - id: Long? = null, - name: String = "name", - inventoryQuantity: Float = 0f, - isMixType: Boolean = false, - materialType: MaterialType? = materialType(), - op: Material.() -> Unit = {} + id: Long? = null, + name: String = "name", + inventoryQuantity: Float = 0f, + isMixType: Boolean = false, + materialType: MaterialType? = materialType(), + op: Material.() -> Unit = {} ) = Material(id, name, inventoryQuantity, isMixType, materialType).apply(op) fun material( - material: Material, - id: Long? = null, - name: String? = null, + material: Material, + id: Long? = null, + name: String? = null, ) = Material( - id ?: material.id, name - ?: material.name, material.inventoryQuantity, material.isMixType, material.materialType + id ?: material.id, name + ?: material.name, material.inventoryQuantity, material.isMixType, material.materialType ) fun materialSaveDto( - name: String = "name", - inventoryQuantity: Float = 0f, - materialTypeId: Long = 0L, - simdutFile: MultipartFile? = null, - op: MaterialSaveDto.() -> Unit = {} + name: String = "name", + inventoryQuantity: Float = 0f, + materialTypeId: Long = 0L, + simdutFile: MultipartFile? = null, + op: MaterialSaveDto.() -> Unit = {} ) = MaterialSaveDto(name, inventoryQuantity, materialTypeId, simdutFile).apply(op) fun materialUpdateDto( - id: Long = 0L, - name: String? = "name", - inventoryQuantity: Float? = 0f, - materialTypeId: Long? = 0L, - simdutFile: MultipartFile? = null, - op: MaterialUpdateDto.() -> Unit = {} + id: Long = 0L, + name: String? = "name", + inventoryQuantity: Float? = 0f, + materialTypeId: Long? = 0L, + simdutFile: MultipartFile? = null, + op: MaterialUpdateDto.() -> Unit = {} ) = MaterialUpdateDto(id, name, inventoryQuantity, materialTypeId, simdutFile).apply(op) +fun materialOutputDto( + material: Material, + simdutUrl: String?, + op: MaterialOutputDto.() -> Unit = {} +) = MaterialOutputDto( + id = material.id!!, + name = material.name, + inventoryQuantity = material.inventoryQuantity, + isMixType = material.isMixType, + materialType = material.materialType!!, + simdutUrl = simdutUrl +).apply(op) + fun materialQuantityDto( - materialId: Long, - quantity: Float, - op: MaterialQuantityDto.() -> Unit = {} + materialId: Long, + quantity: Float, + op: MaterialQuantityDto.() -> Unit = {} ) = MaterialQuantityDto(materialId, quantity).apply(op) // ==== Exceptions ==== @@ -130,42 +161,42 @@ private const val MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete materi private const val MATERIAL_EXCEPTION_ERROR_CODE = "material" fun materialIdNotFoundException(id: Long) = - NotFoundException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_NOT_FOUND_EXCEPTION_TITLE, - "A material with the id $id could not be found", - id - ) + NotFoundException( + MATERIAL_EXCEPTION_ERROR_CODE, + MATERIAL_NOT_FOUND_EXCEPTION_TITLE, + "A material with the id $id could not be found", + id + ) fun materialNameNotFoundException(name: String) = - NotFoundException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_NOT_FOUND_EXCEPTION_TITLE, - "A material with the name $name could not be found", - name, - "name" - ) + NotFoundException( + MATERIAL_EXCEPTION_ERROR_CODE, + MATERIAL_NOT_FOUND_EXCEPTION_TITLE, + "A material with the name $name could not be found", + name, + "name" + ) fun materialIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE, - "A material with the id $id already exists", - id - ) + AlreadyExistsException( + MATERIAL_EXCEPTION_ERROR_CODE, + MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE, + "A material with the id $id already exists", + id + ) fun materialNameAlreadyExistsException(name: String) = - AlreadyExistsException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE, - "A material with the name $name already exists", - name, - "name" - ) + AlreadyExistsException( + MATERIAL_EXCEPTION_ERROR_CODE, + MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE, + "A material with the name $name already exists", + name, + "name" + ) fun cannotDeleteMaterialException(material: Material) = - CannotDeleteException( - MATERIAL_EXCEPTION_ERROR_CODE, - MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE, - "Cannot delete the material ${material.name} because one or more recipes depends on it" - ) + CannotDeleteException( + MATERIAL_EXCEPTION_ERROR_CODE, + MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE, + "Cannot delete the material ${material.name} because one or more recipes depends on it" + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt index a44d493..e9801f4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/MaterialController.kt @@ -1,13 +1,18 @@ package dev.fyloz.colorrecipesexplorer.rest import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog +import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties import dev.fyloz.colorrecipesexplorer.model.* +import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH import dev.fyloz.colorrecipesexplorer.service.MaterialService import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile +import java.net.URI +import java.net.URLEncoder +import java.nio.charset.StandardCharsets import javax.validation.Valid private const val MATERIAL_CONTROLLER_PATH = "api/material" @@ -15,78 +20,89 @@ private const val MATERIAL_CONTROLLER_PATH = "api/material" @RestController @RequestMapping(MATERIAL_CONTROLLER_PATH) @PreAuthorizeViewCatalog -class MaterialController(private val materialService: MaterialService) { +class MaterialController( + private val materialService: MaterialService, + private val creProperties: CreProperties +) { @GetMapping fun getAll() = - ok(materialService.getAll()) + ok(materialService.getAll()) @GetMapping("notmixtype") fun getAllNotMixType() = - ok(materialService.getAllNotMixType()) + ok(materialService.getAllNotMixType()) @GetMapping("{id}") fun getById(@PathVariable id: Long) = - ok(materialService.getById(id)) + ok(materialService.getById(id)) @PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasAuthority('EDIT_MATERIALS')") fun save(@Valid material: MaterialSaveDto, simdutFile: MultipartFile?) = - created(MATERIAL_CONTROLLER_PATH) { - materialService.save( - materialSaveDto( - name = material.name, - inventoryQuantity = material.inventoryQuantity, - materialTypeId = material.materialTypeId, - simdutFile = simdutFile + created { + materialService.save( + materialSaveDto( + name = material.name, + inventoryQuantity = material.inventoryQuantity, + materialTypeId = material.materialTypeId, + simdutFile = simdutFile + ) ) - ) - } + } @PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorize("hasAuthority('EDIT_MATERIALS')") fun update(@Valid material: MaterialUpdateDto, simdutFile: MultipartFile?) = - noContent { - materialService.update( - materialUpdateDto( - id = material.id, - name = material.name, - inventoryQuantity = material.inventoryQuantity, - materialTypeId = material.materialTypeId, - simdutFile = simdutFile + noContent { + materialService.update( + materialUpdateDto( + id = material.id, + name = material.name, + inventoryQuantity = material.inventoryQuantity, + materialTypeId = material.materialTypeId, + simdutFile = simdutFile + ) ) - ) - } + } @DeleteMapping("{id}") @PreAuthorize("hasAuthority('REMOVE_MATERIALS')") fun deleteById(@PathVariable id: Long) = - noContent { - materialService.deleteById(id) - } - - @GetMapping("{id}/simdut/exists") - fun hasSimdut(@PathVariable id: Long) = - ok(materialService.hasSimdut(id)) - - @GetMapping("{id}/simdut", produces = [MediaType.APPLICATION_PDF_VALUE]) - fun getSimdut(@PathVariable id: Long): ResponseEntity = with(materialService.getSimdut(id)) { - if (this.isEmpty()) { - notFound() - } else { - ok(this, httpHeaders(contentType = MediaType.APPLICATION_PDF)) + noContent { + materialService.deleteById(id) } - } - - @GetMapping("/simdut") - fun getAllIdsWithSimdut() = - ok(materialService.getAllIdsWithSimdut()) @GetMapping("mix/create/{recipeId}") fun getAllForMixCreation(@PathVariable recipeId: Long) = - ok(materialService.getAllForMixCreation(recipeId)) + ok(materialService.getAllForMixCreation(recipeId)) @GetMapping("mix/update/{mixId}") fun getAllForMixUpdate(@PathVariable mixId: Long) = - ok(materialService.getAllForMixUpdate(mixId)) + ok(materialService.getAllForMixUpdate(mixId)) + + private fun ok(material: Material) = + ok(material.toOutput()) + + private fun ok(materials: Collection) = + ok(materials.map { it.toOutput() }) + + private fun created(producer: () -> Material): ResponseEntity = with(producer().toOutput()) { + ResponseEntity + .created(URI.create("$MATERIAL_CONTROLLER_PATH/${this.id}")) + .body(this) + } + + private fun Material.toOutput() = materialOutputDto( + this, + if (materialService.hasSimdut(this)) this.simdutUrl() else null + ) + + private fun Material.simdutUrl() = + "${creProperties.deploymentUrl}$FILE_CONTROLLER_PATH?path=${ + URLEncoder.encode( + this.simdutFilePath, + StandardCharsets.UTF_8 + ) + }" } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt index 46004d1..2f8fa7c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RestUtils.kt @@ -9,11 +9,11 @@ import java.net.URI /** Creates a HTTP OK [ResponseEntity] from the given [body]. */ fun ok(body: T): ResponseEntity = - ResponseEntity.ok(body) + ResponseEntity.ok(body) /** Creates a HTTP OK [ResponseEntity] from the given [body] and [headers]. */ fun ok(body: T, headers: HttpHeaders): ResponseEntity = - ResponseEntity(body, headers, HttpStatus.OK) + ResponseEntity(body, headers, HttpStatus.OK) /** Executes the given [action] then returns an HTTP OK [ResponseEntity] form the given [body]. */ fun ok(action: () -> Unit): ResponseEntity { @@ -23,19 +23,23 @@ fun ok(action: () -> Unit): ResponseEntity { /** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ fun created(controllerPath: String, body: T): ResponseEntity = - ResponseEntity.created(URI.create("$controllerPath/${body.id}")).body(body) + created(controllerPath, body, body.id!!) /** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */ fun created(controllerPath: String, producer: () -> T): ResponseEntity = - created(controllerPath, producer()) + created(controllerPath, producer()) + +/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */ +fun created(controllerPath: String, body: T, id: Any): ResponseEntity = + ResponseEntity.created(URI.create("$controllerPath/$id")).body(body) /** Creates a HTTP NOT FOUND [ResponseEntity]. */ fun notFound(): ResponseEntity = - ResponseEntity.notFound().build() + ResponseEntity.notFound().build() /** Creates a HTTP NO CONTENT [ResponseEntity]. */ fun noContent(): ResponseEntity = - ResponseEntity.noContent().build() + ResponseEntity.noContent().build() /** Executes the given [action] then returns an HTTP NO CONTENT [ResponseEntity]. */ fun noContent(action: () -> Unit): ResponseEntity { @@ -45,12 +49,12 @@ fun noContent(action: () -> Unit): ResponseEntity { /** Creates a HTTP FORBIDDEN [ResponseEntity]. */ fun forbidden(): ResponseEntity = - ResponseEntity.status(HttpStatus.FORBIDDEN).build() + ResponseEntity.status(HttpStatus.FORBIDDEN).build() /** Creates an [HttpHeaders] instance from the given options. */ fun httpHeaders( - contentType: MediaType = MediaType.APPLICATION_JSON, - op: HttpHeaders.() -> Unit = {} + contentType: MediaType = MediaType.APPLICATION_JSON, + op: HttpHeaders.() -> Unit = {} ) = HttpHeaders().apply { this.contentType = contentType diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt index 2735583..3dff79b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/files/FileController.kt @@ -24,6 +24,7 @@ class FileController( fun upload(@RequestParam path: String): ResponseEntity { val file = fileService.read(path) return ResponseEntity.ok() + .header("Content-Disposition", "attachment; filename=${getFileNameFromPath(path)}") .contentLength(file.contentLength()) .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(file) @@ -52,4 +53,7 @@ class FileController( ResponseEntity .created(URI.create("${creProperties.deploymentUrl}$FILE_CONTROLLER_PATH?path=$path")) .build() + + private fun getFileNameFromPath(path: String) = + path.split("/").last() } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt index 0b9831b..6facd49 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt @@ -1,23 +1,19 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository -import dev.fyloz.colorrecipesexplorer.service.files.SimdutService +import dev.fyloz.colorrecipesexplorer.service.files.FileService import io.jsonwebtoken.lang.Assert import org.springframework.context.annotation.Lazy import org.springframework.stereotype.Service interface MaterialService : - ExternalNamedModelService { + ExternalNamedModelService { /** Checks if a material with the given [materialType] exists. */ fun existsByMaterialType(materialType: MaterialType): Boolean - /** Checks if the material with the given [id] has a SIMDUT file. */ - fun hasSimdut(id: Long): Boolean - - /** Gets the SIMDUT file of the material with the given [id]. */ - fun getSimdut(id: Long): ByteArray + /** Checks if the given [material] has a SIMDUT file. */ + fun hasSimdut(material: Material): Boolean /** Gets all materials that are not a mix type. */ fun getAllNotMixType(): Collection @@ -28,53 +24,48 @@ interface MaterialService : /** Gets all materials available for updating the mix with the given [mixId], including normal materials and materials from [MixType]s included in the mix recipe, excluding the material of the [MixType] of the said mix. */ fun getAllForMixUpdate(mixId: Long): Collection - /** Gets the identifier of materials for which a SIMDUT exists. */ - fun getAllIdsWithSimdut(): Collection - /** Updates the quantity of the given [material] with the given [factor] and returns the updated quantity. */ fun updateQuantity(material: Material, factor: Float): Float } @Service class MaterialServiceImpl( - materialRepository: MaterialRepository, - val simdutService: SimdutService, - val recipeService: RecipeService, - val mixService: MixService, - @Lazy val materialTypeService: MaterialTypeService + materialRepository: MaterialRepository, + val recipeService: RecipeService, + val mixService: MixService, + @Lazy val materialTypeService: MaterialTypeService, + val fileService: FileService ) : - AbstractExternalNamedModelService( - materialRepository - ), - MaterialService { + AbstractExternalNamedModelService( + materialRepository + ), + MaterialService { override fun idNotFoundException(id: Long) = materialIdNotFoundException(id) override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id) override fun nameNotFoundException(name: String) = materialNameNotFoundException(name) override fun nameAlreadyExistsException(name: String) = materialNameAlreadyExistsException(name) override fun existsByMaterialType(materialType: MaterialType): Boolean = - repository.existsByMaterialType(materialType) + repository.existsByMaterialType(materialType) - override fun hasSimdut(id: Long): Boolean = simdutService.exists(getById(id)) - override fun getSimdut(id: Long): ByteArray = simdutService.read(getById(id)) + override fun hasSimdut(material: Material): Boolean = fileService.exists(material.simdutFilePath) override fun getAllNotMixType(): Collection = getAll().filter { !it.isMixType } - override fun getAllIdsWithSimdut(): Collection = - getAllNotMixType() - .filter { simdutService.exists(it) } - .map { it.id!! } - override fun save(entity: MaterialSaveDto): Material = - save(with(entity) { - material( - name = entity.name, - inventoryQuantity = entity.inventoryQuantity, - materialType = materialTypeService.getById(materialTypeId), - isMixType = false - ) - }).apply { - if (entity.simdutFile != null && !entity.simdutFile.isEmpty) simdutService.write(this, entity.simdutFile) - } + save(with(entity) { + material( + name = entity.name, + inventoryQuantity = entity.inventoryQuantity, + materialType = materialTypeService.getById(materialTypeId), + isMixType = false + ) + }).apply { + if (entity.simdutFile != null && !entity.simdutFile.isEmpty) fileService.write( + entity.simdutFile, + this.simdutFilePath, + false + ) + } override fun update(entity: MaterialUpdateDto): Material { val persistedMaterial by lazy { @@ -83,14 +74,18 @@ class MaterialServiceImpl( return update(with(entity) { material( - id = id, - name = if (name != null && name.isNotBlank()) name else persistedMaterial.name, - inventoryQuantity = if (inventoryQuantity != null && inventoryQuantity != Float.MIN_VALUE) inventoryQuantity else persistedMaterial.inventoryQuantity, - isMixType = persistedMaterial.isMixType, - materialType = if (materialTypeId != null) materialTypeService.getById(materialTypeId) else persistedMaterial.materialType + id = id, + name = if (name != null && name.isNotBlank()) name else persistedMaterial.name, + inventoryQuantity = if (inventoryQuantity != null && inventoryQuantity != Float.MIN_VALUE) inventoryQuantity else persistedMaterial.inventoryQuantity, + isMixType = persistedMaterial.isMixType, + materialType = if (materialTypeId != null) materialTypeService.getById(materialTypeId) else persistedMaterial.materialType ) }).apply { - if (entity.simdutFile != null && !entity.simdutFile.isEmpty) simdutService.update(entity.simdutFile, this) + if (entity.simdutFile != null && !entity.simdutFile.isEmpty) fileService.write( + entity.simdutFile, + this.simdutFilePath, + true + ) } } @@ -103,15 +98,15 @@ class MaterialServiceImpl( override fun getAllForMixCreation(recipeId: Long): Collection { val recipesMixTypes = recipeService.getById(recipeId).mixTypes return getAll() - .filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } + .filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } } override fun getAllForMixUpdate(mixId: Long): Collection { val mix = mixService.getById(mixId) val recipesMixTypes = mix.recipe.mixTypes return getAll() - .filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } - .filter { it.id != mix.mixType.material.id } + .filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } + .filter { it.id != mix.mixType.material.id } } private fun assertPersistedMaterial(material: Material) { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutService.kt deleted file mode 100644 index c781859..0000000 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutService.kt +++ /dev/null @@ -1,70 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.service.files - -import dev.fyloz.colorrecipesexplorer.exception.RestException -import dev.fyloz.colorrecipesexplorer.model.Material -import org.slf4j.Logger -import org.springframework.http.HttpStatus -import org.springframework.stereotype.Service -import org.springframework.web.multipart.MultipartFile -import java.io.IOException - -const val SIMDUT_DIRECTORY = "simdut" - -@Service -class SimdutService( - private val fileService: FileService, - private val logger: Logger -) { - /** Checks if the given [material] has a SIMDUT file. */ - fun exists(material: Material) = - fileService.exists(getPath(material)) - - /** Reads the SIMDUT file of the given [material]. */ - // TODO change return type to ByteArrayResource - fun read(material: Material): ByteArray { - val path = getPath(material) - if (!fileService.exists(path)) return ByteArray(0) - - return try { - fileService.read(path).byteArray - } catch (ex: IOException) { - logger.error("Could not read SIMDUT file", ex) - ByteArray(0) - } - } - - /** Writes the given [simdut] file for the given [material] to the disk. */ - fun write(material: Material, simdut: MultipartFile) { - try { - fileService.write(simdut, getPath(material), true) - } catch (ex: FileWriteException) { - throw SimdutWriteException(material) - } - } - - /** Updates the SIMDUT file of the given [material] with the given [simdut]. */ - fun update(simdut: MultipartFile, material: Material) { - delete(material) - write(material, simdut) - } - - /** Deletes the SIMDUT file of the given [material]. */ - fun delete(material: Material) = - fileService.delete(getPath(material)) - - /** Gets the path of the SIMDUT file of the given [material]. */ - fun getPath(material: Material) = - "$SIMDUT_DIRECTORY/${getSimdutFileName(material)}" - - /** Gets the name of the SIMDUT file of the given [material]. */ - fun getSimdutFileName(material: Material) = - material.id.toString() -} - -class SimdutWriteException(material: Material) : - RestException( - "simdut-write", - "Could not write SIMDUT file", - HttpStatus.INTERNAL_SERVER_ERROR, - "Could not write the SIMDUT file for the material ${material.name} to the disk" - ) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt index 9def652..0025064 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt @@ -4,7 +4,7 @@ import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository -import dev.fyloz.colorrecipesexplorer.service.files.SimdutService +import dev.fyloz.colorrecipesexplorer.service.files.FileService import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows @@ -16,12 +16,12 @@ import kotlin.test.assertTrue class MaterialServiceTest : AbstractExternalNamedModelServiceTest() { override val repository: MaterialRepository = mock() - private val simdutService: SimdutService = mock() private val recipeService: RecipeService = mock() private val mixService: MixService = mock() private val materialTypeService: MaterialTypeService = mock() + private val fileService: FileService = mock() override val service: MaterialService = - spy(MaterialServiceImpl(repository, simdutService, recipeService, mixService, materialTypeService)) + spy(MaterialServiceImpl(repository, recipeService, mixService, materialTypeService, fileService)) override val entity: Material = material(id = 0L, name = "material") override val anotherEntity: Material = material(id = 1L, name = "another material") @@ -33,7 +33,7 @@ class MaterialServiceTest : @AfterEach override fun afterEach() { - reset(simdutService) + reset(recipeService, mixService, materialTypeService, fileService) super.afterEach() } @@ -61,20 +61,20 @@ class MaterialServiceTest : @Test fun `hasSimdut() returns false when simdutService_exists() returns false`() { - whenever(simdutService.exists(entity)).doReturn(false) + whenever(fileService.exists(any())).doReturn(false) doReturn(entity).whenever(service).getById(entity.id!!) - val found = service.hasSimdut(entity.id!!) + val found = service.hasSimdut(entity) assertFalse(found) } @Test fun `hasSimdut() returns true when simdutService_exists() returns true`() { - whenever(simdutService.exists(entity)).doReturn(true) + whenever(fileService.exists(any())).doReturn(true) doReturn(entity).whenever(service).getById(entity.id!!) - val found = service.hasSimdut(entity.id!!) + val found = service.hasSimdut(entity) assertTrue(found) } @@ -94,33 +94,6 @@ class MaterialServiceTest : assertFalse(found.contains(mixTypeMaterial)) } - // getAllIdsWithSimdut() - - @Test - fun `getAllIdsWithSimdut() returns a list containing the identifier of every material with a SIMDUT file`() { - val materials = listOf( - material(id = 0L), - material(id = 1L), - material(id = 2L), - material(id = 3L) - ) - val hasSimdut = mapOf( - *materials - .map { it.id!! to it.evenId } - .toTypedArray() - ) - val expectedIds = hasSimdut - .filter { it.value } - .map { it.key } - - whenever(simdutService.exists(any())).doAnswer { hasSimdut[(it.arguments[0] as Material).id] } - doReturn(materials).whenever(service).getAllNotMixType() - - val found = service.getAllIdsWithSimdut() - - assertEquals(expectedIds, found) - } - // save() @Test @@ -146,7 +119,7 @@ class MaterialServiceTest : service.save(materialSaveDto) - verify(simdutService).write(entity, mockMultipartFile) + verify(fileService).write(mockMultipartFile, entity.simdutFilePath, false) } // update() @@ -163,6 +136,21 @@ class MaterialServiceTest : .assertErrorCode("name") } + @Test + override fun `update(dto) calls and returns update() with the created entity`() { + val mockSimdutFile = MockMultipartFile("simdut", byteArrayOf(1, 2, 3, 4, 5)) + val materialUpdateDto = spy(materialUpdateDto(id = 0L, simdutFile = mockSimdutFile)) + +// doReturn(entity).whenever(service).getById(materialUpdateDto.id) + doReturn(entity).whenever(service).getById(any()) + doReturn(entity).whenever(service).update(any()) + doReturn(entity).whenever(materialUpdateDto).toEntity() + + service.update(materialUpdateDto) + + verify(fileService).write(mockSimdutFile, entity.simdutFilePath, true) + } + // updateQuantity() @Test @@ -220,22 +208,6 @@ class MaterialServiceTest : assertFalse(anotherMixTypeMaterial in found) } - // update() - - @Test - override fun `update(dto) calls and returns update() with the created entity`() { - val mockSimdutFile = MockMultipartFile("simdut", byteArrayOf(1, 2, 3, 4, 5)) - val materialUpdateDto = spy(materialUpdateDto(id = 0L, simdutFile = mockSimdutFile)) - - doReturn(entity).whenever(service).getById(materialUpdateDto.id) - doReturn(entity).whenever(service).update(any()) - doReturn(entity).whenever(materialUpdateDto).toEntity() - - service.update(materialUpdateDto) - - verify(simdutService).update(eq(mockSimdutFile), any()) - } - // delete() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutServiceTest.kt deleted file mode 100644 index 22ecfc0..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/files/SimdutServiceTest.kt +++ /dev/null @@ -1,140 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.service.files - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.model.Material -import dev.fyloz.colorrecipesexplorer.model.material -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.springframework.core.io.ByteArrayResource -import org.springframework.web.multipart.MultipartFile -import java.io.IOException -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class SimdutServiceTest { - private val fileService = mock() - private val service = spy(SimdutService(fileService, mock())) - - private val material = material(id = 0L) - - @AfterEach - fun afterEach() { - reset(fileService, service) - } - - @JvmName("withNullableMaterialPath") - private inline fun withMaterialPath(material: Material? = null, exists: Boolean = true, test: (String) -> Unit) = - withMaterialPath(material ?: this.material, exists, test) - - private inline fun withMaterialPath(material: Material, exists: Boolean = true, test: (String) -> Unit) { - val path = "data/simdut/${material.id}" - doReturn(path).whenever(service).getPath(material) - whenever(fileService.exists(path)).doReturn(exists) - - test(path) - } - - // exists() - - @Test - fun `exists() returns true when a SIMDUT file exists for the given material`() { - withMaterialPath { - assertTrue { service.exists(material) } - } - } - - @Test - fun `exists() returns false when no SIMDUT file exists for the given material`() { - withMaterialPath(exists = false) { - assertFalse { service.exists(material) } - } - } - - // read() - - @Test - fun `read() returns a filled ByteArray when a SIMDUT exists for the given material`() { - withMaterialPath { path -> - val simdutContent = byteArrayOf(0xf) - - whenever(fileService.read(path)).doReturn(ByteArrayResource(simdutContent)) - - val found = service.read(material) - - assertEquals(simdutContent, found) - } - } - - @Test - fun `read() returns a empty ByteArray when no SIMDUT exists for the given material`() { - withMaterialPath(exists = false) { - val found = service.read(material) - - assertTrue { found.isEmpty() } - } - } - - @Test - fun `read() returns a empty ByteArray when reading the SIMDUT throws an IOException`() { - withMaterialPath { path -> - whenever(fileService.read(path)).doAnswer { throw IOException() } - - val found = service.read(material) - - assertTrue { found.isEmpty() } - } - } - - // write() - - @Test - fun `write() writes the given MultipartFile to the disk for the given material`() { - withMaterialPath { path -> - val simdutMultipart = mock() - - service.write(material, simdutMultipart) - - verify(fileService).write(simdutMultipart, path, true) - } - } - - @Test - fun `write() throws a SimdutWriteException when writing the given MultipartFile to the disk fails`() { - withMaterialPath { path -> - val simdutMultipart = mock() - - whenever(fileService.write(simdutMultipart, path, true)).doAnswer { throw FileCreateException(path) } - - assertThrows { service.write(material, simdutMultipart) } - } - } - - // update() - - @Test - fun `update() deletes and write the SIMDUT for the given material`() { - val simdutMultipart = mock() - - // Prevents calling the actual implementation - doAnswer { }.whenever(service).delete(material) - doAnswer { }.whenever(service).write(material, simdutMultipart) - - service.update(simdutMultipart, material) - - verify(service).delete(material) - verify(service).write(material, simdutMultipart) - } - - // delete() - - @Test - fun `delete() deletes the SIMDUT of the given material from the disk`() { - withMaterialPath { path -> - service.delete(material) - - verify(fileService).delete(path) - } - } -}