Ajustement de MaterialService pour utiliser FileService
This commit is contained in:
parent
ee4385ccb4
commit
0f649f983c
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}"
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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"
|
||||
)
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue