Merge branch 'master' into features

# Conflicts:
#	src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeService.kt
#	src/main/kotlin/dev/fyloz/colorrecipesexplorer/service/RecipeStepService.kt
This commit is contained in:
FyloZ 2021-04-15 16:36:22 -04:00
commit 2b1c8c2555
78 changed files with 2243 additions and 1779 deletions

View File

@ -4,7 +4,6 @@ plugins {
id("java")
id("org.jetbrains.kotlin.jvm") version "1.4.30"
id("org.jetbrains.dokka") version "1.4.20"
id("com.leobia.gradle.sassjavacompiler") version "0.2.1"
id("org.springframework.boot") version "2.3.4.RELEASE"
id("org.jetbrains.kotlin.plugin.spring") version "1.4.30"
id("org.jetbrains.kotlin.plugin.jpa") version "1.4.30"

View File

@ -1,6 +1,5 @@
package dev.fyloz.colorrecipesexplorer.service.files;
import dev.fyloz.colorrecipesexplorer.utils.PdfBuilder;
import dev.fyloz.colorrecipesexplorer.utils.PdfBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ResourceLoader;
@ -15,7 +14,7 @@ public class TouchUpKitService {
private static final String TOUCH_UP_EN = "TOUCH UP KIT";
public static final int FONT_SIZE = 42;
private ResourceLoader resourceLoader;
private final ResourceLoader resourceLoader;
@Autowired
public TouchUpKitService(ResourceLoader resourceLoader) {

View File

@ -1,8 +1,5 @@
package dev.fyloz.colorrecipesexplorer.service.files;
import dev.fyloz.colorrecipesexplorer.model.Recipe;
import dev.fyloz.colorrecipesexplorer.service.RecipeService;
import dev.fyloz.colorrecipesexplorer.xlsx.XlsxExporter;
import dev.fyloz.colorrecipesexplorer.model.Recipe;
import dev.fyloz.colorrecipesexplorer.service.RecipeService;
import dev.fyloz.colorrecipesexplorer.xlsx.XlsxExporter;
@ -19,8 +16,8 @@ import java.util.zip.ZipOutputStream;
@Service
public class XlsService {
private RecipeService recipeService;
private Logger logger;
private final RecipeService recipeService;
private final Logger logger;
@Autowired
public XlsService(RecipeService recipeService, Logger logger) {

View File

@ -16,14 +16,14 @@ public class PdfBuilder {
private static final String PATH_FONT_ARIAL_BOLD = "classpath:fonts/arialbd.ttf";
private PDFont font;
private PDDocument document = new PDDocument();
private PDPage page = new PDPage();
private Collection<PdfLine> lines = new ArrayList<>();
private boolean duplicated;
private int fontSize;
private int fontSizeBold;
private int lineSpacing;
private final PDFont font;
private final PDDocument document = new PDDocument();
private final PDPage page = new PDPage();
private final Collection<PdfLine> lines = new ArrayList<>();
private final boolean duplicated;
private final int fontSize;
private final int fontSizeBold;
private final int lineSpacing;
public PdfBuilder(ResourceLoader resourceLoader, boolean duplicated, int fontSize) throws IOException {
this.duplicated = duplicated;

View File

@ -1,22 +1,12 @@
package dev.fyloz.colorrecipesexplorer.xlsx;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Table;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.DescriptionCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.SectionTitleCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.TitleCell;
import dev.fyloz.colorrecipesexplorer.xlsx.util.Position;
import dev.fyloz.colorrecipesexplorer.model.Mix;
import dev.fyloz.colorrecipesexplorer.model.MixMaterial;
import dev.fyloz.colorrecipesexplorer.model.Recipe;
import dev.fyloz.colorrecipesexplorer.model.RecipeStep;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Catalog;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Table;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.DescriptionCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.NoteCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.SectionTitleCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.TitleCell;
import dev.fyloz.colorrecipesexplorer.xlsx.util.Position;
@ -27,7 +17,7 @@ import java.io.IOException;
import java.util.Collection;
public class XlsxExporter {
private Logger logger;
private final Logger logger;
public XlsxExporter(Logger logger) {
this.logger = logger;

View File

@ -1,10 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.builder;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.Cell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IColoredCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IFontCell;
import dev.fyloz.colorrecipesexplorer.xlsx.exception.InvalidCellTypeException;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.Cell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IColoredCell;
@ -18,9 +13,9 @@ import org.apache.poi.xssf.usermodel.XSSFFont;
public class CellBuilder {
private Document document;
private Cell cell;
private XSSFCell xcell;
private final Document document;
private final Cell cell;
private final XSSFCell xcell;
public CellBuilder(Document document, Cell cell, int x, int y) {
this.document = document;

View File

@ -1,10 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.builder;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.Cell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.exception.InvalidCellTypeException;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.Cell;
@ -15,8 +10,8 @@ import org.apache.poi.ss.util.CellRangeAddress;
public class SheetBuilder {
private Sheet sheet;
private Iterable<Cell> cells;
private final Sheet sheet;
private final Iterable<Cell> cells;
public SheetBuilder(Sheet sheet) {
this.sheet = sheet;

View File

@ -1,8 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.component;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.Cell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IFontCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.Cell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IFontCell;
@ -14,7 +11,7 @@ import java.util.List;
public class Catalog {
private List<String> content;
private final List<String> content;
public Catalog() {
content = new ArrayList<>();
@ -39,7 +36,7 @@ public class Catalog {
private class CatalogValueCell extends Cell implements IBiggerCell, IFontCell {
private String value;
private final String value;
public CatalogValueCell(String value) {
super(CellType.STRING);

View File

@ -1,6 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.component;
import dev.fyloz.colorrecipesexplorer.xlsx.exception.InvalidCellTypeException;
import dev.fyloz.colorrecipesexplorer.xlsx.builder.SheetBuilder;
import dev.fyloz.colorrecipesexplorer.xlsx.exception.InvalidCellTypeException;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
@ -12,8 +11,8 @@ public class Document extends XSSFWorkbook {
public static final int WIDTH = 8;
private static final XSSFWorkbookType WORKBOOK_TYPE = XSSFWorkbookType.XLSX;
private Sheet sheet;
private Logger logger;
private final Sheet sheet;
private final Logger logger;
public Document(String name, Logger logger) {
super(WORKBOOK_TYPE);

View File

@ -1,6 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.component;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.Cell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.xssf.usermodel.XSSFCell;
@ -11,11 +10,11 @@ import java.util.*;
public class Sheet {
private Document document;
private XSSFSheet xsheet;
private final Document document;
private final XSSFSheet xsheet;
private Map<Integer, XSSFRow> rows;
private List<Cell> registeredCells;
private final Map<Integer, XSSFRow> rows;
private final List<Cell> registeredCells;
Sheet(Document document, XSSFSheet xsheet) {
this.document = document;

View File

@ -1,10 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.component;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.Cell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IColoredCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IFontCell;
import dev.fyloz.colorrecipesexplorer.xlsx.exception.IndexOutOfTableBoundsException;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.Cell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IColoredCell;
@ -19,16 +14,16 @@ import java.util.*;
public class Table {
private static DecimalFormat format = new DecimalFormat("#.###");
private static final DecimalFormat format = new DecimalFormat("#.###");
private int width;
private int height;
private int cellWidth;
private final int width;
private final int height;
private final int cellWidth;
private Map<Position, Cell> content;
private final Map<Position, Cell> content;
private List<TableSubNameCell> columns;
private List<TableSubNameCell> rows;
private final List<TableSubNameCell> columns;
private final List<TableSubNameCell> rows;
public Table(int width, int height, String name) {
this.width = width;
@ -140,9 +135,9 @@ public class Table {
private final IndexedColors COLOR = IndexedColors.GREY_40_PERCENT;
private String name;
private int width;
private int height = 2;
private final String name;
private final int width;
private final int height = 2;
public TableNameCell(String name, int width) {
super(CellType.STRING);
@ -175,8 +170,8 @@ public class Table {
private class TableSubNameCell extends Cell implements IFontCell, IBiggerCell, IColoredCell {
private String name;
private int width;
private int height = 2;
private final int width;
private final int height = 2;
public TableSubNameCell(String name, int width) {
super(CellType.STRING);
@ -212,9 +207,9 @@ public class Table {
private class TableStringValueCell extends Cell implements IFontCell, IBiggerCell {
private String value;
private int width;
private int height = 2;
private final String value;
private final int width;
private final int height = 2;
public TableStringValueCell(String value, int width) {
super(CellType.STRING);
@ -246,9 +241,9 @@ public class Table {
private class TableNumValueCell extends Cell implements IFontCell, IBiggerCell {
private double value;
private int width;
private int height = 2;
private final double value;
private final int width;
private final int height = 2;
public TableNumValueCell(double value, int width) {
super(CellType.NUMERIC);

View File

@ -1,8 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.component.cells;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IColoredCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IFontCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IColoredCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IFontCell;
@ -17,7 +14,7 @@ public class DescriptionCell extends Cell implements IColoredCell, IBiggerCell,
private static final IndexedColors BG_COLOR_NAME = IndexedColors.GREY_40_PERCENT;
private static final short FONT_HEIGHT = 18;
private DescriptionCellType type;
private final DescriptionCellType type;
private String valueStr;
private double valueNbr;

View File

@ -1,7 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.component.cells;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IFontCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IFontCell;
@ -9,7 +7,7 @@ import org.apache.poi.ss.usermodel.CellType;
public class NoteCell extends Cell implements IBiggerCell, IFontCell {
private String value;
private final String value;
public NoteCell(String value) {
super(CellType.STRING);

View File

@ -1,8 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.component.cells;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IColoredCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IFontCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IColoredCell;
@ -19,7 +16,7 @@ public class SectionTitleCell extends Cell implements IColoredCell, IBiggerCell,
private static final IndexedColors FONT_COLOR = IndexedColors.WHITE;
private static final int FONT_HEIGHT = 24;
private String title;
private final String title;
public SectionTitleCell(String title) {
super(CellType.STRING);

View File

@ -1,8 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.component.cells;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IColoredCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IFontCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Document;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IBiggerCell;
import dev.fyloz.colorrecipesexplorer.xlsx.component.cells.styles.IColoredCell;
@ -18,7 +15,7 @@ public class TitleCell extends Cell implements IColoredCell, IBiggerCell, IFontC
private static final IndexedColors FONT_COLOR = IndexedColors.WHITE;
private static IndexedColors BACKGROUND_COLOR = IndexedColors.BLACK;
private String title;
private final String title;
public TitleCell(String title) {
super(TYPE);

View File

@ -1,6 +1,5 @@
package dev.fyloz.colorrecipesexplorer.xlsx.util;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet;
import dev.fyloz.colorrecipesexplorer.xlsx.component.Sheet;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.util.CellRangeAddress;
@ -28,11 +27,7 @@ public class CellUtils {
return true;
}
if (cell.getCellType() == CellType.STRING && cell.getStringCellValue().trim().isEmpty()) {
return true;
}
return false;
return cell.getCellType() == CellType.STRING && cell.getStringCellValue().trim().isEmpty();
}
public static boolean isCellInMergedCell(Sheet sheet, int x, int y) {

View File

@ -1,7 +1,7 @@
package dev.fyloz.colorrecipesexplorer
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
import org.springframework.boot.context.properties.EnableConfigurationProperties
@ -9,9 +9,9 @@ import org.springframework.boot.runApplication
@SpringBootApplication(exclude = [LiquibaseAutoConfiguration::class])
@EnableConfigurationProperties(
MaterialTypeProperties::class,
CreProperties::class,
DatabaseUpdaterProperties::class
MaterialTypeProperties::class,
CreProperties::class,
DatabaseUpdaterProperties::class
)
class ColorRecipesExplorerApplication

View File

@ -20,18 +20,18 @@ class DataSourceConfiguration {
@Bean(name = ["dataSource"])
@ConfigurationProperties(prefix = "spring.datasource")
fun customDataSource(
logger: Logger,
environment: Environment,
databaseUpdaterProperties: DatabaseUpdaterProperties
logger: Logger,
environment: Environment,
databaseUpdaterProperties: DatabaseUpdaterProperties
): DataSource {
val databaseUrl: String = environment.getProperty("spring.datasource.url")!!
runDatabaseVersionCheck(logger, databaseUrl, databaseUpdaterProperties)
return DataSourceBuilder
.create()
.url(databaseUrl) // Hikari won't start without that
.build()
.create()
.url(databaseUrl) // Hikari won't start without that
.build()
}
}
@ -75,24 +75,24 @@ fun runDatabaseUpdate(logger: Logger, database: CreDatabase) {
}
fun getDatabase(
databaseUrl: String,
databaseUpdaterProperties: DatabaseUpdaterProperties,
logger: Logger
databaseUrl: String,
databaseUpdaterProperties: DatabaseUpdaterProperties,
logger: Logger
): CreDatabase {
val databaseName =
(DATABASE_NAME_REGEX.find(databaseUrl) ?: throw DatabaseVersioningException.InvalidUrl(databaseUrl)).value
(DATABASE_NAME_REGEX.find(databaseUrl) ?: throw DatabaseVersioningException.InvalidUrl(databaseUrl)).value
return CreDatabase(
databaseContext(
properties = databaseUpdaterProperties(
targetVersion = SUPPORTED_DATABASE_VERSION,
url = databaseUrl.removeSuffix(databaseName),
dbName = databaseName,
username = databaseUpdaterProperties.username,
password = databaseUpdaterProperties.password
),
logger
)
databaseContext(
properties = databaseUpdaterProperties(
targetVersion = SUPPORTED_DATABASE_VERSION,
url = databaseUrl.removeSuffix(databaseName),
dbName = databaseName,
username = databaseUpdaterProperties.username,
password = databaseUpdaterProperties.password
),
logger
)
)
}
@ -101,7 +101,7 @@ fun throwUnsupportedDatabaseVersion(version: Int, logger: Logger) {
logger.error("Version $version of the database is not supported; Only version $SUPPORTED_DATABASE_VERSION is currently supported; Update this application to use the database.")
} else {
logger.error(
"""Version $version of the database is not supported; Only version $SUPPORTED_DATABASE_VERSION is currently supported.
"""Version $version of the database is not supported; Only version $SUPPORTED_DATABASE_VERSION is currently supported.
|You can update the database to the supported version by either:
| - Setting the environment variable '$ENV_VAR_ENABLE_DATABASE_UPDATE_NAME' to '1' to update the database automatically
| - Updating the database manually with the database manager utility (https://git.fyloz.dev/color-recipes-explorer/database-manager)
@ -122,5 +122,5 @@ class DatabaseUpdaterProperties {
sealed class DatabaseVersioningException(message: String) : Exception(message) {
class InvalidUrl(url: String) : DatabaseVersioningException("Invalid database url: $url")
class UnsupportedDatabaseVersion(version: Int) :
DatabaseVersioningException("Unsupported database version: $version; Only version $SUPPORTED_DATABASE_VERSION is currently supported")
DatabaseVersioningException("Unsupported database version: $version; Only version $SUPPORTED_DATABASE_VERSION is currently supported")
}

View File

@ -11,9 +11,9 @@ import org.springframework.core.annotation.Order
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
class InitialDataLoader(
private val materialTypeService: MaterialTypeService,
private val materialTypeProperties: MaterialTypeProperties
private val materialTypeService: MaterialTypeService,
private val materialTypeProperties: MaterialTypeProperties
) : ApplicationListener<ApplicationReadyEvent> {
override fun onApplicationEvent(event: ApplicationReadyEvent) =
materialTypeService.saveSystemTypes(materialTypeProperties.systemTypes)
materialTypeService.saveSystemTypes(materialTypeProperties.systemTypes)
}

View File

@ -1,12 +1,13 @@
package dev.fyloz.colorrecipesexplorer.config
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.Employee
import dev.fyloz.colorrecipesexplorer.model.EmployeeLoginRequest
import dev.fyloz.colorrecipesexplorer.model.EmployeePermission
import dev.fyloz.colorrecipesexplorer.service.EmployeeService
import dev.fyloz.colorrecipesexplorer.service.EmployeeServiceImpl
import dev.fyloz.colorrecipesexplorer.service.EmployeeUserDetailsService
import dev.fyloz.colorrecipesexplorer.service.EmployeeUserDetailsServiceImpl
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
@ -51,11 +52,11 @@ import javax.servlet.http.HttpServletResponse
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableConfigurationProperties(SecurityConfigurationProperties::class)
class WebSecurityConfig(
val securityConfigurationProperties: SecurityConfigurationProperties,
@Lazy val userDetailsService: EmployeeUserDetailsServiceImpl,
@Lazy val employeeService: EmployeeServiceImpl,
val environment: Environment,
val logger: Logger
val securityConfigurationProperties: SecurityConfigurationProperties,
@Lazy val userDetailsService: EmployeeUserDetailsServiceImpl,
@Lazy val employeeService: EmployeeServiceImpl,
val environment: Environment,
val logger: Logger
) : WebSecurityConfigurerAdapter() {
var debugMode = false
@ -65,36 +66,36 @@ class WebSecurityConfig(
@Bean
fun passwordEncoder() =
BCryptPasswordEncoder()
BCryptPasswordEncoder()
@Bean
override fun authenticationManagerBean(): AuthenticationManager =
super.authenticationManagerBean()
super.authenticationManagerBean()
@Bean
fun corsConfigurationSource() =
UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", CorsConfiguration().apply {
allowedOrigins = listOf("http://localhost:4200") // Angular development server
allowedMethods = listOf(
HttpMethod.GET.name,
HttpMethod.POST.name,
HttpMethod.PUT.name,
HttpMethod.DELETE.name,
HttpMethod.OPTIONS.name,
HttpMethod.HEAD.name
)
allowCredentials = true
}.applyPermitDefaultValues())
}
UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration("/**", CorsConfiguration().apply {
allowedOrigins = listOf("http://localhost:4200") // Angular development server
allowedMethods = listOf(
HttpMethod.GET.name,
HttpMethod.POST.name,
HttpMethod.PUT.name,
HttpMethod.DELETE.name,
HttpMethod.OPTIONS.name,
HttpMethod.HEAD.name
)
allowCredentials = true
}.applyPermitDefaultValues())
}
@PostConstruct
fun initWebSecurity() {
fun createUser(
credentials: SecurityConfigurationProperties.SystemUserCredentials?,
firstName: String,
lastName: String,
permissions: List<EmployeePermission>
credentials: SecurityConfigurationProperties.SystemUserCredentials?,
firstName: String,
lastName: String,
permissions: List<EmployeePermission>
) {
Assert.notNull(credentials, "No root user has been defined.")
credentials!!
@ -102,14 +103,14 @@ class WebSecurityConfig(
Assert.notNull(credentials.password, "The root user has no password defined.")
if (!employeeService.existsById(credentials.id!!)) {
employeeService.save(
Employee(
id = credentials.id!!,
firstName = firstName,
lastName = lastName,
password = passwordEncoder().encode(credentials.password!!),
isSystemUser = true,
permissions = permissions.toMutableSet()
)
Employee(
id = credentials.id!!,
firstName = firstName,
lastName = lastName,
password = passwordEncoder().encode(credentials.password!!),
isSystemUser = true,
permissions = permissions.toMutableSet()
)
)
}
}
@ -121,37 +122,37 @@ class WebSecurityConfig(
override fun configure(http: HttpSecurity) {
http
.headers().frameOptions().disable()
.and()
.csrf().disable()
.addFilter(
JwtAuthenticationFilter(
authenticationManager(),
employeeService,
securityConfigurationProperties
)
.headers().frameOptions().disable()
.and()
.csrf().disable()
.addFilter(
JwtAuthenticationFilter(
authenticationManager(),
employeeService,
securityConfigurationProperties
)
.addFilter(
JwtAuthorizationFilter(
userDetailsService,
securityConfigurationProperties,
authenticationManager()
)
)
.addFilter(
JwtAuthorizationFilter(
userDetailsService,
securityConfigurationProperties,
authenticationManager()
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
if (!debugMode) {
http.authorizeRequests()
.antMatchers("/api/login").permitAll()
.antMatchers("/api/logout").authenticated()
.antMatchers("/api/employee/current").authenticated()
.anyRequest().authenticated()
.antMatchers("/api/login").permitAll()
.antMatchers("/api/logout").authenticated()
.antMatchers("/api/employee/current").authenticated()
.anyRequest().authenticated()
} else {
http
.cors()
.and()
.authorizeRequests()
.antMatchers("**").permitAll()
.cors()
.and()
.authorizeRequests()
.antMatchers("**").permitAll()
}
}
}
@ -159,9 +160,9 @@ class WebSecurityConfig(
@Component
class RestAuthenticationEntryPoint : AuthenticationEntryPoint {
override fun commence(
request: HttpServletRequest,
response: HttpServletResponse,
authException: AuthenticationException
request: HttpServletRequest,
response: HttpServletResponse,
authException: AuthenticationException
) = response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized")
}
@ -170,9 +171,9 @@ const val defaultGroupCookieName = "Default-Group"
val blacklistedJwtTokens = mutableListOf<String>()
class JwtAuthenticationFilter(
private val authManager: AuthenticationManager,
private val employeeService: EmployeeService,
private val securityConfigurationProperties: SecurityConfigurationProperties
private val authManager: AuthenticationManager,
private val employeeService: EmployeeService,
private val securityConfigurationProperties: SecurityConfigurationProperties
) : UsernamePasswordAuthenticationFilter() {
private var debugMode = false
@ -187,10 +188,10 @@ class JwtAuthenticationFilter(
}
override fun successfulAuthentication(
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain,
authResult: Authentication
request: HttpServletRequest,
response: HttpServletResponse,
chain: FilterChain,
authResult: Authentication
) {
val jwtSecret = securityConfigurationProperties.jwtSecret
val jwtDuration = securityConfigurationProperties.jwtDuration
@ -201,17 +202,17 @@ class JwtAuthenticationFilter(
val expirationMs = System.currentTimeMillis() + jwtDuration!!
val expirationDate = Date(expirationMs)
val token = Jwts.builder()
.setSubject(employeeId)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray())
.compact()
.setSubject(employeeId)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, jwtSecret!!.toByteArray())
.compact()
response.addHeader("Access-Control-Expose-Headers", "X-Authentication-Expiration")
var bearerCookie =
"$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; SameSite=strict"
"$authorizationCookieName=Bearer$token; Max-Age=${jwtDuration / 1000}; HttpOnly; SameSite=strict"
if (!debugMode) bearerCookie += "; Secure;"
response.addHeader(
"Set-Cookie",
bearerCookie
"Set-Cookie",
bearerCookie
)
response.addHeader(authorizationCookieName, "Bearer $token")
response.addHeader("X-Authentication-Expiration", "$expirationMs")
@ -219,9 +220,9 @@ class JwtAuthenticationFilter(
}
class JwtAuthorizationFilter(
private val userDetailsService: EmployeeUserDetailsServiceImpl,
private val securityConfigurationProperties: SecurityConfigurationProperties,
authenticationManager: AuthenticationManager
private val userDetailsService: EmployeeUserDetailsService,
private val securityConfigurationProperties: SecurityConfigurationProperties,
authenticationManager: AuthenticationManager
) : BasicAuthenticationFilter(authenticationManager) {
override fun doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, chain: FilterChain) {
fun tryLoginFromBearer(): Boolean {
@ -259,10 +260,10 @@ class JwtAuthorizationFilter(
Assert.notNull(jwtSecret, "No JWT secret has been defined.")
return try {
val employeeId = Jwts.parser()
.setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", ""))
.body
.subject
.setSigningKey(jwtSecret!!.toByteArray())
.parseClaimsJws(token.replace("Bearer", ""))
.body
.subject
if (employeeId != null) getAuthenticationToken(employeeId) else null
} catch (_: ExpiredJwtException) {
null
@ -272,7 +273,7 @@ class JwtAuthorizationFilter(
private fun getAuthenticationToken(employeeId: String): UsernamePasswordAuthenticationToken? = try {
val employeeDetails = userDetailsService.loadUserByEmployeeId(employeeId.toLong(), false)
UsernamePasswordAuthenticationToken(employeeDetails.username, null, employeeDetails.authorities)
} catch (_: EntityNotFoundException) {
} catch (_: NotFoundException) {
null
}
}

View File

@ -13,9 +13,9 @@ class MaterialTypeProperties {
var baseName: String = ""
data class MaterialTypeProperty(
var name: String = "",
var prefix: String = "",
var usePercentages: Boolean = false
var name: String = "",
var prefix: String = "",
var usePercentages: Boolean = false
) {
fun toMaterialType(): MaterialType {
Assert.hasText(name, "A system material type has an empty name")

View File

@ -1,9 +1,5 @@
package dev.fyloz.colorrecipesexplorer.exception
import com.fasterxml.jackson.annotation.JsonProperty
import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.model.MaterialQuantityDto
import org.springframework.context.annotation.Profile
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
@ -11,91 +7,91 @@ import org.springframework.validation.FieldError
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.context.request.ServletWebRequest
import org.springframework.web.context.request.WebRequest
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
abstract class RestException(val exceptionMessage: String, val httpStatus: HttpStatus) :
RuntimeException(exceptionMessage) {
abstract fun buildBody(): RestExceptionBody
abstract class RestException(
val errorCode: String,
val title: String,
val status: HttpStatus,
val details: String,
val extensions: Map<String, Any> = mapOf()
) : RuntimeException(details) {
fun buildExceptionBody() = mapOf(
"type" to errorCode,
"title" to title,
"status" to status.value(),
"detail" to details,
@Suppress("unused")
open inner class RestExceptionBody(
val status: Int = httpStatus.value(),
@JsonProperty("message") val message: String = exceptionMessage
*extensions.map { it.key to it.value }.toTypedArray()
)
}
class EntityAlreadyExistsException(val value: Any) :
RestException("An entity with the given identifier already exists", HttpStatus.CONFLICT) {
@Suppress("unused")
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {
val id = value
}
}
class NotFoundException(
errorCode: String,
title: String,
details: String,
identifierValue: Any,
identifierName: String = "id"
) : RestException(
errorCode = "notfound-$errorCode-$identifierName",
title = title,
status = HttpStatus.NOT_FOUND,
details = details,
extensions = mapOf(
identifierName to identifierValue
)
)
class EntityNotFoundException(val value: Any) :
RestException("An entity could not be found with the given identifier", HttpStatus.NOT_FOUND) {
@Suppress("unused")
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {
val id = value
}
}
class AlreadyExistsException(
errorCode: String,
title: String,
details: String,
identifierValue: Any,
identifierName: String = "id"
) : RestException(
errorCode = "exists-$errorCode-$identifierName",
title = title,
status = HttpStatus.CONFLICT,
details = details,
extensions = mapOf(
identifierName to identifierValue
)
)
class CannotDeleteEntityException(val value: Long) :
RestException(
"The entity with the given identifier could not be deleted because it is required by other entities",
HttpStatus.CONFLICT
) {
@Suppress("unused")
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {
val id = value
}
}
class SimdutWriteException(val material: Material) :
RestException(
"Could not write the SIMDUT file to disk",
HttpStatus.INTERNAL_SERVER_ERROR
) {
@Suppress("unused")
override fun buildBody(): RestExceptionBody = RestExceptionBody()
}
class LowQuantityException(val materialQuantity: MaterialQuantityDto) :
RestException(
"There is not enough of the given material in the inventory",
HttpStatus.CONFLICT
) {
@Suppress("unused")
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {
val material = materialQuantity.material
val quantity = materialQuantity.quantity
}
}
class LowQuantitiesException(val materialQuantities: Collection<MaterialQuantityDto>) :
RestException(
"There is not enough of one or more given materials in the inventory",
HttpStatus.CONFLICT
) {
@Suppress
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {
val lowQuantities = materialQuantities
}
}
class CannotDeleteException(
errorCode: String,
title: String,
details: String
) : RestException(
errorCode = "cannotdelete-$errorCode",
title = title,
status = HttpStatus.CONFLICT,
details = details
)
@ControllerAdvice
class RestResponseEntityExceptionHandler : ResponseEntityExceptionHandler() {
@ExceptionHandler(RestException::class)
fun handleRestExceptions(exception: RestException, request: WebRequest): ResponseEntity<Any> {
return handleExceptionInternal(exception, exception.buildBody(), HttpHeaders(), exception.httpStatus, request)
val finalBody = exception.buildExceptionBody().toMutableMap()
finalBody["instance"] = (request as ServletWebRequest).request.requestURI
return handleExceptionInternal(
exception,
finalBody,
HttpHeaders(),
exception.status,
request
)
}
override fun handleMethodArgumentNotValid(
ex: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
ex: MethodArgumentNotValidException,
headers: HttpHeaders,
status: HttpStatus,
request: WebRequest
): ResponseEntity<Any> {
val errors = hashMapOf<String, String>()
ex.bindingResult.allErrors.forEach {

View File

@ -1,331 +0,0 @@
package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
private const val EMPLOYEE_ID_NULL_MESSAGE = "Un numéro d'employé est requis"
private const val EMPLOYEE_LAST_NAME_EMPTY_MESSAGE = "Un nom est requis"
private const val EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE = "Un prénom est requis"
private const val EMPLOYEE_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis"
private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères"
@Entity
@Table(name = "employee")
data class Employee(
@Id
override val id: Long,
@Column(name = "first_name")
val firstName: String = "",
@Column(name = "last_name")
val lastName: String = "",
@JsonIgnore
val password: String = "",
@JsonIgnore
@Column(name = "default_group_user")
val isDefaultGroupUser: Boolean = false,
@JsonIgnore
@Column(name = "system_user")
val isSystemUser: Boolean = false,
@ManyToOne
@JoinColumn(name = "group_id")
@Fetch(FetchMode.SELECT)
var group: EmployeeGroup? = null,
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "employee_permission", joinColumns = [JoinColumn(name = "employee_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
@get:JsonProperty("explicitPermissions")
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
@Column(name = "last_login_time")
var lastLoginTime: LocalDateTime? = null
) : Model {
@get:JsonProperty("permissions")
val flatPermissions: Set<EmployeePermission>
get() = permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toMutableSet()
.apply {
if (group != null) this.addAll(group!!.flatPermissions)
}
@get:JsonIgnore
val authorities: Set<GrantedAuthority>
get() = flatPermissions.map { it.toAuthority() }.toMutableSet()
}
/** DTO for creating employees. Allows a [password] a [groupId]. */
open class EmployeeSaveDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String,
@field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String,
@field:NotBlank(message = EMPLOYEE_PASSWORD_EMPTY_MESSAGE)
@field:Size(min = 8, message = EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE)
val password: String,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: MutableSet<EmployeePermission> = mutableSetOf()
) : EntityDto<Employee>
open class EmployeeUpdateDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String?,
@field:NullOrNotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String?,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: Set<EmployeePermission>?
) : EntityDto<Employee>
private const val GROUP_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val GROUP_NAME_NULL_MESSAGE = "Un nom est requis"
private const val GROUP_PERMISSIONS_EMPTY_MESSAGE = "Au moins une permission est requise"
@Entity
@Table(name = "employee_group")
data class EmployeeGroup(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override var id: Long? = null,
@Column(unique = true)
override val name: String = "",
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
@get:JsonProperty("explicitPermissions")
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
) : NamedModel {
@get:JsonProperty("permissions")
val flatPermissions: Set<EmployeePermission>
get() = this.permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toSet()
}
open class EmployeeGroupSaveDto(
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
override fun toEntity(): EmployeeGroup =
EmployeeGroup(null, name, permissions)
}
open class EmployeeGroupUpdateDto(
@field:NotNull(message = GROUP_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
override fun toEntity(): EmployeeGroup =
EmployeeGroup(id, name, permissions)
}
data class EmployeeLoginRequest(val id: Long, val password: String)
enum class EmployeePermission(
val impliedPermissions: List<EmployeePermission> = listOf(),
val deprecated: Boolean = false
) {
VIEW_RECIPES,
VIEW_CATALOG,
VIEW_USERS,
PRINT_MIXES(listOf(VIEW_RECIPES)),
EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)),
EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA)),
EDIT_MATERIALS(listOf(VIEW_CATALOG)),
EDIT_MATERIAL_TYPES(listOf(VIEW_CATALOG)),
EDIT_COMPANIES(listOf(VIEW_CATALOG)),
EDIT_USERS(listOf(VIEW_USERS)),
EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)),
ADD_TO_INVENTORY(listOf(VIEW_CATALOG)),
DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)),
REMOVE_RECIPES(listOf(EDIT_RECIPES)),
REMOVE_MATERIALS(listOf(EDIT_MATERIALS)),
REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES)),
REMOVE_COMPANIES(listOf(EDIT_COMPANIES)),
REMOVE_USERS(listOf(EDIT_USERS)),
REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES)),
ADMIN(
listOf(
EDIT_CATALOG,
REMOVE_RECIPES,
REMOVE_USERS,
REMOVE_CATALOG,
PRINT_MIXES,
ADD_TO_INVENTORY,
DEDUCT_FROM_INVENTORY
)
),
// deprecated permissions
VIEW_RECIPE(listOf(VIEW_RECIPES), true),
VIEW_MATERIAL(listOf(VIEW_CATALOG), true),
VIEW_MATERIAL_TYPE(listOf(VIEW_CATALOG), true),
VIEW_COMPANY(listOf(VIEW_CATALOG), true),
VIEW(listOf(VIEW_RECIPES, VIEW_CATALOG), true),
VIEW_EMPLOYEE(listOf(VIEW_USERS), true),
VIEW_EMPLOYEE_GROUP(listOf(VIEW_USERS), true),
EDIT_RECIPE(listOf(EDIT_RECIPES), true),
EDIT_MATERIAL(listOf(EDIT_MATERIALS), true),
EDIT_MATERIAL_TYPE(listOf(EDIT_MATERIAL_TYPES), true),
EDIT_COMPANY(listOf(EDIT_COMPANIES), true),
EDIT(listOf(EDIT_RECIPES, EDIT_CATALOG), true),
EDIT_EMPLOYEE(listOf(EDIT_USERS), true),
EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_USERS), true),
EDIT_EMPLOYEE_GROUP(listOf(EDIT_USERS), true),
REMOVE_RECIPE(listOf(REMOVE_RECIPES), true),
REMOVE_MATERIAL(listOf(REMOVE_MATERIALS), true),
REMOVE_MATERIAL_TYPE(listOf(REMOVE_MATERIAL_TYPES), true),
REMOVE_COMPANY(listOf(REMOVE_COMPANIES), true),
REMOVE(listOf(REMOVE_RECIPES, REMOVE_CATALOG), true),
REMOVE_EMPLOYEE(listOf(REMOVE_USERS), true),
REMOVE_EMPLOYEE_GROUP(listOf(REMOVE_USERS), true),
SET_BROWSER_DEFAULT_GROUP(listOf(VIEW_USERS), true),
;
operator fun contains(permission: EmployeePermission): Boolean {
return permission == this || impliedPermissions.any { permission in it }
}
}
fun EmployeePermission.flat(): Iterable<EmployeePermission> {
return mutableSetOf(this).apply {
impliedPermissions.forEach {
addAll(it.flat())
}
}
}
/** Converts the given [EmployeePermission] to a [GrantedAuthority]. */
private fun EmployeePermission.toAuthority(): GrantedAuthority {
return SimpleGrantedAuthority(name)
}
// ==== DSL ====
fun employee(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: EmployeeGroup? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
op: Employee.() -> Unit = {}
) = Employee(
id,
firstName,
lastName,
password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun employeeSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
groupId: Long? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeSaveDto.() -> Unit = {}
) = EmployeeSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)
fun employeeUpdateDto(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
groupId: Long? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeUpdateDto.() -> Unit = {}
) = EmployeeUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
fun employeeGroup(
id: Long? = null,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroup.() -> Unit = {}
) = EmployeeGroup(id, name, permissions).apply(op)
fun employeeGroupSaveDto(
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupSaveDto.() -> Unit = {}
) = EmployeeGroupSaveDto(name, permissions).apply(op)
fun employeeGroupUpdateDto(
id: Long = 0L,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupUpdateDto.() -> Unit = {}
) = EmployeeGroupUpdateDto(id, name, permissions).apply(op)

View File

@ -1,5 +1,8 @@
package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import javax.persistence.*
import javax.validation.constraints.NotBlank
@ -11,47 +14,94 @@ private const val COMPANY_NAME_NULL_MESSAGE = "Un nom est requis"
@Entity
@Table(name = "company")
data class Company(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@Column(unique = true)
override val name: String
@Column(unique = true)
override val name: String
) : NamedModel
open class CompanySaveDto(
@field:NotBlank(message = COMPANY_NAME_NULL_MESSAGE)
val name: String
@field:NotBlank(message = COMPANY_NAME_NULL_MESSAGE)
val name: String
) : EntityDto<Company> {
override fun toEntity(): Company = Company(null, name)
}
open class CompanyUpdateDto(
@field:NotNull(message = COMPANY_ID_NULL_MESSAGE)
val id: Long,
@field:NotNull(message = COMPANY_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = COMPANY_NAME_NULL_MESSAGE)
val name: String?
@field:NullOrNotBlank(message = COMPANY_NAME_NULL_MESSAGE)
val name: String?
) : EntityDto<Company> {
override fun toEntity(): Company = Company(id, name ?: "")
}
// ==== DSL ====
fun company(
id: Long? = null,
name: String = "name",
op: Company.() -> Unit = {}
id: Long? = null,
name: String = "name",
op: Company.() -> Unit = {}
) = Company(id, name).apply(op)
fun companySaveDto(
name: String = "name",
op: CompanySaveDto.() -> Unit = {}
name: String = "name",
op: CompanySaveDto.() -> Unit = {}
) = CompanySaveDto(name).apply(op)
fun companyUpdateDto(
id: Long = 0L,
name: String? = "name",
op: CompanyUpdateDto.() -> Unit = {}
id: Long = 0L,
name: String? = "name",
op: CompanyUpdateDto.() -> Unit = {}
) = CompanyUpdateDto(id, name).apply(op)
// ==== Exceptions ====
private const val COMPANY_NOT_FOUND_EXCEPTION_TITLE = "Company not found"
private const val COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE = "Company already exists"
private const val COMPANY_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete company"
private const val COMPANY_EXCEPTION_ERROR_CODE = "company"
fun companyIdNotFoundException(id: Long) =
NotFoundException(
COMPANY_EXCEPTION_ERROR_CODE,
COMPANY_NOT_FOUND_EXCEPTION_TITLE,
"A company with the id $id could not be found",
id
)
fun companyNameNotFoundException(name: String) =
NotFoundException(
COMPANY_EXCEPTION_ERROR_CODE,
COMPANY_NOT_FOUND_EXCEPTION_TITLE,
"A company with the name $name could not be found",
name,
"name"
)
fun companyIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
COMPANY_EXCEPTION_ERROR_CODE,
COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE,
"A company with the id $id already exists",
id
)
fun companyNameAlreadyExistsException(name: String) =
AlreadyExistsException(
COMPANY_EXCEPTION_ERROR_CODE,
COMPANY_ALREADY_EXISTS_EXCEPTION_TITLE,
"A company with the name $name already exists",
name,
"name"
)
fun cannotDeleteCompany(company: Company) =
CannotDeleteException(
COMPANY_EXCEPTION_ERROR_CODE,
COMPANY_CANNOT_DELETE_EXCEPTION_TITLE,
"Cannot delete the company ${company.name} because one or more recipes depends on it"
)

View File

@ -0,0 +1,191 @@
package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import java.time.LocalDateTime
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
private const val EMPLOYEE_ID_NULL_MESSAGE = "Un numéro d'employé est requis"
private const val EMPLOYEE_LAST_NAME_EMPTY_MESSAGE = "Un nom est requis"
private const val EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE = "Un prénom est requis"
private const val EMPLOYEE_PASSWORD_EMPTY_MESSAGE = "Un mot de passe est requis"
private const val EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE = "Le mot de passe doit contenir au moins 8 caractères"
@Entity
@Table(name = "employee")
data class Employee(
@Id
override val id: Long,
@Column(name = "first_name")
val firstName: String = "",
@Column(name = "last_name")
val lastName: String = "",
@JsonIgnore
val password: String = "",
@JsonIgnore
@Column(name = "default_group_user")
val isDefaultGroupUser: Boolean = false,
@JsonIgnore
@Column(name = "system_user")
val isSystemUser: Boolean = false,
@ManyToOne
@JoinColumn(name = "group_id")
@Fetch(FetchMode.SELECT)
var group: EmployeeGroup? = null,
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "employee_permission", joinColumns = [JoinColumn(name = "employee_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
@get:JsonProperty("explicitPermissions")
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
@Column(name = "last_login_time")
var lastLoginTime: LocalDateTime? = null
) : Model {
@get:JsonProperty("permissions")
val flatPermissions: Set<EmployeePermission>
get() = permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toMutableSet()
.apply {
if (group != null) this.addAll(group!!.flatPermissions)
}
@get:JsonIgnore
val authorities: Set<GrantedAuthority>
get() = flatPermissions.map { it.toAuthority() }.toMutableSet()
}
/** DTO for creating employees. Allows a [password] a [groupId]. */
open class EmployeeSaveDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String,
@field:NotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String,
@field:NotBlank(message = EMPLOYEE_PASSWORD_EMPTY_MESSAGE)
@field:Size(min = 8, message = EMPLOYEE_PASSWORD_TOO_SHORT_MESSAGE)
val password: String,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: MutableSet<EmployeePermission> = mutableSetOf()
) : EntityDto<Employee>
open class EmployeeUpdateDto(
@field:NotNull(message = EMPLOYEE_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = EMPLOYEE_FIRST_NAME_EMPTY_MESSAGE)
val firstName: String?,
@field:NullOrNotBlank(message = EMPLOYEE_LAST_NAME_EMPTY_MESSAGE)
val lastName: String?,
val groupId: Long?,
@Enumerated(EnumType.STRING)
val permissions: Set<EmployeePermission>?
) : EntityDto<Employee>
data class EmployeeLoginRequest(val id: Long, val password: String)
// ==== DSL ====
fun employee(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
isDefaultGroupUser: Boolean = false,
isSystemUser: Boolean = false,
group: EmployeeGroup? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
lastLoginTime: LocalDateTime? = null,
op: Employee.() -> Unit = {}
) = Employee(
id,
firstName,
lastName,
password,
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
).apply(op)
fun employeeSaveDto(
passwordEncoder: PasswordEncoder = BCryptPasswordEncoder(),
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
password: String = passwordEncoder.encode("password"),
groupId: Long? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeSaveDto.() -> Unit = {}
) = EmployeeSaveDto(id, firstName, lastName, password, groupId, permissions).apply(op)
fun employeeUpdateDto(
id: Long = 0L,
firstName: String = "firstName",
lastName: String = "lastName",
groupId: Long? = null,
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeUpdateDto.() -> Unit = {}
) = EmployeeUpdateDto(id, firstName, lastName, groupId, permissions).apply(op)
// ==== Exceptions ====
private const val EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE = "Employee not found"
private const val EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE = "Employee already exists"
private const val EMPLOYEE_EXCEPTION_ERROR_CODE = "employee"
fun employeeIdNotFoundException(id: Long) =
NotFoundException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE,
"An employee with the id $id could not be found",
id
)
fun employeeIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee with the id $id already exists",
id
)
fun employeeFullNameAlreadyExistsException(firstName: String, lastName: String) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee with the name '$firstName $lastName' already exists",
"$firstName $lastName",
"fullName"
)

View File

@ -0,0 +1,136 @@
package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonProperty
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import org.hibernate.annotations.Fetch
import org.hibernate.annotations.FetchMode
import org.springframework.http.HttpStatus
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.Size
private const val GROUP_ID_NULL_MESSAGE = "Un identifiant est requis"
private const val GROUP_NAME_NULL_MESSAGE = "Un nom est requis"
private const val GROUP_PERMISSIONS_EMPTY_MESSAGE = "Au moins une permission est requise"
@Entity
@Table(name = "employee_group")
data class EmployeeGroup(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override var id: Long? = null,
@Column(unique = true)
override val name: String = "",
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "group_permission", joinColumns = [JoinColumn(name = "group_id")])
@Column(name = "permission")
@Fetch(FetchMode.SUBSELECT)
@get:JsonProperty("explicitPermissions")
val permissions: MutableSet<EmployeePermission> = mutableSetOf(),
) : NamedModel {
@get:JsonProperty("permissions")
val flatPermissions: Set<EmployeePermission>
get() = this.permissions
.flatMap { it.flat() }
.filter { !it.deprecated }
.toSet()
}
open class EmployeeGroupSaveDto(
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
override fun toEntity(): EmployeeGroup =
EmployeeGroup(null, name, permissions)
}
open class EmployeeGroupUpdateDto(
@field:NotNull(message = GROUP_ID_NULL_MESSAGE)
val id: Long,
@field:NotBlank(message = GROUP_NAME_NULL_MESSAGE)
@field:Size(min = 3)
val name: String,
@field:Size(min = 1, message = GROUP_PERMISSIONS_EMPTY_MESSAGE)
val permissions: MutableSet<EmployeePermission>
) : EntityDto<EmployeeGroup> {
override fun toEntity(): EmployeeGroup =
EmployeeGroup(id, name, permissions)
}
fun employeeGroup(
id: Long? = null,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroup.() -> Unit = {}
) = EmployeeGroup(id, name, permissions).apply(op)
fun employeeGroupSaveDto(
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupSaveDto.() -> Unit = {}
) = EmployeeGroupSaveDto(name, permissions).apply(op)
fun employeeGroupUpdateDto(
id: Long = 0L,
name: String = "name",
permissions: MutableSet<EmployeePermission> = mutableSetOf(),
op: EmployeeGroupUpdateDto.() -> Unit = {}
) = EmployeeGroupUpdateDto(id, name, permissions).apply(op)
// ==== Exceptions ====
private const val EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE = "Employee group not found"
private const val EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE = "Employee group already exists"
private const val EMPLOYEE_EXCEPTION_ERROR_CODE = "employeegroup"
class NoDefaultGroupException : RestException(
"nodefaultgroup",
"No default group",
HttpStatus.NOT_FOUND,
"No default group cookie is defined in the current request"
)
fun employeeGroupIdNotFoundException(id: Long) =
NotFoundException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE,
"An employee group with the id $id could not be found",
id
)
fun employeeGroupNameNotFoundException(name: String) =
NotFoundException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_NOT_FOUND_EXCEPTION_TITLE,
"An employee group with the name $name could not be found",
name,
"name"
)
fun employeeGroupIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee group with the id $id already exists",
id,
)
fun employeeGroupNameAlreadyExistsException(name: String) =
AlreadyExistsException(
EMPLOYEE_EXCEPTION_ERROR_CODE,
EMPLOYEE_ALREADY_EXISTS_EXCEPTION_TITLE,
"An employee group with the name $name already exists",
name,
"name"
)

View File

@ -0,0 +1,94 @@
package dev.fyloz.colorrecipesexplorer.model
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import java.util.*
enum class EmployeePermission(
val impliedPermissions: List<EmployeePermission> = listOf(),
val deprecated: Boolean = false
) {
VIEW_RECIPES,
VIEW_CATALOG,
VIEW_USERS,
PRINT_MIXES(listOf(VIEW_RECIPES)),
EDIT_RECIPES_PUBLIC_DATA(listOf(VIEW_RECIPES)),
EDIT_RECIPES(listOf(EDIT_RECIPES_PUBLIC_DATA)),
EDIT_MATERIALS(listOf(VIEW_CATALOG)),
EDIT_MATERIAL_TYPES(listOf(VIEW_CATALOG)),
EDIT_COMPANIES(listOf(VIEW_CATALOG)),
EDIT_USERS(listOf(VIEW_USERS)),
EDIT_CATALOG(listOf(EDIT_MATERIALS, EDIT_MATERIAL_TYPES, EDIT_COMPANIES)),
ADD_TO_INVENTORY(listOf(VIEW_CATALOG)),
DEDUCT_FROM_INVENTORY(listOf(VIEW_RECIPES)),
REMOVE_RECIPES(listOf(EDIT_RECIPES)),
REMOVE_MATERIALS(listOf(EDIT_MATERIALS)),
REMOVE_MATERIAL_TYPES(listOf(EDIT_MATERIAL_TYPES)),
REMOVE_COMPANIES(listOf(EDIT_COMPANIES)),
REMOVE_USERS(listOf(EDIT_USERS)),
REMOVE_CATALOG(listOf(REMOVE_MATERIALS, REMOVE_MATERIAL_TYPES, REMOVE_COMPANIES)),
ADMIN(
listOf(
EDIT_CATALOG,
REMOVE_RECIPES,
REMOVE_USERS,
REMOVE_CATALOG,
PRINT_MIXES,
ADD_TO_INVENTORY,
DEDUCT_FROM_INVENTORY
)
),
// deprecated permissions
VIEW_RECIPE(listOf(VIEW_RECIPES), true),
VIEW_MATERIAL(listOf(VIEW_CATALOG), true),
VIEW_MATERIAL_TYPE(listOf(VIEW_CATALOG), true),
VIEW_COMPANY(listOf(VIEW_CATALOG), true),
VIEW(listOf(VIEW_RECIPES, VIEW_CATALOG), true),
VIEW_EMPLOYEE(listOf(VIEW_USERS), true),
VIEW_EMPLOYEE_GROUP(listOf(VIEW_USERS), true),
EDIT_RECIPE(listOf(EDIT_RECIPES), true),
EDIT_MATERIAL(listOf(EDIT_MATERIALS), true),
EDIT_MATERIAL_TYPE(listOf(EDIT_MATERIAL_TYPES), true),
EDIT_COMPANY(listOf(EDIT_COMPANIES), true),
EDIT(listOf(EDIT_RECIPES, EDIT_CATALOG), true),
EDIT_EMPLOYEE(listOf(EDIT_USERS), true),
EDIT_EMPLOYEE_PASSWORD(listOf(EDIT_USERS), true),
EDIT_EMPLOYEE_GROUP(listOf(EDIT_USERS), true),
REMOVE_RECIPE(listOf(REMOVE_RECIPES), true),
REMOVE_MATERIAL(listOf(REMOVE_MATERIALS), true),
REMOVE_MATERIAL_TYPE(listOf(REMOVE_MATERIAL_TYPES), true),
REMOVE_COMPANY(listOf(REMOVE_COMPANIES), true),
REMOVE(listOf(REMOVE_RECIPES, REMOVE_CATALOG), true),
REMOVE_EMPLOYEE(listOf(REMOVE_USERS), true),
REMOVE_EMPLOYEE_GROUP(listOf(REMOVE_USERS), true),
SET_BROWSER_DEFAULT_GROUP(listOf(VIEW_USERS), true),
;
operator fun contains(permission: EmployeePermission): Boolean {
return permission == this || impliedPermissions.any { permission in it }
}
}
fun EmployeePermission.flat(): Iterable<EmployeePermission> {
return mutableSetOf(this).apply {
impliedPermissions.forEach {
addAll(it.flat())
}
}
}
/** Converts the given [EmployeePermission] to a [GrantedAuthority]. */
fun EmployeePermission.toAuthority(): GrantedAuthority {
return SimpleGrantedAuthority(name)
}

View File

@ -1,5 +1,8 @@
package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
import org.springframework.web.multipart.MultipartFile
@ -21,101 +24,148 @@ private const val MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE = "La quantité do
@Entity
@Table(name = "material")
data class Material(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@Column(unique = true)
override var name: String,
@Column(unique = true)
override var name: String,
@Column(name = "inventory_quantity")
var inventoryQuantity: Float,
@Column(name = "inventory_quantity")
var inventoryQuantity: Float,
@Column(name = "mix_type")
val isMixType: Boolean,
@Column(name = "mix_type")
val isMixType: Boolean,
@ManyToOne
@JoinColumn(name = "material_type_id")
var materialType: MaterialType?
@ManyToOne
@JoinColumn(name = "material_type_id")
var materialType: MaterialType?
) : NamedModel
open class MaterialSaveDto(
@field:NotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
val name: String,
@field:NotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
val name: String,
@field:NotNull(message = MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
val inventoryQuantity: Float,
@field:NotNull(message = MATERIAL_INVENTORY_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
val inventoryQuantity: Float,
@field:NotNull(message = MATERIAL_TYPE_NULL_MESSAGE)
val materialTypeId: Long,
@field:NotNull(message = MATERIAL_TYPE_NULL_MESSAGE)
val materialTypeId: Long,
val simdutFile: MultipartFile? = null
val simdutFile: MultipartFile? = null
) : EntityDto<Material>
open class MaterialUpdateDto(
@field:NotNull(message = MATERIAL_ID_NULL_MESSAGE)
val id: Long,
@field:NotNull(message = MATERIAL_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
val name: String?,
@field:NullOrNotBlank(message = MATERIAL_NAME_NULL_MESSAGE)
val name: String?,
@field:NullOrSize(min = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
val inventoryQuantity: Float?,
@field:NullOrSize(min = 0, message = MATERIAL_INVENTORY_QUANTITY_NEGATIVE_MESSAGE)
val inventoryQuantity: Float?,
val materialTypeId: Long?,
val materialTypeId: Long?,
val simdutFile: MultipartFile? = null
val simdutFile: MultipartFile? = null
) : EntityDto<Material>
data class MaterialQuantityDto(
@field:NotNull(message = MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE)
val material: Long,
@field:NotNull(message = MATERIAL_QUANTITY_MATERIAL_NULL_MESSAGE)
val material: Long,
@field:NotNull(message = MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE)
val quantity: Float
@field:NotNull(message = MATERIAL_QUANTITY_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MATERIAL_QUANTITY_QUANTITY_NEGATIVE_MESSAGE)
val quantity: Float
)
// === DSL ===
fun material(
id: Long? = null,
name: String = "name",
inventoryQuantity: Float = 0f,
isMixType: Boolean = false,
materialType: MaterialType? = materialType(),
op: Material.() -> Unit = {}
id: Long? = null,
name: String = "name",
inventoryQuantity: Float = 0f,
isMixType: Boolean = false,
materialType: MaterialType? = materialType(),
op: Material.() -> Unit = {}
) = Material(id, name, inventoryQuantity, isMixType, materialType).apply(op)
fun material(
material: Material,
id: Long? = null,
name: String? = null,
material: Material,
id: Long? = null,
name: String? = null,
) = Material(
id ?: material.id, name
?: material.name, material.inventoryQuantity, material.isMixType, material.materialType
id ?: material.id, name
?: material.name, material.inventoryQuantity, material.isMixType, material.materialType
)
fun materialSaveDto(
name: String = "name",
inventoryQuantity: Float = 0f,
materialTypeId: Long = 0L,
simdutFile: MultipartFile? = null,
op: MaterialSaveDto.() -> Unit = {}
name: String = "name",
inventoryQuantity: Float = 0f,
materialTypeId: Long = 0L,
simdutFile: MultipartFile? = null,
op: MaterialSaveDto.() -> Unit = {}
) = MaterialSaveDto(name, inventoryQuantity, materialTypeId, simdutFile).apply(op)
fun materialUpdateDto(
id: Long = 0L,
name: String? = "name",
inventoryQuantity: Float? = 0f,
materialTypeId: Long? = 0L,
simdutFile: MultipartFile? = null,
op: MaterialUpdateDto.() -> Unit = {}
id: Long = 0L,
name: String? = "name",
inventoryQuantity: Float? = 0f,
materialTypeId: Long? = 0L,
simdutFile: MultipartFile? = null,
op: MaterialUpdateDto.() -> Unit = {}
) = MaterialUpdateDto(id, name, inventoryQuantity, materialTypeId, simdutFile).apply(op)
fun materialQuantityDto(
materialId: Long,
quantity: Float,
op: MaterialQuantityDto.() -> Unit = {}
materialId: Long,
quantity: Float,
op: MaterialQuantityDto.() -> Unit = {}
) = MaterialQuantityDto(materialId, quantity).apply(op)
// ==== Exceptions ====
private const val MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Material not found"
private const val MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Material already exists"
private const val MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material"
private const val MATERIAL_EXCEPTION_ERROR_CODE = "material"
fun materialIdNotFoundException(id: Long) =
NotFoundException(
MATERIAL_EXCEPTION_ERROR_CODE,
MATERIAL_NOT_FOUND_EXCEPTION_TITLE,
"A material with the id $id could not be found",
id
)
fun materialNameNotFoundException(name: String) =
NotFoundException(
MATERIAL_EXCEPTION_ERROR_CODE,
MATERIAL_NOT_FOUND_EXCEPTION_TITLE,
"A material with the name $name could not be found",
name,
"name"
)
fun materialIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
MATERIAL_EXCEPTION_ERROR_CODE,
MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
"A material with the id $id already exists",
id
)
fun materialNameAlreadyExistsException(name: String) =
AlreadyExistsException(
MATERIAL_EXCEPTION_ERROR_CODE,
MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
"A material with the name $name already exists",
name,
"name"
)
fun cannotDeleteMaterialException(material: Material) =
CannotDeleteException(
MATERIAL_EXCEPTION_ERROR_CODE,
MATERIAL_CANNOT_DELETE_EXCEPTION_TITLE,
"Cannot delete the material ${material.name} because one or more recipes depends on it"
)

View File

@ -1,5 +1,8 @@
package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
import org.hibernate.annotations.ColumnDefault
@ -16,88 +19,144 @@ private const val MATERIAL_TYPE_PREFIX_SIZE_MESSAGE = "Le préfixe doit faire ex
@Entity
@Table(name = "material_type")
data class MaterialType(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long? = null,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long? = null,
@Column(unique = true)
override val name: String = "",
@Column(unique = true)
override val name: String = "",
@Column(unique = true)
val prefix: String = "",
@Column(unique = true)
val prefix: String = "",
@Column(name = "use_percentages")
@ColumnDefault("false")
val usePercentages: Boolean = false,
@Column(name = "use_percentages")
@ColumnDefault("false")
val usePercentages: Boolean = false,
@Column(name = "system_type")
@ColumnDefault("false")
val systemType: Boolean = false
@Column(name = "system_type")
@ColumnDefault("false")
val systemType: Boolean = false
) : NamedModel
open class MaterialTypeSaveDto(
@field:NotBlank(message = MATERIAL_TYPE_NAME_NULL_MESSAGE)
val name: String,
@field:NotBlank(message = MATERIAL_TYPE_NAME_NULL_MESSAGE)
val name: String,
@field:NotBlank(message = MATERIAL_TYPE_PREFIX_NULL_MESSAGE)
@field:Size(min = 3, max = 3, message = MATERIAL_TYPE_PREFIX_SIZE_MESSAGE)
val prefix: String,
@field:NotBlank(message = MATERIAL_TYPE_PREFIX_NULL_MESSAGE)
@field:Size(min = 3, max = 3, message = MATERIAL_TYPE_PREFIX_SIZE_MESSAGE)
val prefix: String,
val usePercentages: Boolean = false
val usePercentages: Boolean = false
) : EntityDto<MaterialType> {
override fun toEntity(): MaterialType =
MaterialType(null, name, prefix, usePercentages)
MaterialType(null, name, prefix, usePercentages)
}
open class MaterialTypeUpdateDto(
@field:NotNull(message = MATERIAL_TYPE_ID_NULL_MESSAGE)
val id: Long,
@field:NotNull(message = MATERIAL_TYPE_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = MATERIAL_TYPE_NAME_NULL_MESSAGE)
val name: String?,
@field:NullOrNotBlank(message = MATERIAL_TYPE_NAME_NULL_MESSAGE)
val name: String?,
@field:NullOrSize(min = 3, max = 3, message = MATERIAL_TYPE_PREFIX_NULL_MESSAGE)
val prefix: String?
@field:NullOrSize(min = 3, max = 3, message = MATERIAL_TYPE_PREFIX_NULL_MESSAGE)
val prefix: String?
) : EntityDto<MaterialType> {
override fun toEntity(): MaterialType =
MaterialType(id, name ?: "", prefix ?: "")
MaterialType(id, name ?: "", prefix ?: "")
}
// ==== DSL ====
fun materialType(
id: Long? = null,
name: String = "name",
prefix: String = "PRE",
usePercentages: Boolean = false,
systemType: Boolean = false,
op: MaterialType.() -> Unit = {}
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
materialType: MaterialType,
newId: Long? = null,
newName: String? = null,
newSystemType: Boolean? = null
) = with(materialType) {
MaterialType(
newId ?: id,
newName ?: name,
prefix,
usePercentages,
newSystemType ?: systemType
newId ?: id,
newName ?: name,
prefix,
usePercentages,
newSystemType ?: systemType
)
}
fun materialTypeSaveDto(
name: String = "name",
prefix: String = "PRE",
usePercentages: Boolean = false,
op: MaterialTypeSaveDto.() -> Unit = {}
name: String = "name",
prefix: String = "PRE",
usePercentages: Boolean = false,
op: MaterialTypeSaveDto.() -> Unit = {}
) = MaterialTypeSaveDto(name, prefix, usePercentages).apply(op)
fun materialTypeUpdateDto(
id: Long = 0L,
name: String? = null,
prefix: String? = null,
op: MaterialTypeUpdateDto.() -> Unit = {}
id: Long = 0L,
name: String? = null,
prefix: String? = null,
op: MaterialTypeUpdateDto.() -> Unit = {}
) = MaterialTypeUpdateDto(id, name, prefix).apply(op)
// ==== Exceptions ====
private const val MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE = "Material type not found"
private const val MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Material type already exists"
private const val MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete material type"
private const val MATERIAL_TYPE_EXCEPTION_ERROR_CODE = "materialtype"
fun materialTypeIdNotFoundException(id: Long) =
NotFoundException(
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE,
"A material type with the id $id could not be found",
id
)
fun materialTypeNameNotFoundException(name: String) =
NotFoundException(
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
MATERIAL_TYPE_NOT_FOUND_EXCEPTION_TITLE,
"A material type with the name $name could not be found",
name,
"name"
)
fun materialTypeIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE,
"A material type with the id $id already exists",
id
)
fun materialTypeNameAlreadyExistsException(name: String) =
AlreadyExistsException(
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE,
"A material type with the name $name already exists",
name,
"name"
)
fun materialTypePrefixAlreadyExistsException(prefix: String) =
AlreadyExistsException(
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
MATERIAL_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE,
"A material type with the prefix $prefix already exists",
prefix,
"prefix"
)
fun cannotDeleteMaterialTypeException(materialType: MaterialType) =
CannotDeleteException(
MATERIAL_TYPE_EXCEPTION_ERROR_CODE,
MATERIAL_TYPE_CANNOT_DELETE_EXCEPTION_TITLE,
"Cannot delete material type ${materialType.name} because one or more materials depends on it"
)

View File

@ -1,6 +1,9 @@
package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import javax.persistence.*
import javax.validation.constraints.Min
@ -19,105 +22,134 @@ private const val MIX_DEDUCT_RATION_NEGATIVE_MESSAGE = "Le ratio doit être éga
@Entity
@Table(name = "mix")
data class Mix(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
var location: String?,
var location: String?,
@JsonIgnore
@ManyToOne
@JoinColumn(name = "recipe_id")
val recipe: Recipe,
@JsonIgnore
@ManyToOne
@JoinColumn(name = "recipe_id")
val recipe: Recipe,
@ManyToOne
@JoinColumn(name = "mix_type_id")
var mixType: MixType,
@ManyToOne
@JoinColumn(name = "mix_type_id")
var mixType: MixType,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "mix_id")
var mixMaterials: MutableSet<MixMaterial>,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "mix_id")
var mixMaterials: MutableSet<MixMaterial>,
) : Model
open class MixSaveDto(
@field:NotBlank(message = MIX_NAME_NULL_MESSAGE)
val name: String,
@field:NotBlank(message = MIX_NAME_NULL_MESSAGE)
val name: String,
@field:NotNull(message = MIX_RECIPE_NULL_MESSAGE)
val recipeId: Long,
@field:NotNull(message = MIX_RECIPE_NULL_MESSAGE)
val recipeId: Long,
@field:NotNull(message = MIX_MATERIAL_TYPE_NULL_MESSAGE)
val materialTypeId: Long,
@field:NotNull(message = MIX_MATERIAL_TYPE_NULL_MESSAGE)
val materialTypeId: Long,
val mixMaterials: Set<MixMaterialDto>?
val mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix> {
override fun toEntity(): Mix = throw UnsupportedOperationException()
}
open class MixUpdateDto(
@field:NotNull(message = MIX_ID_NULL_MESSAGE)
val id: Long,
@field:NotNull(message = MIX_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = MIX_NAME_NULL_MESSAGE)
val name: String?,
@field:NullOrNotBlank(message = MIX_NAME_NULL_MESSAGE)
val name: String?,
val materialTypeId: Long?,
val materialTypeId: Long?,
var mixMaterials: Set<MixMaterialDto>?
var mixMaterials: Set<MixMaterialDto>?
) : EntityDto<Mix> {
override fun toEntity(): Mix = throw UnsupportedOperationException()
}
data class MixDeductDto(
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
val id: Long,
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
val id: Long,
@field:NotNull(message = MIX_DEDUCT_RATIO_NULL_MESSAGE)
@field:Min(value = 0, message = MIX_DEDUCT_RATION_NEGATIVE_MESSAGE)
val ratio: Float
@field:NotNull(message = MIX_DEDUCT_RATIO_NULL_MESSAGE)
@field:Min(value = 0, message = MIX_DEDUCT_RATION_NEGATIVE_MESSAGE)
val ratio: Float
)
data class MixLocationDto(
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
val mixId: Long,
@field:NotNull(message = MIX_DEDUCT_MIX_ID_NULL_MESSAGE)
val mixId: Long,
val location: String?
val location: String?
)
// ==== DSL ====
fun mix(
id: Long? = null,
location: String? = "location",
recipe: Recipe = recipe(),
mixType: MixType = mixType(),
mixMaterials: MutableSet<MixMaterial> = mutableSetOf(),
op: Mix.() -> Unit = {}
id: Long? = null,
location: String? = "location",
recipe: Recipe = recipe(),
mixType: MixType = mixType(),
mixMaterials: MutableSet<MixMaterial> = mutableSetOf(),
op: Mix.() -> Unit = {}
) = Mix(id, location, recipe, mixType, mixMaterials).apply(op)
fun mixSaveDto(
name: String = "name",
recipeId: Long = 0L,
materialTypeId: Long = 0L,
mixMaterials: Set<MixMaterialDto>? = setOf(),
op: MixSaveDto.() -> Unit = {}
name: String = "name",
recipeId: Long = 0L,
materialTypeId: Long = 0L,
mixMaterials: Set<MixMaterialDto>? = setOf(),
op: MixSaveDto.() -> Unit = {}
) = MixSaveDto(name, recipeId, materialTypeId, mixMaterials).apply(op)
fun mixUpdateDto(
id: Long = 0L,
name: String? = "name",
materialTypeId: Long? = 0L,
mixMaterials: Set<MixMaterialDto>? = setOf(),
op: MixUpdateDto.() -> Unit = {}
id: Long = 0L,
name: String? = "name",
materialTypeId: Long? = 0L,
mixMaterials: Set<MixMaterialDto>? = setOf(),
op: MixUpdateDto.() -> Unit = {}
) = MixUpdateDto(id, name, materialTypeId, mixMaterials).apply(op)
fun mixRatio(
id: Long = 0L,
ratio: Float = 1f,
op: MixDeductDto.() -> Unit = {}
id: Long = 0L,
ratio: Float = 1f,
op: MixDeductDto.() -> Unit = {}
) = MixDeductDto(id, ratio).apply(op)
fun mixLocationDto(
mixId: Long = 0L,
location: String? = "location",
op: MixLocationDto.() -> Unit = {}
mixId: Long = 0L,
location: String? = "location",
op: MixLocationDto.() -> Unit = {}
) = MixLocationDto(mixId, location).apply(op)
// ==== Exceptions ====
private const val MIX_NOT_FOUND_EXCEPTION_TITLE = "Mix not found"
private const val MIX_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix already exists"
private const val MIX_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete mix"
private const val MIX_EXCEPTION_ERROR_CODE = "mix"
fun mixIdNotFoundException(id: Long) =
NotFoundException(
MIX_EXCEPTION_ERROR_CODE,
MIX_NOT_FOUND_EXCEPTION_TITLE,
"A mix with the id $id could not be found",
id
)
fun mixIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
MIX_EXCEPTION_ERROR_CODE,
MIX_ALREADY_EXISTS_EXCEPTION_TITLE,
"A mix with the id $id already exists",
id
)
fun cannotDeleteMixException(mix: Mix) =
CannotDeleteException(
MIX_EXCEPTION_ERROR_CODE,
MIX_CANNOT_DELETE_EXCEPTION_TITLE,
"Cannot delete the mix ${mix.mixType.name} because one or more mixes depends on it"
)

View File

@ -1,5 +1,7 @@
package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import javax.persistence.*
import javax.validation.constraints.Min
import javax.validation.constraints.NotNull
@ -11,42 +13,63 @@ private const val MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE = "La quantité ne
@Entity
@Table(name = "mix_material")
data class MixMaterial(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@ManyToOne
@JoinColumn(name = "material_id")
val material: Material,
@ManyToOne
@JoinColumn(name = "material_id")
val material: Material,
var quantity: Float,
var quantity: Float,
var position: Int
var position: Int
) : Model
data class MixMaterialDto(
@field:NotNull(message = MIX_MATERIAL_DTO_MATERIAL_ID_NULL_MESSAGE)
val materialId: Long,
@field:NotNull(message = MIX_MATERIAL_DTO_MATERIAL_ID_NULL_MESSAGE)
val materialId: Long,
@field:NotNull(message = MIX_MATERIAL_DTO_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE)
val quantity: Float,
@field:NotNull(message = MIX_MATERIAL_DTO_QUANTITY_NULL_MESSAGE)
@field:Min(value = 0, message = MIX_MATERIAL_DTO_QUANTITY_NEGATIVE_MESSAGE)
val quantity: Float,
val position: Int
val position: Int
)
// ==== DSL ====
fun mixMaterial(
id: Long? = null,
material: Material = material(),
quantity: Float = 0f,
position: Int = 0,
op: MixMaterial.() -> Unit = {}
id: Long? = null,
material: Material = material(),
quantity: Float = 0f,
position: Int = 0,
op: MixMaterial.() -> Unit = {}
) = MixMaterial(id, material, quantity, position).apply(op)
fun mixMaterialDto(
materialId: Long = 0L,
quantity: Float = 0f,
position: Int = 0,
op: MixMaterialDto.() -> Unit = {}
materialId: Long = 0L,
quantity: Float = 0f,
position: Int = 0,
op: MixMaterialDto.() -> Unit = {}
) = MixMaterialDto(materialId, quantity, position).apply(op)
// ==== Exceptions ====
private const val MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE = "Mix material not found"
private const val MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix material already exists"
private const val MIX_MATERIAL_EXCEPTION_ERROR_CODE = "mixmaterial"
fun mixMaterialIdNotFoundException(id: Long) =
NotFoundException(
MIX_MATERIAL_EXCEPTION_ERROR_CODE,
MIX_MATERIAL_NOT_FOUND_EXCEPTION_TITLE,
"A mix material with the id $id could not be found",
id
)
fun mixMaterialIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
MIX_MATERIAL_EXCEPTION_ERROR_CODE,
MIX_MATERIAL_ALREADY_EXISTS_EXCEPTION_TITLE,
"A mix material with the id $id already exists",
id
)

View File

@ -1,38 +1,100 @@
package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import org.springframework.http.HttpStatus
import javax.persistence.*
const val IDENTIFIER_MATERIAL_NAME = "material"
@Entity
@Table(name = "mix_type")
data class MixType(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@Column(unique = true)
override var name: String,
@Column(unique = true)
override var name: String,
@OneToOne(cascade = [CascadeType.ALL])
@JoinColumn(name = "material_id")
var material: Material
@OneToOne(cascade = [CascadeType.ALL])
@JoinColumn(name = "material_id")
var material: Material
) : NamedModel
// ==== DSL ====
fun mixType(
id: Long? = null,
name: String = "name",
material: Material = material(),
op: MixType.() -> Unit = {}
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 = {}
name: String = "name",
materialType: MaterialType = materialType(),
op: MixType.() -> Unit = {}
) = mixType(
id = null,
name,
material = material(name = name, inventoryQuantity = 0f, isMixType = true, materialType = materialType)
id = null,
name,
material = material(name = name, inventoryQuantity = 0f, isMixType = true, materialType = materialType)
).apply(op)
// ==== Exceptions ====
private const val MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE = "Mix type not found"
private const val MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Mix type already exists"
private const val MIX_TYPE_CANNOT_DELETE_EXCEPTION_TITLE = "Cannot delete mix type"
private const val MIX_TYPE_EXCEPTION_ERROR_CODE = "mixtype"
class MixTypeNameAndMaterialTypeNotFoundException(name: String, materialType: MaterialType) :
RestException(
"notfound-mixtype-namematerialtype",
MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE,
HttpStatus.NOT_FOUND,
"A mix type with the name $name and material type ${materialType.name} could not be found",
mapOf(
"name" to name,
"materialType" to materialType.name
)
)
fun mixTypeIdNotFoundException(id: Long) =
NotFoundException(
MIX_TYPE_EXCEPTION_ERROR_CODE,
MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE,
"A mix type with the id $id could not be found",
id
)
fun mixTypeIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
MIX_TYPE_EXCEPTION_ERROR_CODE,
MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE,
"A mix type with the id $id already exists",
id
)
fun mixTypeNameNotFoundException(name: String) =
NotFoundException(
MIX_TYPE_EXCEPTION_ERROR_CODE,
MIX_TYPE_NOT_FOUND_EXCEPTION_TITLE,
"A mix type with the name $name could not be found",
name,
"name"
)
fun mixTypeNameAlreadyExistsException(name: String) =
AlreadyExistsException(
MIX_TYPE_EXCEPTION_ERROR_CODE,
MIX_TYPE_ALREADY_EXISTS_EXCEPTION_TITLE,
"A mix type with the name $name already exists",
name,
"name"
)
fun cannotDeleteMixTypeException(mixType: MixType) =
CannotDeleteException(
MIX_TYPE_EXCEPTION_ERROR_CODE,
MIX_TYPE_CANNOT_DELETE_EXCEPTION_TITLE,
"Cannot delete the mix type ${mixType.name} because one or more mixes depends on it"
)

View File

@ -1,8 +1,12 @@
package dev.fyloz.colorrecipesexplorer.model
import com.fasterxml.jackson.annotation.JsonIgnore
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrNotBlank
import dev.fyloz.colorrecipesexplorer.model.validation.NullOrSize
import org.springframework.http.HttpStatus
import java.time.LocalDate
import javax.persistence.*
import javax.validation.constraints.*
@ -24,39 +28,39 @@ private const val NOTE_GROUP_ID_NULL_MESSAGE = "Un identifiant de groupe est req
@Entity
@Table(name = "recipe")
data class Recipe(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
/** The name of the recipe. It is not unique in the entire system, but is unique in the scope of a [Company]. */
val name: String,
/** The name of the recipe. It is not unique in the entire system, but is unique in the scope of a [Company]. */
val name: String,
val description: String,
val description: String,
/** The color produced by the recipe. The string should be formatted as a hexadecimal color without the sharp (#). */
val color: String,
/** The color produced by the recipe. The string should be formatted as a hexadecimal color without the sharp (#). */
val color: String,
/** The gloss of the color in percents. (0-100) */
val gloss: Byte,
/** The gloss of the color in percents. (0-100) */
val gloss: Byte,
val sample: Int?,
val sample: Int?,
@Column(name = "approbation_date")
val approbationDate: LocalDate?,
@Column(name = "approbation_date")
val approbationDate: LocalDate?,
/** A remark given by the creator of the recipe. */
val remark: String,
/** A remark given by the creator of the recipe. */
val remark: String,
@ManyToOne
@JoinColumn(name = "company_id")
val company: Company,
@ManyToOne
@JoinColumn(name = "company_id")
val company: Company,
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
val mixes: MutableList<Mix>,
@OneToMany(cascade = [CascadeType.ALL], mappedBy = "recipe")
val mixes: MutableList<Mix>,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "recipe_id")
val groupsInformation: Set<RecipeGroupInformation>
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "recipe_id")
val groupsInformation: Set<RecipeGroupInformation>
) : Model {
/** The mix types contained in this recipe. */
val mixTypes: Collection<MixType>
@ -64,185 +68,218 @@ data class Recipe(
get() = mixes.map { it.mixType }
fun groupInformationForGroup(groupId: Long) =
groupsInformation.firstOrNull { it.group.id == groupId }
groupsInformation.firstOrNull { it.group.id == groupId }
}
open class RecipeSaveDto(
@field:NotBlank(message = RECIPE_NAME_NULL_MESSAGE)
val name: String,
@field:NotBlank(message = RECIPE_NAME_NULL_MESSAGE)
val name: String,
@field:NotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
val description: String,
@field:NotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
val description: String,
@field:NotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
val color: String,
@field:NotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
val color: String,
@field:NotNull(message = RECIPE_GLOSS_NULL_MESSAGE)
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
val gloss: Byte,
@field:NotNull(message = RECIPE_GLOSS_NULL_MESSAGE)
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
val gloss: Byte,
@field:Min(value = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
val sample: Int?,
@field:Min(value = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
val sample: Int?,
val approbationDate: LocalDate?,
val approbationDate: LocalDate?,
val remark: String?,
val remark: String?,
@field:Min(value = 0, message = RECIPE_COMPANY_NULL_MESSAGE)
val companyId: Long = -1L,
@field:Min(value = 0, message = RECIPE_COMPANY_NULL_MESSAGE)
val companyId: Long = -1L,
) : EntityDto<Recipe> {
override fun toEntity(): Recipe = recipe(
name = name,
description = description,
sample = sample,
approbationDate = approbationDate,
remark = remark ?: "",
company = company(id = companyId)
name = name,
description = description,
sample = sample,
approbationDate = approbationDate,
remark = remark ?: "",
company = company(id = companyId)
)
}
open class RecipeUpdateDto(
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
val id: Long,
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
val id: Long,
@field:NullOrNotBlank(message = RECIPE_NAME_NULL_MESSAGE)
val name: String?,
@field:NullOrNotBlank(message = RECIPE_NAME_NULL_MESSAGE)
val name: String?,
@field:NullOrNotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
val description: String?,
@field:NullOrNotBlank(message = RECIPE_DESCRIPTION_NULL_MESSAGE)
val description: String?,
@field:NullOrNotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
val color: String?,
@field:NullOrNotBlank(message = RECIPE_COLOR_NULL_MESSAGE)
@field:Pattern(regexp = "^#([0-9a-f]{6})$")
val color: String?,
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
val gloss: Byte?,
@field:Min(value = 0, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
@field:Max(value = 100, message = RECIPE_GLOSS_OUTSIDE_RANGE_MESSAGE)
val gloss: Byte?,
@field:NullOrSize(min = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
val sample: Int?,
@field:NullOrSize(min = 0, message = RECIPE_SAMPLE_TOO_SMALL_MESSAGE)
val sample: Int?,
val approbationDate: LocalDate?,
val approbationDate: LocalDate?,
val remark: String?,
val remark: String?,
val steps: Set<RecipeStepsDto>?
val steps: Set<RecipeStepsDto>?
) : EntityDto<Recipe>
@Entity
@Table(name = "recipe_group_information")
data class RecipeGroupInformation(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long?,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long?,
@ManyToOne
@JoinColumn(name = "group_id")
val group: EmployeeGroup,
@ManyToOne
@JoinColumn(name = "group_id")
val group: EmployeeGroup,
var note: String?,
var note: String?,
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "recipe_group_information_id")
var steps: MutableSet<RecipeStep>?
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(name = "recipe_group_information_id")
var steps: MutableSet<RecipeStep>?
)
data class RecipeStepsDto(
@field:NotNull(message = RECIPE_STEPS_DTO_GROUP_ID_NULL_MESSAGE)
val groupId: Long,
@field:NotNull(message = RECIPE_STEPS_DTO_GROUP_ID_NULL_MESSAGE)
val groupId: Long,
@field:NotNull(message = RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE)
val steps: Set<RecipeStep>
@field:NotNull(message = RECIPE_STEPS_DTO_MESSAGES_NULL_MESSAGE)
val steps: Set<RecipeStep>
)
data class RecipePublicDataDto(
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
val recipeId: Long,
@field:NotNull(message = RECIPE_ID_NULL_MESSAGE)
val recipeId: Long,
val notes: Set<NoteDto>?,
val notes: Set<NoteDto>?,
val mixesLocation: Set<MixLocationDto>?
val mixesLocation: Set<MixLocationDto>?
)
data class NoteDto(
@field:NotNull(message = NOTE_GROUP_ID_NULL_MESSAGE)
val groupId: Long,
@field:NotNull(message = NOTE_GROUP_ID_NULL_MESSAGE)
val groupId: Long,
val content: String?
val content: String?
)
// ==== DSL ====
fun recipe(
id: Long? = null,
name: String = "name",
description: String = "description",
color: String = "ffffff",
gloss: Byte = 0,
sample: Int? = -1,
approbationDate: LocalDate? = LocalDate.MIN,
remark: String = "remark",
company: Company = company(),
mixes: MutableList<Mix> = mutableListOf(),
groupsInformation: Set<RecipeGroupInformation> = setOf(),
op: Recipe.() -> Unit = {}
id: Long? = null,
name: String = "name",
description: String = "description",
color: String = "ffffff",
gloss: Byte = 0,
sample: Int? = -1,
approbationDate: LocalDate? = LocalDate.MIN,
remark: String = "remark",
company: Company = company(),
mixes: MutableList<Mix> = mutableListOf(),
groupsInformation: Set<RecipeGroupInformation> = setOf(),
op: Recipe.() -> Unit = {}
) = Recipe(
id,
name,
description,
color,
gloss,
sample,
approbationDate,
remark,
company,
mixes,
groupsInformation
id,
name,
description,
color,
gloss,
sample,
approbationDate,
remark,
company,
mixes,
groupsInformation
).apply(op)
fun recipeSaveDto(
name: String = "name",
description: String = "description",
color: String = "ffffff",
gloss: Byte = 0,
sample: Int? = -1,
approbationDate: LocalDate? = LocalDate.MIN,
remark: String = "remark",
companyId: Long = 0L,
op: RecipeSaveDto.() -> Unit = {}
name: String = "name",
description: String = "description",
color: String = "ffffff",
gloss: Byte = 0,
sample: Int? = -1,
approbationDate: LocalDate? = LocalDate.MIN,
remark: String = "remark",
companyId: Long = 0L,
op: RecipeSaveDto.() -> Unit = {}
) = RecipeSaveDto(name, description, color, gloss, sample, approbationDate, remark, companyId).apply(op)
fun recipeUpdateDto(
id: Long = 0L,
name: String? = "name",
description: String? = "description",
color: String? = "ffffff",
gloss: Byte? = 0,
sample: Int? = -1,
approbationDate: LocalDate? = LocalDate.MIN,
remark: String? = "remark",
steps: Set<RecipeStepsDto>? = setOf(),
op: RecipeUpdateDto.() -> Unit = {}
id: Long = 0L,
name: String? = "name",
description: String? = "description",
color: String? = "ffffff",
gloss: Byte? = 0,
sample: Int? = -1,
approbationDate: LocalDate? = LocalDate.MIN,
remark: String? = "remark",
steps: Set<RecipeStepsDto>? = setOf(),
op: RecipeUpdateDto.() -> Unit = {}
) = RecipeUpdateDto(id, name, description, color, gloss, sample, approbationDate, remark, steps).apply(op)
fun recipeGroupInformation(
id: Long? = null,
group: EmployeeGroup = employeeGroup(),
note: String? = null,
steps: MutableSet<RecipeStep>? = mutableSetOf(),
op: RecipeGroupInformation.() -> Unit = {}
id: Long? = null,
group: EmployeeGroup = employeeGroup(),
note: String? = null,
steps: MutableSet<RecipeStep>? = mutableSetOf(),
op: RecipeGroupInformation.() -> Unit = {}
) = RecipeGroupInformation(id, group, note, steps).apply(op)
fun recipePublicDataDto(
recipeId: Long = 0L,
notes: Set<NoteDto>? = null,
mixesLocation: Set<MixLocationDto>? = null,
op: RecipePublicDataDto.() -> Unit = {}
recipeId: Long = 0L,
notes: Set<NoteDto>? = null,
mixesLocation: Set<MixLocationDto>? = null,
op: RecipePublicDataDto.() -> Unit = {}
) = RecipePublicDataDto(recipeId, notes, mixesLocation).apply(op)
fun noteDto(
groupId: Long = 0L,
content: String? = "note",
op: NoteDto.() -> Unit = {}
groupId: Long = 0L,
content: String? = "note",
op: NoteDto.() -> Unit = {}
) = NoteDto(groupId, content).apply(op)
// ==== Exceptions ====
private const val RECIPE_NOT_FOUND_EXCEPTION_TITLE = "Recipe not found"
private const val RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe already exists"
private const val RECIPE_EXCEPTION_ERROR_CODE = "recipe"
class RecipeImageNotFoundException(id: Long, recipe: Recipe) :
RestException(
"notfound-recipeimage-id",
"Recipe image not found",
HttpStatus.NOT_FOUND,
"A recipe image with the id $id could no be found for the recipe ${recipe.name}",
mapOf(
"id" to id,
"recipe" to recipe.name
)
)
fun recipeIdNotFoundException(id: Long) =
NotFoundException(
RECIPE_EXCEPTION_ERROR_CODE,
RECIPE_NOT_FOUND_EXCEPTION_TITLE,
"A recipe with the id $id could not be found",
id
)
fun recipeIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
RECIPE_EXCEPTION_ERROR_CODE,
RECIPE_ALREADY_EXISTS_EXCEPTION_TITLE,
"A recipe with the id $id already exists",
id
)

View File

@ -1,23 +1,46 @@
package dev.fyloz.colorrecipesexplorer.model
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import javax.persistence.*
@Entity
@Table(name = "recipe_step")
data class RecipeStep(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
override val id: Long?,
val position: Int,
val position: Int,
val message: String
val message: String
) : Model
// ==== DSL ====
fun recipeStep(
id: Long? = null,
position: Int = 0,
message: String = "message",
op: RecipeStep.() -> Unit = {}
id: Long? = null,
position: Int = 0,
message: String = "message",
op: RecipeStep.() -> Unit = {}
) = RecipeStep(id, position, message).apply(op)
// ==== Exceptions ====
private const val RECIPE_STEP_NOT_FOUND_EXCEPTION_TITLE = "Recipe step not found"
private const val RECIPE_STEP_ALREADY_EXISTS_EXCEPTION_TITLE = "Recipe step already exists"
private const val RECIPE_STEP_EXCEPTION_ERROR_CODE = "recipestep"
fun recipeStepIdNotFoundException(id: Long) =
NotFoundException(
RECIPE_STEP_EXCEPTION_ERROR_CODE,
RECIPE_STEP_NOT_FOUND_EXCEPTION_TITLE,
"A recipe step with the id $id could not be found",
id
)
fun recipeStepIdAlreadyExistsException(id: Long) =
AlreadyExistsException(
RECIPE_STEP_EXCEPTION_ERROR_CODE,
RECIPE_STEP_ALREADY_EXISTS_EXCEPTION_TITLE,
"A recipe step with the id $id already exists",
id
)

View File

@ -14,9 +14,9 @@ private const val MESSAGE = "must be null or not blank"
@MustBeDocumented
@Constraint(validatedBy = [NullOrNotBlankValidator::class])
annotation class NullOrNotBlank(
val message: String = MESSAGE,
val groups: Array<KClass<*>> = [],
@Suppress("unused") val payload: Array<KClass<out Payload>> = []
val message: String = MESSAGE,
val groups: Array<KClass<*>> = [],
@Suppress("unused") val payload: Array<KClass<out Payload>> = []
)
class NullOrNotBlankValidator : ConstraintValidator<NullOrNotBlank, String> {

View File

@ -14,11 +14,11 @@ private const val MESSAGE = "must be null or have a correct length"
@MustBeDocumented
@Constraint(validatedBy = [NullOrSizeValidator::class])
annotation class NullOrSize(
val min: Long = MIN_SIZE,
val max: Long = MAX_SIZE,
val message: String = MESSAGE,
val groups: Array<KClass<*>> = [],
@Suppress("unused") val payload: Array<KClass<out Payload>> = []
val min: Long = MIN_SIZE,
val max: Long = MAX_SIZE,
val message: String = MESSAGE,
val groups: Array<KClass<*>> = [],
@Suppress("unused") val payload: Array<KClass<out Payload>> = []
)
class NullOrSizeValidator : ConstraintValidator<NullOrSize, Any> {

View File

@ -7,7 +7,7 @@ import org.springframework.stereotype.Repository
@Repository
interface CompanyRepository : NamedJpaRepository<Company> {
@Query(
"""
"""
select case when(count(r.id) > 0) then false else true end
from Company c
left join Recipe r on c.id = r.company.id

View File

@ -17,7 +17,7 @@ interface MaterialRepository : NamedJpaRepository<Material> {
fun updateInventoryQuantityById(id: Long, inventoryQuantity: Float)
@Query(
"""
"""
select case when(count(mm.id) + count(mt.id) > 0) then false else true end
from Material m
left join MixMaterial mm on m.id = mm.material.id

View File

@ -16,7 +16,7 @@ interface MaterialTypeRepository : NamedJpaRepository<MaterialType> {
fun findByPrefix(prefix: String): MaterialType?
@Query(
"""
"""
select case when(count(m.id) > 0) then false else true end
from MaterialType t
left join Material m on t.id = m.materialType.id

View File

@ -16,7 +16,7 @@ interface MixRepository : JpaRepository<Mix, Long> {
fun updateLocationById(id: Long, location: String?)
@Query(
"""
"""
select case when(count(mm.id) > 0) then false else true end
from Mix m
left join MixMaterial mm on m.mixType.material.id = mm.material.id

View File

@ -19,7 +19,7 @@ interface MixTypeRepository : NamedJpaRepository<MixType> {
fun findByNameAndMaterialType(name: String, materialType: MaterialType): MixType?
@Query(
"""
"""
select case when(count(m.id) > 0) then false else true end
from MixType t
left join Mix m on t.id = m.mixType.id

View File

@ -23,52 +23,52 @@ class EmployeeController(private val employeeService: EmployeeService) {
@GetMapping
@PreAuthorizeViewUsers
fun getAll() =
ok(employeeService.getAll())
ok(employeeService.getAll())
@GetMapping("{id}")
@PreAuthorizeViewUsers
fun getById(@PathVariable id: Long) =
ok(employeeService.getById(id))
ok(employeeService.getById(id))
@GetMapping("current")
fun getCurrent(loggedInEmployee: Principal?) =
if (loggedInEmployee != null)
ok(
employeeService.getById(
loggedInEmployee.name.toLong(),
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = false
)
if (loggedInEmployee != null)
ok(
employeeService.getById(
loggedInEmployee.name.toLong(),
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = false
)
else
forbidden()
)
else
forbidden()
@PostMapping
@PreAuthorizeEditUsers
fun save(@Valid @RequestBody employee: EmployeeSaveDto) =
created<Employee>(EMPLOYEE_CONTROLLER_PATH) {
employeeService.save(employee)
}
created<Employee>(EMPLOYEE_CONTROLLER_PATH) {
employeeService.save(employee)
}
@PutMapping
@PreAuthorizeEditUsers
fun update(@Valid @RequestBody employee: EmployeeUpdateDto) =
noContent {
employeeService.update(employee)
}
noContent {
employeeService.update(employee)
}
@PutMapping("{id}/password", consumes = [MediaType.TEXT_PLAIN_VALUE])
@PreAuthorizeEditUsers
fun updatePassword(@PathVariable id: Long, @RequestBody password: String) =
noContent {
employeeService.updatePassword(id, password)
}
noContent {
employeeService.updatePassword(id, password)
}
@PutMapping("{employeeId}/permissions/{permission}")
@PreAuthorizeEditUsers
fun addPermission(
@PathVariable employeeId: Long,
@PathVariable permission: EmployeePermission
@PathVariable employeeId: Long,
@PathVariable permission: EmployeePermission
) = noContent {
employeeService.addPermission(employeeId, permission)
}
@ -76,8 +76,8 @@ class EmployeeController(private val employeeService: EmployeeService) {
@DeleteMapping("{employeeId}/permissions/{permission}")
@PreAuthorizeEditUsers
fun removePermission(
@PathVariable employeeId: Long,
@PathVariable permission: EmployeePermission
@PathVariable employeeId: Long,
@PathVariable permission: EmployeePermission
) = noContent {
employeeService.removePermission(employeeId, permission)
}
@ -85,7 +85,7 @@ class EmployeeController(private val employeeService: EmployeeService) {
@DeleteMapping("{id}")
@PreAuthorizeRemoveUsers
fun deleteById(@PathVariable id: Long) =
employeeService.deleteById(id)
employeeService.deleteById(id)
}
@RestController
@ -94,50 +94,50 @@ class GroupsController(private val groupService: EmployeeGroupServiceImpl) {
@GetMapping
@PreAuthorize("hasAnyAuthority('VIEW_RECIPES', 'VIEW_USERS')")
fun getAll() =
ok(groupService.getAll())
ok(groupService.getAll())
@GetMapping("{id}")
@PreAuthorizeViewUsers
fun getById(@PathVariable id: Long) =
ok(groupService.getById(id))
ok(groupService.getById(id))
@GetMapping("{id}/employees")
@PreAuthorizeViewUsers
fun getEmployeesForGroup(@PathVariable id: Long) =
ok(groupService.getEmployeesForGroup(id))
ok(groupService.getEmployeesForGroup(id))
@PostMapping("default/{groupId}")
@PreAuthorizeViewUsers
fun setDefaultGroup(@PathVariable groupId: Long, response: HttpServletResponse) =
noContent {
groupService.setResponseDefaultGroup(groupId, response)
}
noContent {
groupService.setResponseDefaultGroup(groupId, response)
}
@GetMapping("default")
@PreAuthorizeViewUsers
fun getRequestDefaultGroup(request: HttpServletRequest) =
ok(groupService.getRequestDefaultGroup(request))
ok(groupService.getRequestDefaultGroup(request))
@PostMapping
@PreAuthorizeEditUsers
fun save(@Valid @RequestBody group: EmployeeGroupSaveDto) =
created<EmployeeGroup>(EMPLOYEE_GROUP_CONTROLLER_PATH) {
groupService.save(group)
}
created<EmployeeGroup>(EMPLOYEE_GROUP_CONTROLLER_PATH) {
groupService.save(group)
}
@PutMapping
@PreAuthorizeEditUsers
fun update(@Valid @RequestBody group: EmployeeGroupUpdateDto) =
noContent {
groupService.update(group)
}
noContent {
groupService.update(group)
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveUsers
fun deleteById(@PathVariable id: Long) =
noContent {
groupService.deleteById(id)
}
noContent {
groupService.deleteById(id)
}
}
@RestController
@ -145,7 +145,7 @@ class GroupsController(private val groupService: EmployeeGroupServiceImpl) {
class LogoutController(private val employeeService: EmployeeService) {
@GetMapping("logout")
fun logout(request: HttpServletRequest) =
ok<Void> {
employeeService.logout(request)
}
ok<Void> {
employeeService.logout(request)
}
}

View File

@ -17,30 +17,30 @@ private const val COMPANY_CONTROLLER_PATH = "api/company"
class CompanyController(private val companyService: CompanyService) {
@GetMapping
fun getAll() =
ok(companyService.getAll())
ok(companyService.getAll())
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(companyService.getById(id))
ok(companyService.getById(id))
@PostMapping
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
fun save(@Valid @RequestBody company: CompanySaveDto) =
created<Company>(COMPANY_CONTROLLER_PATH) {
companyService.save(company)
}
created<Company>(COMPANY_CONTROLLER_PATH) {
companyService.save(company)
}
@PutMapping
@PreAuthorize("hasAuthority('EDIT_COMPANIES')")
fun update(@Valid @RequestBody company: CompanyUpdateDto) =
noContent {
companyService.update(company)
}
noContent {
companyService.update(company)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_COMPANIES')")
fun deleteById(@PathVariable id: Long) =
noContent {
companyService.deleteById(id)
}
noContent {
companyService.deleteById(id)
}
}

View File

@ -15,7 +15,7 @@ private const val INVENTORY_CONTROLLER_PATH = "api/inventory"
@RestController
@RequestMapping(INVENTORY_CONTROLLER_PATH)
class InventoryController(
private val inventoryService: InventoryService
private val inventoryService: InventoryService
) {
@PutMapping("add")
@PreAuthorize("hasAuthority('ADD_TO_INVENTORY')")
@ -26,10 +26,10 @@ class InventoryController(
@PutMapping("deduct")
@PreAuthorize("hasAuthority('DEDUCT_FROM_INVENTORY')")
fun deduct(@RequestBody quantities: Collection<MaterialQuantityDto>) =
ok(inventoryService.deduct(quantities))
ok(inventoryService.deduct(quantities))
@PutMapping("deduct/mix")
@PreAuthorize("hasAuthority('DEDUCT_FROM_INVENTORY')")
fun deduct(@RequestBody mixRatio: MixDeductDto) =
ok(inventoryService.deductMix(mixRatio))
ok(inventoryService.deductMix(mixRatio))
}

View File

@ -18,55 +18,55 @@ private const val MATERIAL_CONTROLLER_PATH = "api/material"
class MaterialController(private val materialService: MaterialService) {
@GetMapping
fun getAll() =
ok(materialService.getAll())
ok(materialService.getAll())
@GetMapping("notmixtype")
fun getAllNotMixType() =
ok(materialService.getAllNotMixType())
ok(materialService.getAllNotMixType())
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(materialService.getById(id))
ok(materialService.getById(id))
@PostMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@PreAuthorize("hasAuthority('EDIT_MATERIALS')")
fun save(@Valid material: MaterialSaveDto, simdutFile: MultipartFile?) =
created<Material>(MATERIAL_CONTROLLER_PATH) {
materialService.save(
materialSaveDto(
name = material.name,
inventoryQuantity = material.inventoryQuantity,
materialTypeId = material.materialTypeId,
simdutFile = simdutFile
)
created<Material>(MATERIAL_CONTROLLER_PATH) {
materialService.save(
materialSaveDto(
name = material.name,
inventoryQuantity = material.inventoryQuantity,
materialTypeId = material.materialTypeId,
simdutFile = simdutFile
)
}
)
}
@PutMapping(consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@PreAuthorize("hasAuthority('EDIT_MATERIALS')")
fun update(@Valid material: MaterialUpdateDto, simdutFile: MultipartFile?) =
noContent {
materialService.update(
materialUpdateDto(
id = material.id,
name = material.name,
inventoryQuantity = material.inventoryQuantity,
materialTypeId = material.materialTypeId,
simdutFile = simdutFile
)
noContent {
materialService.update(
materialUpdateDto(
id = material.id,
name = material.name,
inventoryQuantity = material.inventoryQuantity,
materialTypeId = material.materialTypeId,
simdutFile = simdutFile
)
}
)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_MATERIALS')")
fun deleteById(@PathVariable id: Long) =
noContent {
materialService.deleteById(id)
}
noContent {
materialService.deleteById(id)
}
@GetMapping("{id}/simdut/exists")
fun hasSimdut(@PathVariable id: Long) =
ok(materialService.hasSimdut(id))
ok(materialService.hasSimdut(id))
@GetMapping("{id}/simdut", produces = [MediaType.APPLICATION_PDF_VALUE])
fun getSimdut(@PathVariable id: Long): ResponseEntity<ByteArray> = with(materialService.getSimdut(id)) {
@ -79,14 +79,14 @@ class MaterialController(private val materialService: MaterialService) {
@GetMapping("/simdut")
fun getAllIdsWithSimdut() =
ok(materialService.getAllIdsWithSimdut())
ok(materialService.getAllIdsWithSimdut())
@GetMapping("mix/create/{recipeId}")
fun getAllForMixCreation(@PathVariable recipeId: Long) =
ok(materialService.getAllForMixCreation(recipeId))
ok(materialService.getAllForMixCreation(recipeId))
@GetMapping("mix/update/{mixId}")
fun getAllForMixUpdate(@PathVariable mixId: Long) =
ok(materialService.getAllForMixUpdate(mixId))
ok(materialService.getAllForMixUpdate(mixId))
}

View File

@ -17,31 +17,31 @@ private const val MATERIAL_TYPE_CONTROLLER_PATH = "api/materialtype"
class MaterialTypeController(private val materialTypeService: MaterialTypeService) {
@GetMapping
fun getAll() =
ok(materialTypeService.getAll())
ok(materialTypeService.getAll())
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(materialTypeService.getById(id))
ok(materialTypeService.getById(id))
@PostMapping
@PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')")
fun save(@Valid @RequestBody materialType: MaterialTypeSaveDto) =
created<MaterialType>(MATERIAL_TYPE_CONTROLLER_PATH) {
materialTypeService.save(materialType)
}
created<MaterialType>(MATERIAL_TYPE_CONTROLLER_PATH) {
materialTypeService.save(materialType)
}
@PutMapping
@PreAuthorize("hasAuthority('EDIT_MATERIAL_TYPES')")
fun update(@Valid @RequestBody materialType: MaterialTypeUpdateDto) =
noContent {
materialTypeService.update(materialType)
}
noContent {
materialTypeService.update(materialType)
}
@DeleteMapping("{id}")
@PreAuthorize("hasAuthority('REMOVE_MATERIAL_TYPES')")
fun deleteById(@PathVariable id: Long) =
noContent {
materialTypeService.deleteById(id)
}
noContent {
materialTypeService.deleteById(id)
}
}

View File

@ -25,39 +25,39 @@ private const val MIX_CONTROLLER_PATH = "api/recipe/mix"
class RecipeController(private val recipeService: RecipeService) {
@GetMapping
fun getAll() =
ok(recipeService.getAll())
ok(recipeService.getAll())
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(recipeService.getById(id))
ok(recipeService.getById(id))
@PostMapping
@PreAuthorizeEditRecipes
fun save(@Valid @RequestBody recipe: RecipeSaveDto) =
created<Recipe>(RECIPE_CONTROLLER_PATH) {
recipeService.save(recipe)
}
created<Recipe>(RECIPE_CONTROLLER_PATH) {
recipeService.save(recipe)
}
@PutMapping
@PreAuthorizeEditRecipes
fun update(@Valid @RequestBody recipe: RecipeUpdateDto) =
noContent {
recipeService.update(recipe)
}
noContent {
recipeService.update(recipe)
}
@PutMapping("public")
@PreAuthorize("hasAuthority('EDIT_RECIPES_PUBLIC_DATA')")
fun updatePublicData(@Valid @RequestBody publicDataDto: RecipePublicDataDto) =
noContent {
recipeService.updatePublicData(publicDataDto)
}
noContent {
recipeService.updatePublicData(publicDataDto)
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveRecipes
fun deleteById(@PathVariable id: Long) =
noContent {
recipeService.deleteById(id)
}
noContent {
recipeService.deleteById(id)
}
}
@RestController
@ -66,11 +66,11 @@ class RecipeController(private val recipeService: RecipeService) {
class RecipeImageController(val recipeImageService: RecipeImageService) {
@GetMapping("{recipeId}/image")
fun getAllIdsForRecipe(@PathVariable recipeId: Long) =
ok(recipeImageService.getAllIdsForRecipe(recipeId))
ok(recipeImageService.getAllIdsForRecipe(recipeId))
@GetMapping("{recipeId}/image/{id}", produces = [MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE])
fun getById(@PathVariable recipeId: Long, @PathVariable id: Long) =
ok(recipeImageService.getByIdForRecipe(id, recipeId))
ok(recipeImageService.getByIdForRecipe(id, recipeId))
@PostMapping("{recipeId}/image", consumes = [MediaType.MULTIPART_FORM_DATA_VALUE])
@PreAuthorizeEditRecipes
@ -82,9 +82,9 @@ class RecipeImageController(val recipeImageService: RecipeImageService) {
@DeleteMapping("{recipeId}/image/{id}")
@PreAuthorizeRemoveRecipes
fun delete(@PathVariable recipeId: Long, @PathVariable id: Long) =
noContent {
recipeImageService.delete(id, recipeId)
}
noContent {
recipeImageService.delete(id, recipeId)
}
}
@RestController
@ -93,26 +93,26 @@ class RecipeImageController(val recipeImageService: RecipeImageService) {
class MixController(private val mixService: MixService) {
@GetMapping("{id}")
fun getById(@PathVariable id: Long) =
ok(mixService.getById(id))
ok(mixService.getById(id))
@PostMapping
@PreAuthorizeEditRecipes
fun save(@Valid @RequestBody mix: MixSaveDto) =
created<Mix>(MIX_CONTROLLER_PATH) {
mixService.save(mix)
}
created<Mix>(MIX_CONTROLLER_PATH) {
mixService.save(mix)
}
@PutMapping
@PreAuthorizeEditRecipes
fun update(@Valid @RequestBody mix: MixUpdateDto) =
noContent {
mixService.update(mix)
}
noContent {
mixService.update(mix)
}
@DeleteMapping("{id}")
@PreAuthorizeRemoveRecipes
fun deleteById(@PathVariable id: Long) =
noContent {
mixService.deleteById(id)
}
noContent {
mixService.deleteById(id)
}
}

View File

@ -9,11 +9,11 @@ import java.net.URI
/** Creates a HTTP OK [ResponseEntity] from the given [body]. */
fun <T> ok(body: T): ResponseEntity<T> =
ResponseEntity.ok(body)
ResponseEntity.ok(body)
/** Creates a HTTP OK [ResponseEntity] from the given [body] and [headers]. */
fun <T> ok(body: T, headers: HttpHeaders): ResponseEntity<T> =
ResponseEntity(body, headers, HttpStatus.OK)
ResponseEntity(body, headers, HttpStatus.OK)
/** Executes the given [action] then returns an HTTP OK [ResponseEntity] form the given [body]. */
fun <T> ok(action: () -> Unit): ResponseEntity<T> {
@ -23,19 +23,19 @@ fun <T> ok(action: () -> Unit): ResponseEntity<T> {
/** Creates a HTTP CREATED [ResponseEntity] from the given [body] with the location set to [controllerPath]/id. */
fun <T : Model> created(controllerPath: String, body: T): ResponseEntity<T> =
ResponseEntity.created(URI.create("$controllerPath/${body.id}")).body(body)
ResponseEntity.created(URI.create("$controllerPath/${body.id}")).body(body)
/** Creates a HTTP CREATED [ResponseEntity] with the result of the given [producer] as its body. */
fun <T : Model> created(controllerPath: String, producer: () -> T): ResponseEntity<T> =
created(controllerPath, producer())
created(controllerPath, producer())
/** Creates a HTTP NOT FOUND [ResponseEntity]. */
fun <T> notFound(): ResponseEntity<T> =
ResponseEntity.notFound().build()
ResponseEntity.notFound().build()
/** Creates a HTTP NO CONTENT [ResponseEntity]. */
fun noContent(): ResponseEntity<Void> =
ResponseEntity.noContent().build()
ResponseEntity.noContent().build()
/** Executes the given [action] then returns an HTTP NO CONTENT [ResponseEntity]. */
fun noContent(action: () -> Unit): ResponseEntity<Void> {
@ -45,12 +45,12 @@ fun noContent(action: () -> Unit): ResponseEntity<Void> {
/** Creates a HTTP FORBIDDEN [ResponseEntity]. */
fun <T> forbidden(): ResponseEntity<T> =
ResponseEntity.status(HttpStatus.FORBIDDEN).build()
ResponseEntity.status(HttpStatus.FORBIDDEN).build()
/** Creates an [HttpHeaders] instance from the given options. */
fun httpHeaders(
contentType: MediaType = MediaType.APPLICATION_JSON,
op: HttpHeaders.() -> Unit = {}
contentType: MediaType = MediaType.APPLICATION_JSON,
op: HttpHeaders.() -> Unit = {}
) = HttpHeaders().apply {
this.contentType = contentType

View File

@ -2,13 +2,11 @@ package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.blacklistedJwtTokens
import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.validation.or
import dev.fyloz.colorrecipesexplorer.repository.EmployeeGroupRepository
import dev.fyloz.colorrecipesexplorer.repository.EmployeeRepository
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.context.annotation.Lazy
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
@ -58,7 +56,7 @@ interface EmployeeService : ExternalModelService<Employee, EmployeeSaveDto, Empl
}
interface EmployeeGroupService :
ExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupRepository> {
ExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupRepository> {
/** Gets all the employees of the group with the given [id]. */
fun getEmployeesForGroup(id: Long): Collection<Employee>
@ -76,67 +74,69 @@ interface EmployeeUserDetailsService : UserDetailsService {
@Service
class EmployeeServiceImpl(
employeeRepository: EmployeeRepository,
@Lazy val passwordEncoder: PasswordEncoder
employeeRepository: EmployeeRepository,
@Lazy val groupService: EmployeeGroupService,
@Lazy val passwordEncoder: PasswordEncoder,
) : AbstractExternalModelService<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeRepository>(employeeRepository),
EmployeeService {
@Autowired
lateinit var groupService: EmployeeGroupServiceImpl
EmployeeService {
override fun idNotFoundException(id: Long) = employeeIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = employeeIdAlreadyExistsException(id)
override fun existsByFirstNameAndLastName(firstName: String, lastName: String): Boolean =
repository.existsByFirstNameAndLastName(firstName, lastName)
repository.existsByFirstNameAndLastName(firstName, lastName)
override fun getAll(): Collection<Employee> =
super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser }
super.getAll().filter { !it.isSystemUser && !it.isDefaultGroupUser }
override fun getById(id: Long): Employee =
getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
override fun getById(id: Long, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee =
super.getById(id).apply {
if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser) throw EntityNotFoundException(
id
)
}
super.getById(id).apply {
if (ignoreSystemUsers && isSystemUser || ignoreDefaultGroupUsers && isDefaultGroupUser)
throw idNotFoundException(id)
}
override fun getByGroup(group: EmployeeGroup): Collection<Employee> =
repository.findAllByGroup(group).filter {
!it.isSystemUser && !it.isDefaultGroupUser
}
repository.findAllByGroup(group).filter {
!it.isSystemUser && !it.isDefaultGroupUser
}
override fun getDefaultGroupEmployee(group: EmployeeGroup): Employee =
repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)
repository.findByIsDefaultGroupUserIsTrueAndGroupIs(group)
override fun save(entity: EmployeeSaveDto): Employee =
save(with(entity) {
Employee(
id,
firstName,
lastName,
passwordEncoder.encode(password),
isDefaultGroupUser = false,
isSystemUser = false,
group = if (groupId != null) groupService.getById(groupId) else null,
permissions = permissions
)
})
save(with(entity) {
Employee(
id,
firstName,
lastName,
passwordEncoder.encode(password),
isDefaultGroupUser = false,
isSystemUser = false,
group = if (groupId != null) groupService.getById(groupId) else null,
permissions = permissions
)
})
override fun save(entity: Employee): Employee {
if (existsById(entity.id))
throw employeeIdAlreadyExistsException(entity.id)
if (existsByFirstNameAndLastName(entity.firstName, entity.lastName))
throw EntityAlreadyExistsException("${entity.firstName} ${entity.lastName}")
throw employeeFullNameAlreadyExistsException(entity.firstName, entity.lastName)
return super<AbstractExternalModelService>.save(entity)
}
override fun saveDefaultGroupEmployee(group: EmployeeGroup) {
save(
Employee(
id = 1000000L + group.id!!,
firstName = group.name,
lastName = "EmployeeModel",
password = passwordEncoder.encode(group.name),
group = group,
isDefaultGroupUser = true
)
employee(
id = 1000000L + group.id!!,
firstName = group.name,
lastName = "EmployeeModel",
password = passwordEncoder.encode(group.name),
group = group,
isDefaultGroupUser = true
)
)
}
@ -144,9 +144,9 @@ class EmployeeServiceImpl(
val employee = getById(employeeId, ignoreDefaultGroupUsers = true, ignoreSystemUsers = false)
employee.lastLoginTime = time
return update(
employee,
ignoreDefaultGroupUsers = true,
ignoreSystemUsers = false
employee,
ignoreDefaultGroupUsers = true,
ignoreSystemUsers = false
)
}
@ -154,26 +154,26 @@ class EmployeeServiceImpl(
val persistedEmployee by lazy { getById(entity.id) }
return update(with(entity) {
Employee(
id = id,
firstName = firstName or persistedEmployee.firstName,
lastName = lastName or persistedEmployee.lastName,
password = persistedEmployee.password,
isDefaultGroupUser = false,
isSystemUser = false,
group = if (entity.groupId != null) groupService.getById(entity.groupId) else persistedEmployee.group,
permissions = permissions?.toMutableSet() ?: persistedEmployee.permissions,
lastLoginTime = persistedEmployee.lastLoginTime
id = id,
firstName = firstName or persistedEmployee.firstName,
lastName = lastName or persistedEmployee.lastName,
password = persistedEmployee.password,
isDefaultGroupUser = false,
isSystemUser = false,
group = if (entity.groupId != null) groupService.getById(entity.groupId) else persistedEmployee.group,
permissions = permissions?.toMutableSet() ?: persistedEmployee.permissions,
lastLoginTime = persistedEmployee.lastLoginTime
)
})
}
override fun update(entity: Employee): Employee =
update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
update(entity, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
override fun update(entity: Employee, ignoreDefaultGroupUsers: Boolean, ignoreSystemUsers: Boolean): Employee {
with(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)) {
if (this != null && id != entity.id)
throw EntityAlreadyExistsException("${entity.firstName} ${entity.lastName}")
throw employeeFullNameAlreadyExistsException(entity.firstName, entity.lastName)
}
return super<AbstractExternalModelService>.update(entity)
@ -183,24 +183,24 @@ class EmployeeServiceImpl(
val persistedEmployee = getById(id, ignoreDefaultGroupUsers = true, ignoreSystemUsers = true)
return super<AbstractExternalModelService>.update(with(persistedEmployee) {
Employee(
id,
firstName,
lastName,
passwordEncoder.encode(password),
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
id,
firstName,
lastName,
passwordEncoder.encode(password),
isDefaultGroupUser,
isSystemUser,
group,
permissions,
lastLoginTime
)
})
}
override fun addPermission(employeeId: Long, permission: EmployeePermission): Employee =
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions += permission })
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions += permission })
override fun removePermission(employeeId: Long, permission: EmployeePermission): Employee =
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions -= permission })
super<AbstractExternalModelService>.update(getById(employeeId).apply { permissions -= permission })
override fun logout(request: HttpServletRequest) {
val authorizationCookie = WebUtils.getCookie(request, "Authorization")
@ -217,16 +217,20 @@ const val defaultGroupCookieMaxAge = 10 * 365 * 24 * 60 * 60 // 10 ans
@Service
class EmployeeGroupServiceImpl(
val employeeService: EmployeeService,
employeeGroupRepository: EmployeeGroupRepository
) :
AbstractExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupRepository>(
employeeGroupRepository
),
EmployeeGroupService {
private val employeeService: EmployeeService,
employeeGroupRepository: EmployeeGroupRepository
) : AbstractExternalNamedModelService<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupRepository>(
employeeGroupRepository
),
EmployeeGroupService {
override fun idNotFoundException(id: Long) = employeeGroupIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = employeeGroupIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = employeeGroupNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = employeeGroupNameAlreadyExistsException(name)
override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getEmployeesForGroup(id: Long): Collection<Employee> =
employeeService.getByGroup(getById(id))
employeeService.getByGroup(getById(id))
@Transactional
override fun save(entity: EmployeeGroup): EmployeeGroup {
@ -239,9 +243,9 @@ class EmployeeGroupServiceImpl(
val persistedGroup by lazy { getById(entity.id) }
return update(with(entity) {
EmployeeGroup(
entity.id,
if (name.isNotBlank()) entity.name else persistedGroup.name,
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
entity.id,
if (name.isNotBlank()) entity.name else persistedGroup.name,
if (permissions.isNotEmpty()) entity.permissions else persistedGroup.permissions
)
})
}
@ -254,11 +258,11 @@ class EmployeeGroupServiceImpl(
override fun getRequestDefaultGroup(request: HttpServletRequest): EmployeeGroup {
val defaultGroupCookie = WebUtils.getCookie(request, defaultGroupCookieName)
?: throw EntityNotFoundException("defaultGroup")
?: throw NoDefaultGroupException()
val defaultGroupUser = employeeService.getById(
defaultGroupCookie.value.toLong(),
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true
defaultGroupCookie.value.toLong(),
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true
)
return defaultGroupUser.group!!
}
@ -267,32 +271,32 @@ class EmployeeGroupServiceImpl(
val group = getById(groupId)
val defaultGroupUser = employeeService.getDefaultGroupEmployee(group)
response.addHeader(
"Set-Cookie",
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict"
"Set-Cookie",
"$defaultGroupCookieName=${defaultGroupUser.id}; Max-Age=${defaultGroupCookieMaxAge}; Path=/api; HttpOnly; Secure; SameSite=strict"
)
}
}
@Service
class EmployeeUserDetailsServiceImpl(
val employeeService: EmployeeService
private val employeeService: EmployeeService
) :
EmployeeUserDetailsService {
EmployeeUserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
try {
return loadUserByEmployeeId(username.toLong(), true)
} catch (ex: EntityNotFoundException) {
} catch (ex: NotFoundException) {
throw UsernameNotFoundException(username)
} catch (ex: EntityNotFoundException) {
} catch (ex: NotFoundException) {
throw UsernameNotFoundException(username)
}
}
override fun loadUserByEmployeeId(employeeId: Long, ignoreDefaultGroupUsers: Boolean): UserDetails {
val employee = employeeService.getById(
employeeId,
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
ignoreSystemUsers = false
employeeId,
ignoreDefaultGroupUsers = ignoreDefaultGroupUsers,
ignoreSystemUsers = false
)
return User(employee.id.toString(), employee.password, employee.authorities)
}

View File

@ -1,10 +1,6 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteEntityException
import dev.fyloz.colorrecipesexplorer.model.Company
import dev.fyloz.colorrecipesexplorer.model.CompanySaveDto
import dev.fyloz.colorrecipesexplorer.model.CompanyUpdateDto
import dev.fyloz.colorrecipesexplorer.model.company
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.CompanyRepository
import org.springframework.context.annotation.Lazy
import org.springframework.stereotype.Service
@ -16,11 +12,16 @@ interface CompanyService : ExternalNamedModelService<Company, CompanySaveDto, Co
@Service
class CompanyServiceImpl(
companyRepository: CompanyRepository,
@Lazy val recipeService: RecipeService
companyRepository: CompanyRepository,
@Lazy val recipeService: RecipeService
) :
AbstractExternalNamedModelService<Company, CompanySaveDto, CompanyUpdateDto, CompanyRepository>(companyRepository),
CompanyService {
AbstractExternalNamedModelService<Company, CompanySaveDto, CompanyUpdateDto, CompanyRepository>(companyRepository),
CompanyService {
override fun idNotFoundException(id: Long) = companyIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = companyIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = companyNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = companyNameAlreadyExistsException(name)
override fun isLinkedToRecipes(company: Company): Boolean = recipeService.existsByCompany(company)
override fun update(entity: CompanyUpdateDto): Company {
@ -29,14 +30,14 @@ class CompanyServiceImpl(
return update(with(entity) {
company(
id = id,
name = if (name != null && name.isNotBlank()) name else persistedCompany.name
id = id,
name = if (name != null && name.isNotBlank()) name else persistedCompany.name
)
})
}
override fun deleteById(id: Long) {
if (!repository.canBeDeleted(id)) throw CannotDeleteEntityException(id)
super.deleteById(id)
override fun delete(entity: Company) {
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteCompany(entity)
super.delete(entity)
}
}

View File

@ -1,12 +1,9 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.LowQuantitiesException
import dev.fyloz.colorrecipesexplorer.exception.LowQuantityException
import dev.fyloz.colorrecipesexplorer.model.MaterialQuantityDto
import dev.fyloz.colorrecipesexplorer.model.MixDeductDto
import dev.fyloz.colorrecipesexplorer.model.MixMaterial
import dev.fyloz.colorrecipesexplorer.model.materialQuantityDto
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.service.utils.mapMayThrow
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import javax.transaction.Transactional
@ -29,20 +26,20 @@ interface InventoryService {
@Service
class InventoryServiceImpl(
private val materialService: MaterialService,
private val mixService: MixService
private val materialService: MaterialService,
private val mixService: MixService
) : InventoryService {
@Transactional
override fun add(materialQuantities: Collection<MaterialQuantityDto>) =
materialQuantities.map {
materialQuantityDto(materialId = it.material, quantity = add(it))
}
materialQuantities.map {
materialQuantityDto(materialId = it.material, quantity = add(it))
}
override fun add(materialQuantity: MaterialQuantityDto) =
materialService.updateQuantity(
materialService.getById(materialQuantity.material),
materialQuantity.quantity
)
materialService.updateQuantity(
materialService.getById(materialQuantity.material),
materialQuantity.quantity
)
@Transactional
override fun deductMix(mixRatio: MixDeductDto): Collection<MaterialQuantityDto> {
@ -51,41 +48,66 @@ class InventoryServiceImpl(
val adjustedFirstMaterialQuantity = firstMixMaterial.quantity * mixRatio.ratio
fun adjustQuantity(mixMaterial: MixMaterial): Float =
if (!mixMaterial.material.materialType!!.usePercentages)
mixMaterial.quantity * mixRatio.ratio
else
(mixMaterial.quantity * adjustedFirstMaterialQuantity) / 100f
if (!mixMaterial.material.materialType!!.usePercentages)
mixMaterial.quantity * mixRatio.ratio
else
(mixMaterial.quantity * adjustedFirstMaterialQuantity) / 100f
return deduct(mix.mixMaterials.map {
materialQuantityDto(
materialId = it.material.id!!,
quantity = adjustQuantity(it)
materialId = it.material.id!!,
quantity = adjustQuantity(it)
)
})
}
@Transactional
override fun deduct(materialQuantities: Collection<MaterialQuantityDto>): Collection<MaterialQuantityDto> {
val thrown = mutableListOf<MaterialQuantityDto>()
val thrown = mutableListOf<NotEnoughInventoryException>()
val updatedQuantities =
materialQuantities.mapMayThrow<MaterialQuantityDto, MaterialQuantityDto, LowQuantityException>(
{ thrown.add(it.materialQuantity) }
) {
materialQuantityDto(materialId = it.material, quantity = deduct(it))
}
materialQuantities.mapMayThrow<MaterialQuantityDto, MaterialQuantityDto, NotEnoughInventoryException>(
{ thrown.add(it) }
) {
materialQuantityDto(materialId = it.material, quantity = deduct(it))
}
if (thrown.isNotEmpty()) {
throw LowQuantitiesException(thrown)
throw MultiplesNotEnoughInventoryException(thrown)
}
return updatedQuantities
}
override fun deduct(materialQuantity: MaterialQuantityDto): Float =
with(materialService.getById(materialQuantity.material)) {
if (this.inventoryQuantity >= materialQuantity.quantity) {
materialService.updateQuantity(this, -materialQuantity.quantity)
} else {
throw LowQuantityException(materialQuantity)
}
}
with(materialService.getById(materialQuantity.material)) {
if (this.inventoryQuantity >= materialQuantity.quantity) {
materialService.updateQuantity(this, -materialQuantity.quantity)
} else {
throw NotEnoughInventoryException(materialQuantity.quantity, this)
}
}
}
class NotEnoughInventoryException(quantity: Float, material: Material) :
RestException(
"notenoughinventory",
"Not enough inventory",
HttpStatus.BAD_REQUEST,
"Cannot deduct ${quantity}mL of ${material.name} because there is only ${material.inventoryQuantity}mL in inventory",
mapOf(
"material" to material.name,
"materialId" to material.id.toString(),
"requestQuantity" to quantity,
"availableQuantity" to material.inventoryQuantity
)
)
class MultiplesNotEnoughInventoryException(exceptions: List<NotEnoughInventoryException>) :
RestException(
"notenoughinventory-multiple",
"Not enough inventory",
HttpStatus.BAD_REQUEST,
"Cannot deduct requested quantities because there is no enough of them in inventory",
mapOf(
"lowQuantities" to exceptions.map { it.extensions }
)
)

View File

@ -1,6 +1,6 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteEntityException
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository
import dev.fyloz.colorrecipesexplorer.service.files.SimdutService
@ -9,7 +9,7 @@ import org.springframework.context.annotation.Lazy
import org.springframework.stereotype.Service
interface MaterialService :
ExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialRepository> {
ExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialRepository> {
/** Checks if a material with the given [materialType] exists. */
fun existsByMaterialType(materialType: MaterialType): Boolean
@ -37,39 +37,44 @@ interface MaterialService :
@Service
class MaterialServiceImpl(
materialRepository: MaterialRepository,
val simdutService: SimdutService,
val recipeService: RecipeService,
val mixService: MixService,
@Lazy val materialTypeService: MaterialTypeService
materialRepository: MaterialRepository,
val simdutService: SimdutService,
val recipeService: RecipeService,
val mixService: MixService,
@Lazy val materialTypeService: MaterialTypeService
) :
AbstractExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialRepository>(
materialRepository
),
MaterialService {
AbstractExternalNamedModelService<Material, MaterialSaveDto, MaterialUpdateDto, MaterialRepository>(
materialRepository
),
MaterialService {
override fun idNotFoundException(id: Long) = materialIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = materialNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = materialNameAlreadyExistsException(name)
override fun existsByMaterialType(materialType: MaterialType): Boolean =
repository.existsByMaterialType(materialType)
repository.existsByMaterialType(materialType)
override fun hasSimdut(id: Long): Boolean = simdutService.exists(getById(id))
override fun getSimdut(id: Long): ByteArray = simdutService.read(getById(id))
override fun getAllNotMixType(): Collection<Material> = getAll().filter { !it.isMixType }
override fun getAllIdsWithSimdut(): Collection<Long> =
getAllNotMixType()
.filter { simdutService.exists(it) }
.map { it.id!! }
getAllNotMixType()
.filter { simdutService.exists(it) }
.map { it.id!! }
override fun save(entity: MaterialSaveDto): Material =
save(with(entity) {
material(
name = entity.name,
inventoryQuantity = entity.inventoryQuantity,
materialType = materialTypeService.getById(materialTypeId),
isMixType = false
)
}).apply {
if (entity.simdutFile != null && !entity.simdutFile.isEmpty) simdutService.write(this, entity.simdutFile)
}
save(with(entity) {
material(
name = entity.name,
inventoryQuantity = entity.inventoryQuantity,
materialType = materialTypeService.getById(materialTypeId),
isMixType = false
)
}).apply {
if (entity.simdutFile != null && !entity.simdutFile.isEmpty) simdutService.write(this, entity.simdutFile)
}
override fun update(entity: MaterialUpdateDto): Material {
val persistedMaterial by lazy {
@ -78,11 +83,11 @@ class MaterialServiceImpl(
return update(with(entity) {
material(
id = id,
name = if (name != null && name.isNotBlank()) name else persistedMaterial.name,
inventoryQuantity = if (inventoryQuantity != null && inventoryQuantity != Float.MIN_VALUE) inventoryQuantity else persistedMaterial.inventoryQuantity,
isMixType = persistedMaterial.isMixType,
materialType = if (materialTypeId != null) materialTypeService.getById(materialTypeId) else persistedMaterial.materialType
id = id,
name = if (name != null && name.isNotBlank()) name else persistedMaterial.name,
inventoryQuantity = if (inventoryQuantity != null && inventoryQuantity != Float.MIN_VALUE) inventoryQuantity else persistedMaterial.inventoryQuantity,
isMixType = persistedMaterial.isMixType,
materialType = if (materialTypeId != null) materialTypeService.getById(materialTypeId) else persistedMaterial.materialType
)
}).apply {
if (entity.simdutFile != null && !entity.simdutFile.isEmpty) simdutService.update(entity.simdutFile, this)
@ -98,23 +103,23 @@ class MaterialServiceImpl(
override fun getAllForMixCreation(recipeId: Long): Collection<Material> {
val recipesMixTypes = recipeService.getById(recipeId).mixTypes
return getAll()
.filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } }
.filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } }
}
override fun getAllForMixUpdate(mixId: Long): Collection<Material> {
val mix = mixService.getById(mixId)
val recipesMixTypes = mix.recipe.mixTypes
return getAll()
.filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } }
.filter { it.id != mix.mixType.material.id }
.filter { !it.isMixType || recipesMixTypes.any { mixType -> mixType.material.id == it.id } }
.filter { it.id != mix.mixType.material.id }
}
private fun assertPersistedMaterial(material: Material) {
Assert.notNull(material.name, "The persisted material with the id ${material.id} has a null name")
}
override fun deleteById(id: Long) {
if (!repository.canBeDeleted(id)) throw CannotDeleteEntityException(id)
super.deleteById(id)
override fun delete(entity: Material) {
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialException(entity)
super.delete(entity)
}
}

View File

@ -1,22 +1,14 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteEntityException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.MaterialType
import dev.fyloz.colorrecipesexplorer.model.MaterialTypeSaveDto
import dev.fyloz.colorrecipesexplorer.model.MaterialTypeUpdateDto
import dev.fyloz.colorrecipesexplorer.model.materialType
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.validation.isNotNullAndNotBlank
import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.ResponseStatus
import kotlin.contracts.ExperimentalContracts
interface MaterialTypeService :
ExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeRepository> {
ExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeRepository> {
/** Checks if a material type with the given [prefix] exists. */
fun existsByPrefix(prefix: String): Boolean
@ -35,32 +27,36 @@ interface MaterialTypeService :
@Service
class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val materialService: MaterialService) :
AbstractExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeRepository>(
repository
), MaterialTypeService {
AbstractExternalNamedModelService<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeRepository>(
repository
), MaterialTypeService {
override fun idNotFoundException(id: Long) = materialTypeIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = materialIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = materialTypeNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = materialTypeNameAlreadyExistsException(name)
override fun existsByPrefix(prefix: String): Boolean = repository.existsByPrefix(prefix)
override fun isUsedByMaterial(materialType: MaterialType): Boolean =
materialService.existsByMaterialType(materialType)
materialService.existsByMaterialType(materialType)
override fun getAllSystemTypes(): Collection<MaterialType> = repository.findAllBySystemTypeIs(true)
override fun getAllNonSystemType(): Collection<MaterialType> = repository.findAllBySystemTypeIs(false)
override fun save(entity: MaterialType): MaterialType {
if (existsByPrefix(entity.prefix))
throw EntityAlreadyExistsException(entity.prefix)
throw materialTypePrefixAlreadyExistsException(entity.prefix)
return super<AbstractExternalNamedModelService>.save(entity)
}
@ExperimentalContracts
override fun update(entity: MaterialTypeUpdateDto): MaterialType {
val persistedMaterialType by lazy { getById(entity.id) }
return update(with(entity) {
MaterialType(
id = id,
name = if (isNotNullAndNotBlank(name)) name else persistedMaterialType.name,
prefix = if (isNotNullAndNotBlank(prefix)) prefix else persistedMaterialType.prefix,
systemType = false
id = id,
name = if (isNotNullAndNotBlank(name)) name else persistedMaterialType.name,
prefix = if (isNotNullAndNotBlank(prefix)) prefix else persistedMaterialType.prefix,
systemType = false
)
})
}
@ -68,15 +64,15 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma
override fun update(entity: MaterialType): MaterialType {
with(repository.findByPrefix(entity.prefix)) {
if (this != null && id != entity.id)
throw EntityAlreadyExistsException(entity.prefix)
throw materialTypePrefixAlreadyExistsException(entity.prefix)
}
return super<AbstractExternalNamedModelService>.update(entity)
}
override fun deleteById(id: Long) {
if (!repository.canBeDeleted(id)) throw CannotDeleteEntityException(id)
super.deleteById(id)
override fun delete(entity: MaterialType) {
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialTypeException(entity)
super.delete(entity)
}
override fun saveSystemTypes(systemTypeProperties: Collection<MaterialTypeProperties.MaterialTypeProperty>) {
@ -102,9 +98,3 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma
oldSystemTypes.forEach { update(materialType(it, newSystemType = false)) }
}
}
@ResponseStatus(HttpStatus.CONFLICT)
class CannotDeleteUsedMaterialTypeRestException :
RestException("Cannot delete a used material type", HttpStatus.CONFLICT) {
override fun buildBody(): RestExceptionBody = object : RestExceptionBody() {}
}

View File

@ -1,9 +1,6 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.model.MixMaterial
import dev.fyloz.colorrecipesexplorer.model.MixMaterialDto
import dev.fyloz.colorrecipesexplorer.model.mixMaterial
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixMaterialRepository
import org.springframework.context.annotation.Lazy
import org.springframework.stereotype.Service
@ -24,22 +21,25 @@ interface MixMaterialService : ModelService<MixMaterial, MixMaterialRepository>
@Service
class MixMaterialServiceImpl(
mixMaterialRepository: MixMaterialRepository,
@Lazy val materialService: MaterialService
mixMaterialRepository: MixMaterialRepository,
@Lazy val materialService: MaterialService
) : AbstractModelService<MixMaterial, MixMaterialRepository>(mixMaterialRepository), MixMaterialService {
override fun idNotFoundException(id: Long) = mixMaterialIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = mixMaterialIdAlreadyExistsException(id)
override fun existsByMaterial(material: Material): Boolean = repository.existsByMaterial(material)
override fun create(mixMaterials: Set<MixMaterialDto>): Set<MixMaterial> =
mixMaterials.map(::create).toSet()
mixMaterials.map(::create).toSet()
override fun create(mixMaterial: MixMaterialDto): MixMaterial =
mixMaterial(
material = materialService.getById(mixMaterial.materialId),
quantity = mixMaterial.quantity,
position = mixMaterial.position
)
mixMaterial(
material = materialService.getById(mixMaterial.materialId),
quantity = mixMaterial.quantity,
position = mixMaterial.position
)
override fun updateQuantity(mixMaterial: MixMaterial, quantity: Float) =
update(mixMaterial.apply {
this.quantity = quantity
})
update(mixMaterial.apply {
this.quantity = quantity
})
}

View File

@ -1,6 +1,6 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteEntityException
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixRepository
import org.springframework.context.annotation.Lazy
@ -23,13 +23,16 @@ interface MixService : ExternalModelService<Mix, MixSaveDto, MixUpdateDto, MixRe
@Service
class MixServiceImpl(
mixRepository: MixRepository,
@Lazy val recipeService: RecipeService,
@Lazy val materialTypeService: MaterialTypeService,
val mixMaterialService: MixMaterialService,
val mixTypeService: MixTypeService
mixRepository: MixRepository,
@Lazy val recipeService: RecipeService,
@Lazy val materialTypeService: MaterialTypeService,
val mixMaterialService: MixMaterialService,
val mixTypeService: MixTypeService
) : AbstractModelService<Mix, MixRepository>(mixRepository),
MixService {
MixService {
override fun idNotFoundException(id: Long) = mixIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = mixIdAlreadyExistsException(id)
override fun getAllByMixType(mixType: MixType): Collection<Mix> = repository.findAllByMixType(mixType)
override fun mixTypeIsShared(mixType: MixType): Boolean = getAllByMixType(mixType).count() > 1
@ -81,12 +84,8 @@ class MixServiceImpl(
@Transactional
override fun delete(entity: Mix) {
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixException(entity)
recipeService.removeMix(entity)
super.delete(entity)
}
override fun deleteById(id: Long) {
if (!repository.canBeDeleted(id)) throw CannotDeleteEntityException(id)
super.deleteById(id)
}
}

View File

@ -1,8 +1,5 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.CannotDeleteEntityException
import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository
import org.springframework.context.annotation.Lazy
@ -30,55 +27,60 @@ interface MixTypeService : NamedModelService<MixType, MixTypeRepository> {
@Service
class MixTypeServiceImpl(
mixTypeRepository: MixTypeRepository,
@Lazy val materialService: MaterialService,
@Lazy val mixService: MixService
mixTypeRepository: MixTypeRepository,
@Lazy val materialService: MaterialService,
@Lazy val mixService: MixService
) :
AbstractNamedModelService<MixType, MixTypeRepository>(mixTypeRepository), MixTypeService {
AbstractNamedModelService<MixType, MixTypeRepository>(mixTypeRepository), MixTypeService {
override fun idNotFoundException(id: Long) = mixTypeIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = mixTypeIdAlreadyExistsException(id)
override fun nameNotFoundException(name: String) = mixTypeNameNotFoundException(name)
override fun nameAlreadyExistsException(name: String) = mixTypeNameAlreadyExistsException(name)
override fun existsByNameAndMaterialType(name: String, materialType: MaterialType): Boolean =
repository.existsByNameAndMaterialType(name, materialType)
repository.existsByNameAndMaterialType(name, materialType)
override fun getByMaterial(material: Material): MixType =
repository.findByMaterial(material) ?: throw EntityNotFoundException(material.name)
repository.findByMaterial(material) ?: throw nameNotFoundException(material.name)
override fun getByNameAndMaterialType(name: String, materialType: MaterialType): MixType =
repository.findByNameAndMaterialType(name, materialType)
?: throw EntityNotFoundException("$name/${materialType.name}")
repository.findByNameAndMaterialType(name, materialType)
?: throw MixTypeNameAndMaterialTypeNotFoundException(name, materialType)
override fun getOrCreateForNameAndMaterialType(name: String, materialType: MaterialType): MixType =
if (existsByNameAndMaterialType(name, materialType))
getByNameAndMaterialType(name, materialType)
else
saveForNameAndMaterialType(name, materialType)
if (existsByNameAndMaterialType(name, materialType))
getByNameAndMaterialType(name, materialType)
else
saveForNameAndMaterialType(name, materialType)
override fun save(entity: MixType): MixType {
if (materialService.existsByName(entity.name))
throw EntityAlreadyExistsException(entity.name)
throw materialNameAlreadyExistsException(entity.name)
return super.save(entity)
}
override fun saveForNameAndMaterialType(name: String, materialType: MaterialType): MixType =
save(
mixType(
name = name,
material = material(
name = name,
inventoryQuantity = Float.MIN_VALUE,
isMixType = true,
materialType = materialType
)
)
save(
mixType(
name = name,
material = material(
name = name,
inventoryQuantity = Float.MIN_VALUE,
isMixType = true,
materialType = materialType
)
)
)
override fun updateForNameAndMaterialType(mixType: MixType, name: String, materialType: MaterialType): MixType =
update(mixType.apply {
this.name = name
material.name = name
material.materialType = materialType
})
update(mixType.apply {
this.name = name
material.name = name
material.materialType = materialType
})
override fun deleteById(id: Long) {
if (!repository.canBeDeleted(id)) throw CannotDeleteEntityException(id)
super.deleteById(id)
override fun delete(entity: MixType) {
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMixTypeException(entity)
super.delete(entity)
}
}

View File

@ -1,6 +1,6 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.model.validation.or
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
@ -32,13 +32,16 @@ interface RecipeService : ExternalModelService<Recipe, RecipeSaveDto, RecipeUpda
@Service
class RecipeServiceImpl(
recipeRepository: RecipeRepository,
val companyService: CompanyService,
val mixService: MixService,
@Lazy val groupService: EmployeeGroupService
recipeRepository: RecipeRepository,
val companyService: CompanyService,
val mixService: MixService,
@Lazy val groupService: EmployeeGroupService
) :
AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeRepository>(recipeRepository),
RecipeService {
AbstractExternalModelService<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeRepository>(recipeRepository),
RecipeService {
override fun idNotFoundException(id: Long) = recipeIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = recipeIdAlreadyExistsException(id)
override fun existsByCompany(company: Company): Boolean = repository.existsByCompany(company)
override fun getAllByCompany(company: Company): Collection<Recipe> = repository.findAllByCompany(company)
@ -46,14 +49,14 @@ class RecipeServiceImpl(
// TODO checks if name is unique in the scope of the [company]
return save(with(entity) {
recipe(
name = name,
description = description,
color = color,
gloss = gloss,
sample = sample,
approbationDate = approbationDate,
remark = remark ?: "",
company = companyService.getById(companyId)
name = name,
description = description,
color = color,
gloss = gloss,
sample = sample,
approbationDate = approbationDate,
remark = remark ?: "",
company = companyService.getById(companyId)
)
})
}
@ -109,7 +112,7 @@ class RecipeServiceImpl(
val recipe = getById(publicDataDto.recipeId)
fun noteForGroup(group: EmployeeGroup) =
publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content
publicDataDto.notes.firstOrNull { it.groupId == group.id }?.content
// Notes
recipe.groupsInformation.map {
@ -128,10 +131,10 @@ class RecipeServiceImpl(
}
override fun addMix(recipe: Recipe, mix: Mix) =
update(recipe.apply { mixes.add(mix) })
update(recipe.apply { mixes.add(mix) })
override fun removeMix(mix: Mix): Recipe =
update(mix.recipe.apply { mixes.remove(mix) })
update(mix.recipe.apply { mixes.remove(mix) })
}
const val RECIPE_IMAGES_DIRECTORY = "images/recipe"
@ -152,11 +155,11 @@ interface RecipeImageService {
@Service
class RecipeImageServiceImpl(val recipeService: RecipeService, val fileService: FileService) : RecipeImageService {
override fun getByIdForRecipe(id: Long, recipeId: Long): ByteArray =
try {
fileService.readAsBytes(getPath(id, recipeId))
} catch (ex: NoSuchFileException) {
throw EntityNotFoundException("$recipeId/$id")
}
try {
fileService.readAsBytes(getPath(id, recipeId))
} catch (ex: NoSuchFileException) {
throw RecipeImageNotFoundException(id, recipeService.getById(recipeId))
}
override fun getAllIdsForRecipe(recipeId: Long): Collection<Long> {
val recipe = recipeService.getById(recipeId)
@ -165,19 +168,19 @@ class RecipeImageServiceImpl(val recipeService: RecipeService, val fileService:
return listOf()
}
return recipeDirectory.listFiles()!! // Should never be null because we check if recipeDirectory is a directory and exists before
.filterNotNull()
.map { it.name.toLong() }
.filterNotNull()
.map { it.name.toLong() }
}
override fun save(image: MultipartFile, recipeId: Long): Long {
/** Gets the next id available for a new image for the recipe with the given [recipeId]. */
fun getNextAvailableId(): Long =
with(getAllIdsForRecipe(recipeId)) {
if (isEmpty())
0
else
maxOrNull()!! + 1L // maxOrNull() cannot return null because existingIds cannot be empty at this point
}
with(getAllIdsForRecipe(recipeId)) {
if (isEmpty())
0
else
maxOrNull()!! + 1L // maxOrNull() cannot return null because existingIds cannot be empty at this point
}
val nextAvailableId = getNextAvailableId()
fileService.write(image, getPath(nextAvailableId, recipeId))
@ -185,7 +188,7 @@ class RecipeImageServiceImpl(val recipeService: RecipeService, val fileService:
}
override fun delete(id: Long, recipeId: Long) =
fileService.delete(getPath(id, recipeId))
fileService.delete(getPath(id, recipeId))
/** Gets the images directory of the recipe with the given [recipeId]. */
fun getRecipeDirectory(recipeId: Long) = File(fileService.getPath("$RECIPE_IMAGES_DIRECTORY/$recipeId"))

View File

@ -1,6 +1,8 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.model.RecipeStep
import dev.fyloz.colorrecipesexplorer.model.recipeStepIdAlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.recipeStepIdNotFoundException
import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository
import org.springframework.stereotype.Service
@ -18,9 +20,12 @@ interface RecipeStepService : ModelService<RecipeStep, RecipeStepRepository> {
@Service
class RecipeStepServiceImpl(recipeStepRepository: RecipeStepRepository) :
AbstractModelService<RecipeStep, RecipeStepRepository>(recipeStepRepository),
RecipeStepService {
// override fun validateStepsCollection(steps: Collection<RecipeStep>): Boolean {
AbstractModelService<RecipeStep, RecipeStepRepository>(recipeStepRepository),
RecipeStepService {
override fun idNotFoundException(id: Long) = recipeStepIdNotFoundException(id)
override fun idAlreadyExistsException(id: Long) = recipeStepIdAlreadyExistsException(id)
// override fun validateStepsCollection(steps: Collection<RecipeStep>): Boolean {
// val sortedSteps = steps.sortedBy { it.position }
//
// fun validateStepPosition(step: RecipeStep) =

View File

@ -1,7 +1,7 @@
package dev.fyloz.colorrecipesexplorer.service
import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.Model
import dev.fyloz.colorrecipesexplorer.model.NamedModel
@ -51,9 +51,6 @@ interface NamedModelService<E : NamedModel, R : JpaRepository<E, *>> : ModelServ
/** Gets the entity with the given [name]. */
fun getByName(name: String): E
/** Deletes the entity with the given [name]. */
fun deleteByName(name: String)
}
@ -65,25 +62,28 @@ abstract class AbstractService<E, R : JpaRepository<E, *>>(override val reposito
}
abstract class AbstractModelService<E : Model, R : JpaRepository<E, Long>>(repository: R) :
AbstractService<E, R>(repository), ModelService<E, R> {
AbstractService<E, R>(repository), ModelService<E, R> {
protected abstract fun idNotFoundException(id: Long): NotFoundException
protected abstract fun idAlreadyExistsException(id: Long): AlreadyExistsException
override fun existsById(id: Long): Boolean = repository.existsById(id)
override fun getById(id: Long): E = repository.findByIdOrNull(id) ?: throw EntityNotFoundException(id)
override fun getById(id: Long): E = repository.findByIdOrNull(id) ?: throw idNotFoundException(id)
override fun save(entity: E): E {
if (entity.id != null && existsById(entity.id!!))
throw EntityAlreadyExistsException(entity.id!!)
throw idAlreadyExistsException(entity.id!!)
return super.save(entity)
}
override fun update(entity: E): E {
assertId(entity.id)
if (!existsById(entity.id!!))
throw EntityNotFoundException(entity.id!!)
throw idNotFoundException(entity.id!!)
return super.update(entity)
}
override fun deleteById(id: Long) =
delete(getById(id)) // Use delete(entity) to prevent code duplication and to ease testing
delete(getById(id)) // Use delete(entity) to prevent code duplication and to ease testing
protected fun assertId(id: Long?) {
Assert.notNull(id, "${javaClass.simpleName}.update() was called with a null identifier")
@ -91,14 +91,16 @@ abstract class AbstractModelService<E : Model, R : JpaRepository<E, Long>>(repos
}
abstract class AbstractNamedModelService<E : NamedModel, R : NamedJpaRepository<E>>(repository: R) :
AbstractModelService<E, R>(repository), NamedModelService<E, R> {
AbstractModelService<E, R>(repository), NamedModelService<E, R> {
protected abstract fun nameNotFoundException(name: String): NotFoundException
protected abstract fun nameAlreadyExistsException(name: String): AlreadyExistsException
override fun existsByName(name: String): Boolean = repository.existsByName(name)
override fun getByName(name: String): E = repository.findByName(name) ?: throw EntityNotFoundException(name)
override fun deleteByName(name: String) = repository.deleteByName(name)
override fun getByName(name: String): E = repository.findByName(name) ?: throw nameNotFoundException(name)
override fun save(entity: E): E {
if (existsByName(entity.name))
throw EntityAlreadyExistsException(entity.name)
throw nameAlreadyExistsException(entity.name)
return super.save(entity)
}
@ -107,7 +109,7 @@ abstract class AbstractNamedModelService<E : NamedModel, R : NamedJpaRepository<
assertName(entity.name)
with(repository.findByName(entity.name)) {
if (this != null && id != entity.id)
throw EntityAlreadyExistsException(entity.name)
throw nameAlreadyExistsException(entity.name)
}
return super.update(entity)
}
@ -134,23 +136,23 @@ interface ExternalService<E, S : EntityDto<E>, U : EntityDto<E>, R : JpaReposito
/** An [ExternalService] for entities implementing the [Model] interface. */
interface ExternalModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>> :
ModelService<E, R>, ExternalService<E, S, U, R>
ModelService<E, R>, ExternalService<E, S, U, R>
/** An [ExternalService] for entities implementing the [NamedModel] interface. */
interface ExternalNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>> :
NamedModelService<E, R>, ExternalModelService<E, S, U, R>
NamedModelService<E, R>, ExternalModelService<E, S, U, R>
/** An [AbstractService] with the functionalities of a [ExternalService]. */
@Suppress("unused")
abstract class AbstractExternalService<E, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, *>>(repository: R) :
AbstractService<E, R>(repository), ExternalService<E, S, U, R>
AbstractService<E, R>(repository), ExternalService<E, S, U, R>
/** An [AbstractModelService] with the functionalities of a [ExternalService]. */
abstract class AbstractExternalModelService<E : Model, S : EntityDto<E>, U : EntityDto<E>, R : JpaRepository<E, Long>>(
repository: R
repository: R
) : AbstractModelService<E, R>(repository), ExternalModelService<E, S, U, R>
/** An [AbstractNamedModelService] with the functionalities of a [ExternalService]. */
abstract class AbstractExternalNamedModelService<E : NamedModel, S : EntityDto<E>, U : EntityDto<E>, R : NamedJpaRepository<E>>(
repository: R
repository: R
) : AbstractNamedModelService<E, R>(repository), ExternalNamedModelService<E, S, U, R>

View File

@ -10,13 +10,12 @@ import java.io.IOException
import java.io.InputStream
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Paths
@Service
class FileService(
private val resourcesLoader: ResourceLoader,
private val creProperties: CreProperties,
private val logger: Logger
private val resourcesLoader: ResourceLoader,
private val creProperties: CreProperties,
private val logger: Logger
) {
/** Reads the resource at the given [path] as a [String]. */
fun readResource(path: String): String = try {
@ -35,18 +34,18 @@ class FileService(
/** Reads the file at the given [path] as a [ByteArray]. */
fun readAsBytes(path: String) =
withFileAt(path) { this.readBytes() }
withFileAt(path) { this.readBytes() }
/** Writes the given [multipartFile] to the file at the given [path]. */
fun write(multipartFile: MultipartFile, path: String): Boolean =
if (multipartFile.size <= 0) true
else try {
multipartFile.transferTo(create(path).toPath())
true
} catch (ex: IOException) {
logger.error("Unable to write multipart file", ex)
false
}
if (multipartFile.size <= 0) true
else try {
multipartFile.transferTo(create(path).toPath())
true
} catch (ex: IOException) {
logger.error("Unable to write multipart file", ex)
false
}
/** Creates a new file at the given [path]. If the file already exists, nothing will be done. */
fun create(path: String) = withFileAt(path) {
@ -77,8 +76,8 @@ class FileService(
/** Runs the given [block] in the context of a file with the given [path]. */
fun <T> withFileAt(path: String, block: File.() -> T) =
File(path).block()
File(path).block()
fun getPath(fileName: String): String =
"${creProperties.workingDirectory}/$fileName"
"${creProperties.workingDirectory}/$fileName"
}

View File

@ -1,10 +1,9 @@
package dev.fyloz.colorrecipesexplorer.service.files
import dev.fyloz.colorrecipesexplorer.exception.SimdutWriteException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.service.MaterialService
import org.slf4j.Logger
import org.springframework.context.annotation.Lazy
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
import java.io.IOException
@ -13,12 +12,12 @@ const val SIMDUT_DIRECTORY = "simdut"
@Service
class SimdutService(
private val fileService: FileService,
private val logger: Logger
private val fileService: FileService,
private val logger: Logger
) {
/** Checks if the given [material] has a SIMDUT file. */
fun exists(material: Material) =
fileService.exists(getPath(material))
fileService.exists(getPath(material))
/** Reads the SIMDUT file of the given [material]. */
fun read(material: Material): ByteArray {
@ -47,13 +46,21 @@ class SimdutService(
/** Deletes the SIMDUT file of the given [material]. */
fun delete(material: Material) =
fileService.delete(getPath(material))
fileService.delete(getPath(material))
/** Gets the path of the SIMDUT file of the given [material]. */
fun getPath(material: Material) =
fileService.getPath("$SIMDUT_DIRECTORY/${getSimdutFileName(material)}")
fileService.getPath("$SIMDUT_DIRECTORY/${getSimdutFileName(material)}")
/** Gets the name of the SIMDUT file of the given [material]. */
fun getSimdutFileName(material: Material) =
material.id.toString()
material.id.toString()
}
class SimdutWriteException(material: Material) :
RestException(
"simdut-write",
"Could not write SIMDUT file",
HttpStatus.INTERNAL_SERVER_ERROR,
"Could not write the SIMDUT file for the material ${material.name} to the disk"
)

View File

@ -2,8 +2,8 @@ package dev.fyloz.colorrecipesexplorer.service.utils
/** Returns a list containing the result of the given [transform] applied to each item of the [Iterable]. If the given [transform] throws, the [Throwable] will be passed to the given [throwableConsumer]. */
inline fun <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
throwableConsumer: (E) -> Unit = {},
transform: (T) -> R
throwableConsumer: (E) -> Unit = {},
transform: (T) -> R
): List<R> = this.mapNotNull {
try {
transform(it)

View File

@ -7,7 +7,6 @@ cre.security.jwt-duration=18000000
# Root user
cre.security.root.id=9999
cre.security.root.password=password
# Default material types
entities.material-types.systemTypes[0].name=Aucun
entities.material-types.systemTypes[0].prefix=
@ -16,11 +15,9 @@ entities.material-types.systemTypes[1].name=Base
entities.material-types.systemTypes[1].prefix=BAS
entities.material-types.systemTypes[1].usepercentages=false
entities.material-types.baseName=Base
# Database manager
databaseupdater.username=root
databaseupdater.password=pass
# DEBUG
spring.jpa.show-sql=true
# Do not modify

View File

@ -1 +1 @@
junit.jupiter.testinstance.lifecycle.default = per_class
junit.jupiter.testinstance.lifecycle.default=per_class

View File

@ -10,8 +10,8 @@ import kotlin.test.assertEquals
@DataJpaTest(excludeAutoConfiguration = [LiquibaseAutoConfiguration::class])
class MaterialRepositoryTest @Autowired constructor(
private val materialRepository: MaterialRepository,
private val entityManager: TestEntityManager
private val materialRepository: MaterialRepository,
private val entityManager: TestEntityManager
) {
// updateInventoryQuantityById()

View File

@ -10,8 +10,8 @@ import kotlin.test.assertEquals
@DataJpaTest(excludeAutoConfiguration = [LiquibaseAutoConfiguration::class])
class MixRepositoryTest @Autowired constructor(
private val mixRepository: MixRepository,
private val entityManager: TestEntityManager
private val mixRepository: MixRepository,
private val entityManager: TestEntityManager
) {
// updateLocationById()

View File

@ -1,8 +1,9 @@
package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.exception.RestException
import dev.fyloz.colorrecipesexplorer.model.EntityDto
import dev.fyloz.colorrecipesexplorer.model.Model
import dev.fyloz.colorrecipesexplorer.model.NamedModel
@ -26,8 +27,8 @@ abstract class AbstractServiceTest<E, S : Service<E, *>, R : JpaRepository<E, *>
protected val entityList: List<E>
get() = listOf(
entity,
anotherEntity
entity,
anotherEntity
)
@AfterEach
@ -90,7 +91,7 @@ abstract class AbstractServiceTest<E, S : Service<E, *>, R : JpaRepository<E, *>
}
abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : JpaRepository<E, Long>> :
AbstractServiceTest1<E, S, R>() {
AbstractServiceTest1<E, S, R>() {
// existsById()
@ -124,23 +125,21 @@ abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : J
}
@Test
open fun `getById() throws EntityNotFoundException when no entity with the given id exists in the repository`() {
open fun `getById() throws NotFoundException when no entity with the given id exists in the repository`() {
whenever(repository.findById(entity.id!!)).doReturn(Optional.empty())
val exception = assertThrows<EntityNotFoundException> { service.getById(entity.id!!) }
assertTrue(exception.value is Long)
assertEquals(entity.id, exception.value as Long)
assertThrows<NotFoundException> { service.getById(entity.id!!) }
.assertErrorCode()
}
// save()
@Test
open fun `save() throws EntityAlreadyExistsException when an entity with the given id exists in the repository`() {
doReturn(true).whenever(repository).existsById(entity.id!!)
open fun `save() throws AlreadyExistsException when an entity with the given id exists in the repository`() {
doReturn(true).whenever(service).existsById(entity.id!!)
val exception = assertThrows<EntityAlreadyExistsException> { service.save(entity) }
assertTrue(exception.value is Long)
assertEquals(entity.id, exception.value as Long)
assertThrows<AlreadyExistsException> { service.save(entity) }
.assertErrorCode()
}
// update()
@ -158,12 +157,11 @@ abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : J
}
@Test
open fun `update() throws EntityNotFoundException when no entity with the given id exists in the repository`() {
open fun `update() throws NotFoundException when no entity with the given id exists in the repository`() {
doReturn(false).whenever(service).existsById(entity.id!!)
val exception = assertThrows<EntityNotFoundException> { service.update(entity) }
assertTrue(exception.value is Long)
assertEquals(entity.id, exception.value as Long)
assertThrows<NotFoundException> { service.update(entity) }
.assertErrorCode()
}
// deleteById()
@ -179,7 +177,7 @@ abstract class AbstractModelServiceTest<E : Model, S : ModelService<E, *>, R : J
}
abstract class AbstractNamedModelServiceTest<E : NamedModel, S : NamedModelService<E, *>, R : NamedJpaRepository<E>> :
AbstractModelServiceTest<E, S, R>() {
AbstractModelServiceTest<E, S, R>() {
protected abstract val entityWithEntityName: E
// existsByName()
@ -214,21 +212,21 @@ abstract class AbstractNamedModelServiceTest<E : NamedModel, S : NamedModelServi
}
@Test
open fun `getByName() throws EntityNotFoundException when no entity with the given name exists`() {
open fun `getByName() throws NotFoundException when no entity with the given name exists`() {
whenever(repository.findByName(entity.name)).doReturn(null)
val exception = assertThrows<EntityNotFoundException> { service.getByName(entity.name) }
assertEquals(entity.name, exception.value)
assertThrows<NotFoundException> { service.getByName(entity.name) }
.assertErrorCode("name")
}
// save()
@Test
open fun `save() throws EntityAlreadyExistsException when an entity with the given name exists`() {
open fun `save() throws AlreadyExistsException when an entity with the given name exists`() {
doReturn(true).whenever(service).existsByName(entity.name)
val exception = assertThrows<EntityAlreadyExistsException> { service.save(entity) }
assertEquals(entity.name, exception.value)
assertThrows<AlreadyExistsException> { service.save(entity) }
.assertErrorCode("name")
}
// update()
@ -247,32 +245,20 @@ abstract class AbstractNamedModelServiceTest<E : NamedModel, S : NamedModelServi
}
@Test
override fun `update() throws EntityNotFoundException when no entity with the given id exists in the repository`() {
override fun `update() throws NotFoundException when no entity with the given id exists in the repository`() {
whenever(repository.findByName(entity.name)).doReturn(null)
doReturn(false).whenever(service).existsById(entity.id!!)
val exception = assertThrows<EntityNotFoundException> { service.update(entity) }
assertTrue(exception.value is Long)
assertEquals(entity.id, exception.value as Long)
assertThrows<NotFoundException> { service.update(entity) }
}
@Test
open fun `update() throws EntityAlreadyExistsException when an entity with the updated name exists`() {
open fun `update() throws AlreadyExistsException when an entity with the updated name exists`() {
whenever(repository.findByName(entity.name)).doReturn(entityWithEntityName)
doReturn(entity).whenever(service).getById(entity.id!!)
val exception = assertThrows<EntityAlreadyExistsException> { service.update(entity) }
assertEquals(entity.name, exception.value)
}
// deleteByName()
@Test
open fun `deleteByName() deletes the entity with the given name in the repository`() {
service.deleteByName(entity.name)
verify(repository).deleteByName(entity.name)
assertThrows<AlreadyExistsException> { service.update(entity) }
.assertErrorCode("name")
}
}
@ -284,7 +270,7 @@ interface ExternalModelServiceTest {
// ==== IMPLEMENTATIONS FOR EXTERNAL SERVICES ====
// Lots of code duplication but I don't have a better solution for now
abstract class AbstractExternalModelServiceTest<E : Model, N : EntityDto<E>, U : EntityDto<E>, S : ExternalModelService<E, N, U, *>, R : JpaRepository<E, Long>> :
AbstractModelServiceTest<E, S, R>(), ExternalModelServiceTest {
AbstractModelServiceTest<E, S, R>(), ExternalModelServiceTest {
protected abstract val entitySaveDto: N
protected abstract val entityUpdateDto: U
@ -296,7 +282,7 @@ abstract class AbstractExternalModelServiceTest<E : Model, N : EntityDto<E>, U :
}
abstract class AbstractExternalNamedModelServiceTest<E : NamedModel, N : EntityDto<E>, U : EntityDto<E>, S : ExternalNamedModelService<E, N, U, *>, R : NamedJpaRepository<E>> :
AbstractNamedModelServiceTest<E, S, R>(), ExternalModelServiceTest {
AbstractNamedModelServiceTest<E, S, R>(), ExternalModelServiceTest {
protected abstract val entitySaveDto: N
protected abstract val entityUpdateDto: U
@ -307,12 +293,25 @@ abstract class AbstractExternalNamedModelServiceTest<E : NamedModel, N : EntityD
}
}
fun NotFoundException.assertErrorCode(identifierName: String = "id") =
this.assertErrorCode("notfound", identifierName)
fun AlreadyExistsException.assertErrorCode(identifierName: String = "id") =
this.assertErrorCode("exists", identifierName)
fun RestException.assertErrorCode(type: String, identifierName: String) {
assertTrue {
this.errorCode.startsWith(type) &&
this.errorCode.endsWith(identifierName)
}
}
fun <E : Model, N : EntityDto<E>> withBaseSaveDtoTest(
entity: E,
entitySaveDto: N,
service: ExternalService<E, N, *, *>,
saveMockMatcher: () -> E = { entity },
op: () -> Unit = {}
entity: E,
entitySaveDto: N,
service: ExternalService<E, N, *, *>,
saveMockMatcher: () -> E = { entity },
op: () -> Unit = {}
) {
doReturn(entity).whenever(service).save(saveMockMatcher())
doReturn(entity).whenever(entitySaveDto).toEntity()
@ -326,11 +325,11 @@ fun <E : Model, N : EntityDto<E>> withBaseSaveDtoTest(
}
fun <E : Model, U : EntityDto<E>> withBaseUpdateDtoTest(
entity: E,
entityUpdateDto: U,
service: ExternalModelService<E, *, U, *>,
updateMockMatcher: () -> E,
op: E.() -> Unit = {}
entity: E,
entityUpdateDto: U,
service: ExternalModelService<E, *, U, *>,
updateMockMatcher: () -> E,
op: E.() -> Unit = {}
) {
doAnswer { it.arguments[0] }.whenever(service).update(updateMockMatcher())
doReturn(entity).whenever(entityUpdateDto).toEntity()

View File

@ -2,13 +2,15 @@ package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.config.defaultGroupCookieName
import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.EmployeeGroupRepository
import dev.fyloz.colorrecipesexplorer.repository.EmployeeRepository
import org.junit.jupiter.api.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.springframework.mock.web.MockHttpServletResponse
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UsernameNotFoundException
@ -16,10 +18,13 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import java.util.*
import javax.servlet.http.Cookie
import javax.servlet.http.HttpServletRequest
import kotlin.test.*
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
class EmployeeServiceTest :
AbstractExternalModelServiceTest<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeService, EmployeeRepository>() {
AbstractExternalModelServiceTest<Employee, EmployeeSaveDto, EmployeeUpdateDto, EmployeeService, EmployeeRepository>() {
private val passwordEncoder = BCryptPasswordEncoder()
override val entity: Employee = employee(passwordEncoder, id = 0L)
@ -31,22 +36,23 @@ class EmployeeServiceTest :
override val entityUpdateDto: EmployeeUpdateDto = spy(employeeUpdateDto(id = 0L))
override val repository: EmployeeRepository = mock()
override val service: EmployeeService = spy(EmployeeServiceImpl(repository, passwordEncoder))
private val employeeGroupService: EmployeeGroupService = mock()
override val service: EmployeeService = spy(EmployeeServiceImpl(repository, employeeGroupService, passwordEncoder))
private val entitySaveDtoEmployee = Employee(
entitySaveDto.id,
entitySaveDto.firstName,
entitySaveDto.lastName,
passwordEncoder.encode(entitySaveDto.password),
isDefaultGroupUser = false,
isSystemUser = false,
group = null,
permissions = entitySaveDto.permissions
entitySaveDto.id,
entitySaveDto.firstName,
entitySaveDto.lastName,
passwordEncoder.encode(entitySaveDto.password),
isDefaultGroupUser = false,
isSystemUser = false,
group = null,
permissions = entitySaveDto.permissions
)
@AfterEach
override fun afterEach() {
reset(entitySaveDto, entityUpdateDto)
reset(employeeGroupService)
super.afterEach()
}
@ -73,33 +79,29 @@ class EmployeeServiceTest :
// getById()
@Test
fun `getById() throws EntityNotFoundException when the corresponding employee is a default group user`() {
fun `getById() throws NotFoundException when the corresponding employee is a default group user`() {
whenever(repository.findById(entityDefaultGroupUser.id)).doReturn(Optional.of(entityDefaultGroupUser))
val exception = assertThrows<EntityNotFoundException> {
assertThrows<NotFoundException> {
service.getById(
entityDefaultGroupUser.id,
ignoreDefaultGroupUsers = true,
ignoreSystemUsers = false
entityDefaultGroupUser.id,
ignoreDefaultGroupUsers = true,
ignoreSystemUsers = false
)
}
assertTrue(exception.value is Long)
assertEquals(entityDefaultGroupUser.id, exception.value as Long)
}.assertErrorCode()
}
@Test
fun `getById() throws EntityNotFoundException when the corresponding employee is a system user`() {
fun `getById() throws NotFoundException when the corresponding employee is a system user`() {
whenever(repository.findById(entitySystemUser.id)).doReturn(Optional.of(entitySystemUser))
val exception = assertThrows<EntityNotFoundException> {
assertThrows<NotFoundException> {
service.getById(
entitySystemUser.id,
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true
entitySystemUser.id,
ignoreDefaultGroupUsers = false,
ignoreSystemUsers = true
)
}
assertTrue(exception.value is Long)
assertEquals(entitySystemUser.id, exception.value as Long)
}.assertErrorCode()
}
// getByGroup()
@ -147,18 +149,20 @@ class EmployeeServiceTest :
}
@Test
fun `save() throws EntityAlreadyExistsException when firstName and lastName exists`() {
fun `save() throws AlreadyExistsException when firstName and lastName exists`() {
doReturn(true).whenever(repository).existsByFirstNameAndLastName(entity.firstName, entity.lastName)
val exception = assertThrows<EntityAlreadyExistsException> { service.save(entity) }
assertEquals("${entity.firstName} ${entity.lastName}", exception.value)
assertThrows<AlreadyExistsException> { service.save(entity) }
.assertErrorCode("fullName")
}
@Test
override fun `save(dto) calls and returns save() with the created entity`() {
withBaseSaveDtoTest(entity, entitySaveDto, service, {argThat {
this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName
}})
withBaseSaveDtoTest(entity, entitySaveDto, service, {
argThat {
this.id == entity.id && this.firstName == entity.firstName && this.lastName == entity.lastName
}
})
}
@Test
@ -175,29 +179,27 @@ class EmployeeServiceTest :
@Test
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
@Test
fun `update() throws EntityAlreadyExistsException when a different employee with the given first name and last name exists`() {
fun `update() throws AlreadyExistsException when a different employee with the given first name and last name exists`() {
whenever(repository.findByFirstNameAndLastName(entity.firstName, entity.lastName)).doReturn(
entityDefaultGroupUser
entityDefaultGroupUser
)
doReturn(entity).whenever(service).getById(eq(entity.id), any(), any())
val exception = assertThrows<EntityAlreadyExistsException> {
assertThrows<AlreadyExistsException> {
service.update(
entity,
true,
ignoreSystemUsers = true
entity,
true,
ignoreSystemUsers = true
)
}
assertTrue(exception.value is String)
assertEquals("${entity.firstName} ${entity.lastName}", exception.value as String)
}.assertErrorCode("fullName")
}
}
class EmployeeGroupServiceTest :
AbstractExternalNamedModelServiceTest<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupService, EmployeeGroupRepository>() {
AbstractExternalNamedModelServiceTest<EmployeeGroup, EmployeeGroupSaveDto, EmployeeGroupUpdateDto, EmployeeGroupService, EmployeeGroupRepository>() {
private val employeeService: EmployeeService = mock()
override val repository: EmployeeGroupRepository = mock()
override val service: EmployeeGroupServiceImpl = spy(EmployeeGroupServiceImpl(employeeService, repository))
@ -257,13 +259,12 @@ class EmployeeGroupServiceTest :
}
@Test
fun `getRequestDefaultGroup() throws EntityNotFoundException when the HTTP request does not contains a cookie for the default group`() {
fun `getRequestDefaultGroup() throws NoDefaultGroupException when the HTTP request does not contains a cookie for the default group`() {
val request: HttpServletRequest = mock()
whenever(request.cookies).doReturn(arrayOf())
val exception = assertThrows<EntityNotFoundException> { service.getRequestDefaultGroup(request) }
assertEquals("defaultGroup", exception.value)
assertThrows<NoDefaultGroupException> { service.getRequestDefaultGroup(request) }
}
// setResponseDefaultGroup()
@ -297,7 +298,7 @@ class EmployeeGroupServiceTest :
@Test
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
}
class EmployeeUserDetailsServiceTest {
@ -317,7 +318,7 @@ class EmployeeUserDetailsServiceTest {
fun `loadUserByUsername() calls loadUserByEmployeeId() with the given username as an id`() {
whenever(employeeService.getById(eq(employee.id), any(), any())).doReturn(employee)
doReturn(User(employee.id.toString(), employee.password, listOf())).whenever(service)
.loadUserByEmployeeId(employee.id)
.loadUserByEmployeeId(employee.id)
service.loadUserByUsername(employee.id.toString())
@ -327,9 +328,7 @@ class EmployeeUserDetailsServiceTest {
@Test
fun `loadUserByUsername() throws UsernameNotFoundException when no employee with the given id exists`() {
whenever(employeeService.getById(eq(employee.id), any(), any())).doThrow(
EntityNotFoundException(
employee.id
)
employeeIdNotFoundException(employee.id)
)
assertThrows<UsernameNotFoundException> { service.loadUserByUsername(employee.id.toString()) }

View File

@ -9,7 +9,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
class CompanyServiceTest :
AbstractExternalNamedModelServiceTest<Company, CompanySaveDto, CompanyUpdateDto, CompanyService, CompanyRepository>() {
AbstractExternalNamedModelServiceTest<Company, CompanySaveDto, CompanyUpdateDto, CompanyService, CompanyRepository>() {
private val recipeService: RecipeService = mock()
override val repository: CompanyRepository = mock()
override val service: CompanyService = spy(CompanyServiceImpl(repository, recipeService))
@ -57,11 +57,27 @@ class CompanyServiceTest :
@Test
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
// delete()
override fun `delete() deletes in the repository`() {
whenCanBeDeleted {
super.`delete() deletes in the repository`()
}
}
// deleteById()
override fun `deleteById() deletes the entity with the given id in the repository`() {
super.`deleteById() deletes the entity with the given id in the repository`()
whenCanBeDeleted {
super.`deleteById() deletes the entity with the given id in the repository`()
}
}
private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) {
whenever(repository.canBeDeleted(id)).doReturn(true)
test()
}
}

View File

@ -1,8 +1,6 @@
package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.exception.LowQuantitiesException
import dev.fyloz.colorrecipesexplorer.exception.LowQuantityException
import dev.fyloz.colorrecipesexplorer.model.*
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
@ -25,15 +23,15 @@ class InventoryServiceTest {
@Test
fun `add(materialQuantities) calls add() for each MaterialQuantityDto`() {
val materialQuantities = listOf(
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val storedQuantity = 2000f
doAnswer { storedQuantity + (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(service)
.add(any<MaterialQuantityDto>())
.add(any<MaterialQuantityDto>())
val found = service.add(materialQuantities)
@ -52,8 +50,8 @@ class InventoryServiceTest {
val found = service.add(this)
verify(materialService).updateQuantity(
argThat { this.id == this@withGivenQuantities.material },
eq(this.quantity)
argThat { this.id == this@withGivenQuantities.material },
eq(this.quantity)
)
assertEquals(updatedQuantity, found)
}
@ -67,15 +65,15 @@ class InventoryServiceTest {
val materialPercents = material(id = 1L, materialType = materialType(usePercentages = true))
val mixRatio = mixRatio(ratio = 1.5f)
val mix = mix(
id = mixRatio.id,
mixMaterials = mutableSetOf(
mixMaterial(id = 0L, material = material, quantity = 1000f, position = 0),
mixMaterial(id = 1L, material = materialPercents, quantity = 50f, position = 1)
)
id = mixRatio.id,
mixMaterials = mutableSetOf(
mixMaterial(id = 0L, material = material, quantity = 1000f, position = 0),
mixMaterial(id = 1L, material = materialPercents, quantity = 50f, position = 1)
)
)
val expectedQuantities = mapOf(
0L to 1500f,
1L to 750f
0L to 1500f,
1L to 750f
)
whenever(mixService.getById(mix.id!!)).doReturn(mix)
@ -99,15 +97,15 @@ class InventoryServiceTest {
@Test
fun `deduct(materialQuantities) calls deduct() for each MaterialQuantityDto`() {
val materialQuantities = listOf(
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val storedQuantity = 5000f
doAnswer { storedQuantity - (it.arguments[0] as MaterialQuantityDto).quantity }.whenever(service)
.deduct(any<MaterialQuantityDto>())
.deduct(any<MaterialQuantityDto>())
val found = service.deduct(materialQuantities)
@ -118,22 +116,20 @@ class InventoryServiceTest {
}
@Test
fun `deduct(materialQuantities) throws LowQuantitiesException when there is not enough inventory of a given material`() {
fun `deduct(materialQuantities) throws MultiplesNotEnoughInventoryException when there is not enough inventory of a given material`() {
val materialQuantities = listOf(
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
materialQuantityDto(materialId = 1, quantity = 1234f),
materialQuantityDto(materialId = 2, quantity = 2345f),
materialQuantityDto(materialId = 3, quantity = 3456f),
materialQuantityDto(materialId = 4, quantity = 4567f)
)
val inventoryQuantity = 3000f
val lowQuantities = materialQuantities.filter { it.quantity > inventoryQuantity }
materialQuantities.forEach {
withGivenQuantities(inventoryQuantity, it)
}
val exception = assertThrows<LowQuantitiesException> { service.deduct(materialQuantities) }
assertTrue { exception.materialQuantities.containsAll(lowQuantities) }
assertThrows<MultiplesNotEnoughInventoryException> { service.deduct(materialQuantities) }
}
@Test
@ -145,26 +141,25 @@ class InventoryServiceTest {
val found = service.deduct(this)
verify(materialService).updateQuantity(
argThat { this.id == this@withGivenQuantities.material },
eq(-this.quantity)
argThat { this.id == this@withGivenQuantities.material },
eq(-this.quantity)
)
assertEquals(updatedQuantity, found)
}
}
@Test
fun `deduct(materialQuantity) throws LowQuantityException when there is not enough inventory of the given material`() {
fun `deduct(materialQuantity) throws NotEnoughInventoryException when there is not enough inventory of the given material`() {
withGivenQuantities(0f, 1000f) {
val exception = assertThrows<LowQuantityException> { service.deduct(this) }
assertEquals(this, exception.materialQuantity)
assertThrows<NotEnoughInventoryException> { service.deduct(this) }
}
}
private fun withGivenQuantities(
stored: Float,
quantity: Float,
materialId: Long = 0L,
test: MaterialQuantityDto.(Float) -> Unit = {}
stored: Float,
quantity: Float,
materialId: Long = 0L,
test: MaterialQuantityDto.(Float) -> Unit = {}
) {
val materialQuantity = materialQuantityDto(materialId = materialId, quantity = quantity)
@ -172,9 +167,9 @@ class InventoryServiceTest {
}
private fun withGivenQuantities(
stored: Float,
materialQuantity: MaterialQuantityDto,
test: MaterialQuantityDto.(Float) -> Unit = {}
stored: Float,
materialQuantity: MaterialQuantityDto,
test: MaterialQuantityDto.(Float) -> Unit = {}
) {
val material = material(id = materialQuantity.material, inventoryQuantity = stored)

View File

@ -1,7 +1,7 @@
package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MaterialRepository
import dev.fyloz.colorrecipesexplorer.service.files.SimdutService
@ -14,14 +14,14 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
class MaterialServiceTest :
AbstractExternalNamedModelServiceTest<Material, MaterialSaveDto, MaterialUpdateDto, MaterialService, MaterialRepository>() {
AbstractExternalNamedModelServiceTest<Material, MaterialSaveDto, MaterialUpdateDto, MaterialService, MaterialRepository>() {
override val repository: MaterialRepository = mock()
private val simdutService: SimdutService = mock()
private val recipeService: RecipeService = mock()
private val mixService: MixService = mock()
private val materialTypeService: MaterialTypeService = mock()
override val service: MaterialService =
spy(MaterialServiceImpl(repository, simdutService, recipeService, mixService, materialTypeService))
spy(MaterialServiceImpl(repository, simdutService, recipeService, mixService, materialTypeService))
override val entity: Material = material(id = 0L, name = "material")
override val anotherEntity: Material = material(id = 1L, name = "another material")
@ -99,19 +99,19 @@ class MaterialServiceTest :
@Test
fun `getAllIdsWithSimdut() returns a list containing the identifier of every material with a SIMDUT file`() {
val materials = listOf(
material(id = 0L),
material(id = 1L),
material(id = 2L),
material(id = 3L)
material(id = 0L),
material(id = 1L),
material(id = 2L),
material(id = 3L)
)
val hasSimdut = mapOf(
*materials
.map { it.id!! to it.evenId }
.toTypedArray()
*materials
.map { it.id!! to it.evenId }
.toTypedArray()
)
val expectedIds = hasSimdut
.filter { it.value }
.map { it.key }
.filter { it.value }
.map { it.key }
whenever(simdutService.exists(any())).doAnswer { hasSimdut[(it.arguments[0] as Material).id] }
doReturn(materials).whenever(service).getAllNotMixType()
@ -124,11 +124,11 @@ class MaterialServiceTest :
// save()
@Test
fun `save() throws EntityAlreadyExistsException when a material with the given name exists in the repository`() {
fun `save() throws AlreadyExistsException when a material with the given name exists in the repository`() {
doReturn(true).whenever(service).existsByName(entity.name)
val exception = assertThrows<EntityAlreadyExistsException> { service.save(entity) }
assertEquals(entity.name, exception.value)
assertThrows<AlreadyExistsException> { service.save(entity) }
.assertErrorCode("name")
}
@Test
@ -152,15 +152,15 @@ class MaterialServiceTest :
// update()
@Test
fun `update() throws EntityAlreadyExistsException when another material with the updated name exists in the repository`() {
fun `update() throws AlreadyExistsException when another material with the updated name exists in the repository`() {
val material = material(id = 0L, name = "name")
val anotherMaterial = material(id = 1L, name = "name")
whenever(repository.findByName(material.name)).doReturn(anotherMaterial)
doReturn(entity).whenever(service).getById(material.id!!)
val exception = assertThrows<EntityAlreadyExistsException> { service.update(material) }
assertEquals(material.name, exception.value)
assertThrows<AlreadyExistsException> { service.update(material) }
.assertErrorCode("name")
}
// updateQuantity()
@ -186,7 +186,7 @@ class MaterialServiceTest :
val anotherMixTypeMaterial = material(id = 2L, isMixType = true)
val materials = listOf(normalMaterial, mixTypeMaterial, anotherMixTypeMaterial)
val recipe =
recipe(id = 0L, mixes = mutableListOf(mix(mixType = mixType(id = 0L, material = mixTypeMaterial))))
recipe(id = 0L, mixes = mutableListOf(mix(mixType = mixType(id = 0L, material = mixTypeMaterial))))
whenever(recipeService.getById(recipe.id!!)).doReturn(recipe)
doReturn(materials).whenever(service).getAll()
@ -239,11 +239,27 @@ class MaterialServiceTest :
// delete()
override fun `delete() deletes in the repository`() {
whenCanBeDeleted {
super.`delete() deletes in the repository`()
}
}
// deleteById()
override fun `deleteById() deletes the entity with the given id in the repository`() {
super.`deleteById() deletes the entity with the given id in the repository`()
whenCanBeDeleted {
super.`deleteById() deletes the entity with the given id in the repository`()
}
}
/** Utility property to check if the identifier of the given [Material] is even. */
val Material.evenId: Boolean
private val Material.evenId: Boolean
get() = this.id!! % 2 == 0L
private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) {
whenever(repository.canBeDeleted(id)).doReturn(true)
test()
}
}

View File

@ -1,8 +1,8 @@
package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
import org.junit.jupiter.api.AfterEach
@ -13,7 +13,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
class MaterialTypeServiceTest :
AbstractExternalNamedModelServiceTest<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeService, MaterialTypeRepository>() {
AbstractExternalNamedModelServiceTest<MaterialType, MaterialTypeSaveDto, MaterialTypeUpdateDto, MaterialTypeService, MaterialTypeRepository>() {
override val repository: MaterialTypeRepository = mock()
private val materialService: MaterialService = mock()
override val service: MaterialTypeService = spy(MaterialTypeServiceImpl(repository, materialService))
@ -24,7 +24,7 @@ class MaterialTypeServiceTest :
private val anotherSystemType = materialType(id = 4L, name = "another systype", prefix = "ASY", systemType = true)
override val entitySaveDto: MaterialTypeSaveDto = spy(materialTypeSaveDto(name = "material type", prefix = "MAT"))
override val entityUpdateDto: MaterialTypeUpdateDto =
spy(materialTypeUpdateDto(id = 0L, name = "material type", prefix = "MAT"))
spy(materialTypeUpdateDto(id = 0L, name = "material type", prefix = "MAT"))
@AfterEach
override fun afterEach() {
@ -106,18 +106,18 @@ class MaterialTypeServiceTest :
// saveMaterialType()
@Test
fun `saveMaterialType() throws EntityAlreadyExistsException when a material type with the given prefix already exists`() {
fun `saveMaterialType() throws AlreadyExistsException when a material type with the given prefix already exists`() {
doReturn(true).whenever(service).existsByPrefix(entity.prefix)
val exception = assertThrows<EntityAlreadyExistsException> { service.save(entity) }
assertEquals(entity.prefix, exception.value)
assertThrows<AlreadyExistsException> { service.save(entity) }
.assertErrorCode("prefix")
}
// update()
@Test
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
override fun `update() saves in the repository and returns the updated value`() {
whenever(repository.save(entity)).doReturn(entity)
@ -132,49 +132,53 @@ class MaterialTypeServiceTest :
assertEquals(entity, found)
}
override fun `update() throws EntityNotFoundException when no entity with the given id exists in the repository`() {
override fun `update() throws NotFoundException when no entity with the given id exists in the repository`() {
whenever(repository.findByName(entity.name)).doReturn(null)
whenever(repository.findByPrefix(entity.prefix)).doReturn(null)
doReturn(false).whenever(service).existsById(entity.id!!)
doReturn(null).whenever(service).getById(entity.id!!)
val exception = assertThrows<EntityNotFoundException> { service.update(entity) }
assertTrue(exception.value is Long)
assertEquals(entity.id, exception.value as Long)
assertThrows<NotFoundException> { service.update(entity) }
.assertErrorCode()
}
override fun `update() throws EntityAlreadyExistsException when an entity with the updated name exists`() {
override fun `update() throws AlreadyExistsException when an entity with the updated name exists`() {
whenever(repository.findByName(entity.name)).doReturn(entityWithEntityName)
whenever(repository.findByPrefix(entity.prefix)).doReturn(null)
doReturn(true).whenever(service).existsById(entity.id!!)
doReturn(entity).whenever(service).getById(entity.id!!)
val exception = assertThrows<EntityAlreadyExistsException> { service.update(entity) }
assertEquals(entity.name, exception.value)
assertThrows<AlreadyExistsException> { service.update(entity) }
.assertErrorCode("name")
}
@Test
fun `update() throws EntityAlreadyExistsException when an entity with the updated prefix exists`() {
fun `update() throws AlreadyExistsException when an entity with the updated prefix exists`() {
val anotherMaterialType = materialType(prefix = entity.prefix)
whenever(repository.findByPrefix(entity.prefix)).doReturn(anotherMaterialType)
doReturn(entity).whenever(service).getById(entity.id!!)
val exception = assertThrows<EntityAlreadyExistsException> { service.update(entity) }
assertEquals(entity.prefix, exception.value)
assertThrows<AlreadyExistsException> { service.update(entity) }
.assertErrorCode("prefix")
}
// delete()
@Test
fun `delete() calls delete() in the repository`() {
doReturn(false).whenever(service).isUsedByMaterial(entity)
service.delete(entity)
verify(repository).delete(entity)
override fun `delete() deletes in the repository`() {
whenCanBeDeleted {
super.`delete() deletes in the repository`()
}
}
override fun `deleteById() deletes the entity with the given id in the repository`() {
super.`deleteById() deletes the entity with the given id in the repository`()
whenCanBeDeleted {
super.`deleteById() deletes the entity with the given id in the repository`()
}
}
private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) {
whenever(repository.canBeDeleted(id)).doReturn(true)
test()
}
}

View File

@ -43,18 +43,18 @@ class MixMaterialServiceTest : AbstractModelServiceTest<MixMaterial, MixMaterial
@Test
fun `create(set) calls create() for each MixMaterialDto`() {
val mixMaterialDtos = setOf(
mixMaterialDto(materialId = 0L, quantity = 1000f, position = 1),
mixMaterialDto(materialId = 1L, quantity = 2000f, position = 2),
mixMaterialDto(materialId = 2L, quantity = 3000f, position = 3),
mixMaterialDto(materialId = 3L, quantity = 4000f, position = 4)
mixMaterialDto(materialId = 0L, quantity = 1000f, position = 1),
mixMaterialDto(materialId = 1L, quantity = 2000f, position = 2),
mixMaterialDto(materialId = 2L, quantity = 3000f, position = 3),
mixMaterialDto(materialId = 3L, quantity = 4000f, position = 4)
)
doAnswer {
with(it.arguments[0] as MixMaterialDto) {
mixMaterial(
material = material(id = this.materialId),
quantity = this.quantity,
position = this.position
material = material(id = this.materialId),
quantity = this.quantity,
position = this.position
)
}
}.whenever(service).create(any<MixMaterialDto>())
@ -81,8 +81,8 @@ class MixMaterialServiceTest : AbstractModelServiceTest<MixMaterial, MixMaterial
assertTrue {
found.material.id == mixMaterialDto.materialId &&
found.quantity == mixMaterialDto.quantity &&
found.position == mixMaterialDto.position
found.quantity == mixMaterialDto.quantity &&
found.position == mixMaterialDto.position
}
}

View File

@ -15,12 +15,12 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
private val mixMaterialService: MixMaterialService = mock()
private val mixTypeService: MixTypeService = mock()
override val service: MixService =
spy(MixServiceImpl(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService))
spy(MixServiceImpl(repository, recipeService, materialTypeService, mixMaterialService, mixTypeService))
override val entity: Mix = mix(id = 0L, location = "location")
override val anotherEntity: Mix = mix(id = 1L)
override val entitySaveDto: MixSaveDto =
spy(mixSaveDto(mixMaterials = setOf(mixMaterialDto(materialId = 1L, quantity = 1000f, position = 0))))
spy(mixSaveDto(mixMaterials = setOf(mixMaterialDto(materialId = 1L, quantity = 1000f, position = 0))))
override val entityUpdateDto: MixUpdateDto = spy(mixUpdateDto(id = entity.id!!))
@AfterEach
@ -49,10 +49,10 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
val recipe = recipe(id = entitySaveDto.recipeId)
val materialType = materialType(id = entitySaveDto.materialTypeId)
val material = material(
name = entitySaveDto.name,
inventoryQuantity = Float.MIN_VALUE,
isMixType = true,
materialType = materialType
name = entitySaveDto.name,
inventoryQuantity = Float.MIN_VALUE,
isMixType = true,
materialType = materialType
)
val mixType = mixType(name = entitySaveDto.name, material = material)
val mix = mix(recipe = recipe, mixType = mixType)
@ -63,10 +63,10 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
whenever(materialTypeService.getById(materialType.id!!)).doReturn(materialType)
whenever(mixMaterialService.create(entitySaveDto.mixMaterials!!)).doReturn(mixMaterials)
whenever(
mixTypeService.getOrCreateForNameAndMaterialType(
mixType.name,
mixType.material.materialType!!
)
mixTypeService.getOrCreateForNameAndMaterialType(
mixType.name,
mixType.material.materialType!!
)
).doReturn(mixType)
doReturn(true).whenever(service).existsById(mixWithId.id!!)
doReturn(mixWithId).whenever(service).save(any<Mix>())
@ -85,9 +85,9 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
// update()
private fun mixUpdateDtoTest(
scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(),
sharedMixType: Boolean = false,
op: MixUpdateDtoTestScope.() -> Unit
scope: MixUpdateDtoTestScope = MixUpdateDtoTestScope(),
sharedMixType: Boolean = false,
op: MixUpdateDtoTestScope.() -> Unit
) {
with(scope) {
doReturn(true).whenever(service).existsById(mix.id!!)
@ -127,7 +127,7 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
fun `update(dto) calls MixTypeService saveForNameAndMaterialType() when mix type is shared`() {
mixUpdateDtoMixTypeTest(sharedMixType = true) {
whenever(mixTypeService.saveForNameAndMaterialType(mixUpdateDto.name!!, materialType))
.doReturn(newMixType)
.doReturn(newMixType)
val found = service.update(mixUpdateDto)
@ -141,7 +141,7 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
fun `update(dto) calls MixTypeService updateForNameAndMaterialType() when mix type is not shared`() {
mixUpdateDtoMixTypeTest {
whenever(mixTypeService.updateForNameAndMaterialType(mixType, mixUpdateDto.name!!, materialType))
.doReturn(newMixType)
.doReturn(newMixType)
val found = service.update(mixUpdateDto)
@ -155,19 +155,19 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
fun `update(dto) update, create and delete mix materials according to the given mix materials map`() {
mixUpdateDtoTest {
val mixMaterials = setOf(
mixMaterialDto(materialId = 0L, quantity = 100f, position = 0),
mixMaterialDto(materialId = 1L, quantity = 200f, position = 1),
mixMaterialDto(materialId = 2L, quantity = 300f, position = 2),
mixMaterialDto(materialId = 3L, quantity = 400f, position = 3),
mixMaterialDto(materialId = 0L, quantity = 100f, position = 0),
mixMaterialDto(materialId = 1L, quantity = 200f, position = 1),
mixMaterialDto(materialId = 2L, quantity = 300f, position = 2),
mixMaterialDto(materialId = 3L, quantity = 400f, position = 3),
)
mixUpdateDto.mixMaterials = mixMaterials
whenever(mixMaterialService.create(any<Set<MixMaterialDto>>())).doAnswer {
(it.arguments[0] as Set<MixMaterialDto>).map { dto ->
mixMaterial(
material = material(id = dto.materialId),
quantity = dto.quantity,
position = dto.position
material = material(id = dto.materialId),
quantity = dto.quantity,
position = dto.position
)
}.toSet()
}
@ -189,10 +189,10 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
@Test
fun `updateLocations() calls updateLocation() for each given MixLocationDto`() {
val locations = setOf(
mixLocationDto(mixId = 0, location = "Loc 0"),
mixLocationDto(mixId = 1, location = "Loc 1"),
mixLocationDto(mixId = 2, location = "Loc 2"),
mixLocationDto(mixId = 3, location = "Loc 3")
mixLocationDto(mixId = 0, location = "Loc 0"),
mixLocationDto(mixId = 1, location = "Loc 1"),
mixLocationDto(mixId = 2, location = "Loc 2"),
mixLocationDto(mixId = 3, location = "Loc 3")
)
service.updateLocations(locations)
@ -215,24 +215,39 @@ class MixServiceTest : AbstractExternalModelServiceTest<Mix, MixSaveDto, MixUpda
// delete()
override fun `delete() deletes in the repository`() {
whenCanBeDeleted {
super.`delete() deletes in the repository`()
}
}
// deleteById()
@Test
override fun `deleteById() deletes the entity with the given id in the repository`() {
whenever(repository.canBeDeleted(entity.id!!)).doReturn(true)
super.`deleteById() deletes the entity with the given id in the repository`()
whenCanBeDeleted {
super.`deleteById() deletes the entity with the given id in the repository`()
}
}
private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) {
whenever(repository.canBeDeleted(id)).doReturn(true)
test()
}
}
data class MixUpdateDtoTestScope(
val mixType: MixType = mixType(name = "mix type"),
val newMixType: MixType = mixType(name = "another mix type"),
val materialType: MaterialType = materialType(id = 0L),
val mix: Mix = mix(id = 0L, mixType = mixType),
val mixUpdateDto: MixUpdateDto = spy(
mixUpdateDto(
id = 0L,
name = null,
materialTypeId = null,
mixMaterials = setOf()
)
val mixType: MixType = mixType(name = "mix type"),
val newMixType: MixType = mixType(name = "another mix type"),
val materialType: MaterialType = materialType(id = 0L),
val mix: Mix = mix(id = 0L, mixType = mixType),
val mixUpdateDto: MixUpdateDto = spy(
mixUpdateDto(
id = 0L,
name = null,
materialTypeId = null,
mixMaterials = setOf()
)
)
)

View File

@ -1,8 +1,8 @@
package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.exception.EntityAlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.MixTypeRepository
import org.junit.jupiter.api.AfterEach
@ -54,11 +54,11 @@ class MixTypeServiceTest : AbstractNamedModelServiceTest<MixType, MixTypeService
}
@Test
fun `getByMaterial() throws EntityNotFoundException when no mix type with the given material exists`() {
fun `getByMaterial() throws NotFoundException when no mix type with the given material exists`() {
whenever(repository.findByMaterial(material)).doReturn(null)
val exception = assertThrows<EntityNotFoundException> { service.getByMaterial(material) }
assertEquals(material.name, exception.value)
assertThrows<NotFoundException> { service.getByMaterial(material) }
.assertErrorCode("name")
}
// getByNameAndMaterialType()
@ -100,11 +100,11 @@ class MixTypeServiceTest : AbstractNamedModelServiceTest<MixType, MixTypeService
// save()
@Test
fun `save() throws EntityAlreadyExistsException when a material with the name of the new mix type exists`() {
fun `save() throws AlreadyExistsException when a material with the name of the new mix type exists`() {
whenever(materialService.existsByName(entity.name)).doReturn(true)
val exception = assertThrows<EntityAlreadyExistsException> { service.save(entity) }
assertEquals(entity.name, exception.value)
assertThrows<AlreadyExistsException> { service.save(entity) }
.assertErrorCode("name")
}
// saveForNameAndMaterialType()
@ -149,8 +149,23 @@ class MixTypeServiceTest : AbstractNamedModelServiceTest<MixType, MixTypeService
// delete()
override fun `delete() deletes in the repository`() {
whenCanBeDeleted {
super.`delete() deletes in the repository`()
}
}
// deleteById()
override fun `deleteById() deletes the entity with the given id in the repository`() {
whenever(repository.canBeDeleted(any())).doReturn(true)
super.`deleteById() deletes the entity with the given id in the repository`()
whenCanBeDeleted {
super.`deleteById() deletes the entity with the given id in the repository`()
}
}
private fun whenCanBeDeleted(id: Long = any(), test: () -> Unit) {
whenever(repository.canBeDeleted(id)).doReturn(true)
test()
}
}

View File

@ -1,7 +1,6 @@
package dev.fyloz.colorrecipesexplorer.service
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.exception.EntityNotFoundException
import dev.fyloz.colorrecipesexplorer.model.*
import dev.fyloz.colorrecipesexplorer.repository.RecipeRepository
import dev.fyloz.colorrecipesexplorer.service.files.FileService
@ -16,7 +15,7 @@ import kotlin.test.assertFalse
import kotlin.test.assertTrue
class RecipeServiceTest :
AbstractExternalModelServiceTest<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService, RecipeRepository>() {
AbstractExternalModelServiceTest<Recipe, RecipeSaveDto, RecipeUpdateDto, RecipeService, RecipeRepository>() {
override val repository: RecipeRepository = mock()
private val companyService: CompanyService = mock()
private val mixService: MixService = mock()
@ -79,22 +78,22 @@ class RecipeServiceTest :
@Test
override fun `update(dto) calls and returns update() with the created entity`() =
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
withBaseUpdateDtoTest(entity, entityUpdateDto, service, { any() })
// updatePublicData()
@Test
fun `updatePublicData() updates the notes of a recipe groups information according to the RecipePublicDataDto`() {
val recipe = recipe(
id = 0L, groupsInformation = setOf(
recipeGroupInformation(id = 0L, group = employeeGroup(id = 1L), note = "Old note"),
recipeGroupInformation(id = 1L, group = employeeGroup(id = 2L), note = "Another note"),
recipeGroupInformation(id = 2L, group = employeeGroup(id = 3L), note = "Up to date note")
)
id = 0L, groupsInformation = setOf(
recipeGroupInformation(id = 0L, group = employeeGroup(id = 1L), note = "Old note"),
recipeGroupInformation(id = 1L, group = employeeGroup(id = 2L), note = "Another note"),
recipeGroupInformation(id = 2L, group = employeeGroup(id = 3L), note = "Up to date note")
)
)
val notes = setOf(
noteDto(groupId = 1, content = "Note 1"),
noteDto(groupId = 2, content = null)
noteDto(groupId = 1, content = "Note 1"),
noteDto(groupId = 2, content = null)
)
val publicData = recipePublicDataDto(recipeId = recipe.id!!, notes = notes)
@ -114,10 +113,12 @@ class RecipeServiceTest :
@Test
fun `updatePublicData() update the location of a recipe mixes in the mix service according to the RecipePublicDataDto`() {
val publicData = recipePublicDataDto(mixesLocation = setOf(
val publicData = recipePublicDataDto(
mixesLocation = setOf(
mixLocationDto(mixId = 0L, location = "Loc 1"),
mixLocationDto(mixId = 1L, location = "Loc 2")
))
)
)
service.updatePublicData(publicData)
@ -193,16 +194,14 @@ class RecipeImageServiceTest {
}
@Test
fun `getByIdForRecipe() throws EntityNotFoundException when no image with the given recipe and image id exists`() {
fun `getByIdForRecipe() throws RecipeImageNotFoundException when no image with the given recipe and image id exists`() {
doReturn(imagePath).whenever(service).getPath(imageId, recipeId)
whenever(recipeService.getById(recipeId)).doReturn(recipe)
whenever(fileService.readAsBytes(imagePath)).doAnswer { throw NoSuchFileException(imagePath) }
val exception =
assertThrows<EntityNotFoundException> { service.getByIdForRecipe(imageId, recipeId) }
assertEquals("$recipeId/$imageId", exception.value)
assertThrows<RecipeImageNotFoundException> { service.getByIdForRecipe(imageId, recipeId) }
}
// getAllIdsForRecipe()
@Test

View File

@ -7,7 +7,7 @@ import dev.fyloz.colorrecipesexplorer.model.recipeStep
import dev.fyloz.colorrecipesexplorer.repository.RecipeStepRepository
class RecipeStepServiceTest :
AbstractModelServiceTest<RecipeStep, RecipeStepService, RecipeStepRepository>() {
AbstractModelServiceTest<RecipeStep, RecipeStepService, RecipeStepRepository>() {
override val repository: RecipeStepRepository = mock()
override val service: RecipeStepService = spy(RecipeStepServiceImpl(repository))

View File

@ -1,7 +1,6 @@
package dev.fyloz.colorrecipesexplorer.service.files
import com.nhaarman.mockitokotlin2.*
import dev.fyloz.colorrecipesexplorer.exception.SimdutWriteException
import dev.fyloz.colorrecipesexplorer.model.Material
import dev.fyloz.colorrecipesexplorer.model.material
import org.junit.jupiter.api.AfterEach
@ -26,7 +25,7 @@ class SimdutServiceTest {
@JvmName("withNullableMaterialPath")
private inline fun withMaterialPath(material: Material? = null, exists: Boolean = true, test: (String) -> Unit) =
withMaterialPath(material ?: this.material, exists, test)
withMaterialPath(material ?: this.material, exists, test)
private inline fun withMaterialPath(material: Material, exists: Boolean = true, test: (String) -> Unit) {
val path = "data/simdut/${material.id}"
@ -109,8 +108,7 @@ class SimdutServiceTest {
whenever(fileService.write(simdutMultipart, path)).doReturn(false)
val exception = assertThrows<SimdutWriteException> { service.write(material, simdutMultipart) }
assertEquals(material, exception.material)
assertThrows<SimdutWriteException> { service.write(material, simdutMultipart) }
}
}