develop #29
|
@ -0,0 +1,15 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config.annotations
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
@Service
|
||||
@RequireDatabase
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class ServiceComponent
|
||||
|
||||
@Service
|
||||
@RequireDatabase
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
annotation class LogicComponent
|
|
@ -0,0 +1,10 @@
|
|||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
||||
data class CompanyDto(
|
||||
override val id: Long = 0L,
|
||||
|
||||
@NotBlank
|
||||
val name: String
|
||||
) : EntityDto
|
|
@ -0,0 +1,5 @@
|
|||
package dev.fyloz.colorrecipesexplorer.dtos
|
||||
|
||||
interface EntityDto {
|
||||
val id: Long
|
||||
}
|
|
@ -1,50 +1,38 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.stereotype.Service
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.Company
|
||||
import dev.fyloz.colorrecipesexplorer.service.CompanyService
|
||||
|
||||
interface CompanyLogic :
|
||||
ExternalNamedModelService<Company, CompanySaveDto, CompanyUpdateDto, Company, CompanyRepository> {
|
||||
/** Checks if the given [company] is used by one or more recipes. */
|
||||
fun isLinkedToRecipes(company: Company): Boolean
|
||||
}
|
||||
interface CompanyLogic : Logic<CompanyDto, CompanyService>
|
||||
|
||||
@Service
|
||||
@RequireDatabase
|
||||
class DefaultCompanyLogic(
|
||||
companyRepository: CompanyRepository,
|
||||
@Lazy val recipeLogic: RecipeLogic
|
||||
) :
|
||||
AbstractExternalNamedModelService<Company, CompanySaveDto, CompanyUpdateDto, Company, CompanyRepository>(
|
||||
companyRepository
|
||||
),
|
||||
CompanyLogic {
|
||||
override fun idNotFoundException(id: Long) = companyIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = companyIdAlreadyExistsException(id)
|
||||
override fun nameNotFoundException(name: String) = companyNameNotFoundException(name)
|
||||
override fun nameAlreadyExistsException(name: String) = companyNameAlreadyExistsException(name)
|
||||
@LogicComponent
|
||||
class DefaultCompanyLogic(service: CompanyService) :
|
||||
BaseLogic<CompanyDto, CompanyService>(service, Company::class.simpleName!!), CompanyLogic {
|
||||
override fun save(dto: CompanyDto): CompanyDto {
|
||||
throwIfNameAlreadyExists(dto.name)
|
||||
|
||||
override fun Company.toOutput() = this
|
||||
|
||||
override fun isLinkedToRecipes(company: Company): Boolean = recipeLogic.existsByCompany(company)
|
||||
|
||||
override fun update(entity: CompanyUpdateDto): Company {
|
||||
// Lazy loaded to prevent checking the database when not necessary
|
||||
val persistedCompany by lazy { getById(entity.id) }
|
||||
|
||||
return update(with(entity) {
|
||||
company(
|
||||
id = id,
|
||||
name = if (name != null && name.isNotBlank()) name else persistedCompany.name
|
||||
)
|
||||
})
|
||||
return super.save(dto)
|
||||
}
|
||||
|
||||
override fun delete(entity: Company) {
|
||||
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteCompany(entity)
|
||||
super.delete(entity)
|
||||
override fun update(dto: CompanyDto): CompanyDto {
|
||||
throwIfNameAlreadyExists(dto.name, dto.id)
|
||||
|
||||
return super.update(dto)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteById(id: Long) {
|
||||
if (service.recipesDependsOnCompanyById(id)) {
|
||||
throw cannotDeleteException("Cannot delete the company with the id '$id' because one or more recipes depends on it")
|
||||
}
|
||||
|
||||
super.deleteById(id)
|
||||
}
|
||||
|
||||
private fun throwIfNameAlreadyExists(name: String, id: Long? = null) {
|
||||
if (service.existsByName(name, id)) {
|
||||
throw alreadyExistsException(value = name)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.service.Service
|
||||
|
||||
/**
|
||||
* Represents the logic for a DTO type.
|
||||
*
|
||||
* @param D The type of the DTO.
|
||||
* @param S The service for the DTO.
|
||||
*/
|
||||
interface Logic<D : EntityDto, S : Service<D, *, *>> {
|
||||
/** Checks if a DTO with the given [id] exists. */
|
||||
fun existsById(id: Long): Boolean
|
||||
|
||||
/** Get all DTOs. */
|
||||
fun getAll(): Collection<D>
|
||||
|
||||
/** Get the DTO for the given [id]. */
|
||||
fun getById(id: Long): D
|
||||
|
||||
/** Saves the given [dto]. */
|
||||
fun save(dto: D): D
|
||||
|
||||
/** Updates the given [dto]. */
|
||||
fun update(dto: D): D
|
||||
|
||||
/** Deletes the dto with the given [id]. */
|
||||
fun deleteById(id: Long)
|
||||
}
|
||||
|
||||
abstract class BaseLogic<D : EntityDto, S : Service<D, *, *>>(
|
||||
protected val service: S,
|
||||
protected val typeName: String
|
||||
) : Logic<D, S> {
|
||||
protected val typeNameLowerCase = typeName.lowercase()
|
||||
|
||||
override fun existsById(id: Long) =
|
||||
service.existsById(id)
|
||||
|
||||
override fun getAll() =
|
||||
service.getAll()
|
||||
|
||||
override fun getById(id: Long) =
|
||||
service.getById(id) ?: throw notFoundException(value = id)
|
||||
|
||||
override fun save(dto: D) =
|
||||
service.save(dto)
|
||||
|
||||
override fun update(dto: D): D {
|
||||
if (!existsById(dto.id)) {
|
||||
throw notFoundException(value = dto.id)
|
||||
}
|
||||
|
||||
return service.save(dto)
|
||||
}
|
||||
|
||||
override fun deleteById(id: Long) =
|
||||
service.deleteById(id)
|
||||
|
||||
protected fun notFoundException(identifierName: String = idIdentifierName, value: Any) =
|
||||
NotFoundException(
|
||||
typeNameLowerCase,
|
||||
"$typeName not found",
|
||||
"A $typeNameLowerCase with the $identifierName '$value' could not be found",
|
||||
value,
|
||||
identifierName
|
||||
)
|
||||
|
||||
protected fun alreadyExistsException(identifierName: String = nameIdentifierName, value: Any) =
|
||||
AlreadyExistsException(
|
||||
typeNameLowerCase,
|
||||
"$typeName already exists",
|
||||
"A $typeNameLowerCase with the $identifierName '$value' already exists",
|
||||
value,
|
||||
identifierName
|
||||
)
|
||||
|
||||
protected fun cannotDeleteException(details: String) =
|
||||
CannotDeleteException(
|
||||
typeNameLowerCase,
|
||||
"Cannot delete $typeNameLowerCase",
|
||||
details
|
||||
)
|
||||
|
||||
companion object {
|
||||
const val idIdentifierName = "id"
|
||||
const val nameIdentifierName = "name"
|
||||
}
|
||||
}
|
|
@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.logic
|
|||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import dev.fyloz.colorrecipesexplorer.model.NamedModel
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity
|
||||
import dev.fyloz.colorrecipesexplorer.repository.NamedJpaRepository
|
||||
import io.jsonwebtoken.lang.Assert
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
|
@ -16,7 +16,7 @@ import org.springframework.data.repository.findByIdOrNull
|
|||
* @param E The entity type
|
||||
* @param R The entity repository type
|
||||
*/
|
||||
interface Service<E, R : JpaRepository<E, *>> {
|
||||
interface OldService<E, R : JpaRepository<E, *>> {
|
||||
val repository: R
|
||||
|
||||
/** Gets all entities. */
|
||||
|
@ -32,8 +32,8 @@ interface Service<E, R : JpaRepository<E, *>> {
|
|||
fun delete(entity: E)
|
||||
}
|
||||
|
||||
/** A service for entities implementing the [Model] interface. This service add supports for numeric identifiers. */
|
||||
interface ModelService<E : Model, R : JpaRepository<E, *>> : Service<E, R> {
|
||||
/** A service for entities implementing the [ModelEntity] interface. This service add supports for numeric identifiers. */
|
||||
interface ModelService<E : ModelEntity, R : JpaRepository<E, *>> : OldService<E, R> {
|
||||
/** Checks if an entity with the given [id] exists. */
|
||||
fun existsById(id: Long): Boolean
|
||||
|
||||
|
@ -44,8 +44,8 @@ interface ModelService<E : Model, R : JpaRepository<E, *>> : Service<E, R> {
|
|||
fun deleteById(id: Long)
|
||||
}
|
||||
|
||||
/** A service for entities implementing the [NamedModel] interface. This service add supports for name identifiers. */
|
||||
interface NamedModelService<E : NamedModel, R : JpaRepository<E, *>> : ModelService<E, R> {
|
||||
/** A service for entities implementing the [NamedModelEntity] interface. This service add supports for name identifiers. */
|
||||
interface NamedModelService<E : NamedModelEntity, R : JpaRepository<E, *>> : ModelService<E, R> {
|
||||
/** Checks if an entity with the given [name] exists. */
|
||||
fun existsByName(name: String): Boolean
|
||||
|
||||
|
@ -54,14 +54,14 @@ interface NamedModelService<E : NamedModel, R : JpaRepository<E, *>> : ModelServ
|
|||
}
|
||||
|
||||
|
||||
abstract class AbstractService<E, R : JpaRepository<E, *>>(override val repository: R) : Service<E, R> {
|
||||
abstract class AbstractService<E, R : JpaRepository<E, *>>(override val repository: R) : OldService<E, R> {
|
||||
override fun getAll(): Collection<E> = repository.findAll()
|
||||
override fun save(entity: E): E = repository.save(entity)
|
||||
override fun update(entity: E): E = repository.save(entity)
|
||||
override fun delete(entity: E) = repository.delete(entity)
|
||||
}
|
||||
|
||||
abstract class AbstractModelService<E : Model, R : JpaRepository<E, Long>>(repository: R) :
|
||||
abstract class AbstractModelService<E : ModelEntity, R : JpaRepository<E, Long>>(repository: R) :
|
||||
AbstractService<E, R>(repository), ModelService<E, R> {
|
||||
protected abstract fun idNotFoundException(id: Long): NotFoundException
|
||||
protected abstract fun idAlreadyExistsException(id: Long): AlreadyExistsException
|
||||
|
@ -90,7 +90,7 @@ abstract class AbstractModelService<E : Model, R : JpaRepository<E, Long>>(repos
|
|||
}
|
||||
}
|
||||
|
||||
abstract class AbstractNamedModelService<E : NamedModel, R : NamedJpaRepository<E>>(repository: R) :
|
||||
abstract class AbstractNamedModelService<E : NamedModelEntity, R : NamedJpaRepository<E>>(repository: R) :
|
||||
AbstractModelService<E, R>(repository), NamedModelService<E, R> {
|
||||
protected abstract fun nameNotFoundException(name: String): NotFoundException
|
||||
protected abstract fun nameAlreadyExistsException(name: String): AlreadyExistsException
|
||||
|
@ -126,7 +126,7 @@ abstract class AbstractNamedModelService<E : NamedModel, R : NamedJpaRepository<
|
|||
* @param S The entity save DTO type
|
||||
* @param U The entity update DTO type
|
||||
*/
|
||||
interface ExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> : Service<E, R> {
|
||||
interface ExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> : OldService<E, R> {
|
||||
/** Gets all entities mapped to their output model. */
|
||||
fun getAllForOutput(): Collection<O>
|
||||
|
||||
|
@ -140,15 +140,15 @@ interface ExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepos
|
|||
fun E.toOutput(): O
|
||||
}
|
||||
|
||||
/** An [ExternalService] for entities implementing the [Model] interface. */
|
||||
interface ExternalModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> :
|
||||
/** An [ExternalService] for entities implementing the [ModelEntity] interface. */
|
||||
interface ExternalModelService<E : ModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> :
|
||||
ModelService<E, R>, ExternalService<E, S, U, O, R> {
|
||||
/** Gets the entity with the given [id] mapped to its output model. */
|
||||
fun getByIdForOutput(id: Long): O
|
||||
}
|
||||
|
||||
/** An [ExternalService] for entities implementing the [NamedModel] interface. */
|
||||
interface ExternalNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> :
|
||||
/** An [ExternalService] for entities implementing the [NamedModelEntity] interface. */
|
||||
interface ExternalNamedModelService<E : NamedModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> :
|
||||
NamedModelService<E, R>, ExternalModelService<E, S, U, O, R>
|
||||
|
||||
/** An [AbstractService] with the functionalities of a [ExternalService]. */
|
||||
|
@ -160,7 +160,7 @@ abstract class AbstractExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O,
|
|||
}
|
||||
|
||||
/** An [AbstractModelService] with the functionalities of a [ExternalService]. */
|
||||
abstract class AbstractExternalModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, Long>>(
|
||||
abstract class AbstractExternalModelService<E : ModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, Long>>(
|
||||
repository: R
|
||||
) : AbstractModelService<E, R>(repository), ExternalModelService<E, S, U, O, R> {
|
||||
override fun getAllForOutput() =
|
||||
|
@ -171,7 +171,7 @@ abstract class AbstractExternalModelService<E : Model, S : EntityDto<E>, U : Ent
|
|||
}
|
||||
|
||||
/** An [AbstractNamedModelService] with the functionalities of a [ExternalService]. */
|
||||
abstract class AbstractExternalNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, O, R : NamedJpaRepository<E>>(
|
||||
abstract class AbstractExternalNamedModelService<E : NamedModelEntity, S : EntityDto<E>, U : EntityDto<E>, O, R : NamedJpaRepository<E>>(
|
||||
repository: R
|
||||
) : AbstractNamedModelService<E, R>(repository), ExternalNamedModelService<E, S, U, O, R> {
|
||||
override fun getAllForOutput() =
|
|
@ -96,7 +96,7 @@ class DefaultRecipeLogic(
|
|||
override fun getAllByCompany(company: Company) = repository.findAllByCompany(company)
|
||||
|
||||
override fun save(entity: RecipeSaveDto): Recipe {
|
||||
val company = companyLogic.getById(entity.companyId)
|
||||
val company = company(companyLogic.getById(entity.companyId))
|
||||
|
||||
if (existsByNameAndCompany(entity.name, company)) {
|
||||
throw recipeNameAlreadyExistsForCompanyException(entity.name, company)
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
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.dtos.CompanyDto
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotNull
|
||||
|
||||
@Entity
|
||||
@Table(name = "company")
|
||||
|
@ -16,30 +11,8 @@ data class Company(
|
|||
override val id: Long?,
|
||||
|
||||
@Column(unique = true)
|
||||
override val name: String
|
||||
) : NamedModel {
|
||||
override fun toString(): String {
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open class CompanySaveDto(
|
||||
@field:NotBlank
|
||||
val name: String
|
||||
) : EntityDto<Company> {
|
||||
override fun toEntity(): Company = Company(null, name)
|
||||
}
|
||||
|
||||
|
||||
open class CompanyUpdateDto(
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank
|
||||
val name: String?
|
||||
) : EntityDto<Company> {
|
||||
override fun toEntity(): Company = Company(id, name ?: "")
|
||||
}
|
||||
) : ModelEntity
|
||||
|
||||
// ==== DSL ====
|
||||
fun company(
|
||||
|
@ -48,60 +21,12 @@ fun company(
|
|||
op: Company.() -> Unit = {}
|
||||
) = Company(id, name).apply(op)
|
||||
|
||||
fun companySaveDto(
|
||||
name: String = "name",
|
||||
op: CompanySaveDto.() -> Unit = {}
|
||||
) = CompanySaveDto(name).apply(op)
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun company(
|
||||
dto: CompanyDto
|
||||
) = Company(dto.id, dto.name)
|
||||
|
||||
fun companyUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
op: CompanyUpdateDto.() -> Unit = {}
|
||||
) = CompanyUpdateDto(id, name).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
private const val COMPANY_NOT_FOUND_EXCEPTION_TITLE = "Company not found"
|
||||
private const val COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE = "Company already exists"
|
||||
private const val COMPANY_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete company"
|
||||
private const val COMPANY_EXCEPTION_ERROR_CODE = "company"
|
||||
|
||||
fun companyIdNotFoundException(id: Long) =
|
||||
NotFoundException(
|
||||
COMPANY_EXCEPTION_ERROR_CODE,
|
||||
COMPANY_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A company with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun companyNameNotFoundException(name: String) =
|
||||
NotFoundException(
|
||||
COMPANY_EXCEPTION_ERROR_CODE,
|
||||
COMPANY_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A company with the name $name could not be found",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun companyIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
COMPANY_EXCEPTION_ERROR_CODE,
|
||||
COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A company with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
||||
fun companyNameAlreadyExistsException(name: String) =
|
||||
AlreadyExistsException(
|
||||
COMPANY_EXCEPTION_ERROR_CODE,
|
||||
COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A company with the name $name already exists",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun cannotDeleteCompany(company: Company) =
|
||||
CannotDeleteException(
|
||||
COMPANY_EXCEPTION_ERROR_CODE,
|
||||
COMPANY_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||
"Cannot delete the company ${company.name} because one or more recipes depends on it"
|
||||
)
|
||||
@Deprecated("Temporary DSL for transition")
|
||||
fun companyDto(
|
||||
entity: Company
|
||||
) = CompanyDto(entity.id!!, entity.name)
|
|
@ -8,7 +8,6 @@ import org.springframework.web.multipart.MultipartFile
|
|||
import javax.persistence.*
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
const val SIMDUT_FILES_PATH = "pdf/simdut"
|
||||
|
||||
|
@ -31,7 +30,7 @@ data class Material(
|
|||
@ManyToOne
|
||||
@JoinColumn(name = "material_type_id")
|
||||
var materialType: MaterialType?
|
||||
) : NamedModel {
|
||||
) : NamedModelEntity {
|
||||
val simdutFilePath
|
||||
@JsonIgnore
|
||||
@Transient
|
||||
|
@ -71,7 +70,7 @@ data class MaterialOutputDto(
|
|||
val isMixType: Boolean,
|
||||
val materialType: MaterialType,
|
||||
val simdutUrl: String?
|
||||
) : Model
|
||||
) : ModelEntity
|
||||
|
||||
data class MaterialQuantityDto(
|
||||
val material: Long,
|
||||
|
|
|
@ -4,12 +4,9 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
|||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotUpdateException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
|
||||
import org.hibernate.annotations.ColumnDefault
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotNull
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
private const val VALIDATION_PREFIX_SIZE = "Must contains exactly 3 characters"
|
||||
|
@ -34,7 +31,7 @@ data class MaterialType(
|
|||
@Column(name = "system_type")
|
||||
@ColumnDefault("false")
|
||||
val systemType: Boolean = false
|
||||
) : NamedModel
|
||||
) : NamedModelEntity
|
||||
|
||||
open class MaterialTypeSaveDto(
|
||||
@field:NotBlank
|
||||
|
|
|
@ -30,7 +30,7 @@ data class Mix(
|
|||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "mix_id")
|
||||
var mixMaterials: MutableSet<MixMaterial>,
|
||||
) : Model
|
||||
) : ModelEntity
|
||||
|
||||
open class MixSaveDto(
|
||||
@field:NotBlank
|
||||
|
|
|
@ -4,7 +4,6 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
|||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotNull
|
||||
|
||||
@Entity
|
||||
@Table(name = "mix_material")
|
||||
|
@ -20,7 +19,7 @@ data class MixMaterial(
|
|||
var quantity: Float,
|
||||
|
||||
var position: Int
|
||||
) : Model
|
||||
) : ModelEntity
|
||||
|
||||
data class MixMaterialDto(
|
||||
val materialId: Long,
|
||||
|
|
|
@ -20,7 +20,7 @@ data class MixType(
|
|||
@OneToOne(cascade = [CascadeType.ALL])
|
||||
@JoinColumn(name = "material_id")
|
||||
var material: Material
|
||||
) : NamedModel
|
||||
) : NamedModelEntity
|
||||
|
||||
// ==== DSL ====
|
||||
fun mixType(
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
/** The model of a stored entity. Each model should implements its own equals and hashCode methods to keep compatibility with the legacy Java and Thymeleaf code. */
|
||||
interface Model {
|
||||
/** Represents an entity, named differently to prevent conflicts with the JPA annotation. */
|
||||
interface ModelEntity {
|
||||
val id: Long?
|
||||
}
|
||||
|
||||
interface NamedModel : Model {
|
||||
interface NamedModelEntity : ModelEntity {
|
||||
val name: String
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ data class Recipe(
|
|||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "recipe_id")
|
||||
val groupsInformation: Set<RecipeGroupInformation>
|
||||
) : Model {
|
||||
) : ModelEntity {
|
||||
/** The mix types contained in this recipe. */
|
||||
val mixTypes: Collection<MixType>
|
||||
@JsonIgnore
|
||||
|
@ -150,7 +150,7 @@ data class RecipeOutputDto(
|
|||
val mixes: Set<MixOutputDto>,
|
||||
val groupsInformation: Set<RecipeGroupInformation>,
|
||||
var imagesUrls: Set<String>
|
||||
) : Model
|
||||
) : ModelEntity
|
||||
|
||||
@Entity
|
||||
@Table(name = "recipe_group_information")
|
||||
|
@ -168,7 +168,7 @@ data class RecipeGroupInformation(
|
|||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "recipe_group_information_id")
|
||||
var steps: MutableSet<RecipeStep>?
|
||||
) : Model
|
||||
) : ModelEntity
|
||||
|
||||
data class RecipeStepsDto(
|
||||
val groupId: Long,
|
||||
|
|
|
@ -14,7 +14,7 @@ data class RecipeStep(
|
|||
val position: Int,
|
||||
|
||||
val message: String
|
||||
) : Model
|
||||
) : ModelEntity
|
||||
|
||||
// ==== DSL ====
|
||||
fun recipeStep(
|
||||
|
|
|
@ -4,14 +4,13 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
|||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import org.hibernate.annotations.Fetch
|
||||
import org.hibernate.annotations.FetchMode
|
||||
import org.springframework.http.HttpStatus
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.NotBlank
|
||||
import javax.validation.constraints.NotEmpty
|
||||
import javax.validation.constraints.NotNull
|
||||
import javax.validation.constraints.Size
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_group")
|
||||
|
@ -29,7 +28,7 @@ data class Group(
|
|||
@Column(name = "permission")
|
||||
@Fetch(FetchMode.SUBSELECT)
|
||||
val permissions: MutableSet<Permission> = mutableSetOf(),
|
||||
) : NamedModel {
|
||||
) : NamedModelEntity {
|
||||
val flatPermissions: Set<Permission>
|
||||
get() = this.permissions
|
||||
.flatMap { it.flat() }
|
||||
|
@ -66,7 +65,7 @@ data class GroupOutputDto(
|
|||
val name: String,
|
||||
val permissions: Set<Permission>,
|
||||
val explicitPermissions: Set<Permission>
|
||||
): Model
|
||||
): ModelEntity
|
||||
|
||||
fun group(
|
||||
id: Long? = null,
|
||||
|
|
|
@ -4,7 +4,7 @@ import dev.fyloz.colorrecipesexplorer.SpringUserDetails
|
|||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import org.hibernate.annotations.Fetch
|
||||
import org.hibernate.annotations.FetchMode
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
|
||||
|
@ -50,7 +50,7 @@ data class User(
|
|||
|
||||
@Column(name = "last_login_time")
|
||||
var lastLoginTime: LocalDateTime? = null
|
||||
) : Model {
|
||||
) : ModelEntity {
|
||||
val flatPermissions: Set<Permission>
|
||||
get() = permissions
|
||||
.flatMap { it.flat() }
|
||||
|
@ -103,7 +103,7 @@ data class UserOutputDto(
|
|||
val permissions: Set<Permission>,
|
||||
val explicitPermissions: Set<Permission>,
|
||||
val lastLoginTime: LocalDateTime?
|
||||
) : Model
|
||||
) : ModelEntity
|
||||
|
||||
data class UserLoginRequest(val id: Long, val password: String)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package dev.fyloz.colorrecipesexplorer.model.touchupkit
|
|||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import dev.fyloz.colorrecipesexplorer.model.VALIDATION_SIZE_GE_ONE
|
||||
import java.time.LocalDate
|
||||
import javax.persistence.*
|
||||
|
@ -43,7 +43,7 @@ data class TouchUpKit(
|
|||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "touch_up_kit_id")
|
||||
val content: Set<TouchUpKitProduct>
|
||||
) : Model {
|
||||
) : ModelEntity {
|
||||
val finish
|
||||
get() = finishConcatenated.split(TOUCH_UP_KIT_DELIMITER)
|
||||
|
||||
|
@ -68,7 +68,7 @@ data class TouchUpKitProduct(
|
|||
val quantity: Float,
|
||||
|
||||
val ready: Boolean
|
||||
) : Model
|
||||
) : ModelEntity
|
||||
|
||||
data class TouchUpKitSaveDto(
|
||||
@field:NotBlank
|
||||
|
@ -140,7 +140,7 @@ data class TouchUpKitOutputDto(
|
|||
val material: List<String>,
|
||||
val content: Set<TouchUpKitProduct>,
|
||||
val pdfUrl: String
|
||||
) : Model
|
||||
) : ModelEntity
|
||||
|
||||
data class TouchUpKitProductDto(
|
||||
val name: String,
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.Company
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.jpa.repository.Query
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface CompanyRepository : NamedJpaRepository<Company> {
|
||||
interface CompanyRepository : JpaRepository<Company, Long> {
|
||||
/** Checks if a company with the given [name] and an id different from the given [id] exists. */
|
||||
fun existsByNameAndIdNot(name: String, id: Long): Boolean
|
||||
|
||||
/** Checks if a recipe depends on the company with the given [id]. */
|
||||
@Query(
|
||||
"""
|
||||
select case when(count(r.id) > 0) then false else true end
|
||||
from Company c
|
||||
left join Recipe r on c.id = r.company.id
|
||||
where c.id = :id
|
||||
"""
|
||||
select case when(count(r) > 0) then true else false end
|
||||
from Recipe r where r.company.id = :id
|
||||
"""
|
||||
)
|
||||
fun canBeDeleted(id: Long): Boolean
|
||||
fun recipesDependsOnCompanyById(id: Long): Boolean
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package dev.fyloz.colorrecipesexplorer.repository
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.NamedModel
|
||||
import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.repository.NoRepositoryBean
|
||||
|
||||
/** Adds support for entities using a name identifier. */
|
||||
@NoRepositoryBean
|
||||
interface NamedJpaRepository<E : NamedModel> : JpaRepository<E, Long> {
|
||||
interface NamedJpaRepository<E : NamedModelEntity> : JpaRepository<E, Long> {
|
||||
/** Checks if an entity with the given [name]. */
|
||||
fun existsByName(name: String): Boolean
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog
|
||||
import dev.fyloz.colorrecipesexplorer.model.Company
|
||||
import dev.fyloz.colorrecipesexplorer.model.CompanySaveDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.CompanyUpdateDto
|
||||
import org.springframework.context.annotation.Profile
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto
|
||||
import dev.fyloz.colorrecipesexplorer.logic.CompanyLogic
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import javax.validation.Valid
|
||||
|
@ -13,35 +12,35 @@ private const val COMPANY_CONTROLLER_PATH = "api/company"
|
|||
|
||||
@RestController
|
||||
@RequestMapping(COMPANY_CONTROLLER_PATH)
|
||||
@Profile("!emergency")
|
||||
@RequireDatabase
|
||||
@PreAuthorizeViewCatalog
|
||||
class CompanyController(private val companyLogic: dev.fyloz.colorrecipesexplorer.logic.CompanyLogic) {
|
||||
class CompanyController(private val companyLogic: CompanyLogic) {
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
ok(companyLogic.getAllForOutput())
|
||||
ok(companyLogic.getAll())
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(companyLogic.getByIdForOutput(id))
|
||||
ok(companyLogic.getById(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
|
||||
fun save(@Valid @RequestBody company: CompanySaveDto) =
|
||||
created<Company>(COMPANY_CONTROLLER_PATH) {
|
||||
companyLogic.save(company)
|
||||
}
|
||||
fun save(@Valid @RequestBody company: CompanyDto) =
|
||||
created<CompanyDto>(COMPANY_CONTROLLER_PATH) {
|
||||
companyLogic.save(company)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
|
||||
fun update(@Valid @RequestBody company: CompanyUpdateDto) =
|
||||
noContent {
|
||||
companyLogic.update(company)
|
||||
}
|
||||
fun update(@Valid @RequestBody company: CompanyDto) =
|
||||
noContent {
|
||||
companyLogic.update(company)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
companyLogic.deleteById(id)
|
||||
}
|
||||
noContent {
|
||||
companyLogic.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import org.springframework.core.io.Resource
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
|
@ -35,11 +36,21 @@ fun okFile(file: Resource, mediaType: String? = null): ResponseEntity<Resource>
|
|||
.body(file)
|
||||
|
||||
/** 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> =
|
||||
fun <T : ModelEntity> created(controllerPath: String, body: T): ResponseEntity<T> =
|
||||
created(controllerPath, body, body.id!!)
|
||||
|
||||
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
|
||||
@JvmName("createdDto")
|
||||
fun <T : EntityDto> created(controllerPath: String, body: T): ResponseEntity<T> =
|
||||
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> =
|
||||
fun <T : ModelEntity> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =
|
||||
created(controllerPath, producer())
|
||||
|
||||
/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */
|
||||
@JvmName("createdDto")
|
||||
fun <T : EntityDto> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =
|
||||
created(controllerPath, producer())
|
||||
|
||||
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.Company
|
||||
import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository
|
||||
|
||||
interface CompanyService : Service<CompanyDto, Company, CompanyRepository> {
|
||||
/** Checks if a company with the given [name] exists. */
|
||||
fun existsByName(name: String, id: Long?): Boolean
|
||||
|
||||
/** Checks if a recipe depends on the company with the given [id]. */
|
||||
fun recipesDependsOnCompanyById(id: Long): Boolean
|
||||
}
|
||||
|
||||
@ServiceComponent
|
||||
class DefaultCompanyService(repository: CompanyRepository) :
|
||||
BaseService<CompanyDto, Company, CompanyRepository>(repository), CompanyService {
|
||||
override fun existsByName(name: String, id: Long?) = repository.existsByNameAndIdNot(name, id ?: 0)
|
||||
override fun recipesDependsOnCompanyById(id: Long) = repository.recipesDependsOnCompanyById(id)
|
||||
|
||||
override fun toDto(entity: Company) =
|
||||
CompanyDto(entity.id!!, entity.name)
|
||||
|
||||
override fun toEntity(dto: CompanyDto) =
|
||||
Company(dto.id, dto.name)
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.data.repository.findByIdOrNull
|
||||
|
||||
/**
|
||||
* Represents a service between the logic and the repository.
|
||||
* Gives access to the repository using a DTO.
|
||||
*
|
||||
* @param D The type of the entity DTO.
|
||||
* @param E The type of the entity.
|
||||
* @param R The repository of the entity.
|
||||
*/
|
||||
interface Service<D : EntityDto, E : ModelEntity, R : JpaRepository<E, Long>> {
|
||||
/** Checks if an entity with the given [id] exists. */
|
||||
fun existsById(id: Long): Boolean
|
||||
|
||||
/** Gets all entities as DTOs. */
|
||||
fun getAll(): Collection<D>
|
||||
|
||||
/** Gets the entity DTO with the given [id].*/
|
||||
fun getById(id: Long): D?
|
||||
|
||||
/** Saves the given [dto]. */
|
||||
fun save(dto: D): D
|
||||
|
||||
/** Deletes the given [dto]. */
|
||||
fun delete(dto: D)
|
||||
|
||||
/** Deletes the entity with the given [id]. */
|
||||
fun deleteById(id: Long)
|
||||
}
|
||||
|
||||
abstract class BaseService<D : EntityDto, E : ModelEntity, R : JpaRepository<E, Long>>(protected val repository: R) :
|
||||
Service<D, E, R> {
|
||||
override fun existsById(id: Long) =
|
||||
repository.existsById(id)
|
||||
|
||||
override fun getAll() =
|
||||
repository.findAll().map(this::toDto)
|
||||
|
||||
override fun getById(id: Long): D? {
|
||||
val entity = repository.findByIdOrNull(id) ?: return null
|
||||
return toDto(entity)
|
||||
}
|
||||
|
||||
override fun save(dto: D): D {
|
||||
val entity = repository.save(toEntity(dto))
|
||||
return toDto(entity)
|
||||
}
|
||||
|
||||
override fun delete(dto: D) {
|
||||
repository.delete(toEntity(dto))
|
||||
}
|
||||
|
||||
override fun deleteById(id: Long) {
|
||||
repository.deleteById(id)
|
||||
}
|
||||
|
||||
abstract fun toDto(entity: E): D
|
||||
abstract fun toEntity(dto: D): E
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.utils
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
|
||||
/** Returns a list containing the result of the given [transform] applied to each item of the [Iterable]. If the given [transform] throws, the [Throwable] will be passed to the given [throwableConsumer]. */
|
||||
inline fun <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
|
||||
|
@ -46,8 +46,8 @@ inline fun <T> MutableCollection<T>.excludeAll(predicate: (T) -> Boolean): Itera
|
|||
return matching
|
||||
}
|
||||
|
||||
/** Merge to [Model] [Iterable]s and prevent id duplication. */
|
||||
fun <T : Model> Iterable<T>.merge(other: Iterable<T>) =
|
||||
/** Merge to [ModelEntity] [Iterable]s and prevent id duplication. */
|
||||
fun <T : ModelEntity> Iterable<T>.merge(other: Iterable<T>) =
|
||||
this
|
||||
.filter { model -> other.all { it.id != model.id } }
|
||||
.plus(other)
|
||||
|
|
|
@ -5,8 +5,8 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
|||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import dev.fyloz.colorrecipesexplorer.model.NamedModel
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import dev.fyloz.colorrecipesexplorer.model.NamedModelEntity
|
||||
import dev.fyloz.colorrecipesexplorer.repository.NamedJpaRepository
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
|
@ -18,7 +18,7 @@ import kotlin.test.assertFalse
|
|||
import kotlin.test.assertTrue
|
||||
import dev.fyloz.colorrecipesexplorer.logic.AbstractServiceTest as AbstractServiceTest1
|
||||
|
||||
abstract class AbstractServiceTest<E, S : Service<E, *>, R : JpaRepository<E, *>> {
|
||||
abstract class AbstractServiceTest<E, S : OldService<E, *>, R : JpaRepository<E, *>> {
|
||||
protected abstract val repository: R
|
||||
protected abstract val logic: S
|
||||
|
||||
|
@ -90,7 +90,7 @@ abstract class AbstractServiceTest<E, S : Service<E, *>, R : JpaRepository<E, *>
|
|||
}
|
||||
}
|
||||
|
||||
abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : JpaRepository<E, Long>> :
|
||||
abstract class AbstractModelServiceTest<E : ModelEntity, S : ModelService<E, *>, R : JpaRepository<E, Long>> :
|
||||
AbstractServiceTest1<E, S, R>() {
|
||||
|
||||
// existsById()
|
||||
|
@ -176,7 +176,7 @@ abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : J
|
|||
}
|
||||
}
|
||||
|
||||
abstract class AbstractNamedModelServiceTest<E : NamedModel, S : NamedModelService<E, *>, R : NamedJpaRepository<E>> :
|
||||
abstract class AbstractNamedModelServiceTest<E : NamedModelEntity, S : NamedModelService<E, *>, R : NamedJpaRepository<E>> :
|
||||
AbstractModelServiceTest<E, S, R>() {
|
||||
protected abstract val entityWithEntityName: E
|
||||
|
||||
|
@ -269,7 +269,7 @@ interface ExternalModelServiceTest {
|
|||
|
||||
// ==== IMPLEMENTATIONS FOR EXTERNAL SERVICES ====
|
||||
// Lots of code duplication but I don't have a better solution for now
|
||||
abstract class AbstractExternalModelServiceTest<E : Model, N : EntityDto<E>, U : EntityDto<E>, S : ExternalModelService<E, N, U, *, *>, R : JpaRepository<E, Long>> :
|
||||
abstract class AbstractExternalModelServiceTest<E : ModelEntity, N : EntityDto<E>, U : EntityDto<E>, S : ExternalModelService<E, N, U, *, *>, R : JpaRepository<E, Long>> :
|
||||
AbstractModelServiceTest<E, S, R>(), ExternalModelServiceTest {
|
||||
protected abstract val entitySaveDto: N
|
||||
protected abstract val entityUpdateDto: U
|
||||
|
@ -281,7 +281,7 @@ abstract class AbstractExternalModelServiceTest<E : Model, N : EntityDto<E>, U :
|
|||
}
|
||||
}
|
||||
|
||||
abstract class AbstractExternalNamedModelServiceTest<E : NamedModel, N : EntityDto<E>, U : EntityDto<E>, S : ExternalNamedModelService<E, N, U, *, *>, R : NamedJpaRepository<E>> :
|
||||
abstract class AbstractExternalNamedModelServiceTest<E : NamedModelEntity, N : EntityDto<E>, U : EntityDto<E>, S : ExternalNamedModelService<E, N, U, *, *>, R : NamedJpaRepository<E>> :
|
||||
AbstractNamedModelServiceTest<E, S, R>(), ExternalModelServiceTest {
|
||||
protected abstract val entitySaveDto: N
|
||||
protected abstract val entityUpdateDto: U
|
||||
|
@ -310,7 +310,7 @@ fun RestException.assertErrorCode(errorCode: String) {
|
|||
assertEquals(errorCode, this.errorCode)
|
||||
}
|
||||
|
||||
fun <E : Model, N : EntityDto<E>> withBaseSaveDtoTest(
|
||||
fun <E : ModelEntity, N : EntityDto<E>> withBaseSaveDtoTest(
|
||||
entity: E,
|
||||
entitySaveDto: N,
|
||||
service: ExternalService<E, N, *, *, *>,
|
||||
|
@ -328,7 +328,7 @@ fun <E : Model, N : EntityDto<E>> withBaseSaveDtoTest(
|
|||
op()
|
||||
}
|
||||
|
||||
fun <E : Model, U : EntityDto<E>> withBaseUpdateDtoTest(
|
||||
fun <E : ModelEntity, U : EntityDto<E>> withBaseUpdateDtoTest(
|
||||
entity: E,
|
||||
entityUpdateDto: U,
|
||||
service: ExternalModelService<E, *, U, *, *>,
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.EntityDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.ModelEntity
|
||||
import dev.fyloz.colorrecipesexplorer.service.Service
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BaseLogicTest {
|
||||
private val serviceMock = mockk<Service<TestEntityDto, ModelEntity, JpaRepository<ModelEntity, Long>>>()
|
||||
|
||||
private val baseLogic = spyk(TestBaseLogic(serviceMock))
|
||||
|
||||
private val dto = TestEntityDto(id = 1L)
|
||||
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun existsById_normalBehavior_returnsTrue() {
|
||||
// Arrange
|
||||
every { serviceMock.existsById(any()) } returns true
|
||||
|
||||
// Act
|
||||
val exists = baseLogic.existsById(dto.id)
|
||||
|
||||
// Assert
|
||||
assertTrue(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun exists_notFound_returnsFalse() {
|
||||
// Arrange
|
||||
every { serviceMock.existsById(any()) } returns false
|
||||
|
||||
// Act
|
||||
val exists = baseLogic.existsById(dto.id)
|
||||
|
||||
// Assert
|
||||
assertFalse(exists)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAll_normalBehavior_returnsAllDtos() {
|
||||
// Arrange
|
||||
val expectedDtos = listOf(dto)
|
||||
|
||||
every { serviceMock.getAll() } returns expectedDtos
|
||||
|
||||
// Act
|
||||
val actualDtos = baseLogic.getAll()
|
||||
|
||||
// Assert
|
||||
assertEquals(expectedDtos, actualDtos)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getById_normalBehavior_returnsDtoWithGivenId() {
|
||||
// Arrange
|
||||
every { serviceMock.getById(any()) } returns dto
|
||||
|
||||
// Act
|
||||
val dtoById = baseLogic.getById(dto.id)
|
||||
|
||||
// Assert
|
||||
assertEquals(dto, dtoById)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getById_notFound_throwsNotFoundException() {
|
||||
// Arrange
|
||||
every { serviceMock.getById(any()) } returns null
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
assertThrows<NotFoundException> { baseLogic.getById(dto.id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun save_normalBehavior_callsServiceSave() {
|
||||
// Arrange
|
||||
every { serviceMock.save(any()) } returns dto
|
||||
|
||||
// Act
|
||||
baseLogic.save(dto)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
serviceMock.save(dto)
|
||||
}
|
||||
confirmVerified(serviceMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun save_normalBehavior_returnsSavedDto() {
|
||||
// Arrange
|
||||
every { serviceMock.save(any()) } returns dto
|
||||
|
||||
// Act
|
||||
val savedDto = baseLogic.save(dto)
|
||||
|
||||
// Assert
|
||||
assertEquals(dto, savedDto)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun update_normalBehavior_callsServiceSave() {
|
||||
// Arrange
|
||||
every { serviceMock.save(any()) } returns dto
|
||||
every { baseLogic.existsById(any()) } returns true
|
||||
|
||||
// Act
|
||||
baseLogic.update(dto)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
serviceMock.save(dto)
|
||||
}
|
||||
confirmVerified(serviceMock)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun update_normalBehavior_returnsUpdatedDto() {
|
||||
// Arrange
|
||||
every { serviceMock.save(any()) } returns dto
|
||||
every { baseLogic.existsById(any()) } returns true
|
||||
|
||||
// Act
|
||||
val updatedDto = baseLogic.update(dto)
|
||||
|
||||
// Assert
|
||||
assertEquals(dto, updatedDto)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun update_notFound_throwsNotFoundException() {
|
||||
// Arrange
|
||||
every { serviceMock.save(any()) } returns dto
|
||||
every { baseLogic.existsById(any()) } returns false
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
assertThrows<NotFoundException> { baseLogic.update(dto) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteById_normalBehavior_callsServiceDeleteById() {
|
||||
// Arrange
|
||||
every { serviceMock.deleteById(any()) } just runs
|
||||
|
||||
// Act
|
||||
baseLogic.deleteById(dto.id)
|
||||
|
||||
// Assert
|
||||
verify {
|
||||
serviceMock.deleteById(dto.id)
|
||||
}
|
||||
confirmVerified(serviceMock)
|
||||
}
|
||||
}
|
||||
|
||||
private data class TestEntityDto(override val id: Long) : EntityDto
|
||||
private class TestBaseLogic<D : EntityDto, S : Service<D, *, *>>(service: S) :
|
||||
BaseLogic<D, S>(service, "UnitTestType")
|
|
@ -1,90 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import com.nhaarman.mockitokotlin2.*
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class CompanyLogicTest :
|
||||
AbstractExternalNamedModelServiceTest<Company, CompanySaveDto, CompanyUpdateDto, CompanyLogic, CompanyRepository>() {
|
||||
private val recipeLogic: RecipeLogic = mock()
|
||||
override val repository: CompanyRepository = mock()
|
||||
override val logic: CompanyLogic = spy(
|
||||
DefaultCompanyLogic(
|
||||
repository,
|
||||
recipeLogic
|
||||
)
|
||||
)
|
||||
|
||||
override val entity: Company = company(id = 0L, name = "company")
|
||||
override val anotherEntity: Company = company(id = 1L, name = "another company")
|
||||
override val entityWithEntityName: Company = company(id = 2L, name = entity.name)
|
||||
override val entitySaveDto: CompanySaveDto = spy(companySaveDto())
|
||||
override val entityUpdateDto: CompanyUpdateDto = spy(companyUpdateDto(id = entity.id!!, name = null))
|
||||
|
||||
@AfterEach
|
||||
override fun afterEach() {
|
||||
reset(recipeLogic)
|
||||
super.afterEach()
|
||||
}
|
||||
|
||||
// isLinkedToRecipes
|
||||
|
||||
@Test
|
||||
fun `isLinkedToRecipes() returns true when a given company is linked to one or more recipes`() {
|
||||
whenever(recipeLogic.existsByCompany(entity)).doReturn(true)
|
||||
|
||||
val found = logic.isLinkedToRecipes(entity)
|
||||
|
||||
assertTrue(found)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isLinkedToRecipes() returns false when a given company is not linked to any recipe`() {
|
||||
whenever(recipeLogic.existsByCompany(entity)).doReturn(false)
|
||||
|
||||
val found = logic.isLinkedToRecipes(entity)
|
||||
|
||||
assertFalse(found)
|
||||
}
|
||||
|
||||
// save()
|
||||
|
||||
@Test
|
||||
override fun `save(dto) calls and returns save() with the created entity`() {
|
||||
withBaseSaveDtoTest(entity, entitySaveDto, logic)
|
||||
}
|
||||
|
||||
// update()
|
||||
|
||||
@Test
|
||||
override fun `update(dto) calls and returns update() with the created entity`() =
|
||||
withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() })
|
||||
|
||||
// delete()
|
||||
|
||||
override fun `delete() deletes in the repository`() {
|
||||
whenCanBeDeleted {
|
||||
super.`delete() deletes in the repository`()
|
||||
}
|
||||
}
|
||||
|
||||
// deleteById()
|
||||
|
||||
override fun `deleteById() deletes the entity with the given id in the repository`() {
|
||||
whenCanBeDeleted {
|
||||
super.`deleteById() deletes the entity with the given id in the repository`()
|
||||
}
|
||||
}
|
||||
|
||||
private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) {
|
||||
whenever(repository.canBeDeleted(id)).doReturn(true)
|
||||
|
||||
test()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package dev.fyloz.colorrecipesexplorer.logic
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.service.CompanyService
|
||||
import io.mockk.clearAllMocks
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
|
||||
class DefaultCompanyLogicTest {
|
||||
private val companyServiceMock = mockk<CompanyService>()
|
||||
|
||||
private val companyLogic = DefaultCompanyLogic(companyServiceMock)
|
||||
|
||||
private val company = CompanyDto(id = 1L, name = "UnitTestCompany")
|
||||
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun save_nameExists_throwsAlreadyExistsException() {
|
||||
// Arrange
|
||||
every { companyServiceMock.existsByName(any(), any()) } returns true
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
assertThrows<AlreadyExistsException> { companyLogic.save(company) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun update_nameExists_throwsAlreadyExistsException() {
|
||||
// Arrange
|
||||
every { companyServiceMock.existsByName(any(), any()) } returns true
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
assertThrows<AlreadyExistsException> { companyLogic.update(company) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteById_recipesDependsOnCompany_throwsCannotDeleteException() {
|
||||
// Arrange
|
||||
every { companyServiceMock.recipesDependsOnCompanyById(company.id) } returns true
|
||||
|
||||
// Act
|
||||
// Assert
|
||||
assertThrows<CannotDeleteException> { companyLogic.deleteById(company.id) }
|
||||
}
|
||||
}
|
|
@ -157,14 +157,14 @@ class RecipeLogicTest :
|
|||
|
||||
@Test
|
||||
override fun `save(dto) calls and returns save() with the created entity`() {
|
||||
whenever(companyLogic.getById(company.id!!)).doReturn(company)
|
||||
whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company))
|
||||
doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company)
|
||||
withBaseSaveDtoTest(entity, entitySaveDto, logic, { argThat { this.id == null && this.color == color } })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `save(dto) throw AlreadyExistsException when a recipe with the given name and company exists in the repository`() {
|
||||
whenever(companyLogic.getById(company.id!!)).doReturn(company)
|
||||
whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company))
|
||||
doReturn(true).whenever(logic).existsByNameAndCompany(entity.name, company)
|
||||
|
||||
with(assertThrows<AlreadyExistsException> { logic.save(entitySaveDto) }) {
|
||||
|
|
Loading…
Reference in New Issue