diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt index 389bd5a..a780be3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/WebSecurityConfig.kt @@ -1,7 +1,7 @@ package dev.fyloz.colorrecipesexplorer.config import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.Employee import dev.fyloz.colorrecipesexplorer.model.EmployeeLoginRequest import dev.fyloz.colorrecipesexplorer.model.EmployeePermission @@ -272,7 +272,7 @@ class JwtAuthorizationFilter( private fun getAuthenticationToken(employeeId: String): UsernamePasswordAuthenticationToken? = try { val employeeDetails = userDetailsService.loadUserByEmployeeId(employeeId.toLong(), false) UsernamePasswordAuthenticationToken(employeeDetails.username, null, employeeDetails.authorities) - } catch (_: EntityNotFoundException) { + } catch (_: NotFoundException) { null } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt index d64c824..03067f2 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/exception/RestException.kt @@ -1,9 +1,7 @@ package dev.fyloz.colorrecipesexplorer.exception -import com.fasterxml.jackson.annotation.JsonProperty import dev.fyloz.colorrecipesexplorer.model.Material import dev.fyloz.colorrecipesexplorer.model.MaterialQuantityDto -import org.springframework.context.annotation.Profile import org.springframework.http.HttpHeaders import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity @@ -11,84 +9,84 @@ import org.springframework.validation.FieldError import org.springframework.web.bind.MethodArgumentNotValidException import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.context.request.ServletWebRequest import org.springframework.web.context.request.WebRequest import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler -abstract class RestException(val exceptionMessage: String, val httpStatus: HttpStatus) : - RuntimeException(exceptionMessage) { - abstract fun buildBody(): RestExceptionBody +abstract class RestException( + val errorCode: String, + val title: String, + val status: HttpStatus, + val details: String, + val extensions: Map = mapOf() +) : RuntimeException(details) { + fun buildExceptionBody() = mapOf( + "type" to errorCode, + "title" to title, + "status" to status.value(), + "detail" to details, - @Suppress("unused") - open inner class RestExceptionBody( - val status: Int = httpStatus.value(), - @JsonProperty("message") val message: String = exceptionMessage + *extensions.map { it.key to it.value }.toTypedArray() ) } -class EntityAlreadyExistsException(val value: Any) : - RestException("An entity with the given identifier already exists", HttpStatus.CONFLICT) { - @Suppress("unused") - override fun buildBody(): RestExceptionBody = object : RestExceptionBody() { - val id = value - } -} +class NotFoundException( + errorCode: String, + title: String, + details: String, + identifierValue: Any, + identifierName: String = "id" +) : RestException( + errorCode = "notfound-$errorCode-$identifierName", + title = title, + status = HttpStatus.NOT_FOUND, + details = details, + extensions = mapOf( + identifierName to identifierValue + ) +) -class EntityNotFoundException(val value: Any) : - RestException("An entity could not be found with the given identifier", HttpStatus.NOT_FOUND) { - @Suppress("unused") - override fun buildBody(): RestExceptionBody = object : RestExceptionBody() { - val id = value - } -} +class AlreadyExistsException( + errorCode: String, + title: String, + details: String, + identifierValue: Any, + identifierName: String = "id" +) : RestException( + errorCode = "exists-$errorCode-$identifierName", + title = title, + status = HttpStatus.CONFLICT, + details = details, + extensions = mapOf( + identifierName to identifierValue + ) +) -class CannotDeleteEntityException(val value: Long) : - RestException( - "The entity with the given identifier could not be deleted because it is required by other entities", - HttpStatus.CONFLICT - ) { - @Suppress("unused") - override fun buildBody(): RestExceptionBody = object : RestExceptionBody() { - val id = value - } -} - -class SimdutWriteException(val material: Material) : - RestException( - "Could not write the SIMDUT file to disk", - HttpStatus.INTERNAL_SERVER_ERROR - ) { - @Suppress("unused") - override fun buildBody(): RestExceptionBody = RestExceptionBody() -} - -class LowQuantityException(val materialQuantity: MaterialQuantityDto) : - RestException( - "There is not enough of the given material in the inventory", - HttpStatus.CONFLICT - ) { - @Suppress("unused") - override fun buildBody(): RestExceptionBody = object : RestExceptionBody() { - val material = materialQuantity.material - val quantity = materialQuantity.quantity - } -} - -class LowQuantitiesException(val materialQuantities: Collection) : - RestException( - "There is not enough of one or more given materials in the inventory", - HttpStatus.CONFLICT - ) { - @Suppress - override fun buildBody(): RestExceptionBody = object : RestExceptionBody() { - val lowQuantities = materialQuantities - } -} +class CannotDeleteException( + errorCode: String, + title: String, + details: String +) : RestException( + errorCode = "cannotdelete-$errorCode", + title = title, + status = HttpStatus.CONFLICT, + details = details +) @ControllerAdvice class RestResponseEntityExceptionHandler : ResponseEntityExceptionHandler() { @ExceptionHandler(RestException::class) fun handleRestExceptions(exception: RestException, request: WebRequest): ResponseEntity { - return handleExceptionInternal(exception, exception.buildBody(), HttpHeaders(), exception.httpStatus, request) + val finalBody = exception.buildExceptionBody().toMutableMap() + finalBody["instance"] = (request as ServletWebRequest).request.requestURI + + return handleExceptionInternal( + exception, + finalBody, + HttpHeaders(), + exception.status, + request + ) } override fun handleMethodArgumentNotValid( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/AccountModel.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/AccountModel.kt deleted file mode 100644 index 1f680f1..0000000 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/AccountModel.kt +++ /dev/null @@ -1,331 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.model - -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonProperty -import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank -import org.hibernate.annotations.Fetch -import org.hibernate.annotations.FetchMode -import org.springframework.security.core.GrantedAuthority -import org.springframework.security.core.authority.SimpleGrantedAuthority -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder -import org.springframework.security.crypto.password.PasswordEncoder -import java.time.LocalDateTime -import javax.persistence.* -import javax.validation.constraints.NotBlank -import javax.validation.constraints.NotNull -import javax.validation.constraints.Size - - -private const val EMPLOYEE_ID_NULL_MESSAGE = "Un numéro d'employé est requis" -private const val EMPLOYEE_LAST_NAME_EMPTY_MESSAGE = "Un nom est requis" -private const val EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE = "Un prénom est requis" -private const val EMPLOYEE_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis" -private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères" - -@Entity -@Table(name = "employee") -data class Employee( - @Id - override val id: Long, - - @Column(name = "first_name") - val firstName: String = "", - - @Column(name = "last_name") - val lastName: String = "", - - @JsonIgnore - val password: String = "", - - @JsonIgnore - @Column(name = "default_group_user") - val isDefaultGroupUser: Boolean = false, - - @JsonIgnore - @Column(name = "system_user") - val isSystemUser: Boolean = false, - - @ManyToOne - @JoinColumn(name = "group_id") - @Fetch(FetchMode.SELECT) - var group: EmployeeGroup? = null, - - @Enumerated(EnumType.STRING) - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "employee_permission", joinColumns = [JoinColumn(name = "employee_id")]) - @Column(name = "permission") - @Fetch(FetchMode.SUBSELECT) - @get:JsonProperty("explicitPermissions") - val permissions: MutableSet = mutableSetOf(), - - @Column(name = "last_login_time") - var lastLoginTime: LocalDateTime? = null -) : Model { - @get:JsonProperty("permissions") - val flatPermissions: Set - get() = permissions - .flatMap { it.flat() } - .filter { !it.deprecated } - .toMutableSet() - .apply { - if (group != null) this.addAll(group!!.flatPermissions) - } - - @get:JsonIgnore - val authorities: Set - get() = flatPermissions.map { it.toAuthority() }.toMutableSet() -} - -/** DTO for creating employees. Allows a [password] a [groupId]. */ -open class EmployeeSaveDto( - @field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE) - val id: Long, - - @field:NotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE) - val firstName: String, - - @field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE) - val lastName: String, - - @field:NotBlank(message = EMPLOYEE_PASSWORD_EMPTY_MESSAGE) - @field:Size(min = 8, message = EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE) - val password: String, - - val groupId: Long?, - - @Enumerated(EnumType.STRING) - val permissions: MutableSet = mutableSetOf() -) : EntityDto - -open class EmployeeUpdateDto( - @field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE) - val id: Long, - - @field:NullOrNotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE) - val firstName: String?, - - @field:NullOrNotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE) - val lastName: String?, - - val groupId: Long?, - - @Enumerated(EnumType.STRING) - val permissions: Set? -) : EntityDto - - -private const val GROUP_ID_NULL_MESSAGE = "Un identifiant est requis" -private const val GROUP_NAME_NULL_MESSAGE = "Un nom est requis" -private const val GROUP_PERMISSIONS_EMPTY_MESSAGE = "Au moins une permission est requise" - -@Entity -@Table(name = "employee_group") -data class EmployeeGroup( - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - override var id: Long? = null, - - @Column(unique = true) - override val name: String = "", - - @Enumerated(EnumType.STRING) - @ElementCollection(fetch = FetchType.EAGER) - @CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")]) - @Column(name = "permission") - @Fetch(FetchMode.SUBSELECT) - @get:JsonProperty("explicitPermissions") - val permissions: MutableSet = mutableSetOf(), -) : NamedModel { - @get:JsonProperty("permissions") - val flatPermissions: Set - get() = this.permissions - .flatMap { it.flat() } - .filter { !it.deprecated } - .toSet() -} - -open class EmployeeGroupSaveDto( - @field:NotBlank(message = GROUP_NAME_NULL_MESSAGE) - @field:Size(min = 3) - val name: String, - - @field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE) - val permissions: MutableSet -) : EntityDto { - override fun toEntity(): EmployeeGroup = - EmployeeGroup(null, name, permissions) -} - -open class EmployeeGroupUpdateDto( - @field:NotNull(message = GROUP_ID_NULL_MESSAGE) - val id: Long, - - @field:NotBlank(message = GROUP_NAME_NULL_MESSAGE) - @field:Size(min = 3) - val name: String, - - @field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE) - val permissions: MutableSet -) : EntityDto { - override fun toEntity(): EmployeeGroup = - EmployeeGroup(id, name, permissions) -} - - -data class EmployeeLoginRequest(val id: Long, val password: String) - - -enum class EmployeePermission( - val impliedPermissions: List = listOf(), - val deprecated: Boolean = false -) { - VIEW_RECIPES, - VIEW_CATALOG, - VIEW_USERS, - - PRINT_MIXES(listOf(VIEW_RECIPES)), - - EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)), - EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA)), - EDIT_MATERIALS(listOf(VIEW_CATALOG)), - EDIT_MATERIAL_TYPES(listOf(VIEW_CATALOG)), - EDIT_COMPANIES(listOf(VIEW_CATALOG)), - EDIT_USERS(listOf(VIEW_USERS)), - EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)), - - ADD_TO_INVENTORY(listOf(VIEW_CATALOG)), - DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)), - - REMOVE_RECIPES(listOf(EDIT_RECIPES)), - REMOVE_MATERIALS(listOf(EDIT_MATERIALS)), - REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES)), - REMOVE_COMPANIES(listOf(EDIT_COMPANIES)), - REMOVE_USERS(listOf(EDIT_USERS)), - REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES)), - - ADMIN( - listOf( - EDIT_CATALOG, - - REMOVE_RECIPES, - REMOVE_USERS, - REMOVE_CATALOG, - - PRINT_MIXES, - ADD_TO_INVENTORY, - DEDUCT_FROM_INVENTORY - ) - ), - - // deprecated permissions - VIEW_RECIPE(listOf(VIEW_RECIPES), true), - VIEW_MATERIAL(listOf(VIEW_CATALOG), true), - VIEW_MATERIAL_TYPE(listOf(VIEW_CATALOG), true), - VIEW_COMPANY(listOf(VIEW_CATALOG), true), - VIEW(listOf(VIEW_RECIPES, VIEW_CATALOG), true), - VIEW_EMPLOYEE(listOf(VIEW_USERS), true), - VIEW_EMPLOYEE_GROUP(listOf(VIEW_USERS), true), - - EDIT_RECIPE(listOf(EDIT_RECIPES), true), - EDIT_MATERIAL(listOf(EDIT_MATERIALS), true), - EDIT_MATERIAL_TYPE(listOf(EDIT_MATERIAL_TYPES), true), - EDIT_COMPANY(listOf(EDIT_COMPANIES), true), - EDIT(listOf(EDIT_RECIPES, EDIT_CATALOG), true), - EDIT_EMPLOYEE(listOf(EDIT_USERS), true), - EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_USERS), true), - EDIT_EMPLOYEE_GROUP(listOf(EDIT_USERS), true), - - REMOVE_RECIPE(listOf(REMOVE_RECIPES), true), - REMOVE_MATERIAL(listOf(REMOVE_MATERIALS), true), - REMOVE_MATERIAL_TYPE(listOf(REMOVE_MATERIAL_TYPES), true), - REMOVE_COMPANY(listOf(REMOVE_COMPANIES), true), - REMOVE(listOf(REMOVE_RECIPES, REMOVE_CATALOG), true), - REMOVE_EMPLOYEE(listOf(REMOVE_USERS), true), - REMOVE_EMPLOYEE_GROUP(listOf(REMOVE_USERS), true), - - SET_BROWSER_DEFAULT_GROUP(listOf(VIEW_USERS), true), - ; - - operator fun contains(permission: EmployeePermission): Boolean { - return permission == this || impliedPermissions.any { permission in it } - } -} - -fun EmployeePermission.flat(): Iterable { - return mutableSetOf(this).apply { - impliedPermissions.forEach { - addAll(it.flat()) - } - } -} - -/** Converts the given [EmployeePermission] to a [GrantedAuthority]. */ -private fun EmployeePermission.toAuthority(): GrantedAuthority { - return SimpleGrantedAuthority(name) -} - - -// ==== DSL ==== -fun employee( - passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(), - id: Long = 0L, - firstName: String = "firstName", - lastName: String = "lastName", - password: String = passwordEncoder.encode("password"), - isDefaultGroupUser: Boolean = false, - isSystemUser: Boolean = false, - group: EmployeeGroup? = null, - permissions: MutableSet = mutableSetOf(), - lastLoginTime: LocalDateTime? = null, - op: Employee.() -> Unit = {} -) = Employee( - id, - firstName, - lastName, - password, - isDefaultGroupUser, - isSystemUser, - group, - permissions, - lastLoginTime -).apply(op) - -fun employeeSaveDto( - passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(), - id: Long = 0L, - firstName: String = "firstName", - lastName: String = "lastName", - password: String = passwordEncoder.encode("password"), - groupId: Long? = null, - permissions: MutableSet = mutableSetOf(), - op: EmployeeSaveDto.() -> Unit = {} -) = EmployeeSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op) - -fun employeeUpdateDto( - id: Long = 0L, - firstName: String = "firstName", - lastName: String = "lastName", - groupId: Long? = null, - permissions: MutableSet = mutableSetOf(), - op: EmployeeUpdateDto.() -> Unit = {} -) = EmployeeUpdateDto(id, firstName, lastName, groupId, permissions).apply(op) - -fun employeeGroup( - id: Long? = null, - name: String = "name", - permissions: MutableSet = mutableSetOf(), - op: EmployeeGroup.() -> Unit = {} -) = EmployeeGroup(id, name, permissions).apply(op) - -fun employeeGroupSaveDto( - name: String = "name", - permissions: MutableSet = mutableSetOf(), - op: EmployeeGroupSaveDto.() -> Unit = {} -) = EmployeeGroupSaveDto(name, permissions).apply(op) - -fun employeeGroupUpdateDto( - id: Long = 0L, - name: String = "name", - permissions: MutableSet = mutableSetOf(), - op: EmployeeGroupUpdateDto.() -> Unit = {} -) = EmployeeGroupUpdateDto(id, name, permissions).apply(op) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt index 9362b8a..1d75903 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt @@ -1,5 +1,8 @@ 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 javax.persistence.* import javax.validation.constraints.NotBlank @@ -55,3 +58,42 @@ fun companyUpdateDto( 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 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" + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Employee.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Employee.kt new file mode 100644 index 0000000..b65e73b --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Employee.kt @@ -0,0 +1,191 @@ +package dev.fyloz.colorrecipesexplorer.model + +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonProperty +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank +import org.hibernate.annotations.Fetch +import org.hibernate.annotations.FetchMode +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder +import org.springframework.security.crypto.password.PasswordEncoder +import java.time.LocalDateTime +import javax.persistence.* +import javax.validation.constraints.NotBlank +import javax.validation.constraints.NotNull +import javax.validation.constraints.Size + +private const val EMPLOYEE_ID_NULL_MESSAGE = "Un numéro d'employé est requis" +private const val EMPLOYEE_LAST_NAME_EMPTY_MESSAGE = "Un nom est requis" +private const val EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE = "Un prénom est requis" +private const val EMPLOYEE_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis" +private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères" + +@Entity +@Table(name = "employee") +data class Employee( + @Id + override val id: Long, + + @Column(name = "first_name") + val firstName: String = "", + + @Column(name = "last_name") + val lastName: String = "", + + @JsonIgnore + val password: String = "", + + @JsonIgnore + @Column(name = "default_group_user") + val isDefaultGroupUser: Boolean = false, + + @JsonIgnore + @Column(name = "system_user") + val isSystemUser: Boolean = false, + + @ManyToOne + @JoinColumn(name = "group_id") + @Fetch(FetchMode.SELECT) + var group: EmployeeGroup? = null, + + @Enumerated(EnumType.STRING) + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "employee_permission", joinColumns = [JoinColumn(name = "employee_id")]) + @Column(name = "permission") + @Fetch(FetchMode.SUBSELECT) + @get:JsonProperty("explicitPermissions") + val permissions: MutableSet = mutableSetOf(), + + @Column(name = "last_login_time") + var lastLoginTime: LocalDateTime? = null +) : Model { + @get:JsonProperty("permissions") + val flatPermissions: Set + get() = permissions + .flatMap { it.flat() } + .filter { !it.deprecated } + .toMutableSet() + .apply { + if (group != null) this.addAll(group!!.flatPermissions) + } + + @get:JsonIgnore + val authorities: Set + get() = flatPermissions.map { it.toAuthority() }.toMutableSet() +} + +/** DTO for creating employees. Allows a [password] a [groupId]. */ +open class EmployeeSaveDto( + @field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE) + val id: Long, + + @field:NotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE) + val firstName: String, + + @field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE) + val lastName: String, + + @field:NotBlank(message = EMPLOYEE_PASSWORD_EMPTY_MESSAGE) + @field:Size(min = 8, message = EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE) + val password: String, + + val groupId: Long?, + + @Enumerated(EnumType.STRING) + val permissions: MutableSet = mutableSetOf() +) : EntityDto + +open class EmployeeUpdateDto( + @field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE) + val id: Long, + + @field:NullOrNotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE) + val firstName: String?, + + @field:NullOrNotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE) + val lastName: String?, + + val groupId: Long?, + + @Enumerated(EnumType.STRING) + val permissions: Set? +) : EntityDto + +data class EmployeeLoginRequest(val id: Long, val password: String) + +// ==== DSL ==== +fun employee( + passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(), + id: Long = 0L, + firstName: String = "firstName", + lastName: String = "lastName", + password: String = passwordEncoder.encode("password"), + isDefaultGroupUser: Boolean = false, + isSystemUser: Boolean = false, + group: EmployeeGroup? = null, + permissions: MutableSet = mutableSetOf(), + lastLoginTime: LocalDateTime? = null, + op: Employee.() -> Unit = {} +) = Employee( + id, + firstName, + lastName, + password, + isDefaultGroupUser, + isSystemUser, + group, + permissions, + lastLoginTime +).apply(op) + +fun employeeSaveDto( + passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(), + id: Long = 0L, + firstName: String = "firstName", + lastName: String = "lastName", + password: String = passwordEncoder.encode("password"), + groupId: Long? = null, + permissions: MutableSet = mutableSetOf(), + op: EmployeeSaveDto.() -> Unit = {} +) = EmployeeSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op) + +fun employeeUpdateDto( + id: Long = 0L, + firstName: String = "firstName", + lastName: String = "lastName", + groupId: Long? = null, + permissions: MutableSet = mutableSetOf(), + op: EmployeeUpdateDto.() -> Unit = {} +) = EmployeeUpdateDto(id, firstName, lastName, groupId, permissions).apply(op) + +// ==== Exceptions ==== +private const val EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE = "Employee not found" +private const val EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE = "Employee already exists" +private const val EMPLOYEE_EXCEPTION_ERROR_CODE = "employee" + +fun employeeIdNotFoundException(id: Long) = + NotFoundException( + EMPLOYEE_EXCEPTION_ERROR_CODE, + EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE, + "An employee with the id $id could not be found", + id + ) + +fun employeeIdAlreadyExistsException(id: Long) = + AlreadyExistsException( + EMPLOYEE_EXCEPTION_ERROR_CODE, + EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE, + "An employee with the id $id already exists", + id + ) + +fun employeeFullNameAlreadyExistsException(firstName: String, lastName: String) = + AlreadyExistsException( + EMPLOYEE_EXCEPTION_ERROR_CODE, + EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE, + "An employee with the name '$firstName $lastName' already exists", + "$firstName $lastName", + "fullName" + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeeGroup.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeeGroup.kt new file mode 100644 index 0000000..32e91bf --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeeGroup.kt @@ -0,0 +1,129 @@ +package dev.fyloz.colorrecipesexplorer.model + +import com.fasterxml.jackson.annotation.JsonProperty +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.exception.RestException +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.NotNull +import javax.validation.constraints.Size + +private const val GROUP_ID_NULL_MESSAGE = "Un identifiant est requis" +private const val GROUP_NAME_NULL_MESSAGE = "Un nom est requis" +private const val GROUP_PERMISSIONS_EMPTY_MESSAGE = "Au moins une permission est requise" + +@Entity +@Table(name = "employee_group") +data class EmployeeGroup( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + override var id: Long? = null, + + @Column(unique = true) + override val name: String = "", + + @Enumerated(EnumType.STRING) + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")]) + @Column(name = "permission") + @Fetch(FetchMode.SUBSELECT) + @get:JsonProperty("explicitPermissions") + val permissions: MutableSet = mutableSetOf(), +) : NamedModel { + @get:JsonProperty("permissions") + val flatPermissions: Set + get() = this.permissions + .flatMap { it.flat() } + .filter { !it.deprecated } + .toSet() +} + +open class EmployeeGroupSaveDto( + @field:NotBlank(message = GROUP_NAME_NULL_MESSAGE) + @field:Size(min = 3) + val name: String, + + @field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE) + val permissions: MutableSet +) : EntityDto { + override fun toEntity(): EmployeeGroup = + EmployeeGroup(null, name, permissions) +} + +open class EmployeeGroupUpdateDto( + @field:NotNull(message = GROUP_ID_NULL_MESSAGE) + val id: Long, + + @field:NotBlank(message = GROUP_NAME_NULL_MESSAGE) + @field:Size(min = 3) + val name: String, + + @field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE) + val permissions: MutableSet +) : EntityDto { + override fun toEntity(): EmployeeGroup = + EmployeeGroup(id, name, permissions) +} + +fun employeeGroup( + id: Long? = null, + name: String = "name", + permissions: MutableSet = mutableSetOf(), + op: EmployeeGroup.() -> Unit = {} +) = EmployeeGroup(id, name, permissions).apply(op) + +fun employeeGroupSaveDto( + name: String = "name", + permissions: MutableSet = mutableSetOf(), + op: EmployeeGroupSaveDto.() -> Unit = {} +) = EmployeeGroupSaveDto(name, permissions).apply(op) + +fun employeeGroupUpdateDto( + id: Long = 0L, + name: String = "name", + permissions: MutableSet = mutableSetOf(), + op: EmployeeGroupUpdateDto.() -> Unit = {} +) = EmployeeGroupUpdateDto(id, name, permissions).apply(op) + +// ==== Exceptions ==== +private const val EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE = "Employee group not found" +private const val EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE = "Employee group already exists" +private const val EMPLOYEE_EXCEPTION_ERROR_CODE = "employeegroup" + +class NoDefaultGroupException : RestException( + "nodefaultgroup", + "No default group", + HttpStatus.NOT_FOUND, + "No default group cookie is defined in the current request" +) + +fun employeeGroupIdNotFoundException(id: Long) = + NotFoundException( + EMPLOYEE_EXCEPTION_ERROR_CODE, + EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE, + "An employee group with the id $id could not be found", + id + ) + +fun employeeGroupNameNotFoundException(name: String) = + NotFoundException( + EMPLOYEE_EXCEPTION_ERROR_CODE, + EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE, + "An employee group with the name $name could not be found", + name, + "name" + ) + +fun employeeGroupNameAlreadyExistsException(name: String) = + AlreadyExistsException( + EMPLOYEE_EXCEPTION_ERROR_CODE, + EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE, + "An employee group with the name $name already exists", + name, + "name" + ) + diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt new file mode 100644 index 0000000..3bcfd42 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/EmployeePermission.kt @@ -0,0 +1,93 @@ +package dev.fyloz.colorrecipesexplorer.model + +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority + +enum class EmployeePermission( + val impliedPermissions: List = listOf(), + val deprecated: Boolean = false +) { + VIEW_RECIPES, + VIEW_CATALOG, + VIEW_USERS, + + PRINT_MIXES(listOf(VIEW_RECIPES)), + + EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)), + EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA)), + EDIT_MATERIALS(listOf(VIEW_CATALOG)), + EDIT_MATERIAL_TYPES(listOf(VIEW_CATALOG)), + EDIT_COMPANIES(listOf(VIEW_CATALOG)), + EDIT_USERS(listOf(VIEW_USERS)), + EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)), + + ADD_TO_INVENTORY(listOf(VIEW_CATALOG)), + DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)), + + REMOVE_RECIPES(listOf(EDIT_RECIPES)), + REMOVE_MATERIALS(listOf(EDIT_MATERIALS)), + REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES)), + REMOVE_COMPANIES(listOf(EDIT_COMPANIES)), + REMOVE_USERS(listOf(EDIT_USERS)), + REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES)), + + ADMIN( + listOf( + EDIT_CATALOG, + + REMOVE_RECIPES, + REMOVE_USERS, + REMOVE_CATALOG, + + PRINT_MIXES, + ADD_TO_INVENTORY, + DEDUCT_FROM_INVENTORY + ) + ), + + // deprecated permissions + VIEW_RECIPE(listOf(VIEW_RECIPES), true), + VIEW_MATERIAL(listOf(VIEW_CATALOG), true), + VIEW_MATERIAL_TYPE(listOf(VIEW_CATALOG), true), + VIEW_COMPANY(listOf(VIEW_CATALOG), true), + VIEW(listOf(VIEW_RECIPES, VIEW_CATALOG), true), + VIEW_EMPLOYEE(listOf(VIEW_USERS), true), + VIEW_EMPLOYEE_GROUP(listOf(VIEW_USERS), true), + + EDIT_RECIPE(listOf(EDIT_RECIPES), true), + EDIT_MATERIAL(listOf(EDIT_MATERIALS), true), + EDIT_MATERIAL_TYPE(listOf(EDIT_MATERIAL_TYPES), true), + EDIT_COMPANY(listOf(EDIT_COMPANIES), true), + EDIT(listOf(EDIT_RECIPES, EDIT_CATALOG), true), + EDIT_EMPLOYEE(listOf(EDIT_USERS), true), + EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_USERS), true), + EDIT_EMPLOYEE_GROUP(listOf(EDIT_USERS), true), + + REMOVE_RECIPE(listOf(REMOVE_RECIPES), true), + REMOVE_MATERIAL(listOf(REMOVE_MATERIALS), true), + REMOVE_MATERIAL_TYPE(listOf(REMOVE_MATERIAL_TYPES), true), + REMOVE_COMPANY(listOf(REMOVE_COMPANIES), true), + REMOVE(listOf(REMOVE_RECIPES, REMOVE_CATALOG), true), + REMOVE_EMPLOYEE(listOf(REMOVE_USERS), true), + REMOVE_EMPLOYEE_GROUP(listOf(REMOVE_USERS), true), + + SET_BROWSER_DEFAULT_GROUP(listOf(VIEW_USERS), true), + ; + + operator fun contains(permission: EmployeePermission): Boolean { + return permission == this || impliedPermissions.any { permission in it } + } +} + +fun EmployeePermission.flat(): Iterable { + return mutableSetOf(this).apply { + impliedPermissions.forEach { + addAll(it.flat()) + } + } +} + +/** Converts the given [EmployeePermission] to a [GrantedAuthority]. */ +fun EmployeePermission.toAuthority(): GrantedAuthority { + return SimpleGrantedAuthority(name) +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 7ebe745..c28e1cd 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -1,5 +1,8 @@ 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.model.validation.NullOrSize import org.springframework.web.multipart.MultipartFile @@ -119,3 +122,42 @@ fun materialQuantityDto( quantity: Float, op: MaterialQuantityDto.() -> Unit = {} ) = MaterialQuantityDto(materialId, quantity).apply(op) + +// ==== Exceptions ==== +private const val MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Material not found" +private const val MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Material already exists" +private const val MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material" +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 + ) + +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" + ) + +fun materialNameAlreadyExistsException(name: String) = + 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" + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt index f9aa6d3..30beff6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt @@ -1,5 +1,8 @@ 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.model.validation.NullOrSize import org.hibernate.annotations.ColumnDefault @@ -101,3 +104,51 @@ fun materialTypeUpdateDto( prefix: String? = null, op: MaterialTypeUpdateDto.() -> Unit = {} ) = MaterialTypeUpdateDto(id, name, prefix).apply(op) + +// ==== Exceptions ==== +private const val MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE = "Material type not found" +private const val MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Material type already exists" +private const val MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material type" +private const val MATERIAL_TYPE_EXCEPTION_ERROR_CODE = "materialtype" + +fun materialTypeIdNotFoundException(id: Long) = + NotFoundException( + MATERIAL_TYPE_EXCEPTION_ERROR_CODE, + MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE, + "A material type with the id $id could not be found", + id + ) + +fun materialTypeNameNotFoundException(name: String) = + NotFoundException( + MATERIAL_TYPE_EXCEPTION_ERROR_CODE, + MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE, + "A material type with the name $name could not be found", + name, + "name" + ) + +fun materialTypeNameAlreadyExistsException(name: String) = + AlreadyExistsException( + MATERIAL_TYPE_EXCEPTION_ERROR_CODE, + MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE, + "A material type with the name $name already exists", + name, + "name" + ) + +fun materialTypePrefixAlreadyExistsException(prefix: String) = + AlreadyExistsException( + MATERIAL_TYPE_EXCEPTION_ERROR_CODE, + MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE, + "A material type with the prefix $prefix already exists", + prefix, + "prefix" + ) + +fun cannotDeleteMaterialTypeException(materialType: MaterialType) = + CannotDeleteException( + MATERIAL_TYPE_EXCEPTION_ERROR_CODE, + MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE, + "Cannot delete material type ${materialType.name} because one or more materials depends on it" + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index f66daa1..6178a54 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -1,6 +1,8 @@ package dev.fyloz.colorrecipesexplorer.model import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank import javax.persistence.* import javax.validation.constraints.Min @@ -121,3 +123,23 @@ fun mixLocationDto( location: String? = "location", op: MixLocationDto.() -> Unit = {} ) = MixLocationDto(mixId, location).apply(op) + +// ==== Exceptions ==== +private const val MIX_NOT_FOUND_EXCEPTION_TITLE = "Mix not found" +private const val MIX_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete mix" +private const val MIX_EXCEPTION_ERROR_CODE = "mix" + +fun mixIdNotFoundException(id: Long) = + NotFoundException( + MIX_EXCEPTION_ERROR_CODE, + MIX_NOT_FOUND_EXCEPTION_TITLE, + "A mix with the id $id could not be found", + id + ) + +fun cannotDeleteMixException(mix: Mix) = + CannotDeleteException( + MIX_EXCEPTION_ERROR_CODE, + MIX_CANNOT_DELETE_EXCEPTION_TITLE, + "Cannot delete the mix ${mix.mixType.name} because one or more mixes depends on it" + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index a0eb56f..90d9614 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.model +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import javax.persistence.* import javax.validation.constraints.Min import javax.validation.constraints.NotNull @@ -50,3 +51,15 @@ fun mixMaterialDto( position: Int = 0, op: MixMaterialDto.() -> Unit = {} ) = MixMaterialDto(materialId, quantity, position).apply(op) + +// ==== Exceptions ==== +private const val MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Mix material not found" +private const val MIX_MATERIAL_EXCEPTION_ERROR_CODE = "mixmaterial" + +fun mixMaterialIdNotFoundException(id: Long) = + NotFoundException( + MIX_MATERIAL_EXCEPTION_ERROR_CODE, + MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE, + "A mix material with the id $id could not be found", + id + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt index 13a677e..8fa0bfc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt @@ -1,9 +1,12 @@ 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.exception.RestException +import org.springframework.http.HttpStatus import javax.persistence.* -const val IDENTIFIER_MATERIAL_NAME = "material" - @Entity @Table(name = "mix_type") data class MixType( @@ -36,3 +39,54 @@ fun mixType( name, material = material(name = name, inventoryQuantity = 0f, isMixType = true, materialType = materialType) ).apply(op) + +// ==== Exceptions ==== +private const val MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE = "Mix type not found" +private const val MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix type already exists" +private const val MIX_TYPE_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete mix type" +private const val MIX_TYPE_EXCEPTION_ERROR_CODE = "mixtype" + +class MixTypeNameAndMaterialTypeNotFoundException(name: String, materialType: MaterialType) : + RestException( + "notfound-mixtype-namematerialtype", + MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE, + HttpStatus.NOT_FOUND, + "A mix type with the name $name and material type ${materialType.name} could not be found", + mapOf( + "name" to name, + "materialType" to materialType.name + ) + ) + +fun mixTypeIdNotFoundException(id: Long) = + NotFoundException( + MIX_TYPE_EXCEPTION_ERROR_CODE, + MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE, + "A mix type with the id $id could not be found", + id + ) + +fun mixTypeNameNotFoundException(name: String) = + NotFoundException( + MIX_TYPE_EXCEPTION_ERROR_CODE, + MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE, + "A mix type with the name $name could not be found", + name, + "name" + ) + +fun mixTypeNameAlreadyExistsException(name: String) = + AlreadyExistsException( + MIX_TYPE_EXCEPTION_ERROR_CODE, + MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE, + "A mix type with the name $name already exists", + name, + "name" + ) + +fun cannotDeleteMixTypeException(mixType: MixType) = + CannotDeleteException( + MIX_TYPE_EXCEPTION_ERROR_CODE, + MIX_TYPE_CANNOT_DELETE_EXCEPTION_TITLE, + "Cannot delete the mix type ${mixType.name} because one or more mixes depends on it" + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index bac32a1..b047f8f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -1,8 +1,11 @@ package dev.fyloz.colorrecipesexplorer.model import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException +import dev.fyloz.colorrecipesexplorer.exception.RestException import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize +import org.springframework.http.HttpStatus import java.time.LocalDate import javax.persistence.* import javax.validation.constraints.* @@ -246,3 +249,27 @@ fun noteDto( content: String? = "note", op: NoteDto.() -> Unit = {} ) = NoteDto(groupId, content).apply(op) + +// ==== Exceptions ==== +private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found" +private const val RECIPE_EXCEPTION_ERROR_CODE = "recipe" + +class RecipeImageNotFoundException(id: Long, recipe: Recipe) : + RestException( + "notfound-recipeimage-id", + "Recipe image not found", + HttpStatus.NOT_FOUND, + "A recipe image with the id $id could no be found for the recipe ${recipe.name}", + mapOf( + "id" to id, + "recipe" to recipe.name + ) + ) + +fun recipeIdNotFoundException(id: Long) = + NotFoundException( + RECIPE_EXCEPTION_ERROR_CODE, + RECIPE_NOT_FOUND_EXCEPTION_TITLE, + "A recipe with the id $id could not be found", + id + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt index 42f6889..830c348 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt @@ -1,5 +1,6 @@ package dev.fyloz.colorrecipesexplorer.model +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import javax.persistence.* @Entity @@ -21,3 +22,15 @@ fun recipeStep( message: String = "message", op: RecipeStep.() -> Unit = {} ) = RecipeStep(id, position, message).apply(op) + +// ==== Exceptions ==== +private const val RECIPE_STEP_NOT_FOUND_EXCEPTION_TITLE = "Recipe step not found" +private const val RECIPE_STEP_EXCEPTION_ERROR_CODE = "recipestep" + +fun recipeStepIdNotFoundException(id: Long) = + NotFoundException( + RECIPE_STEP_EXCEPTION_ERROR_CODE, + RECIPE_STEP_NOT_FOUND_EXCEPTION_TITLE, + "A recipe step with the id $id could not be found", + id + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt index 48029c4..7462285 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountService.kt @@ -2,13 +2,11 @@ package dev.fyloz.colorrecipesexplorer.service import dev.fyloz.colorrecipesexplorer.config.blacklistedJwtTokens import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName -import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.validation.or import dev.fyloz.colorrecipesexplorer.repository.EmployeeGroupRepository import dev.fyloz.colorrecipesexplorer.repository.EmployeeRepository -import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Lazy import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.UserDetails @@ -77,11 +75,11 @@ interface EmployeeUserDetailsService : UserDetailsService { @Service class EmployeeServiceImpl( employeeRepository: EmployeeRepository, - @Lazy val passwordEncoder: PasswordEncoder + @Lazy val groupService: EmployeeGroupService, + @Lazy val passwordEncoder: PasswordEncoder, ) : AbstractExternalModelService(employeeRepository), EmployeeService { - @Autowired - lateinit var groupService: EmployeeGroupServiceImpl + override fun idNotFoundException(id: Long) = employeeIdNotFoundException(id) override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean = repository.existsByFirstNameAndLastName(firstName, lastName) @@ -94,9 +92,8 @@ class EmployeeServiceImpl( override fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee = super.getById(id).apply { - if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser) throw EntityNotFoundException( - id - ) + if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser) + throw idNotFoundException(id) } override fun getByGroup(group: EmployeeGroup): Collection = @@ -122,8 +119,10 @@ class EmployeeServiceImpl( }) override fun save(entity: Employee): Employee { + if (existsById(entity.id)) + throw employeeIdAlreadyExistsException(entity.id) if (existsByFirstNameAndLastName(entity.firstName, entity.lastName)) - throw EntityAlreadyExistsException("${entity.firstName} ${entity.lastName}") + throw employeeFullNameAlreadyExistsException(entity.firstName, entity.lastName) return super.save(entity) } @@ -173,7 +172,7 @@ class EmployeeServiceImpl( override fun update(entity: Employee, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee { with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) { if (this != null && id != entity.id) - throw EntityAlreadyExistsException("${entity.firstName} ${entity.lastName}") + throw employeeFullNameAlreadyExistsException(entity.firstName, entity.lastName) } return super.update(entity) @@ -219,11 +218,14 @@ const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans class EmployeeGroupServiceImpl( val employeeService: EmployeeService, employeeGroupRepository: EmployeeGroupRepository -) : - AbstractExternalNamedModelService( - employeeGroupRepository - ), +) : AbstractExternalNamedModelService( + employeeGroupRepository +), EmployeeGroupService { + override fun idNotFoundException(id: Long) = employeeGroupIdNotFoundException(id) + override fun nameNotFoundException(name: String) = employeeGroupNameNotFoundException(name) + override fun nameAlreadyExistsException(name: String) = employeeGroupNameAlreadyExistsException(name) + override fun existsByName(name: String): Boolean = repository.existsByName(name) override fun getEmployeesForGroup(id: Long): Collection = employeeService.getByGroup(getById(id)) @@ -254,7 +256,7 @@ class EmployeeGroupServiceImpl( override fun getRequestDefaultGroup(request: HttpServletRequest): EmployeeGroup { val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName) - ?: throw EntityNotFoundException("defaultGroup") + ?: throw NoDefaultGroupException() val defaultGroupUser = employeeService.getById( defaultGroupCookie.value.toLong(), ignoreDefaultGroupUsers = false, @@ -281,9 +283,9 @@ class EmployeeUserDetailsServiceImpl( override fun loadUserByUsername(username: String): UserDetails { try { return loadUserByEmployeeId(username.toLong(), true) - } catch (ex: EntityNotFoundException) { + } catch (ex: NotFoundException) { throw UsernameNotFoundException(username) - } catch (ex: EntityNotFoundException) { + } catch (ex: NotFoundException) { throw UsernameNotFoundException(username) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt index 1f48547..65f037f 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/CompanyService.kt @@ -1,10 +1,6 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteEntityException -import dev.fyloz.colorrecipesexplorer.model.Company -import dev.fyloz.colorrecipesexplorer.model.CompanySaveDto -import dev.fyloz.colorrecipesexplorer.model.CompanyUpdateDto -import dev.fyloz.colorrecipesexplorer.model.company +import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository import org.springframework.context.annotation.Lazy import org.springframework.stereotype.Service @@ -21,6 +17,10 @@ class CompanyServiceImpl( ) : AbstractExternalNamedModelService(companyRepository), CompanyService { + override fun idNotFoundException(id: Long) = companyIdNotFoundException(id) + override fun nameNotFoundException(name: String) = companyNameNotFoundException(name) + override fun nameAlreadyExistsException(name: String) = companyNameAlreadyExistsException(name) + override fun isLinkedToRecipes(company: Company): Boolean = recipeService.existsByCompany(company) override fun update(entity: CompanyUpdateDto): Company { @@ -35,8 +35,8 @@ class CompanyServiceImpl( }) } - override fun deleteById(id: Long) { - if (!repository.canBeDeleted(id)) throw CannotDeleteEntityException(id) - super.deleteById(id) + override fun delete(entity: Company) { + if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteCompany(entity) + super.delete(entity) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt index 58ea131..0dc05b3 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryService.kt @@ -1,12 +1,9 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.exception.LowQuantitiesException -import dev.fyloz.colorrecipesexplorer.exception.LowQuantityException -import dev.fyloz.colorrecipesexplorer.model.MaterialQuantityDto -import dev.fyloz.colorrecipesexplorer.model.MixDeductDto -import dev.fyloz.colorrecipesexplorer.model.MixMaterial -import dev.fyloz.colorrecipesexplorer.model.materialQuantityDto +import dev.fyloz.colorrecipesexplorer.exception.RestException +import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.service.utils.mapMayThrow +import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import javax.transaction.Transactional @@ -66,16 +63,16 @@ class InventoryServiceImpl( @Transactional override fun deduct(materialQuantities: Collection): Collection { - val thrown = mutableListOf() + val thrown = mutableListOf() val updatedQuantities = - materialQuantities.mapMayThrow( - { thrown.add(it.materialQuantity) } + materialQuantities.mapMayThrow( + { thrown.add(it) } ) { materialQuantityDto(materialId = it.material, quantity = deduct(it)) } if (thrown.isNotEmpty()) { - throw LowQuantitiesException(thrown) + throw MultiplesNotEnoughInventoryException(thrown) } return updatedQuantities } @@ -85,7 +82,31 @@ class InventoryServiceImpl( if (this.inventoryQuantity >= materialQuantity.quantity) { materialService.updateQuantity(this, -materialQuantity.quantity) } else { - throw LowQuantityException(materialQuantity) + throw NotEnoughInventoryException(materialQuantity.quantity, this) } } } + +class NotEnoughInventoryException(quantity: Float, material: Material) : + RestException( + "notenoughinventory", + "Not enough inventory", + HttpStatus.BAD_REQUEST, + "Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory", + mapOf( + "material" to material.name, + "requestQuantity" to quantity, + "availableQuantity" to material.inventoryQuantity + ) + ) + +class MultiplesNotEnoughInventoryException(exceptions: List) : + RestException( + "notenoughinventory-multiple", + "Not enough inventory", + HttpStatus.BAD_REQUEST, + "Cannot deduct requested quantities because there is no enough of them in inventory", + mapOf( + "lowQuantities" to exceptions.map { it.extensions } + ) + ) diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt index c0f5b0c..384e04c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialService.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteEntityException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository import dev.fyloz.colorrecipesexplorer.service.files.SimdutService @@ -47,6 +46,10 @@ class MaterialServiceImpl( materialRepository ), MaterialService { + override fun idNotFoundException(id: Long) = materialIdNotFoundException(id) + override fun nameNotFoundException(name: String) = materialNameNotFoundException(name) + override fun nameAlreadyExistsException(name: String) = materialNameAlreadyExistsException(name) + override fun existsByMaterialType(materialType: MaterialType): Boolean = repository.existsByMaterialType(materialType) @@ -113,8 +116,8 @@ class MaterialServiceImpl( Assert.notNull(material.name, "The persisted material with the id ${material.id} has a null name") } - override fun deleteById(id: Long) { - if (!repository.canBeDeleted(id)) throw CannotDeleteEntityException(id) - super.deleteById(id) + override fun delete(entity: Material) { + if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialException(entity) + super.delete(entity) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt index 6c807df..192c9e1 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeService.kt @@ -1,19 +1,10 @@ package dev.fyloz.colorrecipesexplorer.service import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteEntityException -import dev.fyloz.colorrecipesexplorer.exception.RestException -import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException -import dev.fyloz.colorrecipesexplorer.model.MaterialType -import dev.fyloz.colorrecipesexplorer.model.MaterialTypeSaveDto -import dev.fyloz.colorrecipesexplorer.model.MaterialTypeUpdateDto -import dev.fyloz.colorrecipesexplorer.model.materialType +import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.validation.isNotNullAndNotBlank import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository -import org.springframework.http.HttpStatus import org.springframework.stereotype.Service -import org.springframework.web.bind.annotation.ResponseStatus -import kotlin.contracts.ExperimentalContracts interface MaterialTypeService : ExternalNamedModelService { @@ -38,6 +29,10 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma AbstractExternalNamedModelService( repository ), MaterialTypeService { + override fun idNotFoundException(id: Long) = materialTypeIdNotFoundException(id) + override fun nameNotFoundException(name: String) = materialTypeNameNotFoundException(name) + override fun nameAlreadyExistsException(name: String) = materialTypeNameAlreadyExistsException(name) + override fun existsByPrefix(prefix: String): Boolean = repository.existsByPrefix(prefix) override fun isUsedByMaterial(materialType: MaterialType): Boolean = materialService.existsByMaterialType(materialType) @@ -47,11 +42,10 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma override fun save(entity: MaterialType): MaterialType { if (existsByPrefix(entity.prefix)) - throw EntityAlreadyExistsException(entity.prefix) + throw materialTypePrefixAlreadyExistsException(entity.prefix) return super.save(entity) } - @ExperimentalContracts override fun update(entity: MaterialTypeUpdateDto): MaterialType { val persistedMaterialType by lazy { getById(entity.id) } @@ -68,15 +62,15 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma override fun update(entity: MaterialType): MaterialType { with(repository.findByPrefix(entity.prefix)) { if (this != null && id != entity.id) - throw EntityAlreadyExistsException(entity.prefix) + throw materialTypePrefixAlreadyExistsException(entity.prefix) } return super.update(entity) } - override fun deleteById(id: Long) { - if (!repository.canBeDeleted(id)) throw CannotDeleteEntityException(id) - super.deleteById(id) + override fun delete(entity: MaterialType) { + if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialTypeException(entity) + super.delete(entity) } override fun saveSystemTypes(systemTypeProperties: Collection) { @@ -102,9 +96,3 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma oldSystemTypes.forEach { update(materialType(it, newSystemType = false)) } } } - -@ResponseStatus(HttpStatus.CONFLICT) -class CannotDeleteUsedMaterialTypeRestException : - RestException("Cannot delete a used material type", HttpStatus.CONFLICT) { - override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {} -} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt index 33ad48e..2431802 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixMaterialService.kt @@ -1,9 +1,6 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.model.Material -import dev.fyloz.colorrecipesexplorer.model.MixMaterial -import dev.fyloz.colorrecipesexplorer.model.MixMaterialDto -import dev.fyloz.colorrecipesexplorer.model.mixMaterial +import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository import org.springframework.context.annotation.Lazy import org.springframework.stereotype.Service @@ -27,6 +24,8 @@ class MixMaterialServiceImpl( mixMaterialRepository: MixMaterialRepository, @Lazy val materialService: MaterialService ) : AbstractModelService(mixMaterialRepository), MixMaterialService { + override fun idNotFoundException(id: Long) = mixMaterialIdNotFoundException(id) + override fun existsByMaterial(material: Material): Boolean = repository.existsByMaterial(material) override fun create(mixMaterials: Set): Set = mixMaterials.map(::create).toSet() diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt index 011a255..b1cafd6 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteEntityException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MixRepository import org.springframework.context.annotation.Lazy @@ -30,6 +29,8 @@ class MixServiceImpl( val mixTypeService: MixTypeService ) : AbstractModelService(mixRepository), MixService { + override fun idNotFoundException(id: Long) = mixIdNotFoundException(id) + override fun getAllByMixType(mixType: MixType): Collection = repository.findAllByMixType(mixType) override fun mixTypeIsShared(mixType: MixType): Boolean = getAllByMixType(mixType).count() > 1 @@ -81,12 +82,8 @@ class MixServiceImpl( @Transactional override fun delete(entity: Mix) { + if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixException(entity) recipeService.removeMix(entity) super.delete(entity) } - - override fun deleteById(id: Long) { - if (!repository.canBeDeleted(id)) throw CannotDeleteEntityException(id) - super.deleteById(id) - } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt index 06f858a..5b6e682 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeService.kt @@ -1,8 +1,5 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteEntityException -import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository import org.springframework.context.annotation.Lazy @@ -35,15 +32,19 @@ class MixTypeServiceImpl( @Lazy val mixService: MixService ) : AbstractNamedModelService(mixTypeRepository), MixTypeService { + override fun idNotFoundException(id: Long) = mixTypeIdNotFoundException(id) + override fun nameNotFoundException(name: String) = mixTypeNameNotFoundException(name) + override fun nameAlreadyExistsException(name: String) = mixTypeNameAlreadyExistsException(name) + override fun existsByNameAndMaterialType(name: String, materialType: MaterialType): Boolean = repository.existsByNameAndMaterialType(name, materialType) override fun getByMaterial(material: Material): MixType = - repository.findByMaterial(material) ?: throw EntityNotFoundException(material.name) + repository.findByMaterial(material) ?: throw nameNotFoundException(material.name) override fun getByNameAndMaterialType(name: String, materialType: MaterialType): MixType = repository.findByNameAndMaterialType(name, materialType) - ?: throw EntityNotFoundException("$name/${materialType.name}") + ?: throw MixTypeNameAndMaterialTypeNotFoundException(name, materialType) override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialType): MixType = if (existsByNameAndMaterialType(name, materialType)) @@ -53,7 +54,7 @@ class MixTypeServiceImpl( override fun save(entity: MixType): MixType { if (materialService.existsByName(entity.name)) - throw EntityAlreadyExistsException(entity.name) + throw materialNameAlreadyExistsException(entity.name) return super.save(entity) } @@ -77,8 +78,8 @@ class MixTypeServiceImpl( material.materialType = materialType }) - override fun deleteById(id: Long) { - if (!repository.canBeDeleted(id)) throw CannotDeleteEntityException(id) - super.deleteById(id) + override fun delete(entity: MixType) { + if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixTypeException(entity) + super.delete(entity) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt index 1173456..b8db07c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.model.validation.or import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository @@ -38,6 +37,8 @@ class RecipeServiceImpl( ) : AbstractExternalModelService(recipeRepository), RecipeService { + override fun idNotFoundException(id: Long) = recipeIdNotFoundException(id) + override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company) override fun getAllByCompany(company: Company): Collection = repository.findAllByCompany(company) @@ -154,7 +155,7 @@ class RecipeImageServiceImpl(val recipeService: RecipeService, val fileService: try { fileService.readAsBytes(getPath(id, recipeId)) } catch (ex: NoSuchFileException) { - throw EntityNotFoundException("$recipeId/$id") + throw RecipeImageNotFoundException(id, recipeService.getById(recipeId)) } override fun getAllIdsForRecipe(recipeId: Long): Collection { diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt index 1128929..42d941a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt @@ -1,6 +1,7 @@ package dev.fyloz.colorrecipesexplorer.service import dev.fyloz.colorrecipesexplorer.model.RecipeStep +import dev.fyloz.colorrecipesexplorer.model.recipeStepIdNotFoundException import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository import org.springframework.stereotype.Service @@ -9,4 +10,6 @@ interface RecipeStepService : ModelService @Service class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) : AbstractModelService(recipeStepRepository), - RecipeStepService + RecipeStepService { + override fun idNotFoundException(id: Long) = recipeStepIdNotFoundException(id) +} diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt index d44f2ff..0414c96 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/Service.kt @@ -1,7 +1,7 @@ package dev.fyloz.colorrecipesexplorer.service -import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException +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 @@ -66,19 +66,21 @@ abstract class AbstractService>(override val reposito abstract class AbstractModelService>(repository: R) : AbstractService(repository), ModelService { + protected abstract fun idNotFoundException(id: Long): NotFoundException + override fun existsById(id: Long): Boolean = repository.existsById(id) - override fun getById(id: Long): E = repository.findByIdOrNull(id) ?: throw EntityNotFoundException(id) + override fun getById(id: Long): E = repository.findByIdOrNull(id) ?: throw idNotFoundException(id) override fun save(entity: E): E { if (entity.id != null && existsById(entity.id!!)) - throw EntityAlreadyExistsException(entity.id!!) + throw idNotFoundException(entity.id!!) return super.save(entity) } override fun update(entity: E): E { assertId(entity.id) if (!existsById(entity.id!!)) - throw EntityNotFoundException(entity.id!!) + throw idNotFoundException(entity.id!!) return super.update(entity) } @@ -92,13 +94,16 @@ abstract class AbstractModelService>(repos abstract class AbstractNamedModelService>(repository: R) : AbstractModelService(repository), NamedModelService { + protected abstract fun nameNotFoundException(name: String): NotFoundException + protected abstract fun nameAlreadyExistsException(name: String): AlreadyExistsException + override fun existsByName(name: String): Boolean = repository.existsByName(name) - override fun getByName(name: String): E = repository.findByName(name) ?: throw EntityNotFoundException(name) + override fun getByName(name: String): E = repository.findByName(name) ?: throw nameNotFoundException(name) override fun deleteByName(name: String) = repository.deleteByName(name) override fun save(entity: E): E { if (existsByName(entity.name)) - throw EntityAlreadyExistsException(entity.name) + throw nameAlreadyExistsException(entity.name) return super.save(entity) } @@ -107,7 +112,7 @@ abstract class AbstractNamedModelService, R : J open fun `getById() throws EntityNotFoundException when no entity with the given id exists in the repository`() { whenever(repository.findById(entity.id!!)).doReturn(Optional.empty()) - val exception = assertThrows { service.getById(entity.id!!) } + val exception = assertThrows { service.getById(entity.id!!) } assertTrue(exception.value is Long) assertEquals(entity.id, exception.value as Long) } @@ -139,8 +139,8 @@ abstract class AbstractModelServiceTest, R : J doReturn(true).whenever(repository).existsById(entity.id!!) val exception = assertThrows { service.save(entity) } - assertTrue(exception.value is Long) - assertEquals(entity.id, exception.value as Long) + assertTrue(exception.id is Long) + assertEquals(entity.id, exception.id as Long) } // update() @@ -161,7 +161,7 @@ abstract class AbstractModelServiceTest, R : J open fun `update() throws EntityNotFoundException when no entity with the given id exists in the repository`() { doReturn(false).whenever(service).existsById(entity.id!!) - val exception = assertThrows { service.update(entity) } + val exception = assertThrows { service.update(entity) } assertTrue(exception.value is Long) assertEquals(entity.id, exception.value as Long) } @@ -217,7 +217,7 @@ abstract class AbstractNamedModelServiceTest { service.getByName(entity.name) } + val exception = assertThrows { service.getByName(entity.name) } assertEquals(entity.name, exception.value) } @@ -228,7 +228,7 @@ abstract class AbstractNamedModelServiceTest { service.save(entity) } - assertEquals(entity.name, exception.value) + assertEquals(entity.name, exception.id) } // update() @@ -251,7 +251,7 @@ abstract class AbstractNamedModelServiceTest { service.update(entity) } + val exception = assertThrows { service.update(entity) } assertTrue(exception.value is Long) assertEquals(entity.id, exception.value as Long) @@ -263,7 +263,7 @@ abstract class AbstractNamedModelServiceTest { service.update(entity) } - assertEquals(entity.name, exception.value) + assertEquals(entity.name, exception.id) } // deleteByName() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt index 3c7e704..ac536f8 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/AccountsServiceTest.kt @@ -3,7 +3,7 @@ package dev.fyloz.colorrecipesexplorer.service import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.EmployeeGroupRepository import dev.fyloz.colorrecipesexplorer.repository.EmployeeRepository @@ -76,7 +76,7 @@ class EmployeeServiceTest : fun `getById() throws EntityNotFoundException when the corresponding employee is a default group user`() { whenever(repository.findById(entityDefaultGroupUser.id)).doReturn(Optional.of(entityDefaultGroupUser)) - val exception = assertThrows { + val exception = assertThrows { service.getById( entityDefaultGroupUser.id, ignoreDefaultGroupUsers = true, @@ -91,7 +91,7 @@ class EmployeeServiceTest : fun `getById() throws EntityNotFoundException when the corresponding employee is a system user`() { whenever(repository.findById(entitySystemUser.id)).doReturn(Optional.of(entitySystemUser)) - val exception = assertThrows { + val exception = assertThrows { service.getById( entitySystemUser.id, ignoreDefaultGroupUsers = false, @@ -151,7 +151,7 @@ class EmployeeServiceTest : doReturn(true).whenever(repository).existsByFirstNameAndLastName(entity.firstName, entity.lastName) val exception = assertThrows { service.save(entity) } - assertEquals("${entity.firstName} ${entity.lastName}", exception.value) + assertEquals("${entity.firstName} ${entity.lastName}", exception.id) } @Test @@ -191,8 +191,8 @@ class EmployeeServiceTest : ignoreSystemUsers = true ) } - assertTrue(exception.value is String) - assertEquals("${entity.firstName} ${entity.lastName}", exception.value as String) + assertTrue(exception.id is String) + assertEquals("${entity.firstName} ${entity.lastName}", exception.id as String) } } @@ -262,7 +262,7 @@ class EmployeeGroupServiceTest : whenever(request.cookies).doReturn(arrayOf()) - val exception = assertThrows { service.getRequestDefaultGroup(request) } + val exception = assertThrows { service.getRequestDefaultGroup(request) } assertEquals("defaultGroup", exception.value) } @@ -327,7 +327,7 @@ class EmployeeUserDetailsServiceTest { @Test fun `loadUserByUsername() throws UsernameNotFoundException when no employee with the given id exists`() { whenever(employeeService.getById(eq(employee.id), any(), any())).doThrow( - EntityNotFoundException( + NotFoundException( employee.id ) ) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt index 24558e1..694eb90 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/InventoryServiceTest.kt @@ -2,7 +2,6 @@ package dev.fyloz.colorrecipesexplorer.service import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.exception.LowQuantitiesException -import dev.fyloz.colorrecipesexplorer.exception.LowQuantityException import dev.fyloz.colorrecipesexplorer.model.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test @@ -155,7 +154,7 @@ class InventoryServiceTest { @Test fun `deduct(materialQuantity) throws LowQuantityException when there is not enough inventory of the given material`() { withGivenQuantities(0f, 1000f) { - val exception = assertThrows { service.deduct(this) } + val exception = assertThrows { service.deduct(this) } assertEquals(this, exception.materialQuantity) } } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt index 0a454b7..43a1b76 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialServiceTest.kt @@ -128,7 +128,7 @@ class MaterialServiceTest : doReturn(true).whenever(service).existsByName(entity.name) val exception = assertThrows { service.save(entity) } - assertEquals(entity.name, exception.value) + assertEquals(entity.name, exception.id) } @Test @@ -160,7 +160,7 @@ class MaterialServiceTest : doReturn(entity).whenever(service).getById(material.id!!) val exception = assertThrows { service.update(material) } - assertEquals(material.name, exception.value) + assertEquals(material.name, exception.id) } // updateQuantity() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt index e74d355..608e180 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MaterialTypeServiceTest.kt @@ -2,7 +2,7 @@ package dev.fyloz.colorrecipesexplorer.service import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository import org.junit.jupiter.api.AfterEach @@ -110,7 +110,7 @@ class MaterialTypeServiceTest : doReturn(true).whenever(service).existsByPrefix(entity.prefix) val exception = assertThrows { service.save(entity) } - assertEquals(entity.prefix, exception.value) + assertEquals(entity.prefix, exception.id) } // update() @@ -138,7 +138,7 @@ class MaterialTypeServiceTest : doReturn(false).whenever(service).existsById(entity.id!!) doReturn(null).whenever(service).getById(entity.id!!) - val exception = assertThrows { service.update(entity) } + val exception = assertThrows { service.update(entity) } assertTrue(exception.value is Long) assertEquals(entity.id, exception.value as Long) } @@ -150,7 +150,7 @@ class MaterialTypeServiceTest : doReturn(entity).whenever(service).getById(entity.id!!) val exception = assertThrows { service.update(entity) } - assertEquals(entity.name, exception.value) + assertEquals(entity.name, exception.id) } @Test @@ -160,7 +160,7 @@ class MaterialTypeServiceTest : doReturn(entity).whenever(service).getById(entity.id!!) val exception = assertThrows { service.update(entity) } - assertEquals(entity.prefix, exception.value) + assertEquals(entity.prefix, exception.id) } // delete() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt index b4a8201..0a0ac47 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/MixTypeServiceTest.kt @@ -2,7 +2,7 @@ package dev.fyloz.colorrecipesexplorer.service import com.nhaarman.mockitokotlin2.* import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository import org.junit.jupiter.api.AfterEach @@ -57,7 +57,7 @@ class MixTypeServiceTest : AbstractNamedModelServiceTest { service.getByMaterial(material) } + val exception = assertThrows { service.getByMaterial(material) } assertEquals(material.name, exception.value) } @@ -104,7 +104,7 @@ class MixTypeServiceTest : AbstractNamedModelServiceTest { service.save(entity) } - assertEquals(entity.name, exception.value) + assertEquals(entity.name, exception.id) } // saveForNameAndMaterialType() diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt index dc771ff..3851792 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeServiceTest.kt @@ -1,7 +1,7 @@ package dev.fyloz.colorrecipesexplorer.service import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException +import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.* import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository import dev.fyloz.colorrecipesexplorer.service.files.FileService @@ -198,7 +198,7 @@ class RecipeImageServiceTest { whenever(fileService.readAsBytes(imagePath)).doAnswer { throw NoSuchFileException(imagePath) } val exception = - assertThrows { service.getByIdForRecipe(imageId, recipeId) } + assertThrows { service.getByIdForRecipe(imageId, recipeId) } assertEquals("$recipeId/$imageId", exception.value) }