Ajustement de MaterialService pour utiliser FileService

This commit is contained in:
FyloZ 2021-04-27 10:58:42 -04:00
parent ee4385ccb4
commit 0f649f983c
8 changed files with 265 additions and 453 deletions

View File

@ -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<Material>
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<Material>
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"
)

View File

@ -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>(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<ByteArray> = 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<Material>) =
ok(materials.map { it.toOutput() })
private fun created(producer: () -> Material): ResponseEntity<MaterialOutputDto> = 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
)
}"
}

View File

@ -9,11 +9,11 @@ import java.net.URI
/** Creates a HTTP OK [ResponseEntity] from the given [body]. */
fun <T> ok(body: T): ResponseEntity<T> =
ResponseEntity.ok(body)
ResponseEntity.ok(body)
/** Creates a HTTP OK [ResponseEntity] from the given [body] and [headers]. */
fun <T> ok(body: T, headers: HttpHeaders): ResponseEntity<T> =
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 <T> ok(action: () -> Unit): ResponseEntity<T> {
@ -23,19 +23,23 @@ fun <T> ok(action: () -> Unit): ResponseEntity<T> {
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
fun <T : Model> created(controllerPath: String, body: T): ResponseEntity<T> =
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 <T : Model> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =
created(controllerPath, producer())
created(controllerPath, producer())
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
fun <T> created(controllerPath: String, body: T, id: Any): ResponseEntity<T> =
ResponseEntity.created(URI.create("$controllerPath/$id")).body(body)
/** Creates a HTTP NOT FOUND [ResponseEntity]. */
fun <T> notFound(): ResponseEntity<T> =
ResponseEntity.notFound().build()
ResponseEntity.notFound().build()
/** Creates a HTTP NO CONTENT [ResponseEntity]. */
fun noContent(): ResponseEntity<Void> =
ResponseEntity.noContent().build()
ResponseEntity.noContent().build()
/** Executes the given [action] then returns an HTTP NO CONTENT [ResponseEntity]. */
fun noContent(action: () -> Unit): ResponseEntity<Void> {
@ -45,12 +49,12 @@ fun noContent(action: () -> Unit): ResponseEntity<Void> {
/** Creates a HTTP FORBIDDEN [ResponseEntity]. */
fun <T> forbidden(): ResponseEntity<T> =
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

View File

@ -24,6 +24,7 @@ class FileController(
fun upload(@RequestParam path: String): ResponseEntity<ByteArrayResource> {
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()
}

View File

@ -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<Material, MaterialSaveDto, MaterialUpdateDto, MaterialRepository> {
ExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialRepository> {
/** 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<Material>
@ -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<Material>
/** Gets the identifier of materials for which a SIMDUT exists. */
fun getAllIdsWithSimdut(): Collection<Long>
/** 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<Material, MaterialSaveDto, MaterialUpdateDto, MaterialRepository>(
materialRepository
),
MaterialService {
AbstractExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialRepository>(
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<Material> = getAll().filter { !it.isMixType }
override fun getAllIdsWithSimdut(): Collection<Long> =
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<Material> {
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<Material> {
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) {

View File

@ -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"
)

View File

@ -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<Material, MaterialSaveDto, MaterialUpdateDto, MaterialService, MaterialRepository>() {
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<Material>())
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<Material>())
doReturn(entity).whenever(materialUpdateDto).toEntity()
service.update(materialUpdateDto)
verify(simdutService).update(eq(mockSimdutFile), any())
}
// delete()

View File

@ -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<FileService>()
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<MultipartFile>()
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<MultipartFile>()
whenever(fileService.write(simdutMultipart, path, true)).doAnswer { throw FileCreateException(path) }
assertThrows<SimdutWriteException> { service.write(material, simdutMultipart) }
}
}
// update()
@Test
fun `update() deletes and write the SIMDUT for the given material`() {
val simdutMultipart = mock<MultipartFile>()
// 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)
}
}
}