Merge branch 'features' into 'master'
Ajout de l'API des fichiers et des kits de retouche See merge request color-recipes-explorer/backend!28
This commit is contained in:
commit
6e2274d981
|
@ -40,9 +40,10 @@ dependencies {
|
|||
implementation("org.springframework.boot:spring-boot-devtools:2.3.4.RELEASE")
|
||||
|
||||
testImplementation("org.springframework:spring-test:5.1.6.RELEASE")
|
||||
testImplementation("org.mockito:mockito-core:3.6.0")
|
||||
testImplementation("org.mockito:mockito-inline:3.6.0")
|
||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||
testImplementation("org.junit.jupiter:junit-jupiter-api:5.3.2")
|
||||
testImplementation("io.mockk:mockk:1.10.6")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test:2.3.4.RELEASE")
|
||||
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:2.3.4.RELEASE")
|
||||
testImplementation("org.jetbrains.kotlin:kotlin-test:1.4.10")
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files;
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.utils.PdfBuilder;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Service
|
||||
public class TouchUpKitService {
|
||||
|
||||
private static final String TOUCH_UP_FR = "KIT DE RETOUCHE";
|
||||
private static final String TOUCH_UP_EN = "TOUCH UP KIT";
|
||||
public static final int FONT_SIZE = 42;
|
||||
|
||||
private final ResourceLoader resourceLoader;
|
||||
|
||||
@Autowired
|
||||
public TouchUpKitService(ResourceLoader resourceLoader) {
|
||||
this.resourceLoader = resourceLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un PDF de kit de retouche pour une job.
|
||||
*
|
||||
* @param jobNumber La job
|
||||
* @return Le PDF de kit de retouche pour la job
|
||||
*/
|
||||
public byte[] generatePdfForJobNumber(String jobNumber) {
|
||||
try {
|
||||
return new PdfBuilder(resourceLoader, true, FONT_SIZE)
|
||||
.addLine(TOUCH_UP_FR, true, 0)
|
||||
.addLine(TOUCH_UP_EN, true, 0)
|
||||
.addLine(jobNumber, false, 10)
|
||||
.build();
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(String.format("Impossible de générer un PDF de kit de retouche pour la job '%s': %s", jobNumber, ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.utils;
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDPage;
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream;
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont;
|
||||
import org.apache.pdfbox.pdmodel.font.PDType0Font;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class PdfBuilder {
|
||||
|
||||
private static final String PATH_FONT_ARIAL_BOLD = "classpath:fonts/arialbd.ttf";
|
||||
|
||||
private final PDFont font;
|
||||
private final PDDocument document = new PDDocument();
|
||||
private final PDPage page = new PDPage();
|
||||
private final Collection<PdfLine> lines = new ArrayList<>();
|
||||
private final boolean duplicated;
|
||||
private final int fontSize;
|
||||
private final int fontSizeBold;
|
||||
private final int lineSpacing;
|
||||
|
||||
public PdfBuilder(ResourceLoader resourceLoader, boolean duplicated, int fontSize) throws IOException {
|
||||
this.duplicated = duplicated;
|
||||
this.fontSize = fontSize;
|
||||
this.fontSizeBold = this.fontSize + 12;
|
||||
this.lineSpacing = (int) (this.fontSize * 1.5f);
|
||||
|
||||
document.addPage(page);
|
||||
font = PDType0Font.load(document, resourceLoader.getResource(PATH_FONT_ARIAL_BOLD).getInputStream());
|
||||
}
|
||||
|
||||
public PdfBuilder addLine(String text, boolean bold, int marginTop) {
|
||||
lines.add(new PdfLine(text, bold, marginTop));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public byte[] build() throws IOException {
|
||||
writeContent();
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
document.save(outputStream);
|
||||
document.close();
|
||||
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
private void writeContent() throws IOException {
|
||||
PDPageContentStream contentStream = new PDPageContentStream(document, page);
|
||||
contentStream.beginText();
|
||||
|
||||
int marginTop = 30;
|
||||
for (PdfLine line : lines) {
|
||||
writeCenteredText(contentStream, line, marginTop);
|
||||
marginTop += lineSpacing;
|
||||
}
|
||||
|
||||
if (duplicated) {
|
||||
marginTop = (int) page.getMediaBox().getHeight() / 2;
|
||||
for (PdfLine line : lines) {
|
||||
writeCenteredText(contentStream, line, marginTop);
|
||||
marginTop += lineSpacing;
|
||||
}
|
||||
}
|
||||
|
||||
contentStream.endText();
|
||||
contentStream.close();
|
||||
}
|
||||
|
||||
private void writeCenteredText(PDPageContentStream contentStream, PdfLine line, int marginTop) throws IOException {
|
||||
float textWidth = font.getStringWidth(line.getText()) / 1000 * (line.isBold() ? fontSizeBold : fontSize);
|
||||
float textHeight = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * (line.isBold() ? fontSizeBold : fontSize);
|
||||
float textX = (page.getMediaBox().getWidth() - textWidth) / 2f;
|
||||
float textY = (page.getMediaBox().getHeight() - (marginTop + line.getMarginTop()) - textHeight);
|
||||
|
||||
if (line.isBold()) contentStream.setFont(font, fontSizeBold);
|
||||
else contentStream.setFont(font, fontSize);
|
||||
|
||||
contentStream.newLineAtOffset(textX, textY);
|
||||
contentStream.showText(line.getText());
|
||||
contentStream.newLineAtOffset(-textX, -textY); // Réinitialise la position pour la prochaine ligne
|
||||
}
|
||||
|
||||
public static class PdfLine {
|
||||
|
||||
private String text;
|
||||
private boolean bold;
|
||||
private int marginTop;
|
||||
|
||||
public PdfLine() {
|
||||
}
|
||||
|
||||
public PdfLine(String text, boolean bold, int marginTop) {
|
||||
this.text = text;
|
||||
this.bold = bold;
|
||||
this.marginTop = marginTop;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public boolean isBold() {
|
||||
return bold;
|
||||
}
|
||||
|
||||
public void setBold(boolean bold) {
|
||||
this.bold = bold;
|
||||
}
|
||||
|
||||
public int getMarginTop() {
|
||||
return marginTop;
|
||||
}
|
||||
|
||||
public void setMarginTop(int marginTop) {
|
||||
this.marginTop = marginTop;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package dev.fyloz.colorrecipesexplorer.config
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
||||
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
||||
import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||
import org.springframework.context.ApplicationListener
|
||||
|
@ -10,10 +12,13 @@ import org.springframework.core.annotation.Order
|
|||
|
||||
@Configuration
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
class InitialDataLoader(
|
||||
private val materialTypeService: MaterialTypeService,
|
||||
private val materialTypeProperties: MaterialTypeProperties
|
||||
class ApplicationReadyListener(
|
||||
private val materialTypeService: MaterialTypeService,
|
||||
private val materialTypeProperties: MaterialTypeProperties,
|
||||
private val creProperties: CreProperties
|
||||
) : ApplicationListener<ApplicationReadyEvent> {
|
||||
override fun onApplicationEvent(event: ApplicationReadyEvent) =
|
||||
materialTypeService.saveSystemTypes(materialTypeProperties.systemTypes)
|
||||
override fun onApplicationEvent(event: ApplicationReadyEvent) {
|
||||
materialTypeService.saveSystemTypes(materialTypeProperties.systemTypes)
|
||||
CRE_PROPERTIES = creProperties
|
||||
}
|
||||
}
|
|
@ -5,4 +5,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties
|
|||
@ConfigurationProperties(prefix = "cre.server")
|
||||
class CreProperties {
|
||||
var workingDirectory: String = "data"
|
||||
var deploymentUrl: String = "http://localhost"
|
||||
var cacheGeneratedFiles: Boolean = false
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
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
|
||||
|
@ -25,139 +23,143 @@ private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit co
|
|||
@Entity
|
||||
@Table(name = "employee")
|
||||
data class Employee(
|
||||
@Id
|
||||
override val id: Long,
|
||||
@Id
|
||||
override val id: Long,
|
||||
|
||||
@Column(name = "first_name")
|
||||
val firstName: String = "",
|
||||
@Column(name = "first_name")
|
||||
val firstName: String = "",
|
||||
|
||||
@Column(name = "last_name")
|
||||
val lastName: String = "",
|
||||
@Column(name = "last_name")
|
||||
val lastName: String = "",
|
||||
|
||||
@JsonIgnore
|
||||
val password: String = "",
|
||||
val password: String = "",
|
||||
|
||||
@JsonIgnore
|
||||
@Column(name = "default_group_user")
|
||||
val isDefaultGroupUser: Boolean = false,
|
||||
@Column(name = "default_group_user")
|
||||
val isDefaultGroupUser: Boolean = false,
|
||||
|
||||
@JsonIgnore
|
||||
@Column(name = "system_user")
|
||||
val isSystemUser: Boolean = false,
|
||||
@Column(name = "system_user")
|
||||
val isSystemUser: Boolean = false,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "group_id")
|
||||
@Fetch(FetchMode.SELECT)
|
||||
var group: EmployeeGroup? = null,
|
||||
@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<EmployeePermission> = mutableSetOf(),
|
||||
@Enumerated(EnumType.STRING)
|
||||
@ElementCollection(fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "employee_permission", joinColumns = [JoinColumn(name = "employee_id")])
|
||||
@Column(name = "permission")
|
||||
@Fetch(FetchMode.SUBSELECT)
|
||||
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
|
||||
|
||||
@Column(name = "last_login_time")
|
||||
var lastLoginTime: LocalDateTime? = null
|
||||
@Column(name = "last_login_time")
|
||||
var lastLoginTime: LocalDateTime? = null
|
||||
) : Model {
|
||||
@get:JsonProperty("permissions")
|
||||
val flatPermissions: Set<EmployeePermission>
|
||||
get() = permissions
|
||||
.flatMap { it.flat() }
|
||||
.filter { !it.deprecated }
|
||||
.toMutableSet()
|
||||
.apply {
|
||||
if (group != null) this.addAll(group!!.flatPermissions)
|
||||
}
|
||||
.flatMap { it.flat() }
|
||||
.filter { !it.deprecated }
|
||||
.toMutableSet()
|
||||
.apply {
|
||||
if (group != null) this.addAll(group!!.flatPermissions)
|
||||
}
|
||||
|
||||
@get:JsonIgnore
|
||||
val authorities: Set<GrantedAuthority>
|
||||
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:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
|
||||
@field:NotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
|
||||
val firstName: String,
|
||||
@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_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,
|
||||
@field:NotBlank(message = EMPLOYEE_PASSWORD_EMPTY_MESSAGE)
|
||||
@field:Size(min = 8, message = EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE)
|
||||
val password: String,
|
||||
|
||||
val groupId: Long?,
|
||||
val groupId: Long?,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
val permissions: MutableSet<EmployeePermission> = mutableSetOf()
|
||||
@Enumerated(EnumType.STRING)
|
||||
val permissions: MutableSet<EmployeePermission> = mutableSetOf()
|
||||
) : EntityDto<Employee>
|
||||
|
||||
open class EmployeeUpdateDto(
|
||||
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
@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_FIRST_NAME_EMPTY_MESSAGE)
|
||||
val firstName: String?,
|
||||
|
||||
@field:NullOrNotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
|
||||
val lastName: String?,
|
||||
@field:NullOrNotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
|
||||
val lastName: String?,
|
||||
|
||||
val groupId: Long?,
|
||||
val groupId: Long?,
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
val permissions: Set<EmployeePermission>?
|
||||
@Enumerated(EnumType.STRING)
|
||||
val permissions: Set<EmployeePermission>?
|
||||
) : EntityDto<Employee>
|
||||
|
||||
data class EmployeeOutputDto(
|
||||
override val id: Long,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val group: EmployeeGroup?,
|
||||
val permissions: Set<EmployeePermission>,
|
||||
val explicitPermissions: Set<EmployeePermission>,
|
||||
val lastLoginTime: LocalDateTime?
|
||||
) : Model
|
||||
|
||||
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<EmployeePermission> = mutableSetOf(),
|
||||
lastLoginTime: LocalDateTime? = null,
|
||||
op: Employee.() -> Unit = {}
|
||||
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<EmployeePermission> = mutableSetOf(),
|
||||
lastLoginTime: LocalDateTime? = null,
|
||||
op: Employee.() -> Unit = {}
|
||||
) = Employee(
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
password,
|
||||
isDefaultGroupUser,
|
||||
isSystemUser,
|
||||
group,
|
||||
permissions,
|
||||
lastLoginTime
|
||||
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<EmployeePermission> = mutableSetOf(),
|
||||
op: EmployeeSaveDto.() -> Unit = {}
|
||||
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
|
||||
id: Long = 0L,
|
||||
firstName: String = "firstName",
|
||||
lastName: String = "lastName",
|
||||
password: String = passwordEncoder.encode("password"),
|
||||
groupId: Long? = null,
|
||||
permissions: MutableSet<EmployeePermission> = 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<EmployeePermission> = mutableSetOf(),
|
||||
op: EmployeeUpdateDto.() -> Unit = {}
|
||||
id: Long = 0L,
|
||||
firstName: String = "firstName",
|
||||
lastName: String = "lastName",
|
||||
groupId: Long? = null,
|
||||
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
|
||||
op: EmployeeUpdateDto.() -> Unit = {}
|
||||
) = EmployeeUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
|
@ -166,26 +168,26 @@ private const val EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE = "Employee already ex
|
|||
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
|
||||
)
|
||||
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
|
||||
)
|
||||
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"
|
||||
)
|
||||
AlreadyExistsException(
|
||||
EMPLOYEE_EXCEPTION_ERROR_CODE,
|
||||
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"An employee with the name '$firstName $lastName' already exists",
|
||||
"$firstName $lastName",
|
||||
"fullName"
|
||||
)
|
||||
|
|
|
@ -31,10 +31,8 @@ data class EmployeeGroup(
|
|||
@CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")])
|
||||
@Column(name = "permission")
|
||||
@Fetch(FetchMode.SUBSELECT)
|
||||
@get:JsonProperty("explicitPermissions")
|
||||
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
|
||||
) : NamedModel {
|
||||
@get:JsonProperty("permissions")
|
||||
val flatPermissions: Set<EmployeePermission>
|
||||
get() = this.permissions
|
||||
.flatMap { it.flat() }
|
||||
|
@ -69,6 +67,13 @@ open class EmployeeGroupUpdateDto(
|
|||
EmployeeGroup(id, name, permissions)
|
||||
}
|
||||
|
||||
data class EmployeeGroupOutputDto(
|
||||
override val id: Long,
|
||||
val name: String,
|
||||
val permissions: Set<EmployeePermission>,
|
||||
val explicitPermissions: Set<EmployeePermission>
|
||||
): Model
|
||||
|
||||
fun employeeGroup(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
|
|
|
@ -2,48 +2,53 @@ package dev.fyloz.colorrecipesexplorer.model
|
|||
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import java.util.*
|
||||
|
||||
enum class EmployeePermission(
|
||||
val impliedPermissions: List<EmployeePermission> = listOf(),
|
||||
val deprecated: Boolean = false
|
||||
val impliedPermissions: List<EmployeePermission> = listOf(),
|
||||
val deprecated: Boolean = false
|
||||
) {
|
||||
VIEW_RECIPES,
|
||||
VIEW_CATALOG,
|
||||
READ_FILE,
|
||||
WRITE_FILE(listOf(READ_FILE)),
|
||||
REMOVE_FILE(listOf(WRITE_FILE)),
|
||||
|
||||
VIEW_RECIPES(listOf(READ_FILE)),
|
||||
VIEW_CATALOG(listOf(READ_FILE)),
|
||||
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_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA, WRITE_FILE)),
|
||||
EDIT_MATERIALS(listOf(VIEW_CATALOG, WRITE_FILE)),
|
||||
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_RECIPES(listOf(EDIT_RECIPES, REMOVE_FILE)),
|
||||
REMOVE_MATERIALS(listOf(EDIT_MATERIALS, REMOVE_FILE)),
|
||||
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)),
|
||||
|
||||
ADD_TO_INVENTORY(listOf(VIEW_CATALOG)),
|
||||
DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)),
|
||||
GENERATE_TOUCH_UP_KIT,
|
||||
|
||||
ADMIN(
|
||||
listOf(
|
||||
EDIT_CATALOG,
|
||||
listOf(
|
||||
EDIT_CATALOG,
|
||||
|
||||
REMOVE_RECIPES,
|
||||
REMOVE_USERS,
|
||||
REMOVE_CATALOG,
|
||||
REMOVE_RECIPES,
|
||||
REMOVE_USERS,
|
||||
REMOVE_CATALOG,
|
||||
|
||||
PRINT_MIXES,
|
||||
ADD_TO_INVENTORY,
|
||||
DEDUCT_FROM_INVENTORY
|
||||
)
|
||||
PRINT_MIXES,
|
||||
ADD_TO_INVENTORY,
|
||||
DEDUCT_FROM_INVENTORY,
|
||||
GENERATE_TOUCH_UP_KIT
|
||||
)
|
||||
),
|
||||
|
||||
// deprecated permissions
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
package dev.fyloz.colorrecipesexplorer.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
|
||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
|
||||
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
|
||||
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
||||
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.Min
|
||||
import javax.validation.constraints.NotBlank
|
||||
|
@ -21,151 +26,170 @@ private const val MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE = "Un produit est requ
|
|||
private const val MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE = "Une quantité est requises"
|
||||
private const val MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE = "La quantité doit être supérieure ou égale à 0"
|
||||
|
||||
const val SIMDUT_FILES_PATH = "pdf/simdut"
|
||||
|
||||
@Entity
|
||||
@Table(name = "material")
|
||||
data class Material(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long?,
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long?,
|
||||
|
||||
@Column(unique = true)
|
||||
override var name: String,
|
||||
@Column(unique = true)
|
||||
override var name: String,
|
||||
|
||||
@Column(name = "inventory_quantity")
|
||||
var inventoryQuantity: Float,
|
||||
@Column(name = "inventory_quantity")
|
||||
var inventoryQuantity: Float,
|
||||
|
||||
@Column(name = "mix_type")
|
||||
val isMixType: Boolean,
|
||||
@Column(name = "mix_type")
|
||||
val isMixType: Boolean,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "material_type_id")
|
||||
var materialType: MaterialType?
|
||||
) : NamedModel
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "material_type_id")
|
||||
var materialType: MaterialType?
|
||||
) : NamedModel {
|
||||
val simdutFilePath
|
||||
@JsonIgnore
|
||||
@Transient
|
||||
get() = "$SIMDUT_FILES_PATH/$name.pdf"
|
||||
|
||||
|
||||
}
|
||||
|
||||
open class MaterialSaveDto(
|
||||
@field:NotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
|
||||
val name: String,
|
||||
@field:NotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
|
||||
val name: String,
|
||||
|
||||
@field:NotNull(message = MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE)
|
||||
@field:Min(value = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
|
||||
val inventoryQuantity: Float,
|
||||
@field:NotNull(message = MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE)
|
||||
@field:Min(value = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
|
||||
val inventoryQuantity: Float,
|
||||
|
||||
@field:NotNull(message = MATERIAL_TYPE_NULL_MESSAGE)
|
||||
val materialTypeId: Long,
|
||||
@field:NotNull(message = MATERIAL_TYPE_NULL_MESSAGE)
|
||||
val materialTypeId: Long,
|
||||
|
||||
val simdutFile: MultipartFile? = null
|
||||
val simdutFile: MultipartFile? = null
|
||||
) : EntityDto<Material>
|
||||
|
||||
open class MaterialUpdateDto(
|
||||
@field:NotNull(message = MATERIAL_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
@field:NotNull(message = MATERIAL_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
|
||||
@field:NullOrNotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
|
||||
val name: String?,
|
||||
@field:NullOrNotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
|
||||
val name: String?,
|
||||
|
||||
@field:NullOrSize(min = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
|
||||
val inventoryQuantity: Float?,
|
||||
@field:NullOrSize(min = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
|
||||
val inventoryQuantity: Float?,
|
||||
|
||||
val materialTypeId: Long?,
|
||||
val materialTypeId: Long?,
|
||||
|
||||
val simdutFile: MultipartFile? = null
|
||||
val simdutFile: MultipartFile? = null
|
||||
) : EntityDto<Material>
|
||||
|
||||
data class MaterialQuantityDto(
|
||||
@field:NotNull(message = MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE)
|
||||
val material: Long,
|
||||
data class MaterialOutputDto(
|
||||
override val id: Long,
|
||||
val name: String,
|
||||
val inventoryQuantity: Float,
|
||||
val isMixType: Boolean,
|
||||
val materialType: MaterialType,
|
||||
val simdutUrl: String?
|
||||
) : Model
|
||||
|
||||
@field:NotNull(message = MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE)
|
||||
@field:Min(value = 0, message = MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE)
|
||||
val quantity: Float
|
||||
data class MaterialQuantityDto(
|
||||
@field:NotNull(message = MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE)
|
||||
val material: Long,
|
||||
|
||||
@field:NotNull(message = MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE)
|
||||
@field:Min(value = 0, message = MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE)
|
||||
val quantity: Float
|
||||
)
|
||||
|
||||
// === DSL ===
|
||||
|
||||
fun material(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
inventoryQuantity: Float = 0f,
|
||||
isMixType: Boolean = false,
|
||||
materialType: MaterialType? = materialType(),
|
||||
op: Material.() -> Unit = {}
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
inventoryQuantity: Float = 0f,
|
||||
isMixType: Boolean = false,
|
||||
materialType: MaterialType? = materialType(),
|
||||
op: Material.() -> Unit = {}
|
||||
) = Material(id, name, inventoryQuantity, isMixType, materialType).apply(op)
|
||||
|
||||
fun material(
|
||||
material: Material,
|
||||
id: Long? = null,
|
||||
name: String? = null,
|
||||
material: Material,
|
||||
id: Long? = null,
|
||||
name: String? = null,
|
||||
) = Material(
|
||||
id ?: material.id, name
|
||||
?: material.name, material.inventoryQuantity, material.isMixType, material.materialType
|
||||
id ?: material.id, name
|
||||
?: material.name, material.inventoryQuantity, material.isMixType, material.materialType
|
||||
)
|
||||
|
||||
fun materialSaveDto(
|
||||
name: String = "name",
|
||||
inventoryQuantity: Float = 0f,
|
||||
materialTypeId: Long = 0L,
|
||||
simdutFile: MultipartFile? = null,
|
||||
op: MaterialSaveDto.() -> Unit = {}
|
||||
name: String = "name",
|
||||
inventoryQuantity: Float = 0f,
|
||||
materialTypeId: Long = 0L,
|
||||
simdutFile: MultipartFile? = null,
|
||||
op: MaterialSaveDto.() -> Unit = {}
|
||||
) = MaterialSaveDto(name, inventoryQuantity, materialTypeId, simdutFile).apply(op)
|
||||
|
||||
fun materialUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
inventoryQuantity: Float? = 0f,
|
||||
materialTypeId: Long? = 0L,
|
||||
simdutFile: MultipartFile? = null,
|
||||
op: MaterialUpdateDto.() -> Unit = {}
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
inventoryQuantity: Float? = 0f,
|
||||
materialTypeId: Long? = 0L,
|
||||
simdutFile: MultipartFile? = null,
|
||||
op: MaterialUpdateDto.() -> Unit = {}
|
||||
) = MaterialUpdateDto(id, name, inventoryQuantity, materialTypeId, simdutFile).apply(op)
|
||||
|
||||
fun materialQuantityDto(
|
||||
materialId: Long,
|
||||
quantity: Float,
|
||||
op: MaterialQuantityDto.() -> Unit = {}
|
||||
materialId: Long,
|
||||
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_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
|
||||
)
|
||||
NotFoundException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A material with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun materialNameNotFoundException(name: String) =
|
||||
NotFoundException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A material with the name $name could not be found",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
NotFoundException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A material with the name $name could not be found",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun materialIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A material with the id $id already exists",
|
||||
id
|
||||
)
|
||||
AlreadyExistsException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A material with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
||||
fun materialNameAlreadyExistsException(name: String) =
|
||||
AlreadyExistsException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A material with the name $name already exists",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
AlreadyExistsException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A material with the name $name already exists",
|
||||
name,
|
||||
"name"
|
||||
)
|
||||
|
||||
fun cannotDeleteMaterialException(material: Material) =
|
||||
CannotDeleteException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||
"Cannot delete the material ${material.name} because one or more recipes depends on it"
|
||||
)
|
||||
CannotDeleteException(
|
||||
MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE,
|
||||
"Cannot delete the material ${material.name} because one or more recipes depends on it"
|
||||
)
|
||||
|
|
|
@ -22,107 +22,116 @@ private const val MIX_DEDUCT_RATION_NEGATIVE_MESSAGE = "Le ratio doit être éga
|
|||
@Entity
|
||||
@Table(name = "mix")
|
||||
data class Mix(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long?,
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long?,
|
||||
|
||||
var location: String?,
|
||||
var location: String?,
|
||||
|
||||
@JsonIgnore
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "recipe_id")
|
||||
val recipe: Recipe,
|
||||
@JsonIgnore
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "recipe_id")
|
||||
val recipe: Recipe,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "mix_type_id")
|
||||
var mixType: MixType,
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "mix_type_id")
|
||||
var mixType: MixType,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "mix_id")
|
||||
var mixMaterials: MutableSet<MixMaterial>,
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "mix_id")
|
||||
var mixMaterials: MutableSet<MixMaterial>,
|
||||
) : Model
|
||||
|
||||
open class MixSaveDto(
|
||||
@field:NotBlank(message = MIX_NAME_NULL_MESSAGE)
|
||||
val name: String,
|
||||
@field:NotBlank(message = MIX_NAME_NULL_MESSAGE)
|
||||
val name: String,
|
||||
|
||||
@field:NotNull(message = MIX_RECIPE_NULL_MESSAGE)
|
||||
val recipeId: Long,
|
||||
@field:NotNull(message = MIX_RECIPE_NULL_MESSAGE)
|
||||
val recipeId: Long,
|
||||
|
||||
@field:NotNull(message = MIX_MATERIAL_TYPE_NULL_MESSAGE)
|
||||
val materialTypeId: Long,
|
||||
@field:NotNull(message = MIX_MATERIAL_TYPE_NULL_MESSAGE)
|
||||
val materialTypeId: Long,
|
||||
|
||||
val mixMaterials: Set<MixMaterialDto>?
|
||||
val mixMaterials: Set<MixMaterialDto>?
|
||||
) : EntityDto<Mix> {
|
||||
override fun toEntity(): Mix = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
open class MixUpdateDto(
|
||||
@field:NotNull(message = MIX_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
@field:NotNull(message = MIX_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
|
||||
@field:NullOrNotBlank(message = MIX_NAME_NULL_MESSAGE)
|
||||
val name: String?,
|
||||
@field:NullOrNotBlank(message = MIX_NAME_NULL_MESSAGE)
|
||||
val name: String?,
|
||||
|
||||
val materialTypeId: Long?,
|
||||
val materialTypeId: Long?,
|
||||
|
||||
var mixMaterials: Set<MixMaterialDto>?
|
||||
var mixMaterials: Set<MixMaterialDto>?
|
||||
) : EntityDto<Mix> {
|
||||
override fun toEntity(): Mix = throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
data class MixDeductDto(
|
||||
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
data class MixOutputDto(
|
||||
val id: Long,
|
||||
val location: String?,
|
||||
val mixType: MixType,
|
||||
val mixMaterials: Set<MixMaterialOutputDto>
|
||||
)
|
||||
|
||||
@field:NotNull(message = MIX_DEDUCT_RATIO_NULL_MESSAGE)
|
||||
@field:Min(value = 0, message = MIX_DEDUCT_RATION_NEGATIVE_MESSAGE)
|
||||
val ratio: Float
|
||||
data class MixDeductDto(
|
||||
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
|
||||
@field:NotNull(message = MIX_DEDUCT_RATIO_NULL_MESSAGE)
|
||||
@field:Min(value = 0, message = MIX_DEDUCT_RATION_NEGATIVE_MESSAGE)
|
||||
val ratio: Float
|
||||
)
|
||||
|
||||
data class MixLocationDto(
|
||||
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
|
||||
val mixId: Long,
|
||||
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
|
||||
val mixId: Long,
|
||||
|
||||
val location: String?
|
||||
val location: String?
|
||||
)
|
||||
|
||||
//fun Mix.toOutput() =
|
||||
|
||||
// ==== DSL ====
|
||||
fun mix(
|
||||
id: Long? = null,
|
||||
location: String? = "location",
|
||||
recipe: Recipe = recipe(),
|
||||
mixType: MixType = mixType(),
|
||||
mixMaterials: MutableSet<MixMaterial> = mutableSetOf(),
|
||||
op: Mix.() -> Unit = {}
|
||||
id: Long? = null,
|
||||
location: String? = "location",
|
||||
recipe: Recipe = recipe(),
|
||||
mixType: MixType = mixType(),
|
||||
mixMaterials: MutableSet<MixMaterial> = mutableSetOf(),
|
||||
op: Mix.() -> Unit = {}
|
||||
) = Mix(id, location, recipe, mixType, mixMaterials).apply(op)
|
||||
|
||||
fun mixSaveDto(
|
||||
name: String = "name",
|
||||
recipeId: Long = 0L,
|
||||
materialTypeId: Long = 0L,
|
||||
mixMaterials: Set<MixMaterialDto>? = setOf(),
|
||||
op: MixSaveDto.() -> Unit = {}
|
||||
name: String = "name",
|
||||
recipeId: Long = 0L,
|
||||
materialTypeId: Long = 0L,
|
||||
mixMaterials: Set<MixMaterialDto>? = setOf(),
|
||||
op: MixSaveDto.() -> Unit = {}
|
||||
) = MixSaveDto(name, recipeId, materialTypeId, mixMaterials).apply(op)
|
||||
|
||||
fun mixUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
materialTypeId: Long? = 0L,
|
||||
mixMaterials: Set<MixMaterialDto>? = setOf(),
|
||||
op: MixUpdateDto.() -> Unit = {}
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
materialTypeId: Long? = 0L,
|
||||
mixMaterials: Set<MixMaterialDto>? = setOf(),
|
||||
op: MixUpdateDto.() -> Unit = {}
|
||||
) = MixUpdateDto(id, name, materialTypeId, mixMaterials).apply(op)
|
||||
|
||||
fun mixRatio(
|
||||
id: Long = 0L,
|
||||
ratio: Float = 1f,
|
||||
op: MixDeductDto.() -> Unit = {}
|
||||
id: Long = 0L,
|
||||
ratio: Float = 1f,
|
||||
op: MixDeductDto.() -> Unit = {}
|
||||
) = MixDeductDto(id, ratio).apply(op)
|
||||
|
||||
fun mixLocationDto(
|
||||
mixId: Long = 0L,
|
||||
location: String? = "location",
|
||||
op: MixLocationDto.() -> Unit = {}
|
||||
mixId: Long = 0L,
|
||||
location: String? = "location",
|
||||
op: MixLocationDto.() -> Unit = {}
|
||||
) = MixLocationDto(mixId, location).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
|
@ -132,24 +141,24 @@ 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
|
||||
)
|
||||
NotFoundException(
|
||||
MIX_EXCEPTION_ERROR_CODE,
|
||||
MIX_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A mix with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun mixIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
MIX_EXCEPTION_ERROR_CODE,
|
||||
MIX_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A mix with the id $id already exists",
|
||||
id
|
||||
)
|
||||
AlreadyExistsException(
|
||||
MIX_EXCEPTION_ERROR_CODE,
|
||||
MIX_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A mix with the id $id already exists",
|
||||
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"
|
||||
)
|
||||
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"
|
||||
)
|
||||
|
|
|
@ -13,44 +13,51 @@ private const val MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE = "La quantité ne
|
|||
@Entity
|
||||
@Table(name = "mix_material")
|
||||
data class MixMaterial(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long?,
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long?,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "material_id")
|
||||
val material: Material,
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "material_id")
|
||||
val material: Material,
|
||||
|
||||
var quantity: Float,
|
||||
var quantity: Float,
|
||||
|
||||
var position: Int
|
||||
var position: Int
|
||||
) : Model
|
||||
|
||||
data class MixMaterialOutputDto(
|
||||
val id: Long,
|
||||
val material: MaterialOutputDto,
|
||||
val quantity: Float,
|
||||
val position: Int
|
||||
)
|
||||
|
||||
data class MixMaterialDto(
|
||||
@field:NotNull(message = MIX_MATERIAL_DTO_MATERIAL_ID_NULL_MESSAGE)
|
||||
val materialId: Long,
|
||||
@field:NotNull(message = MIX_MATERIAL_DTO_MATERIAL_ID_NULL_MESSAGE)
|
||||
val materialId: Long,
|
||||
|
||||
@field:NotNull(message = MIX_MATERIAL_DTO_QUANTITY_NULL_MESSAGE)
|
||||
@field:Min(value = 0, message = MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE)
|
||||
val quantity: Float,
|
||||
@field:NotNull(message = MIX_MATERIAL_DTO_QUANTITY_NULL_MESSAGE)
|
||||
@field:Min(value = 0, message = MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE)
|
||||
val quantity: Float,
|
||||
|
||||
val position: Int
|
||||
val position: Int
|
||||
)
|
||||
|
||||
// ==== DSL ====
|
||||
fun mixMaterial(
|
||||
id: Long? = null,
|
||||
material: Material = material(),
|
||||
quantity: Float = 0f,
|
||||
position: Int = 0,
|
||||
op: MixMaterial.() -> Unit = {}
|
||||
id: Long? = null,
|
||||
material: Material = material(),
|
||||
quantity: Float = 0f,
|
||||
position: Int = 0,
|
||||
op: MixMaterial.() -> Unit = {}
|
||||
) = MixMaterial(id, material, quantity, position).apply(op)
|
||||
|
||||
fun mixMaterialDto(
|
||||
materialId: Long = 0L,
|
||||
quantity: Float = 0f,
|
||||
position: Int = 0,
|
||||
op: MixMaterialDto.() -> Unit = {}
|
||||
materialId: Long = 0L,
|
||||
quantity: Float = 0f,
|
||||
position: Int = 0,
|
||||
op: MixMaterialDto.() -> Unit = {}
|
||||
) = MixMaterialDto(materialId, quantity, position).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
|
@ -59,17 +66,17 @@ private const val MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix material al
|
|||
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
|
||||
)
|
||||
NotFoundException(
|
||||
MIX_MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A mix material with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun mixMaterialIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
MIX_MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A mix material with the id $id already exists",
|
||||
id
|
||||
)
|
||||
AlreadyExistsException(
|
||||
MIX_MATERIAL_EXCEPTION_ERROR_CODE,
|
||||
MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A mix material with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
|
|
@ -3,10 +3,12 @@ package dev.fyloz.colorrecipesexplorer.model
|
|||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
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 dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
||||
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.LocalDate
|
||||
import javax.persistence.*
|
||||
import javax.validation.constraints.*
|
||||
|
@ -25,230 +27,260 @@ private const val RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE = "Des messages sont re
|
|||
|
||||
private const val NOTE_GROUP_ID_NULL_MESSAGE = "Un identifiant de groupe est requis"
|
||||
|
||||
const val RECIPE_IMAGES_DIRECTORY = "images/recipes"
|
||||
|
||||
@Entity
|
||||
@Table(name = "recipe")
|
||||
data class Recipe(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long?,
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
override val id: Long?,
|
||||
|
||||
/** The name of the recipe. It is not unique in the entire system, but is unique in the scope of a [Company]. */
|
||||
val name: String,
|
||||
/** The name of the recipe. It is not unique in the entire system, but is unique in the scope of a [Company]. */
|
||||
val name: String,
|
||||
|
||||
val description: String,
|
||||
val description: String,
|
||||
|
||||
/** The color produced by the recipe. The string should be formatted as a hexadecimal color without the sharp (#). */
|
||||
val color: String,
|
||||
/** The color produced by the recipe. The string should be formatted as a hexadecimal color without the sharp (#). */
|
||||
val color: String,
|
||||
|
||||
/** The gloss of the color in percents. (0-100) */
|
||||
val gloss: Byte,
|
||||
/** The gloss of the color in percents. (0-100) */
|
||||
val gloss: Byte,
|
||||
|
||||
val sample: Int?,
|
||||
val sample: Int?,
|
||||
|
||||
@Column(name = "approbation_date")
|
||||
val approbationDate: LocalDate?,
|
||||
@Column(name = "approbation_date")
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
/** A remark given by the creator of the recipe. */
|
||||
val remark: String,
|
||||
/** A remark given by the creator of the recipe. */
|
||||
val remark: String,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "company_id")
|
||||
val company: Company,
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "company_id")
|
||||
val company: Company,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
|
||||
val mixes: MutableList<Mix>,
|
||||
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
|
||||
val mixes: MutableList<Mix>,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "recipe_id")
|
||||
val groupsInformation: Set<RecipeGroupInformation>
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "recipe_id")
|
||||
val groupsInformation: Set<RecipeGroupInformation>
|
||||
) : Model {
|
||||
/** The mix types contained in this recipe. */
|
||||
val mixTypes: Collection<MixType>
|
||||
@JsonIgnore
|
||||
get() = mixes.map { it.mixType }
|
||||
|
||||
val imagesDirectoryPath
|
||||
@JsonIgnore
|
||||
@Transient
|
||||
get() = "$RECIPE_IMAGES_DIRECTORY/$id"
|
||||
|
||||
fun groupInformationForGroup(groupId: Long) =
|
||||
groupsInformation.firstOrNull { it.group.id == groupId }
|
||||
groupsInformation.firstOrNull { it.group.id == groupId }
|
||||
|
||||
fun imageUrl(name: String) =
|
||||
"${CRE_PROPERTIES.deploymentUrl}$FILE_CONTROLLER_PATH?path=${
|
||||
URLEncoder.encode(
|
||||
"${this.imagesDirectoryPath}/$name",
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
}"
|
||||
}
|
||||
|
||||
open class RecipeSaveDto(
|
||||
@field:NotBlank(message = RECIPE_NAME_NULL_MESSAGE)
|
||||
val name: String,
|
||||
@field:NotBlank(message = RECIPE_NAME_NULL_MESSAGE)
|
||||
val name: String,
|
||||
|
||||
@field:NotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
|
||||
val description: String,
|
||||
@field:NotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
|
||||
val description: String,
|
||||
|
||||
@field:NotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
|
||||
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
|
||||
val color: String,
|
||||
@field:NotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
|
||||
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
|
||||
val color: String,
|
||||
|
||||
@field:NotNull(message = RECIPE_GLOSS_NULL_MESSAGE)
|
||||
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
|
||||
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
|
||||
val gloss: Byte,
|
||||
@field:NotNull(message = RECIPE_GLOSS_NULL_MESSAGE)
|
||||
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
|
||||
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
|
||||
val gloss: Byte,
|
||||
|
||||
@field:Min(value = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
|
||||
val sample: Int?,
|
||||
@field:Min(value = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val remark: String?,
|
||||
val remark: String?,
|
||||
|
||||
@field:Min(value = 0, message = RECIPE_COMPANY_NULL_MESSAGE)
|
||||
val companyId: Long = -1L,
|
||||
@field:Min(value = 0, message = RECIPE_COMPANY_NULL_MESSAGE)
|
||||
val companyId: Long = -1L,
|
||||
) : EntityDto<Recipe> {
|
||||
override fun toEntity(): Recipe = recipe(
|
||||
name = name,
|
||||
description = description,
|
||||
sample = sample,
|
||||
approbationDate = approbationDate,
|
||||
remark = remark ?: "",
|
||||
company = company(id = companyId)
|
||||
name = name,
|
||||
description = description,
|
||||
sample = sample,
|
||||
approbationDate = approbationDate,
|
||||
remark = remark ?: "",
|
||||
company = company(id = companyId)
|
||||
)
|
||||
}
|
||||
|
||||
open class RecipeUpdateDto(
|
||||
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
|
||||
val id: Long,
|
||||
|
||||
@field:NullOrNotBlank(message = RECIPE_NAME_NULL_MESSAGE)
|
||||
val name: String?,
|
||||
@field:NullOrNotBlank(message = RECIPE_NAME_NULL_MESSAGE)
|
||||
val name: String?,
|
||||
|
||||
@field:NullOrNotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
|
||||
val description: String?,
|
||||
@field:NullOrNotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
|
||||
val description: String?,
|
||||
|
||||
@field:NullOrNotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
|
||||
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
|
||||
val color: String?,
|
||||
@field:NullOrNotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
|
||||
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
|
||||
val color: String?,
|
||||
|
||||
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
|
||||
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
|
||||
val gloss: Byte?,
|
||||
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
|
||||
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
|
||||
val gloss: Byte?,
|
||||
|
||||
@field:NullOrSize(min = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
|
||||
val sample: Int?,
|
||||
@field:NullOrSize(min = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
|
||||
val sample: Int?,
|
||||
|
||||
val approbationDate: LocalDate?,
|
||||
val approbationDate: LocalDate?,
|
||||
|
||||
val remark: String?,
|
||||
val remark: String?,
|
||||
|
||||
val steps: Set<RecipeStepsDto>?
|
||||
val steps: Set<RecipeStepsDto>?
|
||||
) : EntityDto<Recipe>
|
||||
|
||||
data class RecipeOutputDto(
|
||||
override val id: Long,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val color: String,
|
||||
val gloss: Byte,
|
||||
val sample: Int?,
|
||||
val approbationDate: LocalDate?,
|
||||
val remark: String?,
|
||||
val company: Company,
|
||||
val mixes: Set<MixOutputDto>,
|
||||
val groupsInformation: Set<RecipeGroupInformation>,
|
||||
var imagesUrls: Set<String>
|
||||
) : Model
|
||||
|
||||
@Entity
|
||||
@Table(name = "recipe_group_information")
|
||||
data class RecipeGroupInformation(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id: Long?,
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id: Long?,
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "group_id")
|
||||
val group: EmployeeGroup,
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "group_id")
|
||||
val group: EmployeeGroup,
|
||||
|
||||
var note: String?,
|
||||
var note: String?,
|
||||
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "recipe_group_information_id")
|
||||
var steps: MutableSet<RecipeStep>?
|
||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||
@JoinColumn(name = "recipe_group_information_id")
|
||||
var steps: MutableSet<RecipeStep>?
|
||||
)
|
||||
|
||||
data class RecipeStepsDto(
|
||||
@field:NotNull(message = RECIPE_STEPS_DTO_GROUP_ID_NULL_MESSAGE)
|
||||
val groupId: Long,
|
||||
@field:NotNull(message = RECIPE_STEPS_DTO_GROUP_ID_NULL_MESSAGE)
|
||||
val groupId: Long,
|
||||
|
||||
@field:NotNull(message = RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE)
|
||||
val steps: Set<RecipeStep>
|
||||
@field:NotNull(message = RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE)
|
||||
val steps: Set<RecipeStep>
|
||||
)
|
||||
|
||||
data class RecipePublicDataDto(
|
||||
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
|
||||
val recipeId: Long,
|
||||
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
|
||||
val recipeId: Long,
|
||||
|
||||
val notes: Set<NoteDto>?,
|
||||
val notes: Set<NoteDto>?,
|
||||
|
||||
val mixesLocation: Set<MixLocationDto>?
|
||||
val mixesLocation: Set<MixLocationDto>?
|
||||
)
|
||||
|
||||
data class NoteDto(
|
||||
@field:NotNull(message = NOTE_GROUP_ID_NULL_MESSAGE)
|
||||
val groupId: Long,
|
||||
@field:NotNull(message = NOTE_GROUP_ID_NULL_MESSAGE)
|
||||
val groupId: Long,
|
||||
|
||||
val content: String?
|
||||
val content: String?
|
||||
)
|
||||
|
||||
// ==== DSL ====
|
||||
fun recipe(
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
description: String = "description",
|
||||
color: String = "ffffff",
|
||||
gloss: Byte = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String = "remark",
|
||||
company: Company = company(),
|
||||
mixes: MutableList<Mix> = mutableListOf(),
|
||||
groupsInformation: Set<RecipeGroupInformation> = setOf(),
|
||||
op: Recipe.() -> Unit = {}
|
||||
id: Long? = null,
|
||||
name: String = "name",
|
||||
description: String = "description",
|
||||
color: String = "ffffff",
|
||||
gloss: Byte = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String = "remark",
|
||||
company: Company = company(),
|
||||
mixes: MutableList<Mix> = mutableListOf(),
|
||||
groupsInformation: Set<RecipeGroupInformation> = setOf(),
|
||||
op: Recipe.() -> Unit = {}
|
||||
) = Recipe(
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
gloss,
|
||||
sample,
|
||||
approbationDate,
|
||||
remark,
|
||||
company,
|
||||
mixes,
|
||||
groupsInformation
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
color,
|
||||
gloss,
|
||||
sample,
|
||||
approbationDate,
|
||||
remark,
|
||||
company,
|
||||
mixes,
|
||||
groupsInformation
|
||||
).apply(op)
|
||||
|
||||
fun recipeSaveDto(
|
||||
name: String = "name",
|
||||
description: String = "description",
|
||||
color: String = "ffffff",
|
||||
gloss: Byte = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String = "remark",
|
||||
companyId: Long = 0L,
|
||||
op: RecipeSaveDto.() -> Unit = {}
|
||||
name: String = "name",
|
||||
description: String = "description",
|
||||
color: String = "ffffff",
|
||||
gloss: Byte = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String = "remark",
|
||||
companyId: Long = 0L,
|
||||
op: RecipeSaveDto.() -> Unit = {}
|
||||
) = RecipeSaveDto(name, description, color, gloss, sample, approbationDate, remark, companyId).apply(op)
|
||||
|
||||
fun recipeUpdateDto(
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
description: String? = "description",
|
||||
color: String? = "ffffff",
|
||||
gloss: Byte? = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String? = "remark",
|
||||
steps: Set<RecipeStepsDto>? = setOf(),
|
||||
op: RecipeUpdateDto.() -> Unit = {}
|
||||
id: Long = 0L,
|
||||
name: String? = "name",
|
||||
description: String? = "description",
|
||||
color: String? = "ffffff",
|
||||
gloss: Byte? = 0,
|
||||
sample: Int? = -1,
|
||||
approbationDate: LocalDate? = LocalDate.MIN,
|
||||
remark: String? = "remark",
|
||||
steps: Set<RecipeStepsDto>? = setOf(),
|
||||
op: RecipeUpdateDto.() -> Unit = {}
|
||||
) = RecipeUpdateDto(id, name, description, color, gloss, sample, approbationDate, remark, steps).apply(op)
|
||||
|
||||
fun recipeGroupInformation(
|
||||
id: Long? = null,
|
||||
group: EmployeeGroup = employeeGroup(),
|
||||
note: String? = null,
|
||||
steps: MutableSet<RecipeStep>? = mutableSetOf(),
|
||||
op: RecipeGroupInformation.() -> Unit = {}
|
||||
id: Long? = null,
|
||||
group: EmployeeGroup = employeeGroup(),
|
||||
note: String? = null,
|
||||
steps: MutableSet<RecipeStep>? = mutableSetOf(),
|
||||
op: RecipeGroupInformation.() -> Unit = {}
|
||||
) = RecipeGroupInformation(id, group, note, steps).apply(op)
|
||||
|
||||
fun recipePublicDataDto(
|
||||
recipeId: Long = 0L,
|
||||
notes: Set<NoteDto>? = null,
|
||||
mixesLocation: Set<MixLocationDto>? = null,
|
||||
op: RecipePublicDataDto.() -> Unit = {}
|
||||
recipeId: Long = 0L,
|
||||
notes: Set<NoteDto>? = null,
|
||||
mixesLocation: Set<MixLocationDto>? = null,
|
||||
op: RecipePublicDataDto.() -> Unit = {}
|
||||
) = RecipePublicDataDto(recipeId, notes, mixesLocation).apply(op)
|
||||
|
||||
fun noteDto(
|
||||
groupId: Long = 0L,
|
||||
content: String? = "note",
|
||||
op: NoteDto.() -> Unit = {}
|
||||
groupId: Long = 0L,
|
||||
content: String? = "note",
|
||||
op: NoteDto.() -> Unit = {}
|
||||
) = NoteDto(groupId, content).apply(op)
|
||||
|
||||
// ==== Exceptions ====
|
||||
|
@ -256,30 +288,18 @@ private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found"
|
|||
private const val RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe already exists"
|
||||
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
|
||||
)
|
||||
NotFoundException(
|
||||
RECIPE_EXCEPTION_ERROR_CODE,
|
||||
RECIPE_NOT_FOUND_EXCEPTION_TITLE,
|
||||
"A recipe with the id $id could not be found",
|
||||
id
|
||||
)
|
||||
|
||||
fun recipeIdAlreadyExistsException(id: Long) =
|
||||
AlreadyExistsException(
|
||||
RECIPE_EXCEPTION_ERROR_CODE,
|
||||
RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A recipe with the id $id already exists",
|
||||
id
|
||||
)
|
||||
AlreadyExistsException(
|
||||
RECIPE_EXCEPTION_ERROR_CODE,
|
||||
RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE,
|
||||
"A recipe with the id $id already exists",
|
||||
id
|
||||
)
|
||||
|
|
|
@ -4,7 +4,7 @@ import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditUsers
|
|||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeRemoveUsers
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.service.EmployeeGroupServiceImpl
|
||||
import dev.fyloz.colorrecipesexplorer.service.EmployeeGroupService
|
||||
import dev.fyloz.colorrecipesexplorer.service.EmployeeService
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
|
@ -23,52 +23,56 @@ class EmployeeController(private val employeeService: EmployeeService) {
|
|||
@GetMapping
|
||||
@PreAuthorizeViewUsers
|
||||
fun getAll() =
|
||||
ok(employeeService.getAll())
|
||||
ok(employeeService.getAllForOutput())
|
||||
|
||||
@GetMapping("{id}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(employeeService.getById(id))
|
||||
ok(employeeService.getByIdForOutput(id))
|
||||
|
||||
@GetMapping("current")
|
||||
fun getCurrent(loggedInEmployee: Principal?) =
|
||||
if (loggedInEmployee != null)
|
||||
ok(
|
||||
employeeService.getById(
|
||||
loggedInEmployee.name.toLong(),
|
||||
ignoreDefaultGroupUsers = false,
|
||||
ignoreSystemUsers = false
|
||||
if (loggedInEmployee != null)
|
||||
ok(
|
||||
with(employeeService) {
|
||||
getById(
|
||||
loggedInEmployee.name.toLong(),
|
||||
ignoreDefaultGroupUsers = false,
|
||||
ignoreSystemUsers = false
|
||||
).toOutput()
|
||||
}
|
||||
)
|
||||
)
|
||||
else
|
||||
forbidden()
|
||||
else
|
||||
forbidden()
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun save(@Valid @RequestBody employee: EmployeeSaveDto) =
|
||||
created<Employee>(EMPLOYEE_CONTROLLER_PATH) {
|
||||
employeeService.save(employee)
|
||||
}
|
||||
created<EmployeeOutputDto>(EMPLOYEE_CONTROLLER_PATH) {
|
||||
with(employeeService) {
|
||||
save(employee).toOutput()
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun update(@Valid @RequestBody employee: EmployeeUpdateDto) =
|
||||
noContent {
|
||||
employeeService.update(employee)
|
||||
}
|
||||
noContent {
|
||||
employeeService.update(employee)
|
||||
}
|
||||
|
||||
@PutMapping("{id}/password", consumes = [MediaType.TEXT_PLAIN_VALUE])
|
||||
@PreAuthorizeEditUsers
|
||||
fun updatePassword(@PathVariable id: Long, @RequestBody password: String) =
|
||||
noContent {
|
||||
employeeService.updatePassword(id, password)
|
||||
}
|
||||
noContent {
|
||||
employeeService.updatePassword(id, password)
|
||||
}
|
||||
|
||||
@PutMapping("{employeeId}/permissions/{permission}")
|
||||
@PreAuthorizeEditUsers
|
||||
fun addPermission(
|
||||
@PathVariable employeeId: Long,
|
||||
@PathVariable permission: EmployeePermission
|
||||
@PathVariable employeeId: Long,
|
||||
@PathVariable permission: EmployeePermission
|
||||
) = noContent {
|
||||
employeeService.addPermission(employeeId, permission)
|
||||
}
|
||||
|
@ -76,8 +80,8 @@ class EmployeeController(private val employeeService: EmployeeService) {
|
|||
@DeleteMapping("{employeeId}/permissions/{permission}")
|
||||
@PreAuthorizeEditUsers
|
||||
fun removePermission(
|
||||
@PathVariable employeeId: Long,
|
||||
@PathVariable permission: EmployeePermission
|
||||
@PathVariable employeeId: Long,
|
||||
@PathVariable permission: EmployeePermission
|
||||
) = noContent {
|
||||
employeeService.removePermission(employeeId, permission)
|
||||
}
|
||||
|
@ -85,59 +89,69 @@ class EmployeeController(private val employeeService: EmployeeService) {
|
|||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeRemoveUsers
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
employeeService.deleteById(id)
|
||||
employeeService.deleteById(id)
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping(EMPLOYEE_GROUP_CONTROLLER_PATH)
|
||||
class GroupsController(private val groupService: EmployeeGroupServiceImpl) {
|
||||
class GroupsController(
|
||||
private val groupService: EmployeeGroupService,
|
||||
private val employeeService: EmployeeService
|
||||
) {
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
|
||||
fun getAll() =
|
||||
ok(groupService.getAll())
|
||||
ok(groupService.getAllForOutput())
|
||||
|
||||
@GetMapping("{id}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(groupService.getById(id))
|
||||
ok(groupService.getByIdForOutput(id))
|
||||
|
||||
@GetMapping("{id}/employees")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getEmployeesForGroup(@PathVariable id: Long) =
|
||||
ok(groupService.getEmployeesForGroup(id))
|
||||
ok(with(employeeService) {
|
||||
groupService.getEmployeesForGroup(id)
|
||||
.map { it.toOutput() }
|
||||
})
|
||||
|
||||
@PostMapping("default/{groupId}")
|
||||
@PreAuthorizeViewUsers
|
||||
fun setDefaultGroup(@PathVariable groupId: Long, response: HttpServletResponse) =
|
||||
noContent {
|
||||
groupService.setResponseDefaultGroup(groupId, response)
|
||||
}
|
||||
noContent {
|
||||
groupService.setResponseDefaultGroup(groupId, response)
|
||||
}
|
||||
|
||||
@GetMapping("default")
|
||||
@PreAuthorizeViewUsers
|
||||
fun getRequestDefaultGroup(request: HttpServletRequest) =
|
||||
ok(groupService.getRequestDefaultGroup(request))
|
||||
ok(with(groupService) {
|
||||
getRequestDefaultGroup(request).toOutput()
|
||||
})
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun save(@Valid @RequestBody group: EmployeeGroupSaveDto) =
|
||||
created<EmployeeGroup>(EMPLOYEE_GROUP_CONTROLLER_PATH) {
|
||||
groupService.save(group)
|
||||
}
|
||||
created<EmployeeGroupOutputDto>(EMPLOYEE_GROUP_CONTROLLER_PATH) {
|
||||
with(groupService) {
|
||||
save(group).toOutput()
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditUsers
|
||||
fun update(@Valid @RequestBody group: EmployeeGroupUpdateDto) =
|
||||
noContent {
|
||||
groupService.update(group)
|
||||
}
|
||||
noContent {
|
||||
groupService.update(group)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeRemoveUsers
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
groupService.deleteById(id)
|
||||
}
|
||||
noContent {
|
||||
groupService.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
|
@ -145,7 +159,7 @@ class GroupsController(private val groupService: EmployeeGroupServiceImpl) {
|
|||
class LogoutController(private val employeeService: EmployeeService) {
|
||||
@GetMapping("logout")
|
||||
fun logout(request: HttpServletRequest) =
|
||||
ok<Void> {
|
||||
employeeService.logout(request)
|
||||
}
|
||||
ok<Void> {
|
||||
employeeService.logout(request)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,11 +17,11 @@ private const val COMPANY_CONTROLLER_PATH = "api/company"
|
|||
class CompanyController(private val companyService: CompanyService) {
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
ok(companyService.getAll())
|
||||
ok(companyService.getAllForOutput())
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(companyService.getById(id))
|
||||
ok(companyService.getByIdForOutput(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
|
||||
|
|
|
@ -15,21 +15,20 @@ private const val INVENTORY_CONTROLLER_PATH = "api/inventory"
|
|||
@RestController
|
||||
@RequestMapping(INVENTORY_CONTROLLER_PATH)
|
||||
class InventoryController(
|
||||
private val inventoryService: InventoryService
|
||||
private val inventoryService: InventoryService
|
||||
) {
|
||||
@PutMapping("add")
|
||||
@PreAuthorize("hasAuthority('ADD_TO_INVENTORY')")
|
||||
fun add(@RequestBody quantities: Collection<MaterialQuantityDto>): ResponseEntity<Collection<MaterialQuantityDto>> {
|
||||
return ResponseEntity.ok(inventoryService.add(quantities))
|
||||
}
|
||||
fun add(@RequestBody quantities: Collection<MaterialQuantityDto>) =
|
||||
ok(inventoryService.add(quantities))
|
||||
|
||||
@PutMapping("deduct")
|
||||
@PreAuthorize("hasAuthority('DEDUCT_FROM_INVENTORY')")
|
||||
fun deduct(@RequestBody quantities: Collection<MaterialQuantityDto>) =
|
||||
ok(inventoryService.deduct(quantities))
|
||||
ok(inventoryService.deduct(quantities))
|
||||
|
||||
@PutMapping("deduct/mix")
|
||||
@PreAuthorize("hasAuthority('DEDUCT_FROM_INVENTORY')")
|
||||
fun deduct(@RequestBody mixRatio: MixDeductDto) =
|
||||
ok(inventoryService.deductMix(mixRatio))
|
||||
ok(inventoryService.deductMix(mixRatio))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewCatalog
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.service.MaterialService
|
||||
import org.springframework.http.MediaType
|
||||
|
@ -8,6 +9,7 @@ import org.springframework.http.ResponseEntity
|
|||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.net.URI
|
||||
import javax.validation.Valid
|
||||
|
||||
private const val MATERIAL_CONTROLLER_PATH = "api/material"
|
||||
|
@ -15,78 +17,65 @@ private const val MATERIAL_CONTROLLER_PATH = "api/material"
|
|||
@RestController
|
||||
@RequestMapping(MATERIAL_CONTROLLER_PATH)
|
||||
@PreAuthorizeViewCatalog
|
||||
class MaterialController(private val materialService: MaterialService) {
|
||||
class MaterialController(
|
||||
private val materialService: MaterialService
|
||||
) {
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
ok(materialService.getAll())
|
||||
ok(materialService.getAllForOutput())
|
||||
|
||||
@GetMapping("notmixtype")
|
||||
fun getAllNotMixType() =
|
||||
ok(materialService.getAllNotMixType())
|
||||
ok(materialService.getAllNotMixType())
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(materialService.getById(id))
|
||||
ok(materialService.getByIdForOutput(id))
|
||||
|
||||
@PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@PreAuthorize("hasAuthority('EDIT_MATERIALS')")
|
||||
fun save(@Valid material: MaterialSaveDto, simdutFile: MultipartFile?) =
|
||||
created<Material>(MATERIAL_CONTROLLER_PATH) {
|
||||
materialService.save(
|
||||
materialSaveDto(
|
||||
name = material.name,
|
||||
inventoryQuantity = material.inventoryQuantity,
|
||||
materialTypeId = material.materialTypeId,
|
||||
simdutFile = simdutFile
|
||||
)
|
||||
)
|
||||
}
|
||||
created<MaterialOutputDto>(MATERIAL_CONTROLLER_PATH) {
|
||||
with(materialService) {
|
||||
save(
|
||||
materialSaveDto(
|
||||
name = material.name,
|
||||
inventoryQuantity = material.inventoryQuantity,
|
||||
materialTypeId = material.materialTypeId,
|
||||
simdutFile = simdutFile
|
||||
)
|
||||
).toOutput()
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@PreAuthorize("hasAuthority('EDIT_MATERIALS')")
|
||||
fun update(@Valid material: MaterialUpdateDto, simdutFile: MultipartFile?) =
|
||||
noContent {
|
||||
materialService.update(
|
||||
materialUpdateDto(
|
||||
id = material.id,
|
||||
name = material.name,
|
||||
inventoryQuantity = material.inventoryQuantity,
|
||||
materialTypeId = material.materialTypeId,
|
||||
simdutFile = simdutFile
|
||||
noContent {
|
||||
materialService.update(
|
||||
materialUpdateDto(
|
||||
id = material.id,
|
||||
name = material.name,
|
||||
inventoryQuantity = material.inventoryQuantity,
|
||||
materialTypeId = material.materialTypeId,
|
||||
simdutFile = simdutFile
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorize("hasAuthority('REMOVE_MATERIALS')")
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
materialService.deleteById(id)
|
||||
}
|
||||
|
||||
@GetMapping("{id}/simdut/exists")
|
||||
fun hasSimdut(@PathVariable id: Long) =
|
||||
ok(materialService.hasSimdut(id))
|
||||
|
||||
@GetMapping("{id}/simdut", produces = [MediaType.APPLICATION_PDF_VALUE])
|
||||
fun getSimdut(@PathVariable id: Long): ResponseEntity<ByteArray> = with(materialService.getSimdut(id)) {
|
||||
if (this.isEmpty()) {
|
||||
notFound()
|
||||
} else {
|
||||
ok(this, httpHeaders(contentType = MediaType.APPLICATION_PDF))
|
||||
noContent {
|
||||
materialService.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/simdut")
|
||||
fun getAllIdsWithSimdut() =
|
||||
ok(materialService.getAllIdsWithSimdut())
|
||||
|
||||
@GetMapping("mix/create/{recipeId}")
|
||||
fun getAllForMixCreation(@PathVariable recipeId: Long) =
|
||||
ok(materialService.getAllForMixCreation(recipeId))
|
||||
ok(materialService.getAllForMixCreation(recipeId))
|
||||
|
||||
@GetMapping("mix/update/{mixId}")
|
||||
fun getAllForMixUpdate(@PathVariable mixId: Long) =
|
||||
ok(materialService.getAllForMixUpdate(mixId))
|
||||
ok(materialService.getAllForMixUpdate(mixId))
|
||||
}
|
||||
|
||||
|
|
|
@ -17,11 +17,11 @@ private const val MATERIAL_TYPE_CONTROLLER_PATH = "api/materialtype"
|
|||
class MaterialTypeController(private val materialTypeService: MaterialTypeService) {
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
ok(materialTypeService.getAll())
|
||||
ok(materialTypeService.getAllForOutput())
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(materialTypeService.getById(id))
|
||||
ok(materialTypeService.getByIdForOutput(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')")
|
||||
|
|
|
@ -3,7 +3,9 @@ package dev.fyloz.colorrecipesexplorer.rest
|
|||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeRemoveRecipes
|
||||
import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
|
||||
import dev.fyloz.colorrecipesexplorer.service.MixService
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeImageService
|
||||
import dev.fyloz.colorrecipesexplorer.service.RecipeService
|
||||
|
@ -12,7 +14,8 @@ import org.springframework.http.ResponseEntity
|
|||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.net.URI
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.validation.Valid
|
||||
|
||||
|
||||
|
@ -22,69 +25,61 @@ private const val MIX_CONTROLLER_PATH = "api/recipe/mix"
|
|||
@RestController
|
||||
@RequestMapping(RECIPE_CONTROLLER_PATH)
|
||||
@PreAuthorizeViewRecipes
|
||||
class RecipeController(private val recipeService: RecipeService) {
|
||||
class RecipeController(
|
||||
private val recipeService: RecipeService,
|
||||
private val recipeImageService: RecipeImageService
|
||||
) {
|
||||
@GetMapping
|
||||
fun getAll() =
|
||||
ok(recipeService.getAll())
|
||||
ok(recipeService.getAllForOutput())
|
||||
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(recipeService.getById(id))
|
||||
ok(recipeService.getByIdForOutput(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditRecipes
|
||||
fun save(@Valid @RequestBody recipe: RecipeSaveDto) =
|
||||
created<Recipe>(RECIPE_CONTROLLER_PATH) {
|
||||
recipeService.save(recipe)
|
||||
}
|
||||
created<RecipeOutputDto>(RECIPE_CONTROLLER_PATH) {
|
||||
with(recipeService) {
|
||||
save(recipe).toOutput()
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditRecipes
|
||||
fun update(@Valid @RequestBody recipe: RecipeUpdateDto) =
|
||||
noContent {
|
||||
recipeService.update(recipe)
|
||||
}
|
||||
noContent {
|
||||
recipeService.update(recipe)
|
||||
}
|
||||
|
||||
@PutMapping("public")
|
||||
@PreAuthorize("hasAuthority('EDIT_RECIPES_PUBLIC_DATA')")
|
||||
fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) =
|
||||
noContent {
|
||||
recipeService.updatePublicData(publicDataDto)
|
||||
}
|
||||
noContent {
|
||||
recipeService.updatePublicData(publicDataDto)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeRemoveRecipes
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
recipeService.deleteById(id)
|
||||
}
|
||||
}
|
||||
noContent {
|
||||
recipeService.deleteById(id)
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping(RECIPE_CONTROLLER_PATH)
|
||||
@PreAuthorizeViewRecipes
|
||||
class RecipeImageController(val recipeImageService: RecipeImageService) {
|
||||
@GetMapping("{recipeId}/image")
|
||||
fun getAllIdsForRecipe(@PathVariable recipeId: Long) =
|
||||
ok(recipeImageService.getAllIdsForRecipe(recipeId))
|
||||
|
||||
@GetMapping("{recipeId}/image/{id}", produces = [MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE])
|
||||
fun getById(@PathVariable recipeId: Long, @PathVariable id: Long) =
|
||||
ok(recipeImageService.getByIdForRecipe(id, recipeId))
|
||||
|
||||
@PostMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@PutMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@PreAuthorizeEditRecipes
|
||||
fun save(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity<Void> {
|
||||
val id = recipeImageService.save(image, recipeId)
|
||||
return ResponseEntity.created(URI.create("/$RECIPE_CONTROLLER_PATH/$recipeId/image/$id")).build()
|
||||
fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity<RecipeOutputDto> {
|
||||
recipeImageService.download(image, recipeService.getById(recipeId))
|
||||
return getById(recipeId)
|
||||
}
|
||||
|
||||
@DeleteMapping("{recipeId}/image/{id}")
|
||||
@PreAuthorizeRemoveRecipes
|
||||
fun delete(@PathVariable recipeId: Long, @PathVariable id: Long) =
|
||||
noContent {
|
||||
recipeImageService.delete(id, recipeId)
|
||||
}
|
||||
@DeleteMapping("{recipeId}/image/{name}")
|
||||
@PreAuthorizeEditRecipes
|
||||
fun deleteImage(@PathVariable recipeId: Long, @PathVariable name: String) =
|
||||
noContent {
|
||||
recipeImageService.delete(recipeService.getById(recipeId), name)
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
|
@ -93,26 +88,26 @@ class RecipeImageController(val recipeImageService: RecipeImageService) {
|
|||
class MixController(private val mixService: MixService) {
|
||||
@GetMapping("{id}")
|
||||
fun getById(@PathVariable id: Long) =
|
||||
ok(mixService.getById(id))
|
||||
ok(mixService.getByIdForOutput(id))
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorizeEditRecipes
|
||||
fun save(@Valid @RequestBody mix: MixSaveDto) =
|
||||
created<Mix>(MIX_CONTROLLER_PATH) {
|
||||
mixService.save(mix)
|
||||
}
|
||||
created<Mix>(MIX_CONTROLLER_PATH) {
|
||||
mixService.save(mix)
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorizeEditRecipes
|
||||
fun update(@Valid @RequestBody mix: MixUpdateDto) =
|
||||
noContent {
|
||||
mixService.update(mix)
|
||||
}
|
||||
noContent {
|
||||
mixService.update(mix)
|
||||
}
|
||||
|
||||
@DeleteMapping("{id}")
|
||||
@PreAuthorizeRemoveRecipes
|
||||
fun deleteById(@PathVariable id: Long) =
|
||||
noContent {
|
||||
mixService.deleteById(id)
|
||||
}
|
||||
noContent {
|
||||
mixService.deleteById(id)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.HttpStatus
|
||||
|
@ -7,13 +8,15 @@ import org.springframework.http.MediaType
|
|||
import org.springframework.http.ResponseEntity
|
||||
import java.net.URI
|
||||
|
||||
lateinit var CRE_PROPERTIES: CreProperties
|
||||
|
||||
/** Creates a HTTP OK [ResponseEntity] from the given [body]. */
|
||||
fun <T> ok(body: T): ResponseEntity<T> =
|
||||
ResponseEntity.ok(body)
|
||||
ResponseEntity.ok(body)
|
||||
|
||||
/** Creates a HTTP OK [ResponseEntity] from the given [body] and [headers]. */
|
||||
fun <T> ok(body: T, headers: HttpHeaders): ResponseEntity<T> =
|
||||
ResponseEntity(body, headers, HttpStatus.OK)
|
||||
ResponseEntity(body, headers, HttpStatus.OK)
|
||||
|
||||
/** Executes the given [action] then returns an HTTP OK [ResponseEntity] form the given [body]. */
|
||||
fun <T> ok(action: () -> Unit): ResponseEntity<T> {
|
||||
|
@ -23,19 +26,23 @@ fun <T> ok(action: () -> Unit): ResponseEntity<T> {
|
|||
|
||||
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
|
||||
fun <T : Model> created(controllerPath: String, body: T): ResponseEntity<T> =
|
||||
ResponseEntity.created(URI.create("$controllerPath/${body.id}")).body(body)
|
||||
created(controllerPath, body, body.id!!)
|
||||
|
||||
/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */
|
||||
fun <T : Model> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =
|
||||
created(controllerPath, producer())
|
||||
created(controllerPath, producer())
|
||||
|
||||
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
|
||||
fun <T> created(controllerPath: String, body: T, id: Any): ResponseEntity<T> =
|
||||
ResponseEntity.created(URI.create("$controllerPath/$id")).body(body)
|
||||
|
||||
/** Creates a HTTP NOT FOUND [ResponseEntity]. */
|
||||
fun <T> notFound(): ResponseEntity<T> =
|
||||
ResponseEntity.notFound().build()
|
||||
ResponseEntity.notFound().build()
|
||||
|
||||
/** Creates a HTTP NO CONTENT [ResponseEntity]. */
|
||||
fun noContent(): ResponseEntity<Void> =
|
||||
ResponseEntity.noContent().build()
|
||||
ResponseEntity.noContent().build()
|
||||
|
||||
/** Executes the given [action] then returns an HTTP NO CONTENT [ResponseEntity]. */
|
||||
fun noContent(action: () -> Unit): ResponseEntity<Void> {
|
||||
|
@ -45,12 +52,12 @@ fun noContent(action: () -> Unit): ResponseEntity<Void> {
|
|||
|
||||
/** Creates a HTTP FORBIDDEN [ResponseEntity]. */
|
||||
fun <T> forbidden(): ResponseEntity<T> =
|
||||
ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
ResponseEntity.status(HttpStatus.FORBIDDEN).build()
|
||||
|
||||
/** Creates an [HttpHeaders] instance from the given options. */
|
||||
fun httpHeaders(
|
||||
contentType: MediaType = MediaType.APPLICATION_JSON,
|
||||
op: HttpHeaders.() -> Unit = {}
|
||||
contentType: MediaType = MediaType.APPLICATION_JSON,
|
||||
op: HttpHeaders.() -> Unit = {}
|
||||
) = HttpHeaders().apply {
|
||||
this.contentType = contentType
|
||||
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.rest.noContent
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.FileService
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.net.URI
|
||||
|
||||
const val FILE_CONTROLLER_PATH = "/api/file"
|
||||
private const val DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_OCTET_STREAM_VALUE
|
||||
|
||||
@RestController
|
||||
@RequestMapping(FILE_CONTROLLER_PATH)
|
||||
class FileController(
|
||||
private val fileService: FileService,
|
||||
private val creProperties: CreProperties
|
||||
) {
|
||||
@GetMapping(produces = [MediaType.APPLICATION_OCTET_STREAM_VALUE])
|
||||
@PreAuthorize("hasAnyAuthority('READ_FILE')")
|
||||
fun upload(
|
||||
@RequestParam path: String,
|
||||
@RequestParam(required = false) mediaType: String?
|
||||
): ResponseEntity<ByteArrayResource> {
|
||||
val file = fileService.read(path)
|
||||
return ResponseEntity.ok()
|
||||
.header("Content-Disposition", "filename=${getFileNameFromPath(path)}")
|
||||
.contentLength(file.contentLength())
|
||||
.contentType(MediaType.parseMediaType(mediaType ?: DEFAULT_MEDIA_TYPE))
|
||||
.body(file)
|
||||
}
|
||||
|
||||
@PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
|
||||
@PreAuthorize("hasAnyAuthority('WRITE_FILE')")
|
||||
fun download(
|
||||
file: MultipartFile,
|
||||
@RequestParam path: String,
|
||||
@RequestParam(required = false) overwrite: Boolean = false
|
||||
): ResponseEntity<Void> {
|
||||
fileService.write(file, path, overwrite)
|
||||
return created(path)
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
@PreAuthorize("hasAnyAuthority('REMOVE_FILE')")
|
||||
fun delete(@RequestParam path: String): ResponseEntity<Void> {
|
||||
return noContent {
|
||||
fileService.delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
private fun created(path: String): ResponseEntity<Void> =
|
||||
ResponseEntity
|
||||
.created(URI.create("${creProperties.deploymentUrl}$FILE_CONTROLLER_PATH?path=$path"))
|
||||
.build()
|
||||
|
||||
private fun getFileNameFromPath(path: String) =
|
||||
path.split("/").last()
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package dev.fyloz.colorrecipesexplorer.rest.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.TouchUpKitService
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/touchup")
|
||||
@PreAuthorize("hasAuthority('GENERATE_TOUCH_UP_KIT')")
|
||||
class TouchUpKitController(
|
||||
private val touchUpKitService: TouchUpKitService
|
||||
) {
|
||||
@GetMapping
|
||||
fun getJobPdf(@RequestParam job: String): ResponseEntity<ByteArrayResource> {
|
||||
with(touchUpKitService.generateJobPdfResource(job)) {
|
||||
return ResponseEntity.ok()
|
||||
.header("Content-Disposition", "filename=TouchUpKit_$job.pdf")
|
||||
.contentLength(this.contentLength())
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(this)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,7 +20,8 @@ import javax.servlet.http.HttpServletRequest
|
|||
import javax.servlet.http.HttpServletResponse
|
||||
import javax.transaction.Transactional
|
||||
|
||||
interface EmployeeService : ExternalModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeRepository> {
|
||||
interface EmployeeService :
|
||||
ExternalModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeOutputDto, EmployeeRepository> {
|
||||
/** Check if an [Employee] with the given [firstName] and [lastName] exists. */
|
||||
fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean
|
||||
|
||||
|
@ -56,7 +57,7 @@ interface EmployeeService : ExternalModelService<Employee, EmployeeSaveDto, Empl
|
|||
}
|
||||
|
||||
interface EmployeeGroupService :
|
||||
ExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupRepository> {
|
||||
ExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupOutputDto, EmployeeGroupRepository> {
|
||||
/** Gets all the employees of the group with the given [id]. */
|
||||
fun getEmployeesForGroup(id: Long): Collection<Employee>
|
||||
|
||||
|
@ -74,50 +75,62 @@ interface EmployeeUserDetailsService : UserDetailsService {
|
|||
|
||||
@Service
|
||||
class EmployeeServiceImpl(
|
||||
employeeRepository: EmployeeRepository,
|
||||
@Lazy val groupService: EmployeeGroupService,
|
||||
@Lazy val passwordEncoder: PasswordEncoder,
|
||||
) : AbstractExternalModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeRepository>(employeeRepository),
|
||||
EmployeeService {
|
||||
employeeRepository: EmployeeRepository,
|
||||
@Lazy val groupService: EmployeeGroupService,
|
||||
@Lazy val passwordEncoder: PasswordEncoder,
|
||||
) : AbstractExternalModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeOutputDto, EmployeeRepository>(
|
||||
employeeRepository
|
||||
),
|
||||
EmployeeService {
|
||||
override fun idNotFoundException(id: Long) = employeeIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = employeeIdAlreadyExistsException(id)
|
||||
|
||||
override fun Employee.toOutput() = EmployeeOutputDto(
|
||||
this.id,
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.group,
|
||||
this.flatPermissions,
|
||||
this.permissions,
|
||||
this.lastLoginTime
|
||||
)
|
||||
|
||||
override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean =
|
||||
repository.existsByFirstNameAndLastName(firstName, lastName)
|
||||
repository.existsByFirstNameAndLastName(firstName, lastName)
|
||||
|
||||
override fun getAll(): Collection<Employee> =
|
||||
super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser }
|
||||
super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser }
|
||||
|
||||
override fun getById(id: Long): Employee =
|
||||
getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
|
||||
getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
|
||||
|
||||
override fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee =
|
||||
super.getById(id).apply {
|
||||
if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser)
|
||||
throw idNotFoundException(id)
|
||||
}
|
||||
super.getById(id).apply {
|
||||
if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser)
|
||||
throw idNotFoundException(id)
|
||||
}
|
||||
|
||||
override fun getByGroup(group: EmployeeGroup): Collection<Employee> =
|
||||
repository.findAllByGroup(group).filter {
|
||||
!it.isSystemUser && !it.isDefaultGroupUser
|
||||
}
|
||||
repository.findAllByGroup(group).filter {
|
||||
!it.isSystemUser && !it.isDefaultGroupUser
|
||||
}
|
||||
|
||||
override fun getDefaultGroupEmployee(group: EmployeeGroup): Employee =
|
||||
repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)
|
||||
repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)
|
||||
|
||||
override fun save(entity: EmployeeSaveDto): Employee =
|
||||
save(with(entity) {
|
||||
Employee(
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordEncoder.encode(password),
|
||||
isDefaultGroupUser = false,
|
||||
isSystemUser = false,
|
||||
group = if (groupId != null) groupService.getById(groupId) else null,
|
||||
permissions = permissions
|
||||
)
|
||||
})
|
||||
save(with(entity) {
|
||||
Employee(
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordEncoder.encode(password),
|
||||
isDefaultGroupUser = false,
|
||||
isSystemUser = false,
|
||||
group = if (groupId != null) groupService.getById(groupId) else null,
|
||||
permissions = permissions
|
||||
)
|
||||
})
|
||||
|
||||
override fun save(entity: Employee): Employee {
|
||||
if (existsById(entity.id))
|
||||
|
@ -129,14 +142,14 @@ class EmployeeServiceImpl(
|
|||
|
||||
override fun saveDefaultGroupEmployee(group: EmployeeGroup) {
|
||||
save(
|
||||
employee(
|
||||
id = 1000000L + group.id!!,
|
||||
firstName = group.name,
|
||||
lastName = "EmployeeModel",
|
||||
password = passwordEncoder.encode(group.name),
|
||||
group = group,
|
||||
isDefaultGroupUser = true
|
||||
)
|
||||
employee(
|
||||
id = 1000000L + group.id!!,
|
||||
firstName = group.name,
|
||||
lastName = "EmployeeModel",
|
||||
password = passwordEncoder.encode(group.name),
|
||||
group = group,
|
||||
isDefaultGroupUser = true
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -144,9 +157,9 @@ class EmployeeServiceImpl(
|
|||
val employee = getById(employeeId, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false)
|
||||
employee.lastLoginTime = time
|
||||
return update(
|
||||
employee,
|
||||
ignoreDefaultGroupUsers = true,
|
||||
ignoreSystemUsers = false
|
||||
employee,
|
||||
ignoreDefaultGroupUsers = true,
|
||||
ignoreSystemUsers = false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -154,21 +167,21 @@ class EmployeeServiceImpl(
|
|||
val persistedEmployee by lazy { getById(entity.id) }
|
||||
return update(with(entity) {
|
||||
Employee(
|
||||
id = id,
|
||||
firstName = firstName or persistedEmployee.firstName,
|
||||
lastName = lastName or persistedEmployee.lastName,
|
||||
password = persistedEmployee.password,
|
||||
isDefaultGroupUser = false,
|
||||
isSystemUser = false,
|
||||
group = if (entity.groupId != null) groupService.getById(entity.groupId) else persistedEmployee.group,
|
||||
permissions = permissions?.toMutableSet() ?: persistedEmployee.permissions,
|
||||
lastLoginTime = persistedEmployee.lastLoginTime
|
||||
id = id,
|
||||
firstName = firstName or persistedEmployee.firstName,
|
||||
lastName = lastName or persistedEmployee.lastName,
|
||||
password = persistedEmployee.password,
|
||||
isDefaultGroupUser = false,
|
||||
isSystemUser = false,
|
||||
group = if (entity.groupId != null) groupService.getById(entity.groupId) else persistedEmployee.group,
|
||||
permissions = permissions?.toMutableSet() ?: persistedEmployee.permissions,
|
||||
lastLoginTime = persistedEmployee.lastLoginTime
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
override fun update(entity: Employee): Employee =
|
||||
update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
|
||||
update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
|
||||
|
||||
override fun update(entity: Employee, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee {
|
||||
with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) {
|
||||
|
@ -183,24 +196,24 @@ class EmployeeServiceImpl(
|
|||
val persistedEmployee = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
|
||||
return super<AbstractExternalModelService>.update(with(persistedEmployee) {
|
||||
Employee(
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordEncoder.encode(password),
|
||||
isDefaultGroupUser,
|
||||
isSystemUser,
|
||||
group,
|
||||
permissions,
|
||||
lastLoginTime
|
||||
id,
|
||||
firstName,
|
||||
lastName,
|
||||
passwordEncoder.encode(password),
|
||||
isDefaultGroupUser,
|
||||
isSystemUser,
|
||||
group,
|
||||
permissions,
|
||||
lastLoginTime
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
override fun addPermission(employeeId: Long, permission: EmployeePermission): Employee =
|
||||
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions += permission })
|
||||
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions += permission })
|
||||
|
||||
override fun removePermission(employeeId: Long, permission: EmployeePermission): Employee =
|
||||
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions -= permission })
|
||||
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions -= permission })
|
||||
|
||||
override fun logout(request: HttpServletRequest) {
|
||||
val authorizationCookie = WebUtils.getCookie(request, "Authorization")
|
||||
|
@ -217,20 +230,27 @@ const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
|
|||
|
||||
@Service
|
||||
class EmployeeGroupServiceImpl(
|
||||
private val employeeService: EmployeeService,
|
||||
employeeGroupRepository: EmployeeGroupRepository
|
||||
) : AbstractExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupRepository>(
|
||||
employeeGroupRepository
|
||||
private val employeeService: EmployeeService,
|
||||
employeeGroupRepository: EmployeeGroupRepository
|
||||
) : AbstractExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupOutputDto, EmployeeGroupRepository>(
|
||||
employeeGroupRepository
|
||||
),
|
||||
EmployeeGroupService {
|
||||
EmployeeGroupService {
|
||||
override fun idNotFoundException(id: Long) = employeeGroupIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = employeeGroupIdAlreadyExistsException(id)
|
||||
override fun nameNotFoundException(name: String) = employeeGroupNameNotFoundException(name)
|
||||
override fun nameAlreadyExistsException(name: String) = employeeGroupNameAlreadyExistsException(name)
|
||||
|
||||
override fun EmployeeGroup.toOutput() = EmployeeGroupOutputDto(
|
||||
this.id!!,
|
||||
this.name,
|
||||
this.permissions,
|
||||
this.flatPermissions
|
||||
)
|
||||
|
||||
override fun existsByName(name: String): Boolean = repository.existsByName(name)
|
||||
override fun getEmployeesForGroup(id: Long): Collection<Employee> =
|
||||
employeeService.getByGroup(getById(id))
|
||||
employeeService.getByGroup(getById(id))
|
||||
|
||||
@Transactional
|
||||
override fun save(entity: EmployeeGroup): EmployeeGroup {
|
||||
|
@ -243,9 +263,9 @@ class EmployeeGroupServiceImpl(
|
|||
val persistedGroup by lazy { getById(entity.id) }
|
||||
return update(with(entity) {
|
||||
EmployeeGroup(
|
||||
entity.id,
|
||||
if (name.isNotBlank()) entity.name else persistedGroup.name,
|
||||
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
|
||||
entity.id,
|
||||
if (name.isNotBlank()) entity.name else persistedGroup.name,
|
||||
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -258,11 +278,11 @@ class EmployeeGroupServiceImpl(
|
|||
|
||||
override fun getRequestDefaultGroup(request: HttpServletRequest): EmployeeGroup {
|
||||
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
|
||||
?: throw NoDefaultGroupException()
|
||||
?: throw NoDefaultGroupException()
|
||||
val defaultGroupUser = employeeService.getById(
|
||||
defaultGroupCookie.value.toLong(),
|
||||
ignoreDefaultGroupUsers = false,
|
||||
ignoreSystemUsers = true
|
||||
defaultGroupCookie.value.toLong(),
|
||||
ignoreDefaultGroupUsers = false,
|
||||
ignoreSystemUsers = true
|
||||
)
|
||||
return defaultGroupUser.group!!
|
||||
}
|
||||
|
@ -271,17 +291,17 @@ class EmployeeGroupServiceImpl(
|
|||
val group = getById(groupId)
|
||||
val defaultGroupUser = employeeService.getDefaultGroupEmployee(group)
|
||||
response.addHeader(
|
||||
"Set-Cookie",
|
||||
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict"
|
||||
"Set-Cookie",
|
||||
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
class EmployeeUserDetailsServiceImpl(
|
||||
private val employeeService: EmployeeService
|
||||
private val employeeService: EmployeeService
|
||||
) :
|
||||
EmployeeUserDetailsService {
|
||||
EmployeeUserDetailsService {
|
||||
override fun loadUserByUsername(username: String): UserDetails {
|
||||
try {
|
||||
return loadUserByEmployeeId(username.toLong(), true)
|
||||
|
@ -294,9 +314,9 @@ class EmployeeUserDetailsServiceImpl(
|
|||
|
||||
override fun loadUserByEmployeeId(employeeId: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
|
||||
val employee = employeeService.getById(
|
||||
employeeId,
|
||||
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
|
||||
ignoreSystemUsers = false
|
||||
employeeId,
|
||||
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
|
||||
ignoreSystemUsers = false
|
||||
)
|
||||
return User(employee.id.toString(), employee.password, employee.authorities)
|
||||
}
|
||||
|
|
|
@ -5,23 +5,28 @@ import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository
|
|||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
interface CompanyService : ExternalNamedModelService<Company, CompanySaveDto, CompanyUpdateDto, CompanyRepository> {
|
||||
interface CompanyService :
|
||||
ExternalNamedModelService<Company, CompanySaveDto, CompanyUpdateDto, Company, CompanyRepository> {
|
||||
/** Checks if the given [company] is used by one or more recipes. */
|
||||
fun isLinkedToRecipes(company: Company): Boolean
|
||||
}
|
||||
|
||||
@Service
|
||||
class CompanyServiceImpl(
|
||||
companyRepository: CompanyRepository,
|
||||
@Lazy val recipeService: RecipeService
|
||||
companyRepository: CompanyRepository,
|
||||
@Lazy val recipeService: RecipeService
|
||||
) :
|
||||
AbstractExternalNamedModelService<Company, CompanySaveDto, CompanyUpdateDto, CompanyRepository>(companyRepository),
|
||||
CompanyService {
|
||||
AbstractExternalNamedModelService<Company, CompanySaveDto, CompanyUpdateDto, Company, CompanyRepository>(
|
||||
companyRepository
|
||||
),
|
||||
CompanyService {
|
||||
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)
|
||||
|
||||
override fun Company.toOutput() = this
|
||||
|
||||
override fun isLinkedToRecipes(company: Company): Boolean = recipeService.existsByCompany(company)
|
||||
|
||||
override fun update(entity: CompanyUpdateDto): Company {
|
||||
|
@ -30,8 +35,8 @@ class CompanyServiceImpl(
|
|||
|
||||
return update(with(entity) {
|
||||
company(
|
||||
id = id,
|
||||
name = if (name != null && name.isNotBlank()) name else persistedCompany.name
|
||||
id = id,
|
||||
name = if (name != null && name.isNotBlank()) name else persistedCompany.name
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package dev.fyloz.colorrecipesexplorer.service
|
|||
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.service.utils.mapMayThrow
|
||||
import dev.fyloz.colorrecipesexplorer.utils.mapMayThrow
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import javax.transaction.Transactional
|
||||
|
|
|
@ -1,35 +1,32 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.SimdutService
|
||||
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
||||
import dev.fyloz.colorrecipesexplorer.rest.files.FILE_CONTROLLER_PATH
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.FileService
|
||||
import io.jsonwebtoken.lang.Assert
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.stereotype.Service
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
interface MaterialService :
|
||||
ExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialRepository> {
|
||||
ExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialOutputDto, MaterialRepository> {
|
||||
/** Checks if a material with the given [materialType] exists. */
|
||||
fun existsByMaterialType(materialType: MaterialType): Boolean
|
||||
|
||||
/** Checks if the material with the given [id] has a SIMDUT file. */
|
||||
fun hasSimdut(id: Long): Boolean
|
||||
|
||||
/** Gets the SIMDUT file of the material with the given [id]. */
|
||||
fun getSimdut(id: Long): ByteArray
|
||||
/** Checks if the given [material] has a SIMDUT file. */
|
||||
fun hasSimdut(material: Material): Boolean
|
||||
|
||||
/** Gets all materials that are not a mix type. */
|
||||
fun getAllNotMixType(): Collection<Material>
|
||||
fun getAllNotMixType(): Collection<MaterialOutputDto>
|
||||
|
||||
/** Gets all materials available for the creation of a mix for the recipe with the given [recipeId], including normal materials and materials from [MixType]s included in the said recipe. */
|
||||
fun getAllForMixCreation(recipeId: Long): Collection<Material>
|
||||
fun getAllForMixCreation(recipeId: Long): Collection<MaterialOutputDto>
|
||||
|
||||
/** Gets all materials available for updating the mix with the given [mixId], including normal materials and materials from [MixType]s included in the mix recipe, excluding the material of the [MixType] of the said mix. */
|
||||
fun getAllForMixUpdate(mixId: Long): Collection<Material>
|
||||
|
||||
/** Gets the identifier of materials for which a SIMDUT exists. */
|
||||
fun getAllIdsWithSimdut(): Collection<Long>
|
||||
fun getAllForMixUpdate(mixId: Long): Collection<MaterialOutputDto>
|
||||
|
||||
/** Updates the quantity of the given [material] with the given [factor] and returns the updated quantity. */
|
||||
fun updateQuantity(material: Material, factor: Float): Float
|
||||
|
@ -37,44 +34,59 @@ interface MaterialService :
|
|||
|
||||
@Service
|
||||
class MaterialServiceImpl(
|
||||
materialRepository: MaterialRepository,
|
||||
val simdutService: SimdutService,
|
||||
val recipeService: RecipeService,
|
||||
val mixService: MixService,
|
||||
@Lazy val materialTypeService: MaterialTypeService
|
||||
materialRepository: MaterialRepository,
|
||||
val recipeService: RecipeService,
|
||||
val mixService: MixService,
|
||||
@Lazy val materialTypeService: MaterialTypeService,
|
||||
val fileService: FileService
|
||||
) :
|
||||
AbstractExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialRepository>(
|
||||
materialRepository
|
||||
),
|
||||
MaterialService {
|
||||
AbstractExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialOutputDto, MaterialRepository>(
|
||||
materialRepository
|
||||
),
|
||||
MaterialService {
|
||||
override fun idNotFoundException(id: Long) = materialIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id)
|
||||
override fun nameNotFoundException(name: String) = materialNameNotFoundException(name)
|
||||
override fun nameAlreadyExistsException(name: String) = materialNameAlreadyExistsException(name)
|
||||
|
||||
override fun Material.toOutput(): MaterialOutputDto =
|
||||
MaterialOutputDto(
|
||||
id = this.id!!,
|
||||
name = this.name,
|
||||
inventoryQuantity = this.inventoryQuantity,
|
||||
isMixType = this.isMixType,
|
||||
materialType = this.materialType!!,
|
||||
simdutUrl = if (fileService.exists(this.simdutFilePath))
|
||||
"${CRE_PROPERTIES.deploymentUrl}$FILE_CONTROLLER_PATH?path=${
|
||||
URLEncoder.encode(
|
||||
this.simdutFilePath,
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
}"
|
||||
else null
|
||||
)
|
||||
|
||||
override fun existsByMaterialType(materialType: MaterialType): Boolean =
|
||||
repository.existsByMaterialType(materialType)
|
||||
repository.existsByMaterialType(materialType)
|
||||
|
||||
override fun hasSimdut(id: Long): Boolean = simdutService.exists(getById(id))
|
||||
override fun getSimdut(id: Long): ByteArray = simdutService.read(getById(id))
|
||||
override fun getAllNotMixType(): Collection<Material> = getAll().filter { !it.isMixType }
|
||||
|
||||
override fun getAllIdsWithSimdut(): Collection<Long> =
|
||||
getAllNotMixType()
|
||||
.filter { simdutService.exists(it) }
|
||||
.map { it.id!! }
|
||||
override fun hasSimdut(material: Material): Boolean = fileService.exists(material.simdutFilePath)
|
||||
override fun getAllNotMixType(): Collection<MaterialOutputDto> = getAllForOutput().filter { !it.isMixType }
|
||||
|
||||
override fun save(entity: MaterialSaveDto): Material =
|
||||
save(with(entity) {
|
||||
material(
|
||||
name = entity.name,
|
||||
inventoryQuantity = entity.inventoryQuantity,
|
||||
materialType = materialTypeService.getById(materialTypeId),
|
||||
isMixType = false
|
||||
)
|
||||
}).apply {
|
||||
if (entity.simdutFile != null && !entity.simdutFile.isEmpty) simdutService.write(this, entity.simdutFile)
|
||||
}
|
||||
save(with(entity) {
|
||||
material(
|
||||
name = entity.name,
|
||||
inventoryQuantity = entity.inventoryQuantity,
|
||||
materialType = materialTypeService.getById(materialTypeId),
|
||||
isMixType = false
|
||||
)
|
||||
}).apply {
|
||||
if (entity.simdutFile != null && !entity.simdutFile.isEmpty) fileService.write(
|
||||
entity.simdutFile,
|
||||
this.simdutFilePath,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
override fun update(entity: MaterialUpdateDto): Material {
|
||||
val persistedMaterial by lazy {
|
||||
|
@ -83,14 +95,18 @@ class MaterialServiceImpl(
|
|||
|
||||
return update(with(entity) {
|
||||
material(
|
||||
id = id,
|
||||
name = if (name != null && name.isNotBlank()) name else persistedMaterial.name,
|
||||
inventoryQuantity = if (inventoryQuantity != null && inventoryQuantity != Float.MIN_VALUE) inventoryQuantity else persistedMaterial.inventoryQuantity,
|
||||
isMixType = persistedMaterial.isMixType,
|
||||
materialType = if (materialTypeId != null) materialTypeService.getById(materialTypeId) else persistedMaterial.materialType
|
||||
id = id,
|
||||
name = if (name != null && name.isNotBlank()) name else persistedMaterial.name,
|
||||
inventoryQuantity = if (inventoryQuantity != null && inventoryQuantity != Float.MIN_VALUE) inventoryQuantity else persistedMaterial.inventoryQuantity,
|
||||
isMixType = persistedMaterial.isMixType,
|
||||
materialType = if (materialTypeId != null) materialTypeService.getById(materialTypeId) else persistedMaterial.materialType
|
||||
)
|
||||
}).apply {
|
||||
if (entity.simdutFile != null && !entity.simdutFile.isEmpty) simdutService.update(entity.simdutFile, this)
|
||||
if (entity.simdutFile != null && !entity.simdutFile.isEmpty) fileService.write(
|
||||
entity.simdutFile,
|
||||
this.simdutFilePath,
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,18 +116,18 @@ class MaterialServiceImpl(
|
|||
updatedQuantity
|
||||
}
|
||||
|
||||
override fun getAllForMixCreation(recipeId: Long): Collection<Material> {
|
||||
override fun getAllForMixCreation(recipeId: Long): Collection<MaterialOutputDto> {
|
||||
val recipesMixTypes = recipeService.getById(recipeId).mixTypes
|
||||
return getAll()
|
||||
.filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } }
|
||||
return getAllForOutput()
|
||||
.filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } }
|
||||
}
|
||||
|
||||
override fun getAllForMixUpdate(mixId: Long): Collection<Material> {
|
||||
override fun getAllForMixUpdate(mixId: Long): Collection<MaterialOutputDto> {
|
||||
val mix = mixService.getById(mixId)
|
||||
val recipesMixTypes = mix.recipe.mixTypes
|
||||
return getAll()
|
||||
.filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } }
|
||||
.filter { it.id != mix.mixType.material.id }
|
||||
return getAllForOutput()
|
||||
.filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } }
|
||||
.filter { it.id != mix.mixType.material.id }
|
||||
}
|
||||
|
||||
private fun assertPersistedMaterial(material: Material) {
|
||||
|
@ -120,6 +136,7 @@ class MaterialServiceImpl(
|
|||
|
||||
override fun delete(entity: Material) {
|
||||
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialException(entity)
|
||||
fileService.delete(entity.simdutFilePath)
|
||||
super.delete(entity)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
|
|||
import org.springframework.stereotype.Service
|
||||
|
||||
interface MaterialTypeService :
|
||||
ExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeRepository> {
|
||||
ExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository> {
|
||||
/** Checks if a material type with the given [prefix] exists. */
|
||||
fun existsByPrefix(prefix: String): Boolean
|
||||
|
||||
|
@ -27,7 +27,7 @@ interface MaterialTypeService :
|
|||
|
||||
@Service
|
||||
class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val materialService: MaterialService) :
|
||||
AbstractExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeRepository>(
|
||||
AbstractExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialType, MaterialTypeRepository>(
|
||||
repository
|
||||
), MaterialTypeService {
|
||||
override fun idNotFoundException(id: Long) = materialTypeIdNotFoundException(id)
|
||||
|
@ -35,6 +35,8 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma
|
|||
override fun nameNotFoundException(name: String) = materialTypeNameNotFoundException(name)
|
||||
override fun nameAlreadyExistsException(name: String) = materialTypeNameAlreadyExistsException(name)
|
||||
|
||||
override fun MaterialType.toOutput() = this
|
||||
|
||||
override fun existsByPrefix(prefix: String): Boolean = repository.existsByPrefix(prefix)
|
||||
override fun isUsedByMaterial(materialType: MaterialType): Boolean =
|
||||
materialService.existsByMaterialType(materialType)
|
||||
|
|
|
@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.service
|
|||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.utils.findDuplicated
|
||||
import dev.fyloz.colorrecipesexplorer.service.utils.hasGaps
|
||||
import dev.fyloz.colorrecipesexplorer.utils.findDuplicated
|
||||
import dev.fyloz.colorrecipesexplorer.utils.hasGaps
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
|
@ -28,31 +28,40 @@ interface MixMaterialService : ModelService<MixMaterial, MixMaterialRepository>
|
|||
* If any of those criteria are not met, an [InvalidGroupStepsPositionsException] will be thrown.
|
||||
*/
|
||||
fun validateMixMaterials(mixMaterials: Set<MixMaterial>)
|
||||
|
||||
fun MixMaterial.toOutput(): MixMaterialOutputDto
|
||||
}
|
||||
|
||||
@Service
|
||||
class MixMaterialServiceImpl(
|
||||
mixMaterialRepository: MixMaterialRepository,
|
||||
@Lazy val materialService: MaterialService
|
||||
mixMaterialRepository: MixMaterialRepository,
|
||||
@Lazy val materialService: MaterialService
|
||||
) : AbstractModelService<MixMaterial, MixMaterialRepository>(mixMaterialRepository), MixMaterialService {
|
||||
override fun idNotFoundException(id: Long) = mixMaterialIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = mixMaterialIdAlreadyExistsException(id)
|
||||
|
||||
override fun MixMaterial.toOutput() = MixMaterialOutputDto(
|
||||
this.id!!,
|
||||
with(materialService) { this@toOutput.material.toOutput() },
|
||||
this.quantity,
|
||||
this.position
|
||||
)
|
||||
|
||||
override fun existsByMaterial(material: Material): Boolean = repository.existsByMaterial(material)
|
||||
override fun create(mixMaterials: Set<MixMaterialDto>): Set<MixMaterial> =
|
||||
mixMaterials.map(::create).toSet()
|
||||
mixMaterials.map(::create).toSet()
|
||||
|
||||
override fun create(mixMaterial: MixMaterialDto): MixMaterial =
|
||||
mixMaterial(
|
||||
material = materialService.getById(mixMaterial.materialId),
|
||||
quantity = mixMaterial.quantity,
|
||||
position = mixMaterial.position
|
||||
)
|
||||
mixMaterial(
|
||||
material = materialService.getById(mixMaterial.materialId),
|
||||
quantity = mixMaterial.quantity,
|
||||
position = mixMaterial.position
|
||||
)
|
||||
|
||||
override fun updateQuantity(mixMaterial: MixMaterial, quantity: Float) =
|
||||
update(mixMaterial.apply {
|
||||
this.quantity = quantity
|
||||
})
|
||||
update(mixMaterial.apply {
|
||||
this.quantity = quantity
|
||||
})
|
||||
|
||||
override fun validateMixMaterials(mixMaterials: Set<MixMaterial>) {
|
||||
if (mixMaterials.isEmpty()) return
|
||||
|
@ -63,17 +72,17 @@ class MixMaterialServiceImpl(
|
|||
|
||||
// Check if the first mix material position is 1
|
||||
fun isFirstMixMaterialPositionInvalid() =
|
||||
sortedMixMaterials[0].position != 1
|
||||
sortedMixMaterials[0].position != 1
|
||||
|
||||
// Check if the first mix material is expressed in percents
|
||||
fun isFirstMixMaterialPercentages() =
|
||||
sortedMixMaterials[0].material.materialType!!.usePercentages
|
||||
sortedMixMaterials[0].material.materialType!!.usePercentages
|
||||
|
||||
// Check if any positions is duplicated
|
||||
fun getDuplicatedPositionsErrors() =
|
||||
sortedMixMaterials
|
||||
.findDuplicated { it.position }
|
||||
.map { duplicatedMixMaterialsPositions(it) }
|
||||
sortedMixMaterials
|
||||
.findDuplicated { it.position }
|
||||
.map { duplicatedMixMaterialsPositions(it) }
|
||||
|
||||
// Find all errors and throw if there is any
|
||||
if (isFirstMixMaterialPositionInvalid()) errors += invalidFirstMixMaterialPosition(sortedMixMaterials[0])
|
||||
|
@ -90,32 +99,32 @@ class MixMaterialServiceImpl(
|
|||
}
|
||||
|
||||
class InvalidMixMaterialsPositionsError(
|
||||
val type: String,
|
||||
val details: String
|
||||
val type: String,
|
||||
val details: String
|
||||
)
|
||||
|
||||
class InvalidMixMaterialsPositionsException(
|
||||
val errors: Set<InvalidMixMaterialsPositionsError>
|
||||
val errors: Set<InvalidMixMaterialsPositionsError>
|
||||
) : RestException(
|
||||
"invalid-mixmaterial-position",
|
||||
"Invalid mix materials positions",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The position of mix materials are invalid",
|
||||
mapOf(
|
||||
"invalidMixMaterials" to errors
|
||||
)
|
||||
"invalid-mixmaterial-position",
|
||||
"Invalid mix materials positions",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The position of mix materials are invalid",
|
||||
mapOf(
|
||||
"invalidMixMaterials" to errors
|
||||
)
|
||||
)
|
||||
|
||||
class InvalidFirstMixMaterial(
|
||||
val mixMaterial: MixMaterial
|
||||
val mixMaterial: MixMaterial
|
||||
) : RestException(
|
||||
"invalid-mixmaterial-first",
|
||||
"Invalid first mix material",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The first mix material is invalid because its material must not be expressed in percents",
|
||||
mapOf(
|
||||
"mixMaterial" to mixMaterial
|
||||
)
|
||||
"invalid-mixmaterial-first",
|
||||
"Invalid first mix material",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The first mix material is invalid because its material must not be expressed in percents",
|
||||
mapOf(
|
||||
"mixMaterial" to mixMaterial
|
||||
)
|
||||
)
|
||||
|
||||
const val INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE = "first"
|
||||
|
@ -123,20 +132,20 @@ const val DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE = "duplicated"
|
|||
const val GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE = "gap"
|
||||
|
||||
private fun invalidFirstMixMaterialPosition(mixMaterial: MixMaterial) =
|
||||
InvalidMixMaterialsPositionsError(
|
||||
INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE,
|
||||
"The position ${mixMaterial.position} is under the minimum of 1"
|
||||
)
|
||||
InvalidMixMaterialsPositionsError(
|
||||
INVALID_FIRST_MIX_MATERIAL_POSITION_ERROR_CODE,
|
||||
"The position ${mixMaterial.position} is under the minimum of 1"
|
||||
)
|
||||
|
||||
private fun duplicatedMixMaterialsPositions(position: Int) =
|
||||
InvalidMixMaterialsPositionsError(
|
||||
DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE,
|
||||
"The position $position is duplicated"
|
||||
)
|
||||
InvalidMixMaterialsPositionsError(
|
||||
DUPLICATED_MIX_MATERIALS_POSITIONS_ERROR_CODE,
|
||||
"The position $position is duplicated"
|
||||
)
|
||||
|
||||
private fun gapBetweenStepsPositions() =
|
||||
InvalidMixMaterialsPositionsError(
|
||||
GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE,
|
||||
"There is a gap between mix materials positions"
|
||||
)
|
||||
InvalidMixMaterialsPositionsError(
|
||||
GAP_BETWEEN_MIX_MATERIALS_POSITIONS_ERROR_CODE,
|
||||
"There is a gap between mix materials positions"
|
||||
)
|
||||
|
||||
|
|
|
@ -2,12 +2,12 @@ package dev.fyloz.colorrecipesexplorer.service
|
|||
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.MixRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.utils.setAll
|
||||
import dev.fyloz.colorrecipesexplorer.utils.setAll
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.stereotype.Service
|
||||
import javax.transaction.Transactional
|
||||
|
||||
interface MixService : ExternalModelService<Mix, MixSaveDto, MixUpdateDto, MixRepository> {
|
||||
interface MixService : ExternalModelService<Mix, MixSaveDto, MixUpdateDto, MixOutputDto, MixRepository> {
|
||||
/** Gets all mixes with the given [mixType]. */
|
||||
fun getAllByMixType(mixType: MixType): Collection<Mix>
|
||||
|
||||
|
@ -23,19 +23,30 @@ interface MixService : ExternalModelService<Mix, MixSaveDto, MixUpdateDto, MixRe
|
|||
|
||||
@Service
|
||||
class MixServiceImpl(
|
||||
mixRepository: MixRepository,
|
||||
@Lazy val recipeService: RecipeService,
|
||||
@Lazy val materialTypeService: MaterialTypeService,
|
||||
val mixMaterialService: MixMaterialService,
|
||||
val mixTypeService: MixTypeService
|
||||
) : AbstractModelService<Mix, MixRepository>(mixRepository),
|
||||
MixService {
|
||||
mixRepository: MixRepository,
|
||||
@Lazy val recipeService: RecipeService,
|
||||
@Lazy val materialTypeService: MaterialTypeService,
|
||||
val mixMaterialService: MixMaterialService,
|
||||
val mixTypeService: MixTypeService
|
||||
) : AbstractExternalModelService<Mix, MixSaveDto, MixUpdateDto, MixOutputDto, MixRepository>(mixRepository),
|
||||
MixService {
|
||||
override fun idNotFoundException(id: Long) = mixIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = mixIdAlreadyExistsException(id)
|
||||
|
||||
override fun getAllByMixType(mixType: MixType): Collection<Mix> = repository.findAllByMixType(mixType)
|
||||
override fun mixTypeIsShared(mixType: MixType): Boolean = getAllByMixType(mixType).count() > 1
|
||||
|
||||
override fun Mix.toOutput() = MixOutputDto(
|
||||
this.id!!,
|
||||
this.location,
|
||||
this.mixType,
|
||||
this.mixMaterials.map {
|
||||
with(mixMaterialService) {
|
||||
return@with it.toOutput()
|
||||
}
|
||||
}.toSet()
|
||||
)
|
||||
|
||||
@Transactional
|
||||
override fun save(entity: MixSaveDto): Mix {
|
||||
val recipe = recipeService.getById(entity.recipeId)
|
||||
|
|
|
@ -4,15 +4,15 @@ import dev.fyloz.colorrecipesexplorer.model.*
|
|||
import dev.fyloz.colorrecipesexplorer.model.validation.or
|
||||
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.FileService
|
||||
import dev.fyloz.colorrecipesexplorer.service.utils.setAll
|
||||
import dev.fyloz.colorrecipesexplorer.utils.setAll
|
||||
import org.springframework.context.annotation.Lazy
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.File
|
||||
import java.nio.file.NoSuchFileException
|
||||
import javax.transaction.Transactional
|
||||
|
||||
interface RecipeService : ExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeRepository> {
|
||||
interface RecipeService :
|
||||
ExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeOutputDto, RecipeRepository> {
|
||||
/** Checks if one or more recipes have the given [company]. */
|
||||
fun existsByCompany(company: Company): Boolean
|
||||
|
||||
|
@ -31,17 +31,41 @@ interface RecipeService : ExternalModelService<Recipe, RecipeSaveDto, RecipeUpda
|
|||
|
||||
@Service
|
||||
class RecipeServiceImpl(
|
||||
recipeRepository: RecipeRepository,
|
||||
val companyService: CompanyService,
|
||||
val mixService: MixService,
|
||||
val recipeStepService: RecipeStepService,
|
||||
@Lazy val groupService: EmployeeGroupService
|
||||
recipeRepository: RecipeRepository,
|
||||
val companyService: CompanyService,
|
||||
val mixService: MixService,
|
||||
val recipeStepService: RecipeStepService,
|
||||
@Lazy val groupService: EmployeeGroupService,
|
||||
val recipeImageService: RecipeImageService
|
||||
) :
|
||||
AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeRepository>(recipeRepository),
|
||||
RecipeService {
|
||||
AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeOutputDto, RecipeRepository>(
|
||||
recipeRepository
|
||||
),
|
||||
RecipeService {
|
||||
override fun idNotFoundException(id: Long) = recipeIdNotFoundException(id)
|
||||
override fun idAlreadyExistsException(id: Long) = recipeIdAlreadyExistsException(id)
|
||||
|
||||
override fun Recipe.toOutput() = RecipeOutputDto(
|
||||
this.id!!,
|
||||
this.name,
|
||||
this.description,
|
||||
this.color,
|
||||
this.gloss,
|
||||
this.sample,
|
||||
this.approbationDate,
|
||||
this.remark,
|
||||
this.company,
|
||||
this.mixes.map {
|
||||
with(mixService) {
|
||||
it.toOutput()
|
||||
}
|
||||
}.toSet(),
|
||||
this.groupsInformation,
|
||||
recipeImageService.getAllImages(this)
|
||||
.map { this.imageUrl(it) }
|
||||
.toSet()
|
||||
)
|
||||
|
||||
override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company)
|
||||
override fun getAllByCompany(company: Company): Collection<Recipe> = repository.findAllByCompany(company)
|
||||
|
||||
|
@ -49,14 +73,14 @@ class RecipeServiceImpl(
|
|||
// TODO checks if name is unique in the scope of the [company]
|
||||
return save(with(entity) {
|
||||
recipe(
|
||||
name = name,
|
||||
description = description,
|
||||
color = color,
|
||||
gloss = gloss,
|
||||
sample = sample,
|
||||
approbationDate = approbationDate,
|
||||
remark = remark ?: "",
|
||||
company = companyService.getById(companyId)
|
||||
name = name,
|
||||
description = description,
|
||||
color = color,
|
||||
gloss = gloss,
|
||||
sample = sample,
|
||||
approbationDate = approbationDate,
|
||||
remark = remark ?: "",
|
||||
company = companyService.getById(companyId)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -67,17 +91,17 @@ class RecipeServiceImpl(
|
|||
|
||||
return update(with(entity) {
|
||||
recipe(
|
||||
id = id,
|
||||
name = name or persistedRecipe.name,
|
||||
description = description or persistedRecipe.description,
|
||||
color = color or persistedRecipe.color,
|
||||
gloss = gloss ?: persistedRecipe.gloss,
|
||||
sample = sample ?: persistedRecipe.sample,
|
||||
approbationDate = approbationDate ?: persistedRecipe.approbationDate,
|
||||
remark = remark or persistedRecipe.remark,
|
||||
company = persistedRecipe.company,
|
||||
mixes = persistedRecipe.mixes,
|
||||
groupsInformation = updateGroupsInformation(persistedRecipe, entity)
|
||||
id = id,
|
||||
name = name or persistedRecipe.name,
|
||||
description = description or persistedRecipe.description,
|
||||
color = color or persistedRecipe.color,
|
||||
gloss = gloss ?: persistedRecipe.gloss,
|
||||
sample = sample ?: persistedRecipe.sample,
|
||||
approbationDate = approbationDate ?: persistedRecipe.approbationDate,
|
||||
remark = remark or persistedRecipe.remark,
|
||||
company = persistedRecipe.company,
|
||||
mixes = persistedRecipe.mixes,
|
||||
groupsInformation = updateGroupsInformation(persistedRecipe, entity)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
@ -96,8 +120,8 @@ class RecipeServiceImpl(
|
|||
this.steps = it.steps.toMutableSet()
|
||||
}
|
||||
} ?: recipeGroupInformation(
|
||||
group = groupService.getById(it.groupId),
|
||||
steps = it.steps.toMutableSet()
|
||||
group = groupService.getById(it.groupId),
|
||||
steps = it.steps.toMutableSet()
|
||||
)
|
||||
|
||||
updatedGroupsInformation.add(updatedGroupInformation)
|
||||
|
@ -114,7 +138,7 @@ class RecipeServiceImpl(
|
|||
val recipe = getById(publicDataDto.recipeId)
|
||||
|
||||
fun noteForGroup(group: EmployeeGroup) =
|
||||
publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content
|
||||
publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content
|
||||
|
||||
// Notes
|
||||
recipe.groupsInformation.map {
|
||||
|
@ -133,68 +157,74 @@ class RecipeServiceImpl(
|
|||
}
|
||||
|
||||
override fun addMix(recipe: Recipe, mix: Mix) =
|
||||
update(recipe.apply { mixes.add(mix) })
|
||||
update(recipe.apply { mixes.add(mix) })
|
||||
|
||||
override fun removeMix(mix: Mix): Recipe =
|
||||
update(mix.recipe.apply { mixes.remove(mix) })
|
||||
update(mix.recipe.apply { mixes.remove(mix) })
|
||||
}
|
||||
|
||||
const val RECIPE_IMAGES_DIRECTORY = "images/recipe"
|
||||
|
||||
interface RecipeImageService {
|
||||
fun getByIdForRecipe(id: Long, recipeId: Long): ByteArray
|
||||
/** Gets the name of every images associated to the recipe with the given [recipe]. */
|
||||
fun getAllImages(recipe: Recipe): Set<String>
|
||||
|
||||
/** Gets the identifier of every images associated to the recipe with the given [recipeId]. */
|
||||
fun getAllIdsForRecipe(recipeId: Long): Collection<Long>
|
||||
/** Saves the given [image] and associate it to the recipe with the given [recipe]. Returns the name of the saved image. */
|
||||
fun download(image: MultipartFile, recipe: Recipe): String
|
||||
|
||||
/** Saves the given [image] and associate it to the recipe with the given [recipeId]. Returns the identifier of the saved image. */
|
||||
fun save(image: MultipartFile, recipeId: Long): Long
|
||||
/** Deletes the image with the given [name] for the given [recipe]. */
|
||||
fun delete(recipe: Recipe, name: String)
|
||||
|
||||
/** Deletes the image with the given [recipeId] and [id]. */
|
||||
fun delete(id: Long, recipeId: Long)
|
||||
/** Gets the directory containing all images of the given [Recipe]. */
|
||||
fun Recipe.getDirectory(): File
|
||||
}
|
||||
|
||||
const val RECIPE_IMAGE_ID_DELIMITER = "_"
|
||||
const val RECIPE_IMAGE_EXTENSION = ".jpg"
|
||||
|
||||
@Service
|
||||
class RecipeImageServiceImpl(val recipeService: RecipeService, val fileService: FileService) : RecipeImageService {
|
||||
override fun getByIdForRecipe(id: Long, recipeId: Long): ByteArray =
|
||||
try {
|
||||
fileService.readAsBytes(getPath(id, recipeId))
|
||||
} catch (ex: NoSuchFileException) {
|
||||
throw RecipeImageNotFoundException(id, recipeService.getById(recipeId))
|
||||
}
|
||||
|
||||
override fun getAllIdsForRecipe(recipeId: Long): Collection<Long> {
|
||||
val recipe = recipeService.getById(recipeId)
|
||||
val recipeDirectory = getRecipeDirectory(recipe.id!!)
|
||||
class RecipeImageServiceImpl(
|
||||
val fileService: FileService
|
||||
) : RecipeImageService {
|
||||
override fun getAllImages(recipe: Recipe): Set<String> {
|
||||
val recipeDirectory = recipe.getDirectory()
|
||||
if (!recipeDirectory.exists() || !recipeDirectory.isDirectory) {
|
||||
return listOf()
|
||||
return setOf()
|
||||
}
|
||||
return recipeDirectory.listFiles()!! // Should never be null because we check if recipeDirectory is a directory and exists before
|
||||
.filterNotNull()
|
||||
.map { it.name.toLong() }
|
||||
return recipeDirectory.listFiles()!! // Should never be null because we check if recipeDirectory exists and is a directory before
|
||||
.filterNotNull()
|
||||
.map { it.name }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override fun save(image: MultipartFile, recipeId: Long): Long {
|
||||
/** Gets the next id available for a new image for the recipe with the given [recipeId]. */
|
||||
override fun download(image: MultipartFile, recipe: Recipe): String {
|
||||
/** Gets the next id available for a new image for the given [recipe]. */
|
||||
fun getNextAvailableId(): Long =
|
||||
with(getAllIdsForRecipe(recipeId)) {
|
||||
if (isEmpty())
|
||||
0
|
||||
else
|
||||
maxOrNull()!! + 1L // maxOrNull() cannot return null because existingIds cannot be empty at this point
|
||||
}
|
||||
with(getAllImages(recipe)) {
|
||||
if (isEmpty())
|
||||
0
|
||||
else
|
||||
maxOf {
|
||||
it.split(RECIPE_IMAGE_ID_DELIMITER)
|
||||
.last()
|
||||
.replace(RECIPE_IMAGE_EXTENSION, "")
|
||||
.toLong()
|
||||
} + 1L
|
||||
}
|
||||
|
||||
val nextAvailableId = getNextAvailableId()
|
||||
fileService.write(image, getPath(nextAvailableId, recipeId))
|
||||
return nextAvailableId
|
||||
return getImageFileName(recipe, getNextAvailableId()).apply {
|
||||
fileService.write(image, getImagePath(recipe, this), true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun delete(id: Long, recipeId: Long) =
|
||||
fileService.delete(getPath(id, recipeId))
|
||||
override fun delete(recipe: Recipe, name: String) =
|
||||
fileService.delete(getImagePath(recipe, name))
|
||||
|
||||
/** Gets the images directory of the recipe with the given [recipeId]. */
|
||||
fun getRecipeDirectory(recipeId: Long) = File(fileService.getPath("$RECIPE_IMAGES_DIRECTORY/$recipeId"))
|
||||
override fun Recipe.getDirectory(): File = File(with(fileService) {
|
||||
this@getDirectory.imagesDirectoryPath.fullPath().path
|
||||
})
|
||||
|
||||
/** Gets the file of the image with the given [recipeId] and [id]. */
|
||||
fun getPath(id: Long, recipeId: Long): String = fileService.getPath("$RECIPE_IMAGES_DIRECTORY/$recipeId/$id")
|
||||
private fun getImageFileName(recipe: Recipe, id: Long) =
|
||||
"${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$id"
|
||||
|
||||
private fun getImagePath(recipe: Recipe, name: String) =
|
||||
"${recipe.imagesDirectoryPath}/$name$RECIPE_IMAGE_EXTENSION"
|
||||
}
|
||||
|
|
|
@ -3,8 +3,8 @@ package dev.fyloz.colorrecipesexplorer.service
|
|||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.utils.findDuplicated
|
||||
import dev.fyloz.colorrecipesexplorer.service.utils.hasGaps
|
||||
import dev.fyloz.colorrecipesexplorer.utils.findDuplicated
|
||||
import dev.fyloz.colorrecipesexplorer.utils.hasGaps
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ abstract class AbstractService<E, R : JpaRepository<E, *>>(override val reposito
|
|||
}
|
||||
|
||||
abstract class AbstractModelService<E : Model, R : JpaRepository<E, Long>>(repository: R) :
|
||||
AbstractService<E, R>(repository), ModelService<E, R> {
|
||||
AbstractService<E, R>(repository), ModelService<E, R> {
|
||||
protected abstract fun idNotFoundException(id: Long): NotFoundException
|
||||
protected abstract fun idAlreadyExistsException(id: Long): AlreadyExistsException
|
||||
|
||||
|
@ -83,7 +83,7 @@ abstract class AbstractModelService<E : Model, R : JpaRepository<E, Long>>(repos
|
|||
}
|
||||
|
||||
override fun deleteById(id: Long) =
|
||||
delete(getById(id)) // Use delete(entity) to prevent code duplication and to ease testing
|
||||
delete(getById(id)) // Use delete(entity) to prevent code duplication and to ease testing
|
||||
|
||||
protected fun assertId(id: Long?) {
|
||||
Assert.notNull(id, "${javaClass.simpleName}.update() was called with a null identifier")
|
||||
|
@ -91,7 +91,7 @@ abstract class AbstractModelService<E : Model, R : JpaRepository<E, Long>>(repos
|
|||
}
|
||||
|
||||
abstract class AbstractNamedModelService<E : NamedModel, R : NamedJpaRepository<E>>(repository: R) :
|
||||
AbstractModelService<E, R>(repository), NamedModelService<E, R> {
|
||||
AbstractModelService<E, R>(repository), NamedModelService<E, R> {
|
||||
protected abstract fun nameNotFoundException(name: String): NotFoundException
|
||||
protected abstract fun nameAlreadyExistsException(name: String): AlreadyExistsException
|
||||
|
||||
|
@ -126,33 +126,57 @@ 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>, R : JpaRepository<E, *>> : Service<E, R> {
|
||||
interface ExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>> : Service<E, R> {
|
||||
/** Gets all entities mapped to their output model. */
|
||||
fun getAllForOutput(): Collection<O>
|
||||
|
||||
/** Saves a given [entity]. */
|
||||
fun save(entity: S): E = save(entity.toEntity())
|
||||
|
||||
/** Updates a given [entity]. */
|
||||
fun update(entity: U): E = update(entity.toEntity())
|
||||
fun update(entity: U): E
|
||||
|
||||
/** Convert the given entity to its output model. */
|
||||
fun E.toOutput(): O
|
||||
}
|
||||
|
||||
/** An [ExternalService] for entities implementing the [Model] interface. */
|
||||
interface ExternalModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>> :
|
||||
ModelService<E, R>, ExternalService<E, S, U, R>
|
||||
interface ExternalModelService<E : Model, 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>, R : JpaRepository<E, *>> :
|
||||
NamedModelService<E, R>, ExternalModelService<E, S, U, R>
|
||||
interface ExternalNamedModelService<E : NamedModel, 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]. */
|
||||
@Suppress("unused")
|
||||
abstract class AbstractExternalService<E, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>>(repository: R) :
|
||||
AbstractService<E, R>(repository), ExternalService<E, S, U, R>
|
||||
abstract class AbstractExternalService<E, S : EntityDto<E>, U : EntityDto<E>, O, R : JpaRepository<E, *>>(repository: R) :
|
||||
AbstractService<E, R>(repository), ExternalService<E, S, U, O, R> {
|
||||
override fun getAllForOutput() =
|
||||
getAll().map { it.toOutput() }
|
||||
}
|
||||
|
||||
/** An [AbstractModelService] with the functionalities of a [ExternalService]. */
|
||||
abstract class AbstractExternalModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, Long>>(
|
||||
repository: R
|
||||
) : AbstractModelService<E, R>(repository), ExternalModelService<E, S, U, R>
|
||||
abstract class AbstractExternalModelService<E : Model, 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() =
|
||||
getAll().map { it.toOutput() }
|
||||
|
||||
override fun getByIdForOutput(id: Long) =
|
||||
getById(id).toOutput()
|
||||
}
|
||||
|
||||
/** An [AbstractNamedModelService] with the functionalities of a [ExternalService]. */
|
||||
abstract class AbstractExternalNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, R : NamedJpaRepository<E>>(
|
||||
repository: R
|
||||
) : AbstractNamedModelService<E, R>(repository), ExternalNamedModelService<E, S, U, R>
|
||||
abstract class AbstractExternalNamedModelService<E : NamedModel, 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() =
|
||||
getAll().map { it.toOutput() }
|
||||
|
||||
override fun getByIdForOutput(id: Long) =
|
||||
getById(id).toOutput()
|
||||
}
|
||||
|
|
|
@ -1,83 +1,215 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.core.io.ResourceLoader
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.nio.file.Files
|
||||
|
||||
@Service
|
||||
class FileService(
|
||||
private val resourcesLoader: ResourceLoader,
|
||||
private val creProperties: CreProperties,
|
||||
private val logger: Logger
|
||||
) {
|
||||
/** Reads the resource at the given [path] as a [String]. */
|
||||
fun readResource(path: String): String = try {
|
||||
resourcesLoader.getResource("classpath:$path").inputStream.use {
|
||||
readInputStreamAsString(it)
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
logger.error("Could not read resource", ex)
|
||||
""
|
||||
}
|
||||
/** Banned path shards. These are banned because they can allow access to files outside the data directory. */
|
||||
val BANNED_FILE_PATH_SHARDS = setOf(
|
||||
"~",
|
||||
"..",
|
||||
"//"
|
||||
)
|
||||
|
||||
/** Reads the given [stream] as a [String]. */
|
||||
fun readInputStreamAsString(stream: InputStream) = with(stream.readAllBytes()) {
|
||||
String(this, StandardCharsets.UTF_8)
|
||||
}
|
||||
interface FileService {
|
||||
/** Checks if the file at the given [path] exists. */
|
||||
fun exists(path: String): Boolean
|
||||
|
||||
/** Reads the file at the given [path] as a [ByteArray]. */
|
||||
fun readAsBytes(path: String) =
|
||||
withFileAt(path) { this.readBytes() }
|
||||
/** Reads the file at the given [path]. */
|
||||
fun read(path: String): ByteArrayResource
|
||||
|
||||
/** Writes the given [multipartFile] to the file at the given [path]. */
|
||||
fun write(multipartFile: MultipartFile, path: String): Boolean =
|
||||
if (multipartFile.size <= 0) true
|
||||
else try {
|
||||
multipartFile.transferTo(create(path).toPath())
|
||||
true
|
||||
} catch (ex: IOException) {
|
||||
logger.error("Unable to write multipart file", ex)
|
||||
false
|
||||
}
|
||||
/** Creates a file at the given [path]. */
|
||||
fun create(path: String)
|
||||
|
||||
/** Creates a new file at the given [path]. If the file already exists, nothing will be done. */
|
||||
fun create(path: String) = withFileAt(path) {
|
||||
if (!exists(path)) {
|
||||
try {
|
||||
Files.createDirectories(this.parentFile.toPath())
|
||||
Files.createFile(this.toPath())
|
||||
} catch (ex: IOException) {
|
||||
logger.error("Unable to create file", ex)
|
||||
}
|
||||
}
|
||||
this
|
||||
}
|
||||
/** Writes the given [file] to the given [path]. If the file already exists, it will be overwritten if [overwrite] is enabled. */
|
||||
fun write(file: MultipartFile, path: String, overwrite: Boolean)
|
||||
|
||||
/** Writes the given [data] to the given [path]. If the file at the path already exists, it will be overwritten if [overwrite] is enabled. */
|
||||
fun write(data: ByteArrayResource, path: String, overwrite: Boolean)
|
||||
|
||||
/** Deletes the file at the given [path]. */
|
||||
fun delete(path: String) = withFileAt(path) {
|
||||
try {
|
||||
if (exists(path)) Files.delete(this.toPath())
|
||||
} catch (ex: IOException) {
|
||||
logger.error("Unable to delete file", ex)
|
||||
}
|
||||
}
|
||||
fun delete(path: String)
|
||||
|
||||
/** Checks if a file with the given [path] exists on the disk. */
|
||||
fun exists(path: String): Boolean = withFileAt(path) {
|
||||
/** Completes the path of the given [String] by adding the working directory. */
|
||||
fun String.fullPath(): FilePath
|
||||
}
|
||||
|
||||
@Service
|
||||
class FileServiceImpl(
|
||||
private val creProperties: CreProperties,
|
||||
private val logger: Logger
|
||||
) : FileService {
|
||||
override fun exists(path: String) = withFileAt(path.fullPath()) {
|
||||
this.exists() && this.isFile
|
||||
}
|
||||
|
||||
/** Runs the given [block] in the context of a file with the given [path]. */
|
||||
fun <T> withFileAt(path: String, block: File.() -> T) =
|
||||
File(path).block()
|
||||
override fun read(path: String) = ByteArrayResource(
|
||||
withFileAt(path.fullPath()) {
|
||||
if (!exists(path)) throw FileNotFoundException(path)
|
||||
try {
|
||||
readBytes()
|
||||
} catch (ex: IOException) {
|
||||
FileReadException(path).logAndThrow(ex, logger)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
fun getPath(fileName: String): String =
|
||||
"${creProperties.workingDirectory}/$fileName"
|
||||
override fun create(path: String) {
|
||||
val fullPath = path.fullPath()
|
||||
if (!exists(path)) {
|
||||
try {
|
||||
withFileAt(fullPath) {
|
||||
this.create()
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
FileCreateException(path).logAndThrow(ex, logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun write(file: MultipartFile, path: String, overwrite: Boolean) =
|
||||
prepareWrite(path, overwrite) {
|
||||
file.transferTo(this.toPath())
|
||||
}
|
||||
|
||||
override fun write(data: ByteArrayResource, path: String, overwrite: Boolean) =
|
||||
prepareWrite(path, overwrite) {
|
||||
this.writeBytes(data.byteArray)
|
||||
}
|
||||
|
||||
override fun delete(path: String) {
|
||||
try {
|
||||
withFileAt(path.fullPath()) {
|
||||
if (!exists(path)) throw FileNotFoundException(path)
|
||||
!this.delete()
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
FileDeleteException(path).logAndThrow(ex, logger)
|
||||
}
|
||||
}
|
||||
|
||||
override fun String.fullPath(): FilePath {
|
||||
BANNED_FILE_PATH_SHARDS
|
||||
.firstOrNull { this.contains(it) }
|
||||
?.let { throw InvalidFilePathException(this, it) }
|
||||
|
||||
return FilePath("${creProperties.workingDirectory}/$this")
|
||||
}
|
||||
|
||||
private fun prepareWrite(path: String, overwrite: Boolean, op: File.() -> Unit) {
|
||||
val fullPath = path.fullPath()
|
||||
|
||||
if (exists(path)) {
|
||||
if (!overwrite) throw FileExistsException(path)
|
||||
} else {
|
||||
create(path)
|
||||
}
|
||||
|
||||
try {
|
||||
withFileAt(fullPath) {
|
||||
this.op()
|
||||
}
|
||||
} catch (ex: IOException) {
|
||||
FileWriteException(path).logAndThrow(ex, logger)
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs the given [block] in the context of a file with the given [fullPath]. */
|
||||
private fun <T> withFileAt(fullPath: FilePath, block: File.() -> T) =
|
||||
fullPath.file.block()
|
||||
}
|
||||
|
||||
data class FilePath(val path: String) {
|
||||
val file: File
|
||||
get() = File(path)
|
||||
}
|
||||
|
||||
/** Shortcut to create a file and its parent directories. */
|
||||
fun File.create() {
|
||||
Files.createDirectories(this.parentFile.toPath())
|
||||
Files.createFile(this.toPath())
|
||||
}
|
||||
|
||||
private const val FILE_IO_EXCEPTION_TITLE = "File IO error"
|
||||
|
||||
class InvalidFilePathException(val path: String, val fragment: String) :
|
||||
RestException(
|
||||
"invalid-filepath",
|
||||
"Invalid file path",
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"The given path is invalid because it contains a potentially malicious String '$fragment'",
|
||||
mapOf(
|
||||
"path" to path,
|
||||
"invalidString" to fragment
|
||||
)
|
||||
)
|
||||
|
||||
class FileExistsException(val path: String) :
|
||||
RestException(
|
||||
"io-exists",
|
||||
FILE_IO_EXCEPTION_TITLE,
|
||||
HttpStatus.BAD_REQUEST,
|
||||
"Could not write file to '$path' because it already exists. To overwrite the file set the overwrite parameter to true",
|
||||
pathMap(path)
|
||||
)
|
||||
|
||||
class FileNotFoundException(val path: String) :
|
||||
RestException(
|
||||
"io-notfound",
|
||||
FILE_IO_EXCEPTION_TITLE,
|
||||
HttpStatus.NOT_FOUND,
|
||||
"Could not access file at '$path' because it does not exists",
|
||||
pathMap(path)
|
||||
)
|
||||
|
||||
sealed class FileIOException(type: String, details: String, val path: String) :
|
||||
RestException(
|
||||
"io-$type",
|
||||
FILE_IO_EXCEPTION_TITLE,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
details,
|
||||
pathMap(path)
|
||||
)
|
||||
|
||||
class FileReadException(path: String) :
|
||||
FileIOException(
|
||||
"read",
|
||||
"Could not read file at '$path'",
|
||||
path
|
||||
)
|
||||
|
||||
class FileWriteException(path: String) :
|
||||
FileIOException(
|
||||
"write",
|
||||
"Could not write file to '$path'",
|
||||
path
|
||||
)
|
||||
|
||||
class FileCreateException(path: String) :
|
||||
FileIOException(
|
||||
"create",
|
||||
"Could not create file at '$path'",
|
||||
path
|
||||
)
|
||||
|
||||
class FileDeleteException(path: String) :
|
||||
FileIOException(
|
||||
"delete",
|
||||
"Could not delete file at '$path'",
|
||||
path
|
||||
)
|
||||
|
||||
private fun pathMap(path: String) =
|
||||
mapOf("path" to path)
|
||||
|
||||
private fun <T : FileIOException> T.logAndThrow(baseException: Exception, logger: Logger): Nothing {
|
||||
logger.error(this.details, baseException)
|
||||
throw this
|
||||
}
|
||||
|
|
|
@ -1,66 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.exception.RestException
|
||||
import dev.fyloz.colorrecipesexplorer.model.Material
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.IOException
|
||||
|
||||
const val SIMDUT_DIRECTORY = "simdut"
|
||||
|
||||
@Service
|
||||
class SimdutService(
|
||||
private val fileService: FileService,
|
||||
private val logger: Logger
|
||||
) {
|
||||
/** Checks if the given [material] has a SIMDUT file. */
|
||||
fun exists(material: Material) =
|
||||
fileService.exists(getPath(material))
|
||||
|
||||
/** Reads the SIMDUT file of the given [material]. */
|
||||
fun read(material: Material): ByteArray {
|
||||
val path = getPath(material)
|
||||
if (!fileService.exists(path)) return ByteArray(0)
|
||||
|
||||
return try {
|
||||
fileService.readAsBytes(path)
|
||||
} catch (ex: IOException) {
|
||||
logger.error("Could not read SIMDUT file", ex)
|
||||
ByteArray(0)
|
||||
}
|
||||
}
|
||||
|
||||
/** Writes the given [simdut] file for the given [material] to the disk. */
|
||||
fun write(material: Material, simdut: MultipartFile) {
|
||||
if (!fileService.write(simdut, getPath(material)))
|
||||
throw SimdutWriteException(material)
|
||||
}
|
||||
|
||||
/** Updates the SIMDUT file of the given [material] with the given [simdut]. */
|
||||
fun update(simdut: MultipartFile, material: Material) {
|
||||
delete(material)
|
||||
write(material, simdut)
|
||||
}
|
||||
|
||||
/** Deletes the SIMDUT file of the given [material]. */
|
||||
fun delete(material: Material) =
|
||||
fileService.delete(getPath(material))
|
||||
|
||||
/** Gets the path of the SIMDUT file of the given [material]. */
|
||||
fun getPath(material: Material) =
|
||||
fileService.getPath("$SIMDUT_DIRECTORY/${getSimdutFileName(material)}")
|
||||
|
||||
/** Gets the name of the SIMDUT file of the given [material]. */
|
||||
fun getSimdutFileName(material: Material) =
|
||||
material.id.toString()
|
||||
}
|
||||
|
||||
class SimdutWriteException(material: Material) :
|
||||
RestException(
|
||||
"simdut-write",
|
||||
"Could not write SIMDUT file",
|
||||
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
"Could not write the SIMDUT file for the material ${material.name} to the disk"
|
||||
)
|
|
@ -0,0 +1,78 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.utils.*
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import org.springframework.stereotype.Service
|
||||
|
||||
private const val TOUCH_UP_KIT_FILES_PATH = "pdf/touchupkits"
|
||||
|
||||
const val TOUCH_UP_TEXT_FR = "KIT DE RETOUCHE"
|
||||
const val TOUCH_UP_TEXT_EN = "TOUCH UP KIT"
|
||||
|
||||
interface TouchUpKitService {
|
||||
/** Generates and returns a [PdfDocument] for the given [job]. */
|
||||
fun generateJobPdf(job: String): PdfDocument
|
||||
|
||||
/**
|
||||
* Generates and returns a [PdfDocument] for the given [job] as a [ByteArrayResource].
|
||||
*
|
||||
* If [CreProperties.cacheGeneratedFiles] is enabled and a file exists for the job, its content will be returned.
|
||||
* If caching is enabled but no file exists for the job, the generated ByteArrayResource will be cached on the disk.
|
||||
*/
|
||||
fun generateJobPdfResource(job: String): ByteArrayResource
|
||||
|
||||
/** Writes the given [document] to the [FileService] if [CreProperties.cacheGeneratedFiles] is enabled. */
|
||||
fun String.cachePdfDocument(document: PdfDocument)
|
||||
}
|
||||
|
||||
@Service
|
||||
class TouchUpKitServiceImpl(
|
||||
private val fileService: FileService,
|
||||
private val creProperties: CreProperties
|
||||
) : TouchUpKitService {
|
||||
override fun generateJobPdf(job: String) = pdf {
|
||||
container {
|
||||
centeredVertically = true
|
||||
drawContainerBottom = true
|
||||
text(TOUCH_UP_TEXT_FR) {
|
||||
bold = true
|
||||
fontSize = PDF_DEFAULT_FONT_SIZE + 12
|
||||
}
|
||||
text(TOUCH_UP_TEXT_EN) {
|
||||
bold = true
|
||||
fontSize = PDF_DEFAULT_FONT_SIZE + 12
|
||||
}
|
||||
text(job) {
|
||||
marginTop = 10f
|
||||
}
|
||||
}
|
||||
|
||||
container(containers[0]) {
|
||||
drawContainerBottom = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun generateJobPdfResource(job: String): ByteArrayResource {
|
||||
if (creProperties.cacheGeneratedFiles) {
|
||||
with(job.pdfDocumentPath()) {
|
||||
if (fileService.exists(this)) {
|
||||
return fileService.read(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return generateJobPdf(job).apply {
|
||||
job.cachePdfDocument(this)
|
||||
}.toByteArrayResource()
|
||||
}
|
||||
|
||||
override fun String.cachePdfDocument(document: PdfDocument) {
|
||||
if (!creProperties.cacheGeneratedFiles) return
|
||||
|
||||
fileService.write(document.toByteArrayResource(), this.pdfDocumentPath(), true)
|
||||
}
|
||||
|
||||
private fun String.pdfDocumentPath() =
|
||||
"$TOUCH_UP_KIT_FILES_PATH/$this.pdf"
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.utils
|
||||
package dev.fyloz.colorrecipesexplorer.utils
|
||||
|
||||
/** 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(
|
|
@ -0,0 +1,125 @@
|
|||
package dev.fyloz.colorrecipesexplorer.utils
|
||||
|
||||
import org.apache.pdfbox.pdmodel.PDDocument
|
||||
import org.apache.pdfbox.pdmodel.PDPage
|
||||
import org.apache.pdfbox.pdmodel.PDPageContentStream
|
||||
import org.apache.pdfbox.pdmodel.font.PDFont
|
||||
import org.apache.pdfbox.pdmodel.font.PDType1Font
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
val PDF_DEFAULT_FONT: PDType1Font = PDType1Font.HELVETICA
|
||||
val PDF_DEFAULT_FONT_BOLD: PDType1Font = PDType1Font.HELVETICA_BOLD
|
||||
const val PDF_DEFAULT_FONT_SIZE = 42f
|
||||
val PDF_DASH_LINE_PATTERN = floatArrayOf(4f)
|
||||
|
||||
/** Creates a [PdfContainer] and apply the given [block]. */
|
||||
fun pdf(block: PdfDocument.() -> Unit = {}) =
|
||||
PdfDocument().apply { block() }
|
||||
|
||||
/** Creates a [PdfContainer] in the given [PdfDocument] and apply the given [block]. If a [container] is given, the receiver of the block will be a clone of it. */
|
||||
fun PdfDocument.container(container: PdfContainer = PdfContainer(), block: PdfContainer.() -> Unit) {
|
||||
this.containers += PdfContainer(container).apply(block)
|
||||
}
|
||||
|
||||
/** Creates a [PdfText] with the given [text] in the given [PdfContainer] and apply the given [block]. */
|
||||
fun PdfContainer.text(text: String, block: PdfText.() -> Unit) {
|
||||
this.texts += PdfText(text = text).apply(block)
|
||||
}
|
||||
|
||||
fun PdfDocument.toByteArrayResource(): ByteArrayResource = PDDocument().use { document ->
|
||||
val page = PDPage()
|
||||
|
||||
document.addPage(page)
|
||||
|
||||
fun PDPageContentStream.drawText(text: PdfText, y: Float) {
|
||||
val font = if (text.bold) fontBold else font
|
||||
val textWidth = font.getStringWidth(text.text) / 1000 * text.fontSize
|
||||
val textX = (page.mediaBox.width - textWidth) / 2f
|
||||
|
||||
beginText()
|
||||
newLineAtOffset(textX, y)
|
||||
setFont(font, text.fontSize)
|
||||
showText(text.text)
|
||||
endText()
|
||||
}
|
||||
|
||||
fun PDPageContentStream.drawDashLine(y: Float) {
|
||||
moveTo(0f, y)
|
||||
lineTo(page.mediaBox.width, y)
|
||||
setLineDashPattern(PDF_DASH_LINE_PATTERN, 0f)
|
||||
stroke()
|
||||
}
|
||||
|
||||
fun PDPageContentStream.drawContainer(container: PdfContainer, y: Float, height: Float) {
|
||||
var textY = y
|
||||
|
||||
if (container.centeredVertically) {
|
||||
val textsHeight = container.texts
|
||||
.map { it.fontSize + it.marginTop }
|
||||
.reduce { acc, textHeight -> acc + textHeight }
|
||||
textY -= (height - textsHeight) / 2f
|
||||
}
|
||||
|
||||
if (container.drawContainerBottom) {
|
||||
this.drawDashLine(y - height)
|
||||
}
|
||||
|
||||
container.texts.forEach { text ->
|
||||
textY -= text.fontSize + text.marginTop
|
||||
this.drawText(text, textY)
|
||||
}
|
||||
}
|
||||
|
||||
PDPageContentStream(document, page).use {
|
||||
var containerY = page.mediaBox.height
|
||||
|
||||
val computedSizeContainerCount = containers
|
||||
.filter { it.height < 0 }
|
||||
.count()
|
||||
val computedSizeContainersHeight = containerY / computedSizeContainerCount
|
||||
|
||||
containers.forEach { container ->
|
||||
val height = if (container.height < 0)
|
||||
computedSizeContainersHeight
|
||||
else
|
||||
container.height
|
||||
|
||||
it.drawContainer(container, containerY, height)
|
||||
|
||||
containerY -= height
|
||||
}
|
||||
}
|
||||
|
||||
ByteArrayOutputStream().use {
|
||||
document.save(it)
|
||||
ByteArrayResource(it.toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
data class PdfDocument(
|
||||
var font: PDFont = PDF_DEFAULT_FONT,
|
||||
var fontBold: PDFont = PDF_DEFAULT_FONT_BOLD,
|
||||
val containers: MutableList<PdfContainer> = mutableListOf()
|
||||
)
|
||||
|
||||
data class PdfContainer(
|
||||
var height: Float = -1f,
|
||||
var centeredVertically: Boolean = false,
|
||||
var drawContainerBottom: Boolean = false,
|
||||
val texts: MutableList<PdfText> = mutableListOf()
|
||||
) {
|
||||
constructor(container: PdfContainer) : this(
|
||||
container.height,
|
||||
container.centeredVertically,
|
||||
container.drawContainerBottom,
|
||||
container.texts
|
||||
)
|
||||
}
|
||||
|
||||
data class PdfText(
|
||||
var text: String = "Text",
|
||||
var bold: Boolean = false,
|
||||
var fontSize: Float = PDF_DEFAULT_FONT_SIZE,
|
||||
var marginTop: Float = 0f
|
||||
)
|
|
@ -2,6 +2,8 @@
|
|||
server.port=9090
|
||||
# CRE
|
||||
cre.server.working-directory=data
|
||||
cre.server.deployment-url=http://localhost:9090
|
||||
cre.server.cache-generated-files=true
|
||||
cre.security.jwt-secret=CtnvGQjgZ44A1fh295gE
|
||||
cre.security.jwt-duration=18000000
|
||||
# Root user
|
||||
|
|
|
@ -27,8 +27,8 @@ abstract class AbstractServiceTest<E, S : Service<E, *>, R : JpaRepository<E, *>
|
|||
|
||||
protected val entityList: List<E>
|
||||
get() = listOf(
|
||||
entity,
|
||||
anotherEntity
|
||||
entity,
|
||||
anotherEntity
|
||||
)
|
||||
|
||||
@AfterEach
|
||||
|
@ -91,7 +91,7 @@ abstract class AbstractServiceTest<E, S : Service<E, *>, R : JpaRepository<E, *>
|
|||
}
|
||||
|
||||
abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : JpaRepository<E, Long>> :
|
||||
AbstractServiceTest1<E, S, R>() {
|
||||
AbstractServiceTest1<E, S, R>() {
|
||||
|
||||
// existsById()
|
||||
|
||||
|
@ -129,7 +129,7 @@ abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : J
|
|||
whenever(repository.findById(entity.id!!)).doReturn(Optional.empty())
|
||||
|
||||
assertThrows<NotFoundException> { service.getById(entity.id!!) }
|
||||
.assertErrorCode()
|
||||
.assertErrorCode()
|
||||
}
|
||||
|
||||
// save()
|
||||
|
@ -139,7 +139,7 @@ abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : J
|
|||
doReturn(true).whenever(service).existsById(entity.id!!)
|
||||
|
||||
assertThrows<AlreadyExistsException> { service.save(entity) }
|
||||
.assertErrorCode()
|
||||
.assertErrorCode()
|
||||
}
|
||||
|
||||
// update()
|
||||
|
@ -161,7 +161,7 @@ abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : J
|
|||
doReturn(false).whenever(service).existsById(entity.id!!)
|
||||
|
||||
assertThrows<NotFoundException> { service.update(entity) }
|
||||
.assertErrorCode()
|
||||
.assertErrorCode()
|
||||
}
|
||||
|
||||
// deleteById()
|
||||
|
@ -177,7 +177,7 @@ abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : J
|
|||
}
|
||||
|
||||
abstract class AbstractNamedModelServiceTest<E : NamedModel, S : NamedModelService<E, *>, R : NamedJpaRepository<E>> :
|
||||
AbstractModelServiceTest<E, S, R>() {
|
||||
AbstractModelServiceTest<E, S, R>() {
|
||||
protected abstract val entityWithEntityName: E
|
||||
|
||||
// existsByName()
|
||||
|
@ -216,7 +216,7 @@ abstract class AbstractNamedModelServiceTest<E : NamedModel, S : NamedModelServi
|
|||
whenever(repository.findByName(entity.name)).doReturn(null)
|
||||
|
||||
assertThrows<NotFoundException> { service.getByName(entity.name) }
|
||||
.assertErrorCode("name")
|
||||
.assertErrorCode("name")
|
||||
}
|
||||
|
||||
// save()
|
||||
|
@ -226,7 +226,7 @@ abstract class AbstractNamedModelServiceTest<E : NamedModel, S : NamedModelServi
|
|||
doReturn(true).whenever(service).existsByName(entity.name)
|
||||
|
||||
assertThrows<AlreadyExistsException> { service.save(entity) }
|
||||
.assertErrorCode("name")
|
||||
.assertErrorCode("name")
|
||||
}
|
||||
|
||||
// update()
|
||||
|
@ -258,7 +258,7 @@ abstract class AbstractNamedModelServiceTest<E : NamedModel, S : NamedModelServi
|
|||
doReturn(entity).whenever(service).getById(entity.id!!)
|
||||
|
||||
assertThrows<AlreadyExistsException> { service.update(entity) }
|
||||
.assertErrorCode("name")
|
||||
.assertErrorCode("name")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -269,8 +269,8 @@ 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>> :
|
||||
AbstractModelServiceTest<E, S, R>(), ExternalModelServiceTest {
|
||||
abstract class AbstractExternalModelServiceTest<E : Model, 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,8 +281,8 @@ 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>> :
|
||||
AbstractNamedModelServiceTest<E, S, R>(), ExternalModelServiceTest {
|
||||
abstract class AbstractExternalNamedModelServiceTest<E : NamedModel, 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
|
||||
|
||||
|
@ -294,10 +294,10 @@ abstract class AbstractExternalNamedModelServiceTest<E : NamedModel, N : EntityD
|
|||
}
|
||||
|
||||
fun NotFoundException.assertErrorCode(identifierName: String = "id") =
|
||||
this.assertErrorCode("notfound", identifierName)
|
||||
this.assertErrorCode("notfound", identifierName)
|
||||
|
||||
fun AlreadyExistsException.assertErrorCode(identifierName: String = "id") =
|
||||
this.assertErrorCode("exists", identifierName)
|
||||
this.assertErrorCode("exists", identifierName)
|
||||
|
||||
fun RestException.assertErrorCode(type: String, identifierName: String) {
|
||||
assertTrue {
|
||||
|
@ -311,11 +311,11 @@ fun RestException.assertErrorCode(errorCode: String) {
|
|||
}
|
||||
|
||||
fun <E : Model, N : EntityDto<E>> withBaseSaveDtoTest(
|
||||
entity: E,
|
||||
entitySaveDto: N,
|
||||
service: ExternalService<E, N, *, *>,
|
||||
saveMockMatcher: () -> E = { entity },
|
||||
op: () -> Unit = {}
|
||||
entity: E,
|
||||
entitySaveDto: N,
|
||||
service: ExternalService<E, N, *, *, *>,
|
||||
saveMockMatcher: () -> E = { entity },
|
||||
op: () -> Unit = {}
|
||||
) {
|
||||
doReturn(entity).whenever(service).save(saveMockMatcher())
|
||||
doReturn(entity).whenever(entitySaveDto).toEntity()
|
||||
|
@ -329,11 +329,11 @@ fun <E : Model, N : EntityDto<E>> withBaseSaveDtoTest(
|
|||
}
|
||||
|
||||
fun <E : Model, U : EntityDto<E>> withBaseUpdateDtoTest(
|
||||
entity: E,
|
||||
entityUpdateDto: U,
|
||||
service: ExternalModelService<E, *, U, *>,
|
||||
updateMockMatcher: () -> E,
|
||||
op: E.() -> Unit = {}
|
||||
entity: E,
|
||||
entityUpdateDto: U,
|
||||
service: ExternalModelService<E, *, U, *, *>,
|
||||
updateMockMatcher: () -> E,
|
||||
op: E.() -> Unit = {}
|
||||
) {
|
||||
doAnswer { it.arguments[0] }.whenever(service).update(updateMockMatcher())
|
||||
doReturn(entity).whenever(entityUpdateDto).toEntity()
|
||||
|
|
|
@ -4,7 +4,7 @@ import com.nhaarman.mockitokotlin2.*
|
|||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.SimdutService
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.FileService
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
|
@ -14,16 +14,17 @@ import kotlin.test.assertFalse
|
|||
import kotlin.test.assertTrue
|
||||
|
||||
class MaterialServiceTest :
|
||||
AbstractExternalNamedModelServiceTest<Material, MaterialSaveDto, MaterialUpdateDto, MaterialService, MaterialRepository>() {
|
||||
AbstractExternalNamedModelServiceTest<Material, MaterialSaveDto, MaterialUpdateDto, MaterialService, MaterialRepository>() {
|
||||
override val repository: MaterialRepository = mock()
|
||||
private val simdutService: SimdutService = mock()
|
||||
private val recipeService: RecipeService = mock()
|
||||
private val mixService: MixService = mock()
|
||||
private val materialTypeService: MaterialTypeService = mock()
|
||||
private val fileService: FileService = mock()
|
||||
override val service: MaterialService =
|
||||
spy(MaterialServiceImpl(repository, simdutService, recipeService, mixService, materialTypeService))
|
||||
spy(MaterialServiceImpl(repository, recipeService, mixService, materialTypeService, fileService))
|
||||
|
||||
override val entity: Material = material(id = 0L, name = "material")
|
||||
private val entityOutput = materialOutputDto(entity)
|
||||
override val anotherEntity: Material = material(id = 1L, name = "another material")
|
||||
override val entityWithEntityName: Material = material(id = 2L, name = "material")
|
||||
override val entitySaveDto: MaterialSaveDto = spy(materialSaveDto())
|
||||
|
@ -33,7 +34,7 @@ class MaterialServiceTest :
|
|||
|
||||
@AfterEach
|
||||
override fun afterEach() {
|
||||
reset(simdutService)
|
||||
reset(recipeService, mixService, materialTypeService, fileService)
|
||||
super.afterEach()
|
||||
}
|
||||
|
||||
|
@ -61,20 +62,20 @@ class MaterialServiceTest :
|
|||
|
||||
@Test
|
||||
fun `hasSimdut() returns false when simdutService_exists() returns false`() {
|
||||
whenever(simdutService.exists(entity)).doReturn(false)
|
||||
whenever(fileService.exists(any())).doReturn(false)
|
||||
doReturn(entity).whenever(service).getById(entity.id!!)
|
||||
|
||||
val found = service.hasSimdut(entity.id!!)
|
||||
val found = service.hasSimdut(entity)
|
||||
|
||||
assertFalse(found)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasSimdut() returns true when simdutService_exists() returns true`() {
|
||||
whenever(simdutService.exists(entity)).doReturn(true)
|
||||
whenever(fileService.exists(any())).doReturn(true)
|
||||
doReturn(entity).whenever(service).getById(entity.id!!)
|
||||
|
||||
val found = service.hasSimdut(entity.id!!)
|
||||
val found = service.hasSimdut(entity)
|
||||
|
||||
assertTrue(found)
|
||||
}
|
||||
|
@ -83,42 +84,16 @@ class MaterialServiceTest :
|
|||
|
||||
@Test
|
||||
fun `getAllNotMixType() returns a list containing every material that are not a mix type`() {
|
||||
val mixTypeMaterial = material(name = "mix type material", isMixType = true)
|
||||
val mixTypeMaterial = material(id = 1L, name = "mix type material", isMixType = true)
|
||||
val mixTypeMaterialOutput = materialOutputDto(mixTypeMaterial)
|
||||
val materialList = listOf(entity, mixTypeMaterial)
|
||||
|
||||
doReturn(materialList).whenever(service).getAll()
|
||||
|
||||
val found = service.getAllNotMixType()
|
||||
|
||||
assertTrue(found.contains(entity))
|
||||
assertFalse(found.contains(mixTypeMaterial))
|
||||
}
|
||||
|
||||
// getAllIdsWithSimdut()
|
||||
|
||||
@Test
|
||||
fun `getAllIdsWithSimdut() returns a list containing the identifier of every material with a SIMDUT file`() {
|
||||
val materials = listOf(
|
||||
material(id = 0L),
|
||||
material(id = 1L),
|
||||
material(id = 2L),
|
||||
material(id = 3L)
|
||||
)
|
||||
val hasSimdut = mapOf(
|
||||
*materials
|
||||
.map { it.id!! to it.evenId }
|
||||
.toTypedArray()
|
||||
)
|
||||
val expectedIds = hasSimdut
|
||||
.filter { it.value }
|
||||
.map { it.key }
|
||||
|
||||
whenever(simdutService.exists(any())).doAnswer { hasSimdut[(it.arguments[0] as Material).id] }
|
||||
doReturn(materials).whenever(service).getAllNotMixType()
|
||||
|
||||
val found = service.getAllIdsWithSimdut()
|
||||
|
||||
assertEquals(expectedIds, found)
|
||||
assertTrue(found.contains(entityOutput))
|
||||
assertFalse(found.contains(mixTypeMaterialOutput))
|
||||
}
|
||||
|
||||
// save()
|
||||
|
@ -128,7 +103,7 @@ class MaterialServiceTest :
|
|||
doReturn(true).whenever(service).existsByName(entity.name)
|
||||
|
||||
assertThrows<AlreadyExistsException> { service.save(entity) }
|
||||
.assertErrorCode("name")
|
||||
.assertErrorCode("name")
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -146,7 +121,7 @@ class MaterialServiceTest :
|
|||
|
||||
service.save(materialSaveDto)
|
||||
|
||||
verify(simdutService).write(entity, mockMultipartFile)
|
||||
verify(fileService).write(mockMultipartFile, entity.simdutFilePath, false)
|
||||
}
|
||||
|
||||
// update()
|
||||
|
@ -160,7 +135,21 @@ class MaterialServiceTest :
|
|||
doReturn(entity).whenever(service).getById(material.id!!)
|
||||
|
||||
assertThrows<AlreadyExistsException> { service.update(material) }
|
||||
.assertErrorCode("name")
|
||||
.assertErrorCode("name")
|
||||
}
|
||||
|
||||
@Test
|
||||
override fun `update(dto) calls and returns update() with the created entity`() {
|
||||
val mockSimdutFile = MockMultipartFile("simdut", byteArrayOf(1, 2, 3, 4, 5))
|
||||
val materialUpdateDto = spy(materialUpdateDto(id = 0L, simdutFile = mockSimdutFile))
|
||||
|
||||
doReturn(entity).whenever(service).getById(any())
|
||||
doReturn(entity).whenever(service).update(any<Material>())
|
||||
doReturn(entity).whenever(materialUpdateDto).toEntity()
|
||||
|
||||
service.update(materialUpdateDto)
|
||||
|
||||
verify(fileService).write(mockSimdutFile, entity.simdutFilePath, true)
|
||||
}
|
||||
|
||||
// updateQuantity()
|
||||
|
@ -186,16 +175,16 @@ class MaterialServiceTest :
|
|||
val anotherMixTypeMaterial = material(id = 2L, isMixType = true)
|
||||
val materials = listOf(normalMaterial, mixTypeMaterial, anotherMixTypeMaterial)
|
||||
val recipe =
|
||||
recipe(id = 0L, mixes = mutableListOf(mix(mixType = mixType(id = 0L, material = mixTypeMaterial))))
|
||||
recipe(id = 0L, mixes = mutableListOf(mix(mixType = mixType(id = 0L, material = mixTypeMaterial))))
|
||||
|
||||
whenever(recipeService.getById(recipe.id!!)).doReturn(recipe)
|
||||
doReturn(materials).whenever(service).getAll()
|
||||
|
||||
val found = service.getAllForMixCreation(recipe.id!!)
|
||||
|
||||
assertTrue(normalMaterial in found)
|
||||
assertTrue(mixTypeMaterial in found)
|
||||
assertFalse(anotherMixTypeMaterial in found)
|
||||
assertTrue(materialOutputDto(normalMaterial) in found)
|
||||
assertTrue(materialOutputDto(mixTypeMaterial) in found)
|
||||
assertFalse(materialOutputDto(anotherMixTypeMaterial) in found)
|
||||
}
|
||||
|
||||
// getAllForMixUpdate()
|
||||
|
@ -215,25 +204,9 @@ class MaterialServiceTest :
|
|||
|
||||
val found = service.getAllForMixUpdate(mix.id!!)
|
||||
|
||||
assertTrue(normalMaterial in found)
|
||||
assertTrue(mixTypeMaterial in found)
|
||||
assertFalse(anotherMixTypeMaterial in found)
|
||||
}
|
||||
|
||||
// update()
|
||||
|
||||
@Test
|
||||
override fun `update(dto) calls and returns update() with the created entity`() {
|
||||
val mockSimdutFile = MockMultipartFile("simdut", byteArrayOf(1, 2, 3, 4, 5))
|
||||
val materialUpdateDto = spy(materialUpdateDto(id = 0L, simdutFile = mockSimdutFile))
|
||||
|
||||
doReturn(entity).whenever(service).getById(materialUpdateDto.id)
|
||||
doReturn(entity).whenever(service).update(any<Material>())
|
||||
doReturn(entity).whenever(materialUpdateDto).toEntity()
|
||||
|
||||
service.update(materialUpdateDto)
|
||||
|
||||
verify(simdutService).update(eq(mockSimdutFile), any())
|
||||
assertTrue(materialOutputDto(normalMaterial) in found)
|
||||
assertTrue(materialOutputDto(mixTypeMaterial) in found)
|
||||
assertFalse(materialOutputDto(anotherMixTypeMaterial) in found)
|
||||
}
|
||||
|
||||
|
||||
|
@ -262,4 +235,13 @@ class MaterialServiceTest :
|
|||
|
||||
test()
|
||||
}
|
||||
|
||||
private fun materialOutputDto(material: Material) = MaterialOutputDto(
|
||||
id = material.id!!,
|
||||
name = material.name,
|
||||
inventoryQuantity = material.inventoryQuantity,
|
||||
isMixType = material.isMixType,
|
||||
materialType = material.materialType!!,
|
||||
simdutUrl = null
|
||||
)
|
||||
}
|
||||
|
|
|
@ -113,7 +113,7 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
|
|||
override fun `update(dto) calls and returns update() with the created entity`() {
|
||||
val mixUpdateDto = spy(mixUpdateDto(id = 0L, name = null, materialTypeId = null))
|
||||
|
||||
doReturn(entity).whenever(service).getById(mixUpdateDto.id)
|
||||
doReturn(entity).whenever(service).getById(any())
|
||||
doReturn(entity).whenever(service).update(entity)
|
||||
|
||||
val found = service.update(mixUpdateDto)
|
||||
|
|
|
@ -4,24 +4,25 @@ import com.nhaarman.mockitokotlin2.*
|
|||
import dev.fyloz.colorrecipesexplorer.model.*
|
||||
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
|
||||
import dev.fyloz.colorrecipesexplorer.service.files.FileService
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.mock.web.MockMultipartFile
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.File
|
||||
import java.nio.file.NoSuchFileException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RecipeServiceTest :
|
||||
AbstractExternalModelServiceTest<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService, RecipeRepository>() {
|
||||
AbstractExternalModelServiceTest<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService, RecipeRepository>() {
|
||||
override val repository: RecipeRepository = mock()
|
||||
private val companyService: CompanyService = mock()
|
||||
private val mixService: MixService = mock()
|
||||
private val groupService: EmployeeGroupService = mock()
|
||||
private val recipeStepService: RecipeStepService = mock()
|
||||
override val service: RecipeService = spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService))
|
||||
override val service: RecipeService =
|
||||
spy(RecipeServiceImpl(repository, companyService, mixService, recipeStepService, groupService, mock()))
|
||||
|
||||
private val company: Company = company(id = 0L)
|
||||
override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company)
|
||||
|
@ -79,22 +80,22 @@ class RecipeServiceTest :
|
|||
|
||||
@Test
|
||||
override fun `update(dto) calls and returns update() with the created entity`() =
|
||||
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
|
||||
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
|
||||
|
||||
// updatePublicData()
|
||||
|
||||
@Test
|
||||
fun `updatePublicData() updates the notes of a recipe groups information according to the RecipePublicDataDto`() {
|
||||
val recipe = recipe(
|
||||
id = 0L, groupsInformation = setOf(
|
||||
recipeGroupInformation(id = 0L, group = employeeGroup(id = 1L), note = "Old note"),
|
||||
recipeGroupInformation(id = 1L, group = employeeGroup(id = 2L), note = "Another note"),
|
||||
recipeGroupInformation(id = 2L, group = employeeGroup(id = 3L), note = "Up to date note")
|
||||
)
|
||||
id = 0L, groupsInformation = setOf(
|
||||
recipeGroupInformation(id = 0L, group = employeeGroup(id = 1L), note = "Old note"),
|
||||
recipeGroupInformation(id = 1L, group = employeeGroup(id = 2L), note = "Another note"),
|
||||
recipeGroupInformation(id = 2L, group = employeeGroup(id = 3L), note = "Up to date note")
|
||||
)
|
||||
)
|
||||
val notes = setOf(
|
||||
noteDto(groupId = 1, content = "Note 1"),
|
||||
noteDto(groupId = 2, content = null)
|
||||
noteDto(groupId = 1, content = "Note 1"),
|
||||
noteDto(groupId = 2, content = null)
|
||||
)
|
||||
val publicData = recipePublicDataDto(recipeId = recipe.id!!, notes = notes)
|
||||
|
||||
|
@ -115,10 +116,10 @@ class RecipeServiceTest :
|
|||
@Test
|
||||
fun `updatePublicData() update the location of a recipe mixes in the mix service according to the RecipePublicDataDto`() {
|
||||
val publicData = recipePublicDataDto(
|
||||
mixesLocation = setOf(
|
||||
mixLocationDto(mixId = 0L, location = "Loc 1"),
|
||||
mixLocationDto(mixId = 1L, location = "Loc 2")
|
||||
)
|
||||
mixesLocation = setOf(
|
||||
mixLocationDto(mixId = 0L, location = "Loc 1"),
|
||||
mixLocationDto(mixId = 1L, location = "Loc 2")
|
||||
)
|
||||
)
|
||||
|
||||
service.updatePublicData(publicData)
|
||||
|
@ -163,133 +164,100 @@ class RecipeServiceTest :
|
|||
}
|
||||
}
|
||||
|
||||
private class RecipeImageServiceTestContext {
|
||||
val fileService = mockk<FileService> {
|
||||
every { write(any<MultipartFile>(), any(), any()) } just Runs
|
||||
every { delete(any()) } just Runs
|
||||
}
|
||||
val recipeImageService = spyk(RecipeImageServiceImpl(fileService))
|
||||
val recipe = spyk(recipe())
|
||||
val recipeImagesIds = setOf(1L, 10L, 21L)
|
||||
val recipeImagesNames = recipeImagesIds.map { it.imageName }.toSet()
|
||||
val recipeImagesFiles = recipeImagesNames.map { File(it) }.toTypedArray()
|
||||
val recipeDirectory = spyk(File(recipe.imagesDirectoryPath)) {
|
||||
every { exists() } returns true
|
||||
every { isDirectory } returns true
|
||||
every { listFiles() } returns recipeImagesFiles
|
||||
}
|
||||
|
||||
init {
|
||||
with(recipeImageService) {
|
||||
every { recipe.getDirectory() } returns recipeDirectory
|
||||
}
|
||||
}
|
||||
|
||||
val Long.imageName
|
||||
get() = "${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$this"
|
||||
|
||||
val String.imagePath
|
||||
get() = "${recipe.imagesDirectoryPath}/$this$RECIPE_IMAGE_EXTENSION"
|
||||
}
|
||||
|
||||
class RecipeImageServiceTest {
|
||||
private val recipeService: RecipeService = mock()
|
||||
private val fileService: FileService = mock()
|
||||
private val service = spy(RecipeImageServiceImpl(recipeService, fileService))
|
||||
|
||||
private val recipeId = 1L
|
||||
private val imageId = 5L
|
||||
private val imagePath = "$RECIPE_IMAGES_DIRECTORY/$recipeId/$imageId"
|
||||
private val recipe = recipe(id = recipeId)
|
||||
private val recipeDirectory: File = mock()
|
||||
private val imagesIds = listOf(1L, 3L, 10L, 21L)
|
||||
private val imageData = byteArrayOf(64, 32, 16, 8, 4, 2, 1)
|
||||
private val image = MockMultipartFile("$imageId", imageData)
|
||||
|
||||
@AfterEach
|
||||
internal fun tearDown() {
|
||||
reset(recipeService, fileService, service, recipeDirectory)
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
// getByIdForRecipe()
|
||||
private fun test(test: RecipeImageServiceTestContext.() -> Unit) {
|
||||
RecipeImageServiceTestContext().test()
|
||||
}
|
||||
|
||||
// getAllImages()
|
||||
|
||||
@Test
|
||||
fun `getByIdForRecipe() returns data for the given recipe and image id red by the file service`() {
|
||||
whenever(fileService.getPath(imagePath)).doReturn(imagePath)
|
||||
whenever(fileService.readAsBytes(imagePath)).doReturn(imageData)
|
||||
fun `getAllImages() returns a Set containing the name of every files in the recipe's directory`() {
|
||||
test {
|
||||
val foundImagesNames = recipeImageService.getAllImages(recipe)
|
||||
|
||||
val found = service.getByIdForRecipe(imageId, recipeId)
|
||||
|
||||
assertEquals(imageData, found)
|
||||
assertEquals(recipeImagesNames, foundImagesNames)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getByIdForRecipe() throws RecipeImageNotFoundException when no image with the given recipe and image id exists`() {
|
||||
doReturn(imagePath).whenever(service).getPath(imageId, recipeId)
|
||||
whenever(recipeService.getById(recipeId)).doReturn(recipe)
|
||||
whenever(fileService.readAsBytes(imagePath)).doAnswer { throw NoSuchFileException(imagePath) }
|
||||
fun `getAllImages() returns an empty Set when the recipe's directory does not exists`() {
|
||||
test {
|
||||
every { recipeDirectory.exists() } returns false
|
||||
|
||||
assertThrows<RecipeImageNotFoundException> { service.getByIdForRecipe(imageId, recipeId) }
|
||||
assertTrue {
|
||||
recipeImageService.getAllImages(recipe).isEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getAllIdsForRecipe()
|
||||
// download()
|
||||
|
||||
@Test
|
||||
fun `getAllIdsForRecipe() returns a list containing all image's identifier of the images of the given recipe`() {
|
||||
val expectedFiles = imagesIds.map { File(it.toString()) }.toTypedArray()
|
||||
fun `download() writes the given image to the FileService and returns its name`() {
|
||||
test {
|
||||
val mockImage = MockMultipartFile("image.jpg", byteArrayOf(*"Random data".encodeToByteArray()))
|
||||
val expectedImageId = recipeImagesIds.maxOrNull()!! + 1L
|
||||
val expectedImageName = expectedImageId.imageName
|
||||
val expectedImagePath = expectedImageName.imagePath
|
||||
|
||||
whenever(recipeService.getById(recipeId)).doReturn(recipe)
|
||||
whenever(recipeDirectory.exists()).doReturn(true)
|
||||
whenever(recipeDirectory.isDirectory).doReturn(true)
|
||||
whenever(recipeDirectory.listFiles()).doReturn(expectedFiles)
|
||||
doReturn(recipeDirectory).whenever(service).getRecipeDirectory(recipeId)
|
||||
val foundImageName = recipeImageService.download(mockImage, recipe)
|
||||
|
||||
val found = service.getAllIdsForRecipe(recipeId)
|
||||
assertEquals(expectedImageName, foundImageName)
|
||||
|
||||
assertEquals(imagesIds, found)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAllIdsForRecipe() returns an empty list when the given recipe's directory does not exists`() {
|
||||
whenever(recipeService.getById(recipeId)).doReturn(recipe)
|
||||
whenever(recipeDirectory.exists()).doReturn(false)
|
||||
whenever(recipeDirectory.isDirectory).doReturn(true)
|
||||
doReturn(recipeDirectory).whenever(service).getRecipeDirectory(recipeId)
|
||||
|
||||
val found = service.getAllIdsForRecipe(recipeId)
|
||||
|
||||
assertTrue(found.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getAllIdsForRecipe() returns an empty list when the given recipe's directory is not a directory`() {
|
||||
whenever(recipeService.getById(recipeId)).doReturn(recipe)
|
||||
whenever(recipeDirectory.exists()).doReturn(true)
|
||||
whenever(recipeDirectory.isDirectory).doReturn(false)
|
||||
doReturn(recipeDirectory).whenever(service).getRecipeDirectory(recipeId)
|
||||
|
||||
val found = service.getAllIdsForRecipe(recipeId)
|
||||
|
||||
assertTrue(found.isEmpty())
|
||||
}
|
||||
|
||||
// save()
|
||||
|
||||
@Test
|
||||
fun `save() writes the given image to the file service with the expected path`() {
|
||||
val expectedNextAvailableId = imagesIds.maxOrNull()!! + 1
|
||||
val imagePath = "$RECIPE_IMAGES_DIRECTORY/$recipeId/$expectedNextAvailableId"
|
||||
|
||||
doReturn(imagesIds).whenever(service).getAllIdsForRecipe(recipeId)
|
||||
doReturn(imagePath).whenever(service).getPath(expectedNextAvailableId, recipeId)
|
||||
|
||||
service.save(image, recipeId)
|
||||
|
||||
verify(fileService).write(image, imagePath)
|
||||
verify {
|
||||
fileService.write(mockImage, expectedImagePath, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete()
|
||||
|
||||
@Test
|
||||
fun `delete() deletes the image with the given recipe and image id from the file service`() {
|
||||
doReturn(imagePath).whenever(service).getPath(imageId, recipeId)
|
||||
fun `delete() deletes the image with the given name in the FileService`() {
|
||||
test {
|
||||
val imageName = recipeImagesIds.first().imageName
|
||||
val imagePath = imageName.imagePath
|
||||
|
||||
service.delete(imageId, recipeId)
|
||||
recipeImageService.delete(recipe, imageName)
|
||||
|
||||
verify(fileService).delete(imagePath)
|
||||
}
|
||||
|
||||
// getRecipeDirectory()
|
||||
|
||||
@Test
|
||||
fun `getRecipeDirectory() returns a file with the expected path`() {
|
||||
val recipeDirectoryPath = "$RECIPE_IMAGES_DIRECTORY/$recipeId"
|
||||
whenever(fileService.getPath(recipeDirectoryPath)).doReturn(recipeDirectoryPath)
|
||||
|
||||
val found = service.getRecipeDirectory(recipeId)
|
||||
|
||||
assertEquals(recipeDirectoryPath, found.path)
|
||||
}
|
||||
|
||||
// getPath()
|
||||
|
||||
@Test
|
||||
fun `getPath() returns the expected path`() {
|
||||
whenever(fileService.getPath(any())).doAnswer { it.arguments[0] as String }
|
||||
|
||||
val found = service.getPath(imageId, recipeId)
|
||||
|
||||
assertEquals(imagePath, found)
|
||||
verify {
|
||||
fileService.delete(imagePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,118 +1,298 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import com.nhaarman.mockitokotlin2.*
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.slf4j.Logger
|
||||
import org.springframework.core.io.Resource
|
||||
import org.springframework.core.io.ResourceLoader
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.mock.web.MockMultipartFile
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Paths
|
||||
import java.nio.file.Path
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
private val creProperties = CreProperties().apply {
|
||||
workingDirectory = "data"
|
||||
deploymentUrl = "http://localhost"
|
||||
}
|
||||
private const val mockFilePath = "existingFile"
|
||||
private val mockFilePathPath = Path.of(mockFilePath)
|
||||
private val mockFileData = byteArrayOf(0x1, 0x8, 0xa, 0xf)
|
||||
|
||||
private class FileServiceTestContext {
|
||||
val fileService = spyk(FileServiceImpl(creProperties, mockk {
|
||||
every { error(any(), any<Exception>()) } just Runs
|
||||
}))
|
||||
val mockFile = mockk<File> {
|
||||
every { path } returns mockFilePath
|
||||
every { exists() } returns true
|
||||
every { isFile } returns true
|
||||
every { toPath() } returns mockFilePathPath
|
||||
}
|
||||
val mockFileFullPath = spyk(FilePath("${creProperties.workingDirectory}/$mockFilePath")) {
|
||||
every { file } returns mockFile
|
||||
|
||||
with(fileService) {
|
||||
every { mockFilePath.fullPath() } returns this@spyk
|
||||
}
|
||||
}
|
||||
val mockMultipartFile = spyk(MockMultipartFile(mockFilePath, mockFileData))
|
||||
}
|
||||
|
||||
class FileServiceTest {
|
||||
private val resourcesLoader = mock<ResourceLoader>()
|
||||
private val logger = mock<Logger>()
|
||||
private val properties = CreProperties()
|
||||
|
||||
private val service = spy(FileService(resourcesLoader, properties, logger))
|
||||
|
||||
private val path = "/var/cre/file"
|
||||
|
||||
@AfterEach
|
||||
fun afterEach() {
|
||||
reset(resourcesLoader, logger, service)
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
// readResource()
|
||||
// exists()
|
||||
|
||||
@Test
|
||||
fun `readResource() returns content of the resource at the given path`() {
|
||||
val resource = mock<Resource>()
|
||||
val resourceStream = mock<InputStream>()
|
||||
val resourceContent = """
|
||||
Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
""".trimIndent()
|
||||
|
||||
whenever(resource.inputStream).doReturn(resourceStream)
|
||||
whenever(resourcesLoader.getResource("classpath:$path")).doReturn(resource)
|
||||
doReturn(resourceContent).whenever(service).readInputStreamAsString(resourceStream)
|
||||
|
||||
val found = service.readResource(path)
|
||||
|
||||
assertEquals(resourceContent, found)
|
||||
fun `exists() returns true when the file at the given path exists and is a file`() {
|
||||
test {
|
||||
assertTrue { fileService.exists(mockFilePath) }
|
||||
}
|
||||
}
|
||||
|
||||
// readInputStreamAsString()
|
||||
@Test
|
||||
fun `exists() returns false when the file at the given path does not exist`() {
|
||||
test {
|
||||
every { mockFile.exists() } returns false
|
||||
|
||||
assertFalse { fileService.exists(mockFilePath) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `readInputStreamAsString() returns a String matching the given input stream's content`() {
|
||||
val stream = mock<InputStream>()
|
||||
val streamContent = """
|
||||
Line 1
|
||||
Line 2
|
||||
Line 3
|
||||
""".trimIndent()
|
||||
fun `exists() returns false when the file at the given path is not a file`() {
|
||||
test {
|
||||
every { mockFile.isFile } returns false
|
||||
|
||||
whenever(stream.readAllBytes()).doAnswer { streamContent.toByteArray() }
|
||||
assertFalse { fileService.exists(mockFilePath) }
|
||||
}
|
||||
}
|
||||
|
||||
val found = service.readInputStreamAsString(stream)
|
||||
// read()
|
||||
|
||||
assertEquals(streamContent, found)
|
||||
@Test
|
||||
fun `read() returns a valid ByteArrayResource`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
mockkStatic(File::readBytes)
|
||||
every { mockFile.readBytes() } returns mockFileData
|
||||
|
||||
val redResource = fileService.read(mockFilePath)
|
||||
|
||||
assertEquals(mockFileData, redResource.byteArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `read() throws FileNotFoundException when no file exists at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
with(assertThrows<FileNotFoundException> { fileService.read(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `read() throws FileReadException when an IOException is thrown`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
mockkStatic(File::readBytes)
|
||||
every { mockFile.readBytes() } throws IOException()
|
||||
|
||||
with(assertThrows<FileReadException> { fileService.read(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create()
|
||||
|
||||
@Test
|
||||
fun `create() creates a file at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
mockkStatic(File::create)
|
||||
every { mockFile.create() } just Runs
|
||||
|
||||
fileService.create(mockFilePath)
|
||||
|
||||
verify {
|
||||
mockFile.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create() does nothing when a file already exists at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
fileService.create(mockFilePath)
|
||||
|
||||
verify(exactly = 0) {
|
||||
mockFile.create()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create() throws FileCreateException when the file creation throws an IOException`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
mockkStatic(File::create)
|
||||
every { mockFile.create() } throws IOException()
|
||||
|
||||
with(assertThrows<FileCreateException> { fileService.create(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// write()
|
||||
|
||||
private inline fun withMultipartFile(size: Long = 1000L, test: (MultipartFile) -> Unit) {
|
||||
val multipartFile = mock<MultipartFile>()
|
||||
whenever(multipartFile.size).doReturn(size)
|
||||
|
||||
test(multipartFile)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write() transfers data from the given MultipartFile to the file at the given path and returns true`() {
|
||||
withMultipartFile { multipartFile ->
|
||||
val file = mock<File>()
|
||||
val filePath = Paths.get(path)
|
||||
fun `write() creates and writes the given MultipartFile to the file at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
every { fileService.create(mockFilePath) } just Runs
|
||||
every { mockMultipartFile.transferTo(mockFilePathPath) } just Runs
|
||||
|
||||
whenever(file.toPath()).doReturn(filePath)
|
||||
doAnswer { file }.whenever(service).create(path)
|
||||
fileService.write(mockMultipartFile, mockFilePath, false)
|
||||
|
||||
assertTrue { service.write(multipartFile, path) }
|
||||
|
||||
verify(multipartFile).transferTo(filePath)
|
||||
verify {
|
||||
fileService.create(mockFilePath)
|
||||
mockMultipartFile.transferTo(mockFilePathPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write() returns true when given MultipartFile is empty`() {
|
||||
withMultipartFile(size = 0L) { multipartFile ->
|
||||
assertTrue { service.write(multipartFile, path) }
|
||||
|
||||
verify(multipartFile, never()).transferTo(any<File>())
|
||||
fun `write() throws FileExistsException when a file at the given path already exists and overwrite is disabled`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
with(assertThrows<FileExistsException> { fileService.write(mockMultipartFile, mockFilePath, false) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write() returns false when the data transfer throw an IOException`() {
|
||||
withMultipartFile { multipartFile ->
|
||||
val file = mock<File>()
|
||||
val filePath = Paths.get(path)
|
||||
fun `write() writes the given MultipartFile to an existing file when overwrite is enabled`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
every { mockMultipartFile.transferTo(mockFilePathPath) } just Runs
|
||||
|
||||
whenever(file.toPath()).doReturn(filePath)
|
||||
whenever(multipartFile.transferTo(filePath)).doThrow(IOException())
|
||||
doAnswer { file }.whenever(service).create(path)
|
||||
fileService.write(mockMultipartFile, mockFilePath, true)
|
||||
|
||||
assertFalse { service.write(multipartFile, path) }
|
||||
verify {
|
||||
mockMultipartFile.transferTo(mockFilePathPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write() throws FileWriteException when writing the given file throws an IOException`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
every { fileService.create(mockFilePath) } just Runs
|
||||
every { mockMultipartFile.transferTo(mockFilePathPath) } throws IOException()
|
||||
|
||||
with(assertThrows<FileWriteException> {
|
||||
fileService.write(mockMultipartFile, mockFilePath, false)
|
||||
}) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// delete()
|
||||
|
||||
@Test
|
||||
fun `delete() deletes the file at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
every { mockFile.delete() } returns true
|
||||
|
||||
fileService.delete(mockFilePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete() throws FileNotFoundException when no file exists at the given path`() {
|
||||
test {
|
||||
whenMockFilePathExists(false) {
|
||||
with(assertThrows<FileNotFoundException> { fileService.delete(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `delete() throws FileDeleteException when deleting throw and IOException`() {
|
||||
test {
|
||||
whenMockFilePathExists {
|
||||
every { mockFile.delete() } throws IOException()
|
||||
|
||||
with(assertThrows<FileDeleteException> { fileService.delete(mockFilePath) }) {
|
||||
assertEquals(mockFilePath, this.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// String.fullPath()
|
||||
|
||||
@Test
|
||||
fun `fullPath() appends the given path to the given working directory`() {
|
||||
test {
|
||||
with(fileService) {
|
||||
val fullFilePath = mockFilePath.fullPath()
|
||||
|
||||
assertEquals("${creProperties.workingDirectory}/$mockFilePath", fullFilePath.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fullPath() throws InvalidFilePathException when the given path contains invalid fragments`() {
|
||||
test {
|
||||
with(fileService) {
|
||||
BANNED_FILE_PATH_SHARDS.forEach {
|
||||
val maliciousPath = "$it/$mockFilePath"
|
||||
|
||||
with(assertThrows<InvalidFilePathException> { maliciousPath.fullPath() }) {
|
||||
assertEquals(maliciousPath, this.path)
|
||||
assertEquals(it, this.fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun test(test: FileServiceTestContext.() -> Unit) {
|
||||
FileServiceTestContext().test()
|
||||
}
|
||||
|
||||
private fun FileServiceTestContext.whenMockFilePathExists(exists: Boolean = true, test: () -> Unit) {
|
||||
every { fileService.exists(mockFilePath) } returns exists
|
||||
test()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import com.nhaarman.mockitokotlin2.*
|
||||
import dev.fyloz.colorrecipesexplorer.model.Material
|
||||
import dev.fyloz.colorrecipesexplorer.model.material
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.io.IOException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class SimdutServiceTest {
|
||||
private val fileService = mock<FileService>()
|
||||
private val service = spy(SimdutService(fileService, mock()))
|
||||
|
||||
private val material = material(id = 0L)
|
||||
|
||||
@AfterEach
|
||||
fun afterEach() {
|
||||
reset(fileService, service)
|
||||
}
|
||||
|
||||
@JvmName("withNullableMaterialPath")
|
||||
private inline fun withMaterialPath(material: Material? = null, exists: Boolean = true, test: (String) -> Unit) =
|
||||
withMaterialPath(material ?: this.material, exists, test)
|
||||
|
||||
private inline fun withMaterialPath(material: Material, exists: Boolean = true, test: (String) -> Unit) {
|
||||
val path = "data/simdut/${material.id}"
|
||||
doReturn(path).whenever(service).getPath(material)
|
||||
whenever(fileService.exists(path)).doReturn(exists)
|
||||
|
||||
test(path)
|
||||
}
|
||||
|
||||
// exists()
|
||||
|
||||
@Test
|
||||
fun `exists() returns true when a SIMDUT file exists for the given material`() {
|
||||
withMaterialPath {
|
||||
assertTrue { service.exists(material) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exists() returns false when no SIMDUT file exists for the given material`() {
|
||||
withMaterialPath(exists = false) {
|
||||
assertFalse { service.exists(material) }
|
||||
}
|
||||
}
|
||||
|
||||
// read()
|
||||
|
||||
@Test
|
||||
fun `read() returns a filled ByteArray when a SIMDUT exists for the given material`() {
|
||||
withMaterialPath { path ->
|
||||
val simdutContent = byteArrayOf(0xf)
|
||||
|
||||
whenever(fileService.readAsBytes(path)).doReturn(simdutContent)
|
||||
|
||||
val found = service.read(material)
|
||||
|
||||
assertEquals(simdutContent, found)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `read() returns a empty ByteArray when no SIMDUT exists for the given material`() {
|
||||
withMaterialPath(exists = false) {
|
||||
val found = service.read(material)
|
||||
|
||||
assertTrue { found.isEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `read() returns a empty ByteArray when reading the SIMDUT throws an IOException`() {
|
||||
withMaterialPath { path ->
|
||||
whenever(fileService.readAsBytes(path)).doAnswer { throw IOException() }
|
||||
|
||||
val found = service.read(material)
|
||||
|
||||
assertTrue { found.isEmpty() }
|
||||
}
|
||||
}
|
||||
|
||||
// write()
|
||||
|
||||
@Test
|
||||
fun `write() writes the given MultipartFile to the disk for the given material`() {
|
||||
withMaterialPath { path ->
|
||||
val simdutMultipart = mock<MultipartFile>()
|
||||
|
||||
whenever(fileService.write(simdutMultipart, path)).doReturn(true)
|
||||
|
||||
service.write(material, simdutMultipart)
|
||||
|
||||
verify(fileService).write(simdutMultipart, path)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `write() throws a SimdutWriteException when writing the given MultipartFile to the disk fails`() {
|
||||
withMaterialPath { path ->
|
||||
val simdutMultipart = mock<MultipartFile>()
|
||||
|
||||
whenever(fileService.write(simdutMultipart, path)).doReturn(false)
|
||||
|
||||
assertThrows<SimdutWriteException> { service.write(material, simdutMultipart) }
|
||||
}
|
||||
}
|
||||
|
||||
// update()
|
||||
|
||||
@Test
|
||||
fun `update() deletes and write the SIMDUT for the given material`() {
|
||||
val simdutMultipart = mock<MultipartFile>()
|
||||
|
||||
// Prevents calling the actual implementation
|
||||
doAnswer { }.whenever(service).delete(material)
|
||||
doAnswer { }.whenever(service).write(material, simdutMultipart)
|
||||
|
||||
service.update(simdutMultipart, material)
|
||||
|
||||
verify(service).delete(material)
|
||||
verify(service).write(material, simdutMultipart)
|
||||
}
|
||||
|
||||
// delete()
|
||||
|
||||
@Test
|
||||
fun `delete() deletes the SIMDUT of the given material from the disk`() {
|
||||
withMaterialPath { path ->
|
||||
service.delete(material)
|
||||
|
||||
verify(fileService).delete(path)
|
||||
}
|
||||
}
|
||||
|
||||
// getPath()
|
||||
|
||||
@Test
|
||||
fun `getPath() returns the appropriate path for the given material`() {
|
||||
val simdutFileName = material.id.toString()
|
||||
val workingDirectory = "data"
|
||||
val expectedPath = "$workingDirectory/$SIMDUT_DIRECTORY/$simdutFileName"
|
||||
|
||||
whenever(fileService.getPath(any())).doAnswer { "$workingDirectory/${it.arguments[0]}" }
|
||||
doAnswer { simdutFileName }.whenever(service).getSimdutFileName(material)
|
||||
|
||||
val found = service.getPath(material)
|
||||
|
||||
assertEquals(expectedPath, found)
|
||||
|
||||
verify(fileService).getPath("$SIMDUT_DIRECTORY/$simdutFileName")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package dev.fyloz.colorrecipesexplorer.service.files
|
||||
|
||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||
import dev.fyloz.colorrecipesexplorer.utils.PdfDocument
|
||||
import dev.fyloz.colorrecipesexplorer.utils.toByteArrayResource
|
||||
import io.mockk.*
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.io.ByteArrayResource
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
private class TouchUpKitServiceTestContext {
|
||||
val fileService = mockk<FileService> {
|
||||
every { write(any<ByteArrayResource>(), any(), any()) } just Runs
|
||||
}
|
||||
val creProperties = mockk<CreProperties> {
|
||||
every { cacheGeneratedFiles } returns false
|
||||
}
|
||||
val touchUpKitService = spyk(TouchUpKitServiceImpl(fileService, creProperties))
|
||||
val pdfDocumentData = mockk<ByteArrayResource>()
|
||||
val pdfDocument = mockk<PdfDocument> {
|
||||
mockkStatic(PdfDocument::toByteArrayResource)
|
||||
mockkStatic(PdfDocument::toByteArrayResource)
|
||||
every { toByteArrayResource() } returns pdfDocumentData
|
||||
}
|
||||
}
|
||||
|
||||
class TouchUpKitServiceTest {
|
||||
private val job = "job"
|
||||
|
||||
@AfterEach
|
||||
internal fun afterEach() {
|
||||
clearAllMocks()
|
||||
}
|
||||
|
||||
// generateJobPdf()
|
||||
|
||||
@Test
|
||||
fun `generateJobPdf() generates a valid PdfDocument for the given job`() {
|
||||
test {
|
||||
val generatedPdfDocument = touchUpKitService.generateJobPdf(job)
|
||||
|
||||
setOf(0, 1).forEach {
|
||||
assertEquals(TOUCH_UP_TEXT_FR, generatedPdfDocument.containers[it].texts[0].text)
|
||||
assertEquals(TOUCH_UP_TEXT_EN, generatedPdfDocument.containers[it].texts[1].text)
|
||||
assertEquals(job, generatedPdfDocument.containers[it].texts[2].text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// generateJobPdfResource()
|
||||
|
||||
@Test
|
||||
fun `generateJobPdfResource() generates and returns a ByteArrayResource for the given job then cache it`() {
|
||||
test {
|
||||
every { touchUpKitService.generateJobPdf(any()) } returns pdfDocument
|
||||
with(touchUpKitService) {
|
||||
every { job.cachePdfDocument(pdfDocument) } just Runs
|
||||
}
|
||||
|
||||
val generatedResource = touchUpKitService.generateJobPdfResource(job)
|
||||
|
||||
assertEquals(pdfDocumentData, generatedResource)
|
||||
|
||||
verify {
|
||||
with(touchUpKitService) {
|
||||
job.cachePdfDocument(pdfDocument)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `generateJobPdfResource() returns a cached ByteArrayResource from the FileService when caching is enabled and a cached file eixsts for the given job`() {
|
||||
test {
|
||||
every { creProperties.cacheGeneratedFiles } returns true
|
||||
every { fileService.exists(any()) } returns true
|
||||
every { fileService.read(any()) } returns pdfDocumentData
|
||||
|
||||
val redResource = touchUpKitService.generateJobPdfResource(job)
|
||||
|
||||
assertEquals(pdfDocumentData, redResource)
|
||||
}
|
||||
}
|
||||
|
||||
// String.cachePdfDocument()
|
||||
|
||||
@Test
|
||||
fun `cachePdfDocument() does nothing when caching is disabled`() {
|
||||
test {
|
||||
every { creProperties.cacheGeneratedFiles } returns false
|
||||
|
||||
with(touchUpKitService) {
|
||||
job.cachePdfDocument(pdfDocument)
|
||||
}
|
||||
|
||||
verify(exactly = 0) {
|
||||
fileService.write(any<ByteArrayResource>(), any(), any())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `cachePdfDocument() writes the given document to the FileService when cache is enabled`() {
|
||||
test {
|
||||
every { creProperties.cacheGeneratedFiles } returns true
|
||||
|
||||
with(touchUpKitService) {
|
||||
job.cachePdfDocument(pdfDocument)
|
||||
}
|
||||
|
||||
verify {
|
||||
fileService.write(pdfDocumentData, any(), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun test(test: TouchUpKitServiceTestContext.() -> Unit) {
|
||||
TouchUpKitServiceTestContext().test()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue