From eb4bb6b52a5bf20d07d9e96c1ead4ad1f00c9392 Mon Sep 17 00:00:00 2001 From: FyloZ Date: Wed, 23 Mar 2022 23:40:48 -0400 Subject: [PATCH] #25 Migrate recipes to new logic --- .../logic/files/XlsService.java | 8 +- .../xlsx/XlsxExporter.java | 16 +- .../fyloz/colorrecipesexplorer/Constants.kt | 9 +- .../config/initializers/MixInitializer.kt | 4 +- .../config/initializers/RecipeInitializer.kt | 29 +- .../properties/MaterialTypeProperties.kt | 2 - .../fyloz/colorrecipesexplorer/dtos/MixDto.kt | 7 +- .../colorrecipesexplorer/dtos/RecipeDto.kt | 122 ++++++ .../fyloz/colorrecipesexplorer/logic/Logic.kt | 13 + .../logic/MaterialLogic.kt | 4 +- .../colorrecipesexplorer/logic/MixLogic.kt | 8 +- .../logic/MixMaterialLogic.kt | 4 +- .../colorrecipesexplorer/logic/RecipeLogic.kt | 335 +++++++--------- .../logic/RecipeStepLogic.kt | 10 +- .../colorrecipesexplorer/model/Company.kt | 20 +- .../colorrecipesexplorer/model/Material.kt | 33 +- .../model/MaterialType.kt | 38 +- .../fyloz/colorrecipesexplorer/model/Mix.kt | 34 +- .../colorrecipesexplorer/model/MixMaterial.kt | 22 +- .../colorrecipesexplorer/model/MixType.kt | 36 +- .../colorrecipesexplorer/model/Recipe.kt | 255 +----------- .../colorrecipesexplorer/model/RecipeStep.kt | 8 +- .../repository/MixRepository.kt | 2 +- .../repository/MixTypeRepository.kt | 9 +- .../repository/RecipeRepository.kt | 18 +- .../rest/RecipeController.kt | 79 ++-- .../service/MixService.kt | 8 +- .../service/RecipeService.kt | 86 ++++ .../colorrecipesexplorer/utils/Collections.kt | 2 +- .../utils/collections/LazyMapList.kt | 26 ++ .../logic/DefaultInventoryLogicTest.kt | 20 +- .../logic/DefaultMaterialLogicTest.kt | 30 +- .../logic/DefaultMixLogicTest.kt | 24 +- .../logic/DefaultRecipeImageLogicTest.kt | 101 +++++ .../logic/DefaultRecipeLogicTest.kt | 217 ++++++++++ .../logic/DefaultRecipeStepLogicTest.kt | 30 +- .../logic/RecipeLogicTest.kt | 371 ------------------ .../repository/MaterialRepositoryTest.kt | 29 -- .../repository/MixRepositoryTest.kt | 41 -- 39 files changed, 866 insertions(+), 1244 deletions(-) create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt create mode 100644 src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/collections/LazyMapList.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt create mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepositoryTest.kt delete mode 100644 src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepositoryTest.kt diff --git a/src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java b/src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java index f9a09e6..845f459 100644 --- a/src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java +++ b/src/main/java/dev/fyloz/colorrecipesexplorer/logic/files/XlsService.java @@ -1,7 +1,7 @@ package dev.fyloz.colorrecipesexplorer.logic.files; +import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto; import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic; -import dev.fyloz.colorrecipesexplorer.model.Recipe; import dev.fyloz.colorrecipesexplorer.xlsx.XlsxExporter; import mu.KotlinLogging; import org.slf4j.Logger; @@ -32,7 +32,7 @@ public class XlsService { * @param recipe La recette * @return Le fichier XLS de la recette */ - public byte[] generate(Recipe recipe) { + public byte[] generate(RecipeDto recipe) { return new XlsxExporter(logger).generate(recipe); } @@ -55,10 +55,10 @@ public class XlsService { logger.info("Exportation de toutes les couleurs en XLS"); byte[] zipContent; - Collection recipes = recipeService.getAll(); + Collection recipes = recipeService.getAll(); try (ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(); ZipOutputStream zipOutput = new ZipOutputStream(byteOutput)) { - for (Recipe recipe : recipes) { + for (RecipeDto recipe : recipes) { byte[] recipeXLS = generate(recipe); zipOutput.putNextEntry(new ZipEntry(String.format("%s_%s.xlsx", recipe.getCompany().getName(), recipe.getName()))); zipOutput.write(recipeXLS, 0, recipeXLS.length); diff --git a/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java b/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java index 79b54e4..c2508e8 100644 --- a/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java +++ b/src/main/java/dev/fyloz/colorrecipesexplorer/xlsx/XlsxExporter.java @@ -1,8 +1,8 @@ package dev.fyloz.colorrecipesexplorer.xlsx; -import dev.fyloz.colorrecipesexplorer.model.Mix; -import dev.fyloz.colorrecipesexplorer.model.MixMaterial; -import dev.fyloz.colorrecipesexplorer.model.Recipe; +import dev.fyloz.colorrecipesexplorer.dtos.MixDto; +import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto; +import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto; import dev.fyloz.colorrecipesexplorer.xlsx.component.Document; import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet; import dev.fyloz.colorrecipesexplorer.xlsx.component.Table; @@ -23,7 +23,7 @@ public class XlsxExporter { this.logger = logger; } - public byte[] generate(Recipe recipe) { + public byte[] generate(RecipeDto recipe) { logger.info(String.format("Génération du XLS de la couleur %s (%s)", recipe.getName(), recipe.getId())); Document document = new Document(recipe.getName(), logger); @@ -44,7 +44,7 @@ public class XlsxExporter { return output; } - private void registerCells(Recipe recipe, Sheet sheet) { + private void registerCells(RecipeDto recipe, Sheet sheet) { // Header sheet.registerCell(new TitleCell(recipe.getName())); sheet.registerCell(new DescriptionCell(DescriptionCell.DescriptionCellType.NAME, "Bannière")); @@ -59,17 +59,17 @@ public class XlsxExporter { sheet.registerCell(new DescriptionCell(DescriptionCell.DescriptionCellType.VALUE_STR, recipe.getRemark())); // Mélanges - Collection recipeMixes = recipe.getMixes(); + Collection recipeMixes = recipe.getMixes(); if (recipeMixes.size() > 0) { sheet.registerCell(new SectionTitleCell("Recette")); - for (Mix mix : recipeMixes) { + for (MixDto mix : recipeMixes) { Table mixTable = new Table(4, mix.getMixMaterials().size() + 1, mix.getMixType().getName()); mixTable.setColumnName(0, "Quantité"); mixTable.setColumnName(2, "Unités"); int row = 0; - for (MixMaterial mixMaterial : mix.getMixMaterials()) { + for (MixMaterialDto mixMaterial : mix.getMixMaterials()) { mixTable.setRowName(row, mixMaterial.getMaterial().getName()); mixTable.setContent(new Position(1, row + 1), mixMaterial.getQuantity()); mixTable.setContent(new Position(3, row + 1), mixMaterial.getMaterial().getMaterialType().getUsePercentages() ? "%" : "mL"); diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt index 4cb51dc..2a8590c 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/Constants.kt @@ -6,12 +6,15 @@ object Constants { const val MATERIAL = "/api/material" const val MATERIAL_TYPE = "/api/materialtype" const val MIX = "/api/recipe/mix" + const val RECIPE = "/api/recipe" } object FilePaths { - const val PDF = "pdf" + private const val PDF = "pdf" + private const val IMAGES = "images" const val SIMDUT = "$PDF/simdut" + const val RECIPE_IMAGES = "$IMAGES/recipes" } object ValidationMessages { @@ -19,4 +22,8 @@ object Constants { const val SIZE_GREATER_OR_EQUALS_ONE = "Must be greater or equals to 1" const val RANGE_OUTSIDE_PERCENTS = "Must be between 0 and 100" } + + object ValidationRegexes { + const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$" + } } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt index bc48f94..71db2e0 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/MixInitializer.kt @@ -34,7 +34,7 @@ class MixInitializer( private fun fixMixPositions(mix: MixDto) { val maxPosition = mix.mixMaterials.maxOf { it.position } - logger.warn("Mix ${mix.id} (${mix.mixType.name}, ${mix.recipe.name}) has invalid positions:") + logger.warn("Mix ${mix.id} (mix name: ${mix.mixType.name}, recipe id: ${mix.recipeId}) has invalid positions:") val invalidMixMaterials: Collection = with(mix.mixMaterials.filter { it.position == 0 }) { if (maxPosition == 0 && this.size > 1) { @@ -47,7 +47,7 @@ class MixInitializer( val fixedMixMaterials = increaseMixMaterialsPosition(invalidMixMaterials, maxPosition + 1) val updatedMixMaterials = mix.mixMaterials.merge(fixedMixMaterials) - with(mix.copy(mixMaterials = updatedMixMaterials.toMutableSet())) { + with(mix.copy(mixMaterials = updatedMixMaterials)) { mixLogic.update(this) } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt index 5890b57..5347e20 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/initializers/RecipeInitializer.kt @@ -1,10 +1,10 @@ package dev.fyloz.colorrecipesexplorer.config.initializers import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase +import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic -import dev.fyloz.colorrecipesexplorer.model.Recipe -import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation -import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.utils.merge import mu.KotlinLogging import org.springframework.context.annotation.Configuration @@ -24,30 +24,29 @@ class RecipeInitializer( private fun fixAllPositions() { logger.debug("Validating recipes steps positions...") - recipeLogic.getAll() + recipeLogic.getAllWithMixesAndGroupsInformation() .forEach(this::fixRecipePositions) logger.debug("Recipes steps positions are valid!") } - private fun fixRecipePositions(recipe: Recipe) { + private fun fixRecipePositions(recipe: RecipeDto) { val fixedGroupInformation = recipe.groupsInformation - .filter { it.steps != null } - .filter { groupInfo -> groupInfo.steps!!.any { it.position == 0 } } + .filter { groupInfo -> groupInfo.steps.any { it.position == 0 } } .map { fixGroupInformationPositions(recipe, it) } val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation) { it.id } - with(recipe.copy(groupsInformation = updatedGroupInformation.toMutableSet())) { + with(recipe.copy(groupsInformation = updatedGroupInformation)) { recipeLogic.update(this) } } private fun fixGroupInformationPositions( - recipe: Recipe, - groupInformation: RecipeGroupInformation - ): RecipeGroupInformation { - val steps = groupInformation.steps!! + recipe: RecipeDto, + groupInformation: RecipeGroupInformationDto + ): RecipeGroupInformationDto { + val steps = groupInformation.steps val maxPosition = steps.maxOf { it.position } logger.warn("Recipe ${recipe.id} (${recipe.name}) has invalid positions:") @@ -56,12 +55,12 @@ class RecipeInitializer( val fixedRecipeSteps = increaseRecipeStepsPosition(groupInformation, invalidRecipeSteps, maxPosition + 1) val updatedRecipeSteps = steps.merge(fixedRecipeSteps) { it.id } - return groupInformation.copy(steps = updatedRecipeSteps.toMutableSet()) + return groupInformation.copy(steps = updatedRecipeSteps) } private fun increaseRecipeStepsPosition( - groupInformation: RecipeGroupInformation, - recipeSteps: Iterable, + groupInformation: RecipeGroupInformationDto, + recipeSteps: Iterable, firstPosition: Int ) = recipeSteps diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt index 736cd91..343935a 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/config/properties/MaterialTypeProperties.kt @@ -1,8 +1,6 @@ package dev.fyloz.colorrecipesexplorer.config.properties import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto -import dev.fyloz.colorrecipesexplorer.model.MaterialType -import dev.fyloz.colorrecipesexplorer.model.materialType import org.springframework.boot.context.properties.ConfigurationProperties import org.springframework.stereotype.Component import org.springframework.util.Assert diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt index 957a59f..3cc26dc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/MixDto.kt @@ -2,7 +2,6 @@ package dev.fyloz.colorrecipesexplorer.dtos import com.fasterxml.jackson.annotation.JsonIgnore import dev.fyloz.colorrecipesexplorer.Constants -import dev.fyloz.colorrecipesexplorer.model.Recipe import javax.validation.constraints.Min import javax.validation.constraints.NotBlank @@ -12,11 +11,11 @@ data class MixDto( val location: String? = null, @JsonIgnore - val recipe: Recipe, // TODO change to dto + val recipeId: Long, val mixType: MixTypeDto, - val mixMaterials: Set + val mixMaterials: List ) : EntityDto data class MixSaveDto( @@ -29,7 +28,7 @@ data class MixSaveDto( val materialTypeId: Long, - val mixMaterials: Set + val mixMaterials: List ) data class MixDeductDto( diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt new file mode 100644 index 0000000..80c22c4 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/dtos/RecipeDto.kt @@ -0,0 +1,122 @@ +package dev.fyloz.colorrecipesexplorer.dtos + +import com.fasterxml.jackson.annotation.JsonIgnore +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.model.account.Group +import java.time.LocalDate +import javax.validation.constraints.Max +import javax.validation.constraints.Min +import javax.validation.constraints.NotBlank +import javax.validation.constraints.Pattern + +data class RecipeDto( + override val id: Long = 0L, + + val name: String, + + val description: String, + + val color: String, + + val gloss: Byte, + + val sample: Int?, + + val approbationDate: LocalDate?, + + val approbationExpired: Boolean, + + val remark: String, + + val company: CompanyDto, + + val mixes: List, + + val groupsInformation: List +) : EntityDto { + val mixTypes: Collection + @JsonIgnore + get() = mixes.map { it.mixType } +} + +data class RecipeSaveDto( + @field:NotBlank + val name: String, + + @field:NotBlank + val description: String, + + @field:NotBlank + @field:Pattern(regexp = Constants.ValidationRegexes.VALIDATION_COLOR_PATTERN) + val color: String, + + @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + val gloss: Byte, + + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) + val sample: Int?, + + val approbationDate: LocalDate?, + + val remark: String?, + + val companyId: Long +) + +data class RecipeUpdateDto( + val id: Long, + + @field:NotBlank + val name: String, + + @field:NotBlank + val description: String, + + @field:NotBlank + @field:Pattern(regexp = Constants.ValidationRegexes.VALIDATION_COLOR_PATTERN) + val color: String, + + @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) + val gloss: Byte, + + @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) + val sample: Int?, + + val approbationDate: LocalDate?, + + val remark: String?, + + val steps: List +) + +data class RecipeGroupInformationDto( + override val id: Long = 0L, + + val group: Group, + + val note: String? = null, + + val steps: List = listOf() +) : EntityDto + +data class RecipeGroupStepsDto( + val groupId: Long, + + val steps: List +) + +data class RecipeGroupNoteDto( + val groupId: Long, + + val content: String? +) + +data class RecipePublicDataDto( + val recipeId: Long, + + val notes: List, + + val mixesLocation: List +) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt index 1214c9e..997d089 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/Logic.kt @@ -5,6 +5,8 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.service.Service +import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList +import org.springframework.transaction.annotation.Transactional /** * Represents the logic for a DTO type. @@ -92,6 +94,17 @@ abstract class BaseLogic>( details ) + private fun loadRelations(dto: D, relationSelectors: Collection<(D) -> Iterable<*>>) { + relationSelectors.map { it(dto) } + .forEach { + if (it is LazyMapList<*, *>) { + it.initialize() + } else { + println("Can't load :(") + } + } + } + companion object { const val ID_IDENTIFIER_NAME = "id" const val NAME_IDENTIFIER_NAME = "name" diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt index 7f3027f..9a9d974 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MaterialLogic.kt @@ -56,9 +56,9 @@ class DefaultMaterialLogic( override fun getAllForMixUpdate(mixId: Long): Collection { val mix = mixLogic.getById(mixId) - val recipesMixTypes = mix.recipe.mixTypes + val recipe = recipeLogic.getById(mix.recipeId) - return getAll().filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } } + return getAll().filter { !it.isMixType || recipe.mixTypes.any { mixType -> mixType.material.id == it.id } } .filter { it.id != mix.mixType.material.id } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt index f719422..16c31d5 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixLogic.kt @@ -34,9 +34,9 @@ class DefaultMixLogic( val materialType = materialTypeLogic.getById(dto.materialTypeId) val mix = MixDto( - recipe = recipe, + recipeId = recipe.id, mixType = mixTypeLogic.getOrCreateForNameAndMaterialType(dto.name, materialType), - mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet() + mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials) ) return save(mix) @@ -50,9 +50,9 @@ class DefaultMixLogic( return update( MixDto( id = dto.id, - recipe = recipeLogic.getById(dto.recipeId), + recipeId = dto.recipeId, mixType = mixTypeLogic.updateOrCreateForNameAndMaterialType(mix.mixType, dto.name, materialType), - mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials).toSet() + mixMaterials = mixMaterialLogic.validateAndSaveAll(dto.mixMaterials) ) ) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt index b6ddf38..1ff78fd 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/MixMaterialLogic.kt @@ -21,7 +21,7 @@ interface MixMaterialLogic : Logic { fun validateMixMaterials(mixMaterials: Set) /** Validates the given mix materials [dtos] and save them. */ - fun validateAndSaveAll(dtos: Collection): Collection + fun validateAndSaveAll(dtos: List): List } @LogicComponent @@ -43,7 +43,7 @@ class DefaultMixMaterialLogic(service: MixMaterialService, @Lazy private val mat } } - override fun validateAndSaveAll(dtos: Collection): Collection { + override fun validateAndSaveAll(dtos: List): List { val dtosWithMaterials = dtos.map { MixMaterialDto( id = it.id, diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt index 19315e3..1daf5cd 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogic.kt @@ -1,256 +1,185 @@ package dev.fyloz.colorrecipesexplorer.logic -import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase -import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.* +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.model.account.Group -import dev.fyloz.colorrecipesexplorer.model.validation.or -import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository -import dev.fyloz.colorrecipesexplorer.utils.setAll -import org.springframework.context.annotation.Lazy -import org.springframework.stereotype.Service +import dev.fyloz.colorrecipesexplorer.model.Recipe +import dev.fyloz.colorrecipesexplorer.service.RecipeService +import dev.fyloz.colorrecipesexplorer.utils.collections.LazyMapList +import dev.fyloz.colorrecipesexplorer.utils.merge +import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile -import java.time.LocalDate -import java.time.Period -import javax.transaction.Transactional -interface RecipeLogic : - ExternalModelService { - /** Checks if one or more recipes have the given [company]. */ - fun existsByCompany(company: Company): Boolean - - /** Checks if a recipe exists with the given [name] and [company]. */ - fun existsByNameAndCompany(name: String, company: Company): Boolean - - /** Checks if the approbation date of the given [recipe] is expired. */ - fun isApprobationExpired(recipe: Recipe): Boolean? +interface RecipeLogic : Logic { + /** Gets all recipes and load their mixes and groupsInformation, to prevent LazyInitializationExceptions */ + fun getAllWithMixesAndGroupsInformation(): Collection /** Gets all recipes with the given [name]. */ - fun getAllByName(name: String): Collection + fun getAllByName(name: String): Collection - /** Gets all recipes with the given [company]. */ - fun getAllByCompany(company: Company): Collection + /** Saves the given [dto]. */ + fun save(dto: RecipeSaveDto): RecipeDto + + /** Updates the given [dto]. */ + fun update(dto: RecipeUpdateDto): RecipeDto /** Updates the public data of a recipe with the given [publicDataDto]. */ fun updatePublicData(publicDataDto: RecipePublicDataDto) - - /** Adds the given [mix] to the given [recipe]. */ - fun addMix(recipe: Recipe, mix: Mix): Recipe - - /** Removes the given [mix] from its recipe. */ - fun removeMix(mix: Mix): Recipe } -@Service -@RequireDatabase +@LogicComponent class DefaultRecipeLogic( - recipeRepository: RecipeRepository, - val companyLogic: CompanyLogic, - val mixLogic: MixLogic, - val recipeStepLogic: RecipeStepLogic, - @Lazy val groupLogic: GroupLogic, - val recipeImageLogic: RecipeImageLogic, - val configService: ConfigurationLogic -) : - AbstractExternalModelService( - recipeRepository - ), - RecipeLogic { - override fun idNotFoundException(id: Long) = recipeIdNotFoundException(id) - override fun idAlreadyExistsException(id: Long) = recipeIdAlreadyExistsException(id) + service: RecipeService, + private val companyLogic: CompanyLogic, + private val recipeStepLogic: RecipeStepLogic, + private val mixLogic: MixLogic, + private val groupLogic: GroupLogic +) : BaseLogic(service, Recipe::class.simpleName!!), RecipeLogic { + @Transactional + override fun getAllWithMixesAndGroupsInformation() = + getAll().onEach { (it.mixes as LazyMapList<*, *>).initialize() } + .onEach { (it.groupsInformation as LazyMapList<*, *>).initialize() } - override fun Recipe.toOutput() = RecipeOutputDto( - this.id!!, - this.name, - this.description, - this.color, - this.gloss, - this.sample, - this.approbationDate, - isApprobationExpired(this), - this.remark, - this.company, - this.mixes.map { mix(it) }.toSet(), - this.groupsInformation, - recipeImageLogic.getAllImages(this) - .map { this.imageUrl(configService.getContent(ConfigurationType.INSTANCE_URL), it) } - .toSet() + override fun getAllByName(name: String) = service.getAllByName(name) + + override fun save(dto: RecipeSaveDto) = save( + RecipeDto( + name = dto.name, + description = dto.description, + color = dto.color, + gloss = dto.gloss, + sample = dto.sample, + approbationDate = dto.approbationDate, + approbationExpired = false, + remark = dto.remark ?: "", + company = companyLogic.getById(dto.companyId), + mixes = listOf(), + groupsInformation = listOf() + ) ) - override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company) - override fun existsByNameAndCompany(name: String, company: Company) = - repository.existsByNameAndCompany(name, company) + override fun save(dto: RecipeDto): RecipeDto { + throwIfNameAndCompanyAlreadyExists(dto.name, dto.company.id) - override fun isApprobationExpired(recipe: Recipe): Boolean? = - with(Period.parse(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) { - recipe.approbationDate?.plus(this)?.isBefore(LocalDate.now()) - } - - override fun getAllByName(name: String) = repository.findAllByName(name) - override fun getAllByCompany(company: Company) = repository.findAllByCompany(company) - - override fun save(entity: RecipeSaveDto): Recipe { - val company = company(companyLogic.getById(entity.companyId)) - - if (existsByNameAndCompany(entity.name, company)) { - throw recipeNameAlreadyExistsForCompanyException(entity.name, company) - } - - return save(with(entity) { - recipe( - name = name, - description = description, - color = color, - gloss = gloss, - sample = sample, - approbationDate = approbationDate, - remark = remark ?: "", - company = company - ) - }) + return super.save(dto) } - @Transactional - override fun update(entity: RecipeUpdateDto): Recipe { - val persistedRecipe = getById(entity.id) - val name = entity.name - val company = persistedRecipe.company + override fun update(dto: RecipeUpdateDto): RecipeDto { + val recipe = getById(dto.id) - if (name != null && - name != persistedRecipe.name && - existsByNameAndCompany(name, company) - ) { - throw recipeNameAlreadyExistsForCompanyException(name, company) - } - - 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 = company, - mixes = persistedRecipe.mixes, - groupsInformation = updateGroupsInformation(persistedRecipe, entity) + return update( + RecipeDto( + id = dto.id, + name = dto.name, + description = dto.description, + color = dto.color, + gloss = dto.gloss, + sample = dto.sample, + approbationDate = dto.approbationDate, + approbationExpired = false, + remark = dto.remark ?: "", + company = recipe.company, + mixes = recipe.mixes, + groupsInformation = updateGroupsInformationSteps(recipe, dto) ) - }) + ) } - private fun updateGroupsInformation(recipe: Recipe, updateDto: RecipeUpdateDto): Set { - val steps = updateDto.steps ?: return recipe.groupsInformation + override fun update(dto: RecipeDto): RecipeDto { + throwIfNameAndCompanyAlreadyExists(dto.name, dto.company.id, dto.id) - val updatedGroupsInformation = mutableSetOf() - steps.forEach { - with(recipe.groupInformationForGroup(it.groupId)) { - // Set steps for the existing RecipeGroupInformation or create a new one - val updatedGroupInformation = this?.apply { - if (this.steps != null) { - this.steps!!.setAll(it.steps) - } else { - this.steps = it.steps.toMutableSet() - } - } ?: recipeGroupInformation( - group = groupLogic.getById(it.groupId), - steps = it.steps.toMutableSet() - ) - - updatedGroupsInformation.add(updatedGroupInformation) - recipeStepLogic.validateGroupInformationSteps(updatedGroupInformation) - } - } - - return updatedGroupsInformation + return super.update(dto) } @Transactional override fun updatePublicData(publicDataDto: RecipePublicDataDto) { - if (publicDataDto.notes != null) { + // Update notes + if (publicDataDto.notes.isNotEmpty()) { val recipe = getById(publicDataDto.recipeId) - - fun noteForGroup(group: Group) = - publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content - - // Notes - recipe.groupsInformation.map { - val updatedNote = noteForGroup(it.group) - it.apply { - note = updatedNote - } - } - - update(recipe) + update(recipe.copy(groupsInformation = updateGroupsInformationNotes(recipe, publicDataDto.notes))) } - if (publicDataDto.mixesLocation != null) { + // Update mixes locations + if (publicDataDto.mixesLocation.isNotEmpty()) { mixLogic.updateLocations(publicDataDto.mixesLocation) } } - override fun addMix(recipe: Recipe, mix: Mix) = - update(recipe.apply { mixes.add(mix) }) + private fun updateGroupsInformationSteps(recipe: RecipeDto, dto: RecipeUpdateDto): List { + val updatedGroupsInformation = dto.steps.map { updateGroupInformationSteps(recipe, it) } + return recipe.groupsInformation.merge(updatedGroupsInformation) + } - override fun removeMix(mix: Mix): Recipe = - update(mix.recipe.apply { mixes.remove(mix) }) + private fun updateGroupInformationSteps(recipe: RecipeDto, groupSteps: RecipeGroupStepsDto) = + getOrCreateGroupInformation(recipe, groupSteps.groupId).copy(steps = groupSteps.steps).also { + recipeStepLogic.validateGroupInformationSteps(it) + } + + private fun updateGroupsInformationNotes( + recipe: RecipeDto, notes: List + ): List { + val updatedGroupsInformation = notes.map { updateGroupInformationNote(recipe, it) } + return recipe.groupsInformation.merge(updatedGroupsInformation) + } + + private fun updateGroupInformationNote(recipe: RecipeDto, groupNote: RecipeGroupNoteDto) = + getOrCreateGroupInformation(recipe, groupNote.groupId).copy(note = groupNote.content) + + private fun getOrCreateGroupInformation(recipe: RecipeDto, groupId: Long) = + recipe.groupsInformation.firstOrNull { it.group.id == groupId } + ?: RecipeGroupInformationDto(group = groupLogic.getById(groupId)) + + private fun throwIfNameAndCompanyAlreadyExists(name: String, companyId: Long, id: Long? = null) { + if (service.existsByNameAndCompany(name, companyId, id)) { + throw AlreadyExistsException( + "$typeNameLowerCase-company", + "$typeName already exists", + "A recipe with the name '$name' already exists for the company with the id '$companyId'", + name, + NAME_IDENTIFIER_NAME, + mutableMapOf( + "companyId" to companyId + ) + ) + } + } } interface RecipeImageLogic { - /** Gets the name of every images associated to the recipe with the given [recipe]. */ - fun getAllImages(recipe: Recipe): Set + /** Gets the id of every image associated to the recipe with the given [recipeId]. */ + fun getAllImages(recipeId: Long): List - /** 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 id of the saved image. */ + fun download(image: MultipartFile, recipeId: Long): String - /** Deletes the image with the given [name] for the given [recipe]. */ - fun delete(recipe: Recipe, name: String) + /** Deletes the image with the given [path] for the given [recipeId]. */ + fun delete(recipeId: Long, path: String) } -const val RECIPE_IMAGE_ID_DELIMITER = "_" -const val RECIPE_IMAGE_EXTENSION = ".jpg" +@LogicComponent +class DefaultRecipeImageLogic(val fileLogic: WriteableFileLogic) : RecipeImageLogic { + override fun getAllImages(recipeId: Long) = + fileLogic.listDirectoryFiles(getRecipeImagesDirectory(recipeId)).map { it.name } -@Service -@RequireDatabase -class DefaultRecipeImageLogic( - val fileService: WriteableFileLogic -) : RecipeImageLogic { - override fun getAllImages(recipe: Recipe) = - fileService.listDirectoryFiles(recipe.imagesDirectoryPath) - .map { it.name } - .toSet() + override fun download(image: MultipartFile, recipeId: Long): String { + /** Gets the next id available for a new image for the given [recipeId]. */ + fun getNextAvailableId(): String = with(getAllImages(recipeId)) { + (if (isEmpty()) 0 else maxOf { it.toLong() } + 1L).toString() + } - 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(getAllImages(recipe)) { - if (isEmpty()) - 0 - else - maxOf { - it.split(RECIPE_IMAGE_ID_DELIMITER) - .last() - .replace(RECIPE_IMAGE_EXTENSION, "") - .toLong() - } + 1L - } - - return getImageFileName(recipe, getNextAvailableId()).also { - with(getImagePath(recipe, it)) { - fileService.writeToDirectory(image, this, recipe.imagesDirectoryPath, true) - } + return getNextAvailableId().also { + val imagePath = getImagePath(recipeId, it) + fileLogic.writeToDirectory(image, imagePath, getRecipeImagesDirectory(recipeId), true) } } - override fun delete(recipe: Recipe, name: String) = - fileService.deleteFromDirectory(getImagePath(recipe, name), recipe.imagesDirectoryPath) + override fun delete(recipeId: Long, path: String) = + fileLogic.deleteFromDirectory(path, getRecipeImagesDirectory(recipeId)) - private fun getImageFileName(recipe: Recipe, id: Long) = - "${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$id" + private fun getImagePath(recipeId: Long, id: String) = "${getRecipeImagesDirectory(recipeId)}/$id" - private fun getImagePath(recipe: Recipe, name: String) = - "${recipe.imagesDirectoryPath}/$name$RECIPE_IMAGE_EXTENSION" + private fun getRecipeImagesDirectory(recipeId: Long) = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId" } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt index b7852fa..d0f8771 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeStepLogic.kt @@ -1,11 +1,11 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.config.annotations.LogicComponent +import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException import dev.fyloz.colorrecipesexplorer.exception.RestException -import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.service.RecipeStepService @@ -14,17 +14,15 @@ import org.springframework.http.HttpStatus interface RecipeStepLogic : Logic { /** Validates the steps of the given [groupInformation], according to the criteria of [PositionUtils.validate]. */ - fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) + fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto) } @LogicComponent class DefaultRecipeStepLogic(recipeStepService: RecipeStepService) : BaseLogic(recipeStepService, RecipeStep::class.simpleName!!), RecipeStepLogic { - override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformation) { - if (groupInformation.steps == null) return - + override fun validateGroupInformationSteps(groupInformation: RecipeGroupInformationDto) { try { - PositionUtils.validate(groupInformation.steps!!.map { it.position }.toList()) + PositionUtils.validate(groupInformation.steps.map { it.position }.toList()) } catch (ex: InvalidPositionsException) { throw InvalidGroupStepsPositionsException(groupInformation.group, ex) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt index b2e1158..f0d37a4 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Company.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.CompanyDto import javax.persistence.* @Entity @@ -12,21 +11,4 @@ data class Company( @Column(unique = true) val name: String -) : ModelEntity - -// ==== DSL ==== -fun company( - id: Long? = null, - name: String = "name", - op: Company.() -> Unit = {} -) = Company(id, name).apply(op) - -@Deprecated("Temporary DSL for transition") -fun company( - dto: CompanyDto -) = Company(dto.id, dto.name) - -@Deprecated("Temporary DSL for transition") -fun companyDto( - entity: Company -) = CompanyDto(entity.id!!, entity.name) \ No newline at end of file +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt index 024e651..ac2eb93 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Material.kt @@ -1,7 +1,6 @@ package dev.fyloz.colorrecipesexplorer.model import dev.fyloz.colorrecipesexplorer.Constants -import dev.fyloz.colorrecipesexplorer.dtos.MaterialDto import javax.persistence.* @Entity @@ -28,34 +27,4 @@ data class Material( fun getSimdutFilePath(name: String) = "${Constants.FilePaths.SIMDUT}/$name.pdf" } -} - -// === DSL === - -fun material( - 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( - id ?: material.id, name - ?: material.name, material.inventoryQuantity, material.isMixType, material.materialType -) - -@Deprecated("Temporary DSL for transition") -fun material( - dto: MaterialDto -) = Material(dto.id, dto.name, dto.inventoryQuantity, dto.isMixType, materialType(dto.materialType)) - -@Deprecated("Temporary DSL for transition") -fun materialDto( - entity: Material -) = MaterialDto(entity.id!!, entity.name, entity.inventoryQuantity, entity.isMixType, materialTypeDto(entity.materialType!!)) \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt index 279c9c4..9082e82 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MaterialType.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.MaterialTypeDto import org.hibernate.annotations.ColumnDefault import javax.persistence.* @@ -24,39 +23,4 @@ data class MaterialType( @Column(name = "system_type") @ColumnDefault("false") val systemType: Boolean = false -) : ModelEntity - -// ==== DSL ==== -fun materialType( - id: Long? = null, - name: String = "name", - prefix: String = "PRE", - usePercentages: Boolean = false, - systemType: Boolean = false, - op: MaterialType.() -> Unit = {} -) = MaterialType(id, name, prefix, usePercentages, systemType).apply(op) - -fun materialType( - materialType: MaterialType, - newId: Long? = null, - newName: String? = null, - newSystemType: Boolean? = null -) = with(materialType) { - MaterialType( - newId ?: id, - newName ?: name, - prefix, - usePercentages, - newSystemType ?: systemType - ) -} - -@Deprecated("Temporary DSL for transition") -fun materialType( - dto: MaterialTypeDto -) = MaterialType(dto.id, dto.name, dto.prefix, dto.usePercentages, dto.systemType) - -@Deprecated("Temporary DSL for transition") -fun materialTypeDto( - entity: MaterialType -) = MaterialTypeDto(entity.id!!, entity.name, entity.prefix, entity.usePercentages, entity.systemType) \ No newline at end of file +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt index 6931638..9656433 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Mix.kt @@ -1,10 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.MixDto -import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import javax.persistence.* @Entity @@ -16,9 +11,8 @@ data class Mix( var location: String?, - @ManyToOne - @JoinColumn(name = "recipe_id") - val recipe: Recipe, + @Column(name = "recipe_id") + val recipeId: Long, @ManyToOne @JoinColumn(name = "mix_type_id") @@ -26,25 +20,5 @@ data class Mix( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "mix_id") - var mixMaterials: Set, -) : ModelEntity - -// ==== DSL ==== -fun mix( - id: Long? = null, - location: String? = "location", - recipe: Recipe = recipe(), - mixType: MixType = mixType(), - mixMaterials: MutableSet = mutableSetOf(), - op: Mix.() -> Unit = {} -) = Mix(id, location, recipe, mixType, mixMaterials).apply(op) - -@Deprecated("Temporary DSL for transition") -fun mix( - dto: MixDto -) = Mix(dto.id, dto.location, dto.recipe, mixType(dto.mixType), dto.mixMaterials.map(::mixMaterial).toSet()) - -@Deprecated("Temporary DSL for transition") -fun mix( - entity: Mix -) = MixDto(entity.id!!, entity.location, entity.recipe, mixTypeDto(entity.mixType), entity.mixMaterials.map(::mixMaterialDto).toSet()) \ No newline at end of file + var mixMaterials: List, +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt index 6ac7569..e243e14 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixMaterial.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.MixMaterialDto import javax.persistence.* @Entity @@ -17,23 +16,4 @@ data class MixMaterial( var quantity: Float, var position: Int -) : ModelEntity - -// ==== DSL ==== -fun mixMaterial( - id: Long? = null, - material: Material = material(), - quantity: Float = 0f, - position: Int = 0, - op: MixMaterial.() -> Unit = {} -) = MixMaterial(id, material, quantity, position).apply(op) - -@Deprecated("Temporary DSL for transition") -fun mixMaterialDto( - entity: MixMaterial -) = MixMaterialDto(entity.id!!, materialDto(entity.material), entity.quantity, entity.position) - -@Deprecated("Temporary DSL for transition") -fun mixMaterial( - dto: MixMaterialDto -) = MixMaterial(dto.id, material(dto.material), dto.quantity, dto.position) \ No newline at end of file +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt index 8b25b67..7d471dc 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/MixType.kt @@ -1,11 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.MixTypeDto -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException -import dev.fyloz.colorrecipesexplorer.exception.RestException -import org.springframework.http.HttpStatus import javax.persistence.* @Entity @@ -21,32 +15,4 @@ data class MixType( @OneToOne(cascade = [CascadeType.ALL]) @JoinColumn(name = "material_id") var material: Material -) : ModelEntity - -// ==== DSL ==== -fun mixType( - id: Long? = null, - name: String = "name", - material: Material = material(), - op: MixType.() -> Unit = {} -) = MixType(id, name, material).apply(op) - -fun mixType( - name: String = "name", - materialType: MaterialType = materialType(), - op: MixType.() -> Unit = {} -) = mixType( - id = null, - name, - material = material(name = name, inventoryQuantity = 0f, isMixType = true, materialType = materialType) -).apply(op) - -@Deprecated("Temporary DSL for transition") -fun mixTypeDto( - entity: MixType -) = MixTypeDto(entity.id!!, entity.name, materialDto(entity.material)) - -@Deprecated("Temporary DSL for transition") -fun mixType( - dto: MixTypeDto -) = MixType(dto.id, dto.name, material(dto.material)) \ No newline at end of file +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt index 3fc6873..9692a56 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/Recipe.kt @@ -1,25 +1,8 @@ package dev.fyloz.colorrecipesexplorer.model -import com.fasterxml.jackson.annotation.JsonIgnore -import dev.fyloz.colorrecipesexplorer.Constants -import dev.fyloz.colorrecipesexplorer.dtos.MixDto -import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.exception.NotFoundException import dev.fyloz.colorrecipesexplorer.model.account.Group -import dev.fyloz.colorrecipesexplorer.model.account.group -import java.net.URLEncoder -import java.nio.charset.StandardCharsets import java.time.LocalDate import javax.persistence.* -import javax.validation.constraints.Max -import javax.validation.constraints.Min -import javax.validation.constraints.NotBlank -import javax.validation.constraints.Pattern - -private const val VALIDATION_COLOR_PATTERN = "^#([0-9a-f]{6})$" - -const val RECIPE_IMAGES_DIRECTORY = "images/recipes" @Entity @Table(name = "recipe") @@ -51,110 +34,12 @@ data class Recipe( @JoinColumn(name = "company_id") val company: Company, - @OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe") - val mixes: MutableList, + @OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipeId") + val mixes: List, - @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) + @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true) @JoinColumn(name = "recipe_id") - val groupsInformation: Set -) : ModelEntity { - /** The mix types contained in this recipe. */ - val mixTypes: Collection - @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 } - - fun imageUrl(deploymentUrl: String, name: String) = - "$deploymentUrl${Constants.ControllerPaths.FILE}?path=${ - URLEncoder.encode( - "${this.imagesDirectoryPath}/$name", - StandardCharsets.UTF_8 - ) - }" -} - -open class RecipeSaveDto( - @field:NotBlank - val name: String, - - @field:NotBlank - val description: String, - - @field:NotBlank - @field:Pattern(regexp = VALIDATION_COLOR_PATTERN) - val color: String, - - @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) - @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) - val gloss: Byte, - - @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) - val sample: Int?, - - val approbationDate: LocalDate?, - - val remark: String?, - - val companyId: Long -) : EntityDto { - override fun toEntity(): Recipe = recipe( - name = name, - description = description, - sample = sample, - approbationDate = approbationDate, - remark = remark ?: "", - company = company(id = companyId) - ) -} - -open class RecipeUpdateDto( - val id: Long, - - @field:NotBlank - val name: String?, - - @field:NotBlank - val description: String?, - - @field:NotBlank - @field:Pattern(regexp = VALIDATION_COLOR_PATTERN) - val color: String?, - - @field:Min(0, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) - @field:Max(100, message = Constants.ValidationMessages.RANGE_OUTSIDE_PERCENTS) - val gloss: Byte?, - - @field:Min(0, message = Constants.ValidationMessages.SIZE_GREATER_OR_EQUALS_ZERO) - val sample: Int?, - - val approbationDate: LocalDate?, - - val remark: String?, - - val steps: Set? -) : EntityDto - -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 approbationExpired: Boolean?, - val remark: String?, - val company: Company, - val mixes: Set, - val groupsInformation: Set, - var imagesUrls: Set + val groupsInformation: List ) : ModelEntity @Entity @@ -172,133 +57,5 @@ data class RecipeGroupInformation( @OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true) @JoinColumn(name = "recipe_group_information_id") - var steps: MutableSet? -) : ModelEntity - -data class RecipeStepsDto( - val groupId: Long, - - val steps: Set -) - -data class RecipePublicDataDto( - val recipeId: Long, - - val notes: Set?, - - val mixesLocation: Set? -) - -data class NoteDto( - val groupId: Long, - - 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 = mutableListOf(), - groupsInformation: Set = setOf(), - op: Recipe.() -> Unit = {} -) = Recipe( - 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 = {} -) = 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? = setOf(), - op: RecipeUpdateDto.() -> Unit = {} -) = RecipeUpdateDto(id, name, description, color, gloss, sample, approbationDate, remark, steps).apply(op) - -fun recipeGroupInformation( - id: Long? = null, - group: Group = group(), - note: String? = null, - steps: MutableSet? = mutableSetOf(), - op: RecipeGroupInformation.() -> Unit = {} -) = RecipeGroupInformation(id, group, note, steps).apply(op) - -fun recipePublicDataDto( - recipeId: Long = 0L, - notes: Set? = null, - mixesLocation: Set? = null, - op: RecipePublicDataDto.() -> Unit = {} -) = RecipePublicDataDto(recipeId, notes, mixesLocation).apply(op) - -fun noteDto( - groupId: Long = 0L, - content: String? = "note", - op: NoteDto.() -> Unit = {} -) = NoteDto(groupId, content).apply(op) - -// ==== Exceptions ==== -private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found" -private const val RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe already exists" -private const val RECIPE_EXCEPTION_ERROR_CODE = "recipe" - -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 - ) - -fun recipeIdAlreadyExistsException(id: Long) = - AlreadyExistsException( - RECIPE_EXCEPTION_ERROR_CODE, - RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE, - "A recipe with the id $id already exists", - id - ) - -fun recipeNameAlreadyExistsForCompanyException(name: String, company: Company) = - AlreadyExistsException( - "${RECIPE_EXCEPTION_ERROR_CODE}-company", - RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE, - "A recipe with the name $name already exists for the company ${company.name}", - name, - "name", - mutableMapOf( - "company" to company.name, - "companyId" to company.id!! - ) - ) + var steps: List? +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt index ddc6eac..e71803b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/model/RecipeStep.kt @@ -1,6 +1,5 @@ package dev.fyloz.colorrecipesexplorer.model -import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import javax.persistence.* @Entity @@ -13,9 +12,4 @@ data class RecipeStep( val position: Int, val message: String -) : ModelEntity - -@Deprecated("Temporary DSL for transition") -fun recipeStepDto( - entity: RecipeStep -) = RecipeStepDto(entity.id!!, entity.position, entity.message) \ No newline at end of file +) : ModelEntity \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt index a363199..cd3288b 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepository.kt @@ -12,6 +12,6 @@ interface MixRepository : JpaRepository { /** Updates the [location] of the [Mix] with the given [id]. */ @Modifying - @Query("update Mix m set m.location = :location where m.id = :id") + @Query("UPDATE Mix m SET m.location = :location WHERE m.id = :id") fun updateLocationById(id: Long, location: String?) } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt index 9e1c7f8..719262d 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixTypeRepository.kt @@ -8,11 +8,16 @@ import org.springframework.stereotype.Repository @Repository interface MixTypeRepository : JpaRepository { /** Checks if a mix type with the given [name], [materialTypeId] and a different [id] exists. */ - @Query("select case when(count(m) > 0) then true else false end from MixType m where m.name = :name and m.material.materialType.id = :materialTypeId and m.id <> :id") + @Query( + """ + SELECT CASE WHEN(COUNT(m) > 0) THEN TRUE ELSE FALSE END + FROM MixType m WHERE m.name = :name AND m.material.materialType.id = :materialTypeId AND m.id <> :id + """ + ) fun existsByNameAndMaterialType(name: String, materialTypeId: Long, id: Long): Boolean /** Finds the mix type with the given [name] and [materialTypeId]. */ - @Query("select m from MixType m where m.name = :name and m.material.materialType.id = :materialTypeId") + @Query("SELECT m FROM MixType m WHERE m.name = :name AND m.material.materialType.id = :materialTypeId") fun findByNameAndMaterialType(name: String, materialTypeId: Long): MixType? /** Checks if a mix depends on the mix type with the given [id]. */ diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt index 251374d..2fa7ca5 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/repository/RecipeRepository.kt @@ -1,19 +1,19 @@ package dev.fyloz.colorrecipesexplorer.repository -import dev.fyloz.colorrecipesexplorer.model.Company import dev.fyloz.colorrecipesexplorer.model.Recipe import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query interface RecipeRepository : JpaRepository { - /** Checks if one or more recipes have the given [company]. */ - fun existsByCompany(company: Company): Boolean - - /** Checks if a recipe exists with the given [name] and [company]. */ - fun existsByNameAndCompany(name: String, company: Company): Boolean + /** Checks if a recipe with the given [name], [companyId] and a different [id] exists. */ + @Query( + """ + SELECT CASE WHEN(COUNT(r) > 0) THEN TRUE ELSE FALSE END + FROM Recipe r WHERE r.name = :name AND r.company.id = :companyId AND r.id <> :id + """ + ) + fun existsByNameAndCompanyAndIdNot(name: String, companyId: Long, id: Long): Boolean /** Gets all recipes with the given [name]. */ fun findAllByName(name: String): Collection - - /** Gets all recipes with the given [company]. */ - fun findAllByCompany(company: Company): Collection } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt index 875a879..44424f7 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/rest/RecipeController.kt @@ -1,11 +1,14 @@ package dev.fyloz.colorrecipesexplorer.rest +import dev.fyloz.colorrecipesexplorer.Constants import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeEditRecipes import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewRecipes -import dev.fyloz.colorrecipesexplorer.logic.MixLogic +import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipePublicDataDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeSaveDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeUpdateDto import dev.fyloz.colorrecipesexplorer.logic.RecipeImageLogic import dev.fyloz.colorrecipesexplorer.logic.RecipeLogic -import dev.fyloz.colorrecipesexplorer.model.* import org.springframework.context.annotation.Profile import org.springframework.http.MediaType import org.springframework.http.ResponseEntity @@ -14,71 +17,63 @@ import org.springframework.web.bind.annotation.* import org.springframework.web.multipart.MultipartFile import javax.validation.Valid - -private const val RECIPE_CONTROLLER_PATH = "api/recipe" - @RestController -@RequestMapping(RECIPE_CONTROLLER_PATH) +@RequestMapping(Constants.ControllerPaths.RECIPE) @Profile("!emergency") @PreAuthorizeViewRecipes -class RecipeController( - private val recipeLogic: RecipeLogic, - private val recipeImageLogic: RecipeImageLogic -) { +class RecipeController(private val recipeLogic: RecipeLogic, private val recipeImageLogic: RecipeImageLogic) { @GetMapping - fun getAll(@RequestParam(required = false) name: String?) = - if (name == null) - ok(recipeLogic.getAllForOutput()) - else - ok(with(recipeLogic) { - getAllByName(name).map { it.toOutput() } - }) + fun getAll(@RequestParam(required = false) name: String?) = ok( + if (name == null) { + recipeLogic.getAll() + } else { + recipeLogic.getAllByName(name) + } + ) @GetMapping("{id}") - fun getById(@PathVariable id: Long) = - ok(recipeLogic.getByIdForOutput(id)) + fun getById(@PathVariable id: Long) = ok(recipeLogic.getById(id)) @PostMapping @PreAuthorizeEditRecipes fun save(@Valid @RequestBody recipe: RecipeSaveDto) = - created(RECIPE_CONTROLLER_PATH) { - with(recipeLogic) { - save(recipe).toOutput() - } + created(Constants.ControllerPaths.RECIPE) { + recipeLogic.save(recipe) } @PutMapping @PreAuthorizeEditRecipes - fun update(@Valid @RequestBody recipe: RecipeUpdateDto) = - noContent { - recipeLogic.update(recipe) - } + fun update(@Valid @RequestBody recipe: RecipeUpdateDto) = noContent { + recipeLogic.update(recipe) + } @PutMapping("public") @PreAuthorize("hasAuthority('EDIT_RECIPES_PUBLIC_DATA')") - fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) = - noContent { - recipeLogic.updatePublicData(publicDataDto) - } + fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) = noContent { + recipeLogic.updatePublicData(publicDataDto) + } @DeleteMapping("{id}") @PreAuthorizeEditRecipes - fun deleteById(@PathVariable id: Long) = - noContent { - recipeLogic.deleteById(id) - } + fun deleteById(@PathVariable id: Long) = noContent { + recipeLogic.deleteById(id) + } + + @GetMapping("{recipeId}/image") + fun getAllImages(@PathVariable recipeId: Long) = ok { + recipeImageLogic.getAllImages(recipeId) + } @PutMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE]) @PreAuthorizeEditRecipes - fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity { - recipeImageLogic.download(image, recipeLogic.getById(recipeId)) + fun downloadImage(@PathVariable recipeId: Long, image: MultipartFile): ResponseEntity { + recipeImageLogic.download(image, recipeId) return getById(recipeId) } - @DeleteMapping("{recipeId}/image/{name}") + @DeleteMapping("{recipeId}/image/{path}") @PreAuthorizeEditRecipes - fun deleteImage(@PathVariable recipeId: Long, @PathVariable name: String) = - noContent { - recipeImageLogic.delete(recipeLogic.getById(recipeId), name) - } + fun deleteImage(@PathVariable recipeId: Long, @PathVariable path: String) = noContent { + recipeImageLogic.delete(recipeId, path) + } } diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt index 5905923..802dd98 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/MixService.kt @@ -26,17 +26,17 @@ class DefaultMixService( MixDto( entity.id!!, entity.location, - entity.recipe, + entity.recipeId, mixTypeService.toDto(entity.mixType), - entity.mixMaterials.map(mixMaterialService::toDto).toSet() + entity.mixMaterials.map(mixMaterialService::toDto) ) override fun toEntity(dto: MixDto) = Mix( dto.id, dto.location, - dto.recipe, + dto.recipeId, mixTypeService.toEntity(dto.mixType), - dto.mixMaterials.map(mixMaterialService::toEntity).toSet() + dto.mixMaterials.map(mixMaterialService::toEntity) ) } \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt new file mode 100644 index 0000000..de556e9 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt @@ -0,0 +1,86 @@ +package dev.fyloz.colorrecipesexplorer.service + +import dev.fyloz.colorrecipesexplorer.config.annotations.ServiceComponent +import dev.fyloz.colorrecipesexplorer.dtos.RecipeDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto +import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic +import dev.fyloz.colorrecipesexplorer.model.ConfigurationType +import dev.fyloz.colorrecipesexplorer.model.Recipe +import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation +import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository +import dev.fyloz.colorrecipesexplorer.utils.collections.lazyMap +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDate +import java.time.Period + +interface RecipeService : Service { + /** Checks if a recipe with the given [name], [companyId] and a different [id] exists. */ + fun existsByNameAndCompany(name: String, companyId: Long, id: Long?): Boolean + + /** Gets all recipes with the given [name]. */ + fun getAllByName(name: String): Collection +} + +@ServiceComponent +class DefaultRecipeService( + repository: RecipeRepository, + private val companyService: CompanyService, + private val mixService: MixService, + private val recipeStepService: RecipeStepService, + private val configLogic: ConfigurationLogic +) : + BaseService(repository), RecipeService { + override fun existsByNameAndCompany(name: String, companyId: Long, id: Long?) = + repository.existsByNameAndCompanyAndIdNot(name, companyId, id ?: 0L) + + override fun getAllByName(name: String) = + repository.findAllByName(name).map(::toDto) + + @Transactional + override fun toDto(entity: Recipe) = + RecipeDto( + entity.id!!, + entity.name, + entity.description, + entity.color, + entity.gloss, + entity.sample, + entity.approbationDate, + isApprobationExpired(entity) ?: false, + entity.remark, + companyService.toDto(entity.company), + entity.mixes.lazyMap(mixService::toDto), + entity.groupsInformation.lazyMap(::groupInformationToDto) + ) + + private fun groupInformationToDto(entity: RecipeGroupInformation) = + RecipeGroupInformationDto( + entity.id!!, + entity.group, + entity.note, + entity.steps?.lazyMap(recipeStepService::toDto) ?: listOf() + ) + + override fun toEntity(dto: RecipeDto) = + Recipe( + dto.id, + dto.name, + dto.description, + dto.color, + dto.gloss, + dto.sample, + dto.approbationDate, + dto.remark, + companyService.toEntity(dto.company), + dto.mixes.map(mixService::toEntity), + dto.groupsInformation.map(::groupInformationToEntity) + ) + + private fun groupInformationToEntity(dto: RecipeGroupInformationDto) = + RecipeGroupInformation(dto.id, dto.group, dto.note, dto.steps.map(recipeStepService::toEntity)) + + private fun isApprobationExpired(recipe: Recipe): Boolean? = + with(Period.parse(configLogic.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION))) { + recipe.approbationDate?.plus(this)?.isBefore(LocalDate.now()) + } +} \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt index 8c4d50b..5ce54ee 100644 --- a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/Collections.kt @@ -60,4 +60,4 @@ fun Iterable.merge(other: Iterable, keyMapper: (T) -> K) = this.associateBy { keyMapper(it) } .filter { pair -> other.all { keyMapper(it) != pair.key } } .map { it.value } - .plus(other) + .plus(other) \ No newline at end of file diff --git a/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/collections/LazyMapList.kt b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/collections/LazyMapList.kt new file mode 100644 index 0000000..7863c65 --- /dev/null +++ b/src/main/kotlin/dev/fyloz/colorrecipesexplorer/utils/collections/LazyMapList.kt @@ -0,0 +1,26 @@ +package dev.fyloz.colorrecipesexplorer.utils.collections + +class LazyMapList(private val sourceList: List, private val transform: (T) -> R) : List { + private val list by lazy { sourceList.map(transform) } + + fun initialize() { + // Call a property so the list is initialized + size + } + + override val size: Int + get() = list.size + + override fun contains(element: R) = list.contains(element) + override fun containsAll(elements: Collection) = list.containsAll(elements) + override fun get(index: Int) = list[index] + override fun indexOf(element: R) = list.indexOf(element) + override fun isEmpty() = list.isEmpty() + override fun iterator() = list.iterator() + override fun lastIndexOf(element: R) = list.lastIndexOf(element) + override fun listIterator() = list.listIterator() + override fun listIterator(index: Int) = list.listIterator(index) + override fun subList(fromIndex: Int, toIndex: Int) = list.subList(fromIndex, toIndex) +} + +fun List.lazyMap(transform: (T) -> R) = LazyMapList(this, transform) \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt index d5496d7..c39a91b 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultInventoryLogicTest.kt @@ -1,8 +1,6 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.dtos.* -import dev.fyloz.colorrecipesexplorer.model.Recipe -import dev.fyloz.colorrecipesexplorer.model.company import io.mockk.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test @@ -87,7 +85,7 @@ class DefaultInventoryLogicTest { fun deductMix_normalBehavior_callsDeductWithMixMaterials() { // Arrange val company = CompanyDto(1L, "Unit test company") - val recipe = Recipe( + val recipe = RecipeDto( 1L, "Unit test recipe", "Unit test recipe", @@ -95,14 +93,15 @@ class DefaultInventoryLogicTest { 0xf, null, null, + false, "Remark", - company(company), + company, mutableListOf(), - setOf() + listOf() ) val mixType = MixTypeDto(1L, "Unit test mix type", material) val mixMaterial = MixMaterialDto(1L, material, 1000f, 1) - val mix = MixDto(1L, null, recipe, mixType, setOf(mixMaterial)) + val mix = MixDto(1L, null, recipe.id, mixType, listOf(mixMaterial)) val dto = MixDeductDto(mix.id, 2f) val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio)) @@ -123,7 +122,7 @@ class DefaultInventoryLogicTest { fun deductMix_normalBehavior_returnsFromDeduct() { // Arrange val company = CompanyDto(1L, "Unit test company") - val recipe = Recipe( + val recipe = RecipeDto( 1L, "Unit test recipe", "Unit test recipe", @@ -131,14 +130,15 @@ class DefaultInventoryLogicTest { 0xf, null, null, + false, "Remark", - company(company), + company, mutableListOf(), - setOf() + listOf() ) val mixType = MixTypeDto(1L, "Unit test mix type", material) val mixMaterial = MixMaterialDto(1L, material, 1000f, 1) - val mix = MixDto(1L, null, recipe, mixType, setOf(mixMaterial)) + val mix = MixDto(1L, null, recipe.id, mixType, listOf(mixMaterial)) val dto = MixDeductDto(mix.id, 2f) val expectedQuantities = listOf(MaterialQuantityDto(material.id, mixMaterial.quantity * dto.ratio)) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt index 1238efe..82e8a5d 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMaterialLogicTest.kt @@ -4,10 +4,7 @@ import dev.fyloz.colorrecipesexplorer.dtos.* import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic -import dev.fyloz.colorrecipesexplorer.model.Company import dev.fyloz.colorrecipesexplorer.model.Material -import dev.fyloz.colorrecipesexplorer.model.Recipe -import dev.fyloz.colorrecipesexplorer.model.mix import dev.fyloz.colorrecipesexplorer.service.MaterialService import io.mockk.* import org.junit.jupiter.api.AfterEach @@ -37,8 +34,8 @@ class DefaultMaterialLogicTest { private val material = MaterialDto(1L, "Unit test material", 1000f, false, materialType) private val materialMixType = material.copy(id = 2L, isMixType = true) private val materialMixType2 = material.copy(id = 3L, isMixType = true) - private val company = Company(1L, "Unit test company") - private val recipe = Recipe( + private val company = CompanyDto(1L, "Unit test company") + private val recipe = RecipeDto( 1L, "Unit test recipe", "Unit test recipe", @@ -46,13 +43,14 @@ class DefaultMaterialLogicTest { 0, 123, null, + false, "A remark", company, mutableListOf(), - setOf() + listOf() ) private val mix = MixDto( - 1L, "location", recipe, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), mutableSetOf() + 1L, "location", recipe.id, mixType = MixTypeDto(1L, "Unit test mix type", materialMixType), listOf() ) private val mix2 = mix.copy(id = 2L, mixType = mix.mixType.copy(id = 2L, material = materialMixType2)) @@ -62,10 +60,6 @@ class DefaultMaterialLogicTest { ) // Put some content in the mock file, so it is not ignored private val materialSaveDto = MaterialSaveDto(1L, "Unit test material", 1000f, materialType.id, simdutFileMock) - init { - recipe.mixes.addAll(listOf(mix(mix), mix(mix2))) - } - @AfterEach internal fun afterEach() { clearAllMocks() @@ -114,7 +108,7 @@ class DefaultMaterialLogicTest { every { recipeLogicMock.getById(any()) } returns recipe // Act - val materials = materialLogic.getAllForMixCreation(recipe.id!!) + val materials = materialLogic.getAllForMixCreation(recipe.id) // Assert assertContains(materials, material) @@ -123,11 +117,13 @@ class DefaultMaterialLogicTest { @Test fun getAllForMixCreation_normalBehavior_returnsRecipeMixTypesMaterials() { // Arrange + val recipe = recipe.copy(mixes = listOf(mix, mix2)) + every { materialLogic.getAll() } returns listOf(material, materialMixType2) every { recipeLogicMock.getById(any()) } returns recipe // Act - val materials = materialLogic.getAllForMixCreation(recipe.id!!) + val materials = materialLogic.getAllForMixCreation(recipe.id) // Assert assertContains(materials, materialMixType2) @@ -137,6 +133,7 @@ class DefaultMaterialLogicTest { fun getAllForMixUpdate_normalBehavior_returnsNonMixTypeMaterials() { // Arrange every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { recipeLogicMock.getById(any()) } returns recipe every { mixLogicMock.getById(any()) } returns mix // Act @@ -149,7 +146,11 @@ class DefaultMaterialLogicTest { @Test fun getAllForMixUpdate_normalBehavior_returnsRecipeMixTypesMaterials() { // Arrange + val recipe = recipe.copy(mixes = listOf(mix, mix2)) + val mix = mix.copy(recipeId = recipe.id) + every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { recipeLogicMock.getById(any()) } returns recipe every { mixLogicMock.getById(any()) } returns mix // Act @@ -163,10 +164,11 @@ class DefaultMaterialLogicTest { fun getAllForMixUpdate_normalBehavior_excludesGivenMixTypeMaterial() { // Arrange every { materialLogic.getAll() } returns listOf(material, materialMixType, materialMixType2) + every { recipeLogicMock.getById(any()) } returns recipe every { mixLogicMock.getById(any()) } returns mix // Act - val materials = materialLogic.getAllForMixUpdate(mix.id!!) + val materials = materialLogic.getAllForMixUpdate(mix.id) // Assert assertFalse { materialMixType in materials } diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt index 1579f71..33204fe 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultMixLogicTest.kt @@ -1,8 +1,6 @@ package dev.fyloz.colorrecipesexplorer.logic import dev.fyloz.colorrecipesexplorer.dtos.* -import dev.fyloz.colorrecipesexplorer.model.Company -import dev.fyloz.colorrecipesexplorer.model.Recipe import dev.fyloz.colorrecipesexplorer.service.MixService import io.mockk.* import org.junit.jupiter.api.AfterEach @@ -25,8 +23,8 @@ class DefaultMixLogicTest { ) ) - private val company = Company(1L, "Unit test company") - private val recipe = Recipe( + private val company = CompanyDto(1L, "Unit test company") + private val recipe = RecipeDto( 1L, "Unit test recipe", "Unit test recipe", @@ -34,17 +32,18 @@ class DefaultMixLogicTest { 0xf, null, null, + false, "A remark", company, mutableListOf(), - setOf() + listOf() ) private val materialType = MaterialTypeDto(1L, "Unit test material type", "UTMT", false) private val mixType = MixTypeDto(1L, "Unit test mix type", MaterialDto(1L, "Unit test mix type material", 1000f, true, materialType)) private val mixMaterial = MixMaterialDto(1L, MaterialDto(2L, "Unit test material", 1000f, false, materialType), 50f, 1) - private val mix = MixDto(recipe = recipe, mixType = mixType, mixMaterials = setOf(mixMaterial)) + private val mix = MixDto(recipeId = recipe.id, mixType = mixType, mixMaterials = listOf(mixMaterial)) @AfterEach internal fun afterEach() { @@ -56,7 +55,6 @@ class DefaultMixLogicTest { every { materialTypeLogicMock.getById(any()) } returns materialType every { mixTypeLogicMock.getOrCreateForNameAndMaterialType(any(), any()) } returns mixType every { mixMaterialLogicMock.validateAndSaveAll(any()) } returns listOf(mixMaterial) - every { recipeLogicMock.addMix(any(), any()) } returns recipe every { mixLogic.save(any()) } returnsArgument 0 } @@ -76,7 +74,7 @@ class DefaultMixLogicTest { val mixMaterialDto = MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position) - val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto)) + val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto)) // Act mixLogic.save(saveDto) @@ -93,7 +91,7 @@ class DefaultMixLogicTest { setup_save_normalBehavior() val mixMaterialDtos = - setOf( + listOf( MixMaterialSaveDto( mixMaterial.id, mixMaterial.material.id, @@ -101,7 +99,7 @@ class DefaultMixLogicTest { mixMaterial.position ) ) - val saveDto = MixSaveDto(0L, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos) + val saveDto = MixSaveDto(0L, mixType.name, recipe.id, materialType.id, mixMaterialDtos) // Act mixLogic.save(saveDto) @@ -120,7 +118,7 @@ class DefaultMixLogicTest { val mixMaterialDto = MixMaterialSaveDto(mixMaterial.id, mixMaterial.material.id, mixMaterial.quantity, mixMaterial.position) - val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, setOf(mixMaterialDto)) + val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, listOf(mixMaterialDto)) // Act mixLogic.update(saveDto) @@ -136,7 +134,7 @@ class DefaultMixLogicTest { // Arrange setup_update_normalBehavior() - val mixMaterialDtos = setOf( + val mixMaterialDtos = listOf( MixMaterialSaveDto( mixMaterial.id, mixMaterial.material.id, @@ -144,7 +142,7 @@ class DefaultMixLogicTest { mixMaterial.position ) ) - val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id!!, materialType.id, mixMaterialDtos) + val saveDto = MixSaveDto(mix.id, mixType.name, recipe.id, materialType.id, mixMaterialDtos) // Act mixLogic.update(saveDto) diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt new file mode 100644 index 0000000..7ab069c --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeImageLogicTest.kt @@ -0,0 +1,101 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.Constants +import dev.fyloz.colorrecipesexplorer.logic.files.CachedFile +import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic +import dev.fyloz.colorrecipesexplorer.utils.FilePath +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.springframework.mock.web.MockMultipartFile +import kotlin.test.assertEquals + +class DefaultRecipeImageLogicTest { + private val fileLogicMock = mockk() + + private val recipeImageLogic = spyk(DefaultRecipeImageLogic(fileLogicMock)) + + private val recipeId = 1L + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun getAllImages_normalBehavior_returnsAllRecipeImages() { + // Arrange + val filePath = FilePath("${Constants.FilePaths.RECIPE_IMAGES}/$recipeId") + val files = listOf( + CachedFile("0", filePath, true), + CachedFile("1", filePath, true) + ) + val expectedImages = files.map { it.name } + + every { fileLogicMock.listDirectoryFiles(any()) } returns files + + // Act + val actualImages = recipeImageLogic.getAllImages(recipeId) + + // Assert + assertEquals(expectedImages, actualImages) + } + + @Test + fun download_normalBehavior_callsWriteToDirectoryInFileLogic() { + // Arrange + val previousImageId = 0L + + every { recipeImageLogic.getAllImages(recipeId) } returns listOf(previousImageId.toString()) + every { fileLogicMock.writeToDirectory(any(), any(), any(), any()) } just runs + + val file = MockMultipartFile("Unit test name", byteArrayOf()) + + val expectedFilePath = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId" + val expectedImageId = previousImageId + 1 + + // Act + recipeImageLogic.download(file, recipeId) + + // Assert + verify { + fileLogicMock.writeToDirectory(file, "$expectedFilePath/$expectedImageId", expectedFilePath, true) + } + } + + @Test + fun download_normalBehavior_returnsImageId() { + // Arrange + val previousImageId = 0L + + every { recipeImageLogic.getAllImages(recipeId) } returns listOf(previousImageId.toString()) + every { fileLogicMock.writeToDirectory(any(), any(), any(), any()) } just runs + + val file = MockMultipartFile("Unit test name", byteArrayOf()) + + val expectedImageId = previousImageId + 1 + + // Act + val downloadedImageId = recipeImageLogic.download(file, recipeId) + + // Assert + assertEquals(expectedImageId.toString(), downloadedImageId) + } + + @Test + fun delete_normalBehavior_callsDeleteFromDirectoryInFileLogic() { + // Arrange + every { fileLogicMock.deleteFromDirectory(any(), any()) } just runs + + val recipeImagesDirectoryPath = "${Constants.FilePaths.RECIPE_IMAGES}/$recipeId" + val imagePath = "$recipeImagesDirectoryPath/1" + + // Act + recipeImageLogic.delete(recipeId, imagePath) + + // Assert + verify { + fileLogicMock.deleteFromDirectory(imagePath, recipeImagesDirectoryPath) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt new file mode 100644 index 0000000..cdc0ae8 --- /dev/null +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeLogicTest.kt @@ -0,0 +1,217 @@ +package dev.fyloz.colorrecipesexplorer.logic + +import dev.fyloz.colorrecipesexplorer.dtos.* +import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException +import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic +import dev.fyloz.colorrecipesexplorer.model.account.Group +import dev.fyloz.colorrecipesexplorer.service.RecipeService +import io.mockk.* +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertContains +import kotlin.test.assertEquals + +class DefaultRecipeLogicTest { + private val recipeServiceMock = mockk() + private val companyLogicMock = mockk() + private val recipeStepLogicMock = mockk() + private val mixLogicMock = mockk() + private val groupLogicMock = mockk() + + private val recipeLogic = + spyk(DefaultRecipeLogic(recipeServiceMock, companyLogicMock, recipeStepLogicMock, mixLogicMock, groupLogicMock)) + + private val company = CompanyDto(1L, "Unit test company") + private val group = Group(1L, "Unit test group") + private val recipe = RecipeDto( + 1L, + "Unit test recipe", + "Unit test recipe", + "FFFFFF", + 0xf, + null, + null, + false, + "Remark", + company, + listOf(), + listOf() + ) + + @AfterEach + internal fun afterEach() { + clearAllMocks() + } + + @Test + fun getAllByName_normalBehavior_returnsFromService() { + // Arrange + val expectedRecipes = listOf(recipe) + + every { recipeServiceMock.getAllByName(any()) } returns expectedRecipes + + // Act + val actualRecipes = recipeLogic.getAllByName(recipe.name) + + // Assert + assertEquals(actualRecipes, expectedRecipes) + } + + @Test + fun save_dto_normalBehavior_returnsFromSave() { + // Arrange + every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns false + every { companyLogicMock.getById(any()) } returns company + every { recipeLogic.save(any()) } returns recipe + + val dto = RecipeSaveDto( + recipe.name, + recipe.description, + recipe.color, + recipe.gloss, + recipe.sample, + recipe.approbationDate, + recipe.remark, + company.id + ) + + // Act + val savedRecipe = recipeLogic.save(dto) + + // Assert + assertEquals(recipe, savedRecipe) + } + + @Test + fun save_nameAndCompanyExists_throwsAlreadyExistsException() { + // Arrange + every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns true + + // Act + // Assert + assertThrows { recipeLogic.save(recipe) } + } + + @Test + fun update_dto_normalBehavior_returnsFromSave() { + // Arrange + every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns false + every { recipeServiceMock.getById(any()) } returns recipe + every { companyLogicMock.getById(any()) } returns company + every { recipeLogic.update(any()) } returns recipe + + val dto = RecipeUpdateDto( + recipe.id, + recipe.name, + recipe.description, + recipe.color, + recipe.gloss, + recipe.sample, + recipe.approbationDate, + recipe.remark, + listOf() + ) + + // Act + val updatedRecipe = recipeLogic.update(dto) + + // Assert + assertEquals(recipe, updatedRecipe) + } + + @Test + fun update_nameAndCompanyExists_throwsAlreadyExistsException() { + // Arrange + every { recipeServiceMock.existsByNameAndCompany(any(), any(), any()) } returns true + + // Act + // Assert + assertThrows { recipeLogic.update(recipe) } + } + + @Test + fun updatePublicData_normalBehavior_callsUpdate() { + // Arrange + every { recipeLogic.getById(any()) } returns recipe + every { recipeLogic.update(any()) } returnsArgument 0 + every { groupLogicMock.getById(any()) } returns group + + val groupNote = RecipeGroupNoteDto(1L, "Unit test note") + val dto = RecipePublicDataDto(recipe.id, listOf(groupNote), listOf()) + + // Act + recipeLogic.updatePublicData(dto) + + // Assert + verify { + recipeLogic.update(any()) + } + } + + @Test + fun updatePublicData_normalBehavior_updatesRecipeGroupsInformation() { + // Arrange + var updatedRecipe = recipe + + every { recipeLogic.getById(any()) } returns recipe + every { recipeLogic.update(any()) } answers { firstArg().also { updatedRecipe = it } } + every { groupLogicMock.getById(any()) } returns group + + val expectedGroupInformation = RecipeGroupInformationDto(0L, group, "Unit test note", listOf()) + + val groupNote = RecipeGroupNoteDto(group.id!!, expectedGroupInformation.note) + val dto = RecipePublicDataDto(recipe.id, listOf(groupNote), listOf()) + + // Act + recipeLogic.updatePublicData(dto) + + // Assert + assertContains(updatedRecipe.groupsInformation, expectedGroupInformation) + } + + @Test + fun updatePublicData_emptyNotes_doesNothing() { + // Arrange + val dto = RecipePublicDataDto(recipe.id, listOf(), listOf()) + + // Act + recipeLogic.updatePublicData(dto) + + // Assert + verify(exactly = 0) { + recipeLogic.update(any()) + } + } + + @Test + fun updatePublicData_normalBehavior_callsUpdateLocationsInMixLogic() { + // Arrange + every { mixLogicMock.updateLocations(any()) } just runs + + val mixesLocation = listOf(MixLocationDto(group.id!!, "location")) + val dto = RecipePublicDataDto(recipe.id, listOf(), mixesLocation) + + // Act + recipeLogic.updatePublicData(dto) + + // Assert + verify { + mixLogicMock.updateLocations(mixesLocation) + } + } + + @Test + fun updatePublicData_emptyMixesLocation_doesNothing() { + // Arrange + val dto = RecipePublicDataDto(recipe.id, listOf(), listOf()) + + // Act + recipeLogic.updatePublicData(dto) + + // Assert + verify(exactly = 0) { + mixLogicMock.updateLocations(any()) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt index c9defbc..d24ef2f 100644 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt +++ b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/DefaultRecipeStepLogicTest.kt @@ -1,9 +1,9 @@ package dev.fyloz.colorrecipesexplorer.logic +import dev.fyloz.colorrecipesexplorer.dtos.RecipeGroupInformationDto +import dev.fyloz.colorrecipesexplorer.dtos.RecipeStepDto import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionError import dev.fyloz.colorrecipesexplorer.exception.InvalidPositionsException -import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation -import dev.fyloz.colorrecipesexplorer.model.RecipeStep import dev.fyloz.colorrecipesexplorer.model.account.Group import dev.fyloz.colorrecipesexplorer.service.RecipeStepService import dev.fyloz.colorrecipesexplorer.utils.PositionUtils @@ -29,8 +29,8 @@ class DefaultRecipeStepLogicTest { every { PositionUtils.validate(any()) } just runs val group = Group(1L, "Unit test group") - val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) - val groupInfo = RecipeGroupInformation(1L, group, "A note", steps) + val steps = listOf(RecipeStepDto(1L, 1, "A message")) + val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps) // Act recipeStepLogic.validateGroupInformationSteps(groupInfo) @@ -41,24 +41,6 @@ class DefaultRecipeStepLogicTest { } } - @Test - fun validateGroupInformationSteps_stepSetIsNull_doesNothing() { - // Arrange - mockkObject(PositionUtils) - every { PositionUtils.validate(any()) } just runs - - val group = Group(1L, "Unit test group") - val groupInfo = RecipeGroupInformation(1L, group, "A note", null) - - // Act - recipeStepLogic.validateGroupInformationSteps(groupInfo) - - // Assert - verify(exactly = 0) { - PositionUtils.validate(any()) - } - } - @Test fun validateGroupInformationSteps_invalidSteps_throwsInvalidGroupStepsPositionsException() { // Arrange @@ -68,8 +50,8 @@ class DefaultRecipeStepLogicTest { every { PositionUtils.validate(any()) } throws InvalidPositionsException(errors) val group = Group(1L, "Unit test group") - val steps = mutableSetOf(RecipeStep(1L, 1, "A message")) - val groupInfo = RecipeGroupInformation(1L, group, "A note", steps) + val steps = listOf(RecipeStepDto(1L, 1, "A message")) + val groupInfo = RecipeGroupInformationDto(1L, group, "A note", steps) // Act // Assert diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt deleted file mode 100644 index 53c34be..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/logic/RecipeLogicTest.kt +++ /dev/null @@ -1,371 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.logic - -import com.nhaarman.mockitokotlin2.* -import dev.fyloz.colorrecipesexplorer.dtos.MixLocationDto -import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException -import dev.fyloz.colorrecipesexplorer.logic.config.ConfigurationLogic -import dev.fyloz.colorrecipesexplorer.logic.files.CachedFile -import dev.fyloz.colorrecipesexplorer.logic.files.WriteableFileLogic -import dev.fyloz.colorrecipesexplorer.logic.users.GroupLogic -import dev.fyloz.colorrecipesexplorer.model.* -import dev.fyloz.colorrecipesexplorer.model.account.group -import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository -import dev.fyloz.colorrecipesexplorer.utils.FilePath -import io.mockk.* -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance -import org.junit.jupiter.api.assertThrows -import org.springframework.mock.web.MockMultipartFile -import org.springframework.web.multipart.MultipartFile -import java.time.LocalDate -import java.time.Period -import kotlin.test.* - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -class RecipeLogicTest : - AbstractExternalModelServiceTest() { - override val repository: RecipeRepository = mock() - private val companyLogic: CompanyLogic = mock() - private val mixService: MixLogic = mock() - private val groupService: GroupLogic = mock() - private val recipeStepService: RecipeStepLogic = mock() - private val configService: ConfigurationLogic = mock() - override val logic: RecipeLogic = - spy( - DefaultRecipeLogic( - repository, - companyLogic, - mixService, - recipeStepService, - groupService, - mock(), - configService - ) - ) - - private val company: Company = company(id = 0L) - override val entity: Recipe = recipe(id = 0L, name = "recipe", company = company) - override val anotherEntity: Recipe = recipe(id = 1L, name = "another recipe", company = company) - override val entitySaveDto: RecipeSaveDto = spy(recipeSaveDto(name = entity.name, companyId = entity.company.id!!)) - override val entityUpdateDto: RecipeUpdateDto = spy(recipeUpdateDto(id = entity.id!!, name = entity.name)) - - @AfterEach - override fun afterEach() { - reset(companyLogic, mixService) - super.afterEach() - } - - // existsByCompany() - - @Test - fun `existsByCompany() returns true when at least one recipe exists for the given company`() { - whenever(repository.existsByCompany(company)).doReturn(true) - - val found = logic.existsByCompany(company) - - assertTrue(found) - } - - @Test - fun `existsByCompany() returns false when no recipe exists for the given company`() { - whenever(repository.existsByCompany(company)).doReturn(false) - - val found = logic.existsByCompany(company) - - assertFalse(found) - } - - // existsByNameAndCompany() - - @Test - fun `existsByNameAndCompany() returns if a recipe exists for the given name and company in the repository`() { - setOf(true, false).forEach { - whenever(repository.existsByNameAndCompany(entity.name, company)).doReturn(it) - - val exists = logic.existsByNameAndCompany(entity.name, company) - - assertEquals(it, exists) - } - } - - // isApprobationExpired() - - @Test - fun `isApprobationExpired() returns false when the approbation date of the given recipe is within the configured period`() { - val period = Period.ofMonths(4) - val recipe = recipe(approbationDate = LocalDate.now()) - - whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString()) - - val approbationExpired = logic.isApprobationExpired(recipe) - - assertNotNull(approbationExpired) - assertFalse(approbationExpired) - } - - @Test - fun `isApprobationExpired() returns true when the approbation date of the given recipe is outside the configured period`() { - val period = Period.ofMonths(4) - val recipe = recipe(approbationDate = LocalDate.now().minus(period).minusMonths(1)) - - whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString()) - - val approbationExpired = logic.isApprobationExpired(recipe) - - assertNotNull(approbationExpired) - assertTrue(approbationExpired) - } - - @Test - fun `isApprobationExpired() returns null when the given recipe as no approbation date`() { - val period = Period.ofMonths(4) - val recipe = recipe(approbationDate = null) - - whenever(configService.getContent(ConfigurationType.RECIPE_APPROBATION_EXPIRATION)).doReturn(period.toString()) - - val approbationExpired = logic.isApprobationExpired(recipe) - - assertNull(approbationExpired) - } - - // getAllByName() - - @Test - fun `getAllByName() returns the recipes with the given name`() { - val recipes = listOf(entity, anotherEntity) - - whenever(repository.findAllByName(entity.name)).doReturn(recipes) - - val found = logic.getAllByName(entity.name) - - assertEquals(recipes, found) - } - - // getAllByCompany() - - @Test - fun `getAllByCompany() returns the recipes with the given company`() { - val companies = listOf(entity, anotherEntity) - whenever(repository.findAllByCompany(company)).doReturn(companies) - - val found = logic.getAllByCompany(company) - - assertEquals(companies, found) - } - - // save() - - @Test - override fun `save(dto) calls and returns save() with the created entity`() { - whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company)) - doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company) - withBaseSaveDtoTest(entity, entitySaveDto, logic, { argThat { this.id == null && this.color == color } }) - } - - @Test - fun `save(dto) throw AlreadyExistsException when a recipe with the given name and company exists in the repository`() { - whenever(companyLogic.getById(company.id!!)).doReturn(companyDto(company)) - doReturn(true).whenever(logic).existsByNameAndCompany(entity.name, company) - - with(assertThrows { logic.save(entitySaveDto) }) { - this.assertErrorCode("company-name") - } - } - - // update() - - @Test - override fun `update(dto) calls and returns update() with the created entity`() { - doReturn(false).whenever(logic).existsByNameAndCompany(entity.name, company) - withBaseUpdateDtoTest(entity, entityUpdateDto, logic, { any() }) - } - - @Test - fun `update(dto) throws AlreadyExistsException when a recipe exists for the given name and company`() { - val name = "another recipe" - - doReturn(entity).whenever(logic).getById(entity.id!!) - doReturn(true).whenever(logic).existsByNameAndCompany(name, company) - doReturn(name).whenever(entityUpdateDto).name - - with(assertThrows { logic.update(entityUpdateDto) }) { - this.assertErrorCode("company-name") - } - } - - // 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 = group(id = 1L), note = "Old note"), - recipeGroupInformation(id = 1L, group = group(id = 2L), note = "Another note"), - recipeGroupInformation(id = 2L, group = group(id = 3L), note = "Up to date note") - ) - ) - val notes = setOf( - noteDto(groupId = 1, content = "Note 1"), - noteDto(groupId = 2, content = null) - ) - val publicData = recipePublicDataDto(recipeId = recipe.id!!, notes = notes) - - doReturn(recipe).whenever(logic).getById(recipe.id!!) - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - logic.updatePublicData(publicData) - - verify(logic).update(argThat { - assertTrue { this.groupsInformation.first { it.group.id == 1L }.note == notes.first { it.groupId == 1L }.content } - assertTrue { this.groupsInformation.first { it.group.id == 2L }.note == notes.first { it.groupId == 2L }.content } - assertTrue { this.groupsInformation.any { it.group.id == 3L } && this.groupsInformation.first { it.group.id == 3L }.note == null } - true - }) - verify(mixService, times(0)).updateLocations(any()) - } - - @Test - fun `updatePublicData() update the location of a recipe mixes in the mix logic according to the RecipePublicDataDto`() { - val publicData = recipePublicDataDto( - mixesLocation = setOf( - MixLocationDto(mixId = 0L, location = "Loc 1"), - MixLocationDto(mixId = 1L, location = "Loc 2") - ) - ) - - logic.updatePublicData(publicData) - - verify(mixService).updateLocations(publicData.mixesLocation!!) - verify(logic, times(0)).update(any()) - } - - // addMix() - - @Test - fun `addMix() adds the given mix to the given recipe and updates it`() { - val mix = mix(id = 0L) - val recipe = recipe(id = 0L, mixes = mutableListOf()) - - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - val found = logic.addMix(recipe, mix) - - verify(logic).update(any()) - - assertEquals(recipe.id, found.id) - assertTrue(found.mixes.contains(mix)) - } - - // removeMix() - - @Test - fun `removeMix() removes the given mix from its recipe and updates it`() { - val recipe = recipe(id = 0L, mixes = mutableListOf()) - val mix = mix(id = 0L, recipe = recipe) - recipe.mixes.add(mix) - - doAnswer { it.arguments[0] }.whenever(logic).update(any()) - - val found = logic.removeMix(mix) - - verify(logic).update(any()) - - assertEquals(recipe.id, found.id) - assertFalse(found.mixes.contains(mix)) - } -} - -private class RecipeImageServiceTestContext { - val fileService = mockk { - every { write(any(), any(), any()) } just Runs - every { delete(any()) } just Runs - } - val recipeImageService = spyk(DefaultRecipeImageLogic(fileService)) - val recipe = spyk(recipe()) - val recipeImagesIds = setOf(1L, 10L, 21L) - val recipeImagesNames = recipeImagesIds.map { it.imageName }.toSet() - val recipeImagesFiles = recipeImagesNames.map { CachedFile(it, FilePath(it), true) } - - val Long.imageName - get() = "${recipe.name}$RECIPE_IMAGE_ID_DELIMITER$this" - - val String.imagePath - get() = "${recipe.imagesDirectoryPath}/$this$RECIPE_IMAGE_EXTENSION" -} - -class RecipeImageLogicTest { - @AfterEach - internal fun afterEach() { - clearAllMocks() - } - - private fun test(test: RecipeImageServiceTestContext.() -> Unit) { - RecipeImageServiceTestContext().test() - } - - // getAllImages() - - @Test - fun `getAllImages() returns a Set containing the name of every files in the recipe's directory`() { - test { - every { fileService.listDirectoryFiles(any()) } returns recipeImagesFiles - - val foundImagesNames = recipeImageService.getAllImages(recipe) - - assertEquals(recipeImagesNames, foundImagesNames) - } - } - - @Test - fun `getAllImages() returns an empty Set when the recipe's directory does not exists`() { - test { - every { fileService.listDirectoryFiles(any()) } returns emptySet() - - assertTrue { - recipeImageService.getAllImages(recipe).isEmpty() - } - } - } - - // download() - - @Test - 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 - - every { fileService.listDirectoryFiles(any()) } returns recipeImagesFiles - every { fileService.writeToDirectory(any(), any(), any(), any()) } just runs - - val foundImageName = recipeImageService.download(mockImage, recipe) - - assertEquals(expectedImageName, foundImageName) - - verify { - fileService.writeToDirectory(mockImage, expectedImagePath, any(), true) - } - } - } - - // delete() - - @Test - fun `delete() deletes the image with the given name in the FileService`() { - test { - val imageName = recipeImagesIds.first().imageName - val imagePath = imageName.imagePath - - every { fileService.deleteFromDirectory(any(), any()) } just runs - - recipeImageService.delete(recipe, imageName) - - verify { - fileService.deleteFromDirectory(imagePath, any()) - } - } - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepositoryTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepositoryTest.kt deleted file mode 100644 index fa622f0..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MaterialRepositoryTest.kt +++ /dev/null @@ -1,29 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.repository - -import dev.fyloz.colorrecipesexplorer.model.material -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager -import kotlin.test.assertEquals - -@DataJpaTest(excludeAutoConfiguration = [LiquibaseAutoConfiguration::class]) -class MaterialRepositoryTest @Autowired constructor( - private val materialRepository: MaterialRepository, - private val entityManager: TestEntityManager -) { - // updateInventoryQuantityById() - - @Test - fun `updateInventoryQuantityById() updates the quantity of the material with the given identifier`() { - var material = entityManager.persist(material(inventoryQuantity = 1000f, materialType = null)) - val updatedQuantity = 1235f - - materialRepository.updateInventoryQuantityById(material.id!!, updatedQuantity) - - material = entityManager.refresh(material) - - assertEquals(updatedQuantity, material.inventoryQuantity) - } -} diff --git a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepositoryTest.kt b/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepositoryTest.kt deleted file mode 100644 index e87c425..0000000 --- a/src/test/kotlin/dev/fyloz/colorrecipesexplorer/repository/MixRepositoryTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -package dev.fyloz.colorrecipesexplorer.repository - -import dev.fyloz.colorrecipesexplorer.model.* -import org.junit.jupiter.api.Test -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest -import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager -import kotlin.test.assertEquals - -@DataJpaTest(excludeAutoConfiguration = [LiquibaseAutoConfiguration::class]) -class MixRepositoryTest @Autowired constructor( - private val mixRepository: MixRepository, - private val entityManager: TestEntityManager -) { - // updateLocationById() - - @Test - fun `updateLocationById() updates the location of the mix with the given identifier`() { - withMixLocation(null) { mix -> - val updatedLocation = "new location" - - mixRepository.updateLocationById(mix.id!!, updatedLocation) - - val updated = entityManager.refresh(mix) - - assertEquals(updatedLocation, updated.location) - } - } - - private fun withMixLocation(location: String?, test: (Mix) -> Unit) { - val materialType = entityManager.persist(materialType()) - val mixType = entityManager.persist(mixType(materialType = materialType)) - - val company = entityManager.persist(company()) - val recipe = entityManager.persist(recipe(company = company)) - - val mix = mix(id = null, location = location, recipe = recipe, mixType = mixType) - test(entityManager.persist(mix)) - } -}