Merge pull request 'Multiple bug fixes' (#23) from develop into master
Reviewed-on: #23
This commit is contained in:
commit
fb725a58f1
19
.drone.yml
19
.drone.yml
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
global-variables:
|
global-variables:
|
||||||
release: &release ${DRONE_BRANCH##**/}
|
release: &release ${DRONE_TAG}
|
||||||
environment: &environment
|
environment: &environment
|
||||||
JAVA_VERSION: 11
|
JAVA_VERSION: 11
|
||||||
GRADLE_VERSION: 7.1
|
GRADLE_VERSION: 7.1
|
||||||
|
@ -44,7 +44,8 @@ steps:
|
||||||
commands:
|
commands:
|
||||||
- echo -n "latest-release,$CRE_RELEASE" > .tags
|
- echo -n "latest-release,$CRE_RELEASE" > .tags
|
||||||
when:
|
when:
|
||||||
branch: release/**
|
event:
|
||||||
|
- tag
|
||||||
|
|
||||||
- name: containerize-dev
|
- name: containerize-dev
|
||||||
image: plugins/docker
|
image: plugins/docker
|
||||||
|
@ -74,7 +75,8 @@ steps:
|
||||||
- CRE_VERSION=${DRONE_BRANCH##**/}
|
- CRE_VERSION=${DRONE_BRANCH##**/}
|
||||||
repo: *docker-registry-repo
|
repo: *docker-registry-repo
|
||||||
when:
|
when:
|
||||||
branch: release/**
|
event:
|
||||||
|
- tag
|
||||||
|
|
||||||
- name: deploy
|
- name: deploy
|
||||||
image: alpine:latest
|
image: alpine:latest
|
||||||
|
@ -107,12 +109,5 @@ steps:
|
||||||
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker pull $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
|
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker pull $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
|
||||||
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker run -d -p $CRE_PORT:9090 --name=$DEPLOY_CONTAINER_NAME -v $DEPLOY_DATA_VOLUME:/usr/bin/data -v $DEPLOY_CONFIG_VOLUME:/usr/bin/config -e spring_profiles_active=$DEPLOY_SPRING_PROFILES $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
|
- ssh -p $DEPLOY_SERVER_SSH_PORT $DEPLOY_SERVER_USERNAME@$DEPLOY_SERVER "docker run -d -p $CRE_PORT:9090 --name=$DEPLOY_CONTAINER_NAME -v $DEPLOY_DATA_VOLUME:/usr/bin/data -v $DEPLOY_CONFIG_VOLUME:/usr/bin/config -e spring_profiles_active=$DEPLOY_SPRING_PROFILES $CRE_REGISTRY_IMAGE:$CRE_RELEASE"
|
||||||
when:
|
when:
|
||||||
branch: release/**
|
event:
|
||||||
|
- tag
|
||||||
trigger:
|
|
||||||
branch:
|
|
||||||
- develop
|
|
||||||
- release/**
|
|
||||||
- master
|
|
||||||
|
|
||||||
|
|
|
@ -3,12 +3,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
group = "dev.fyloz.colorrecipesexplorer"
|
group = "dev.fyloz.colorrecipesexplorer"
|
||||||
|
|
||||||
val kotlinVersion = "1.6.0"
|
val kotlinVersion = "1.6.0"
|
||||||
val springBootVersion = "2.5.6"
|
val springBootVersion = "2.6.1"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
// Outer scope variables can't be accessed in the plugins section, so we have to redefine them here
|
// Outer scope variables can't be accessed in the plugins section, so we have to redefine them here
|
||||||
val kotlinVersion = "1.6.0"
|
val kotlinVersion = "1.6.0"
|
||||||
val springBootVersion = "2.5.6"
|
val springBootVersion = "2.6.1"
|
||||||
|
|
||||||
id("java")
|
id("java")
|
||||||
id("org.jetbrains.kotlin.jvm") version kotlinVersion
|
id("org.jetbrains.kotlin.jvm") version kotlinVersion
|
||||||
|
@ -28,18 +28,18 @@ repositories {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(platform("org.jetbrains.kotlin:kotlin-bom:${kotlinVersion}"))
|
implementation(platform("org.jetbrains.kotlin:kotlin-bom:${kotlinVersion}"))
|
||||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
|
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.0")
|
implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.2.1")
|
||||||
implementation("javax.xml.bind:jaxb-api:2.3.0")
|
implementation("io.github.microutils:kotlin-logging-jvm:2.1.21")
|
||||||
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
|
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
|
||||||
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")
|
implementation("io.jsonwebtoken:jjwt-impl:0.11.2")
|
||||||
implementation("io.jsonwebtoken:jjwt-jackson:0.11.2")
|
implementation("io.jsonwebtoken:jjwt-jackson:0.11.2")
|
||||||
|
implementation("javax.xml.bind:jaxb-api:2.3.0")
|
||||||
implementation("org.apache.poi:poi-ooxml:4.1.0")
|
implementation("org.apache.poi:poi-ooxml:4.1.0")
|
||||||
implementation("org.apache.pdfbox:pdfbox:2.0.4")
|
implementation("org.apache.pdfbox:pdfbox:2.0.4")
|
||||||
implementation("org.apache.logging.log4j:log4j-api:2.16.0")
|
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
|
||||||
implementation("org.apache.logging.log4j:log4j-to-slf4j:2.16.0")
|
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
|
||||||
implementation("dev.fyloz.colorrecipesexplorer:database-manager:5.2.1")
|
|
||||||
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}")
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}")
|
implementation("org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}")
|
||||||
|
@ -49,13 +49,13 @@ dependencies {
|
||||||
implementation("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
|
implementation("org.springframework.boot:spring-boot-configuration-processor:${springBootVersion}")
|
||||||
implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
|
implementation("org.springframework.boot:spring-boot-devtools:${springBootVersion}")
|
||||||
|
|
||||||
testImplementation("org.springframework:spring-test:5.3.13")
|
|
||||||
testImplementation("org.mockito:mockito-inline:3.11.2")
|
|
||||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||||
testImplementation("io.mockk:mockk:1.12.0")
|
testImplementation("io.mockk:mockk:1.12.0")
|
||||||
|
testImplementation("org.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
|
||||||
|
testImplementation("org.mockito:mockito-inline:3.11.2")
|
||||||
|
testImplementation("org.springframework:spring-test:5.3.13")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootVersion}")
|
testImplementation("org.springframework.boot:spring-boot-starter-test:${springBootVersion}")
|
||||||
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:${springBootVersion}")
|
testImplementation("org.springframework.boot:spring-boot-test-autoconfigure:${springBootVersion}")
|
||||||
testImplementation("org.jetbrains.kotlin:kotlin-test:${kotlinVersion}")
|
|
||||||
|
|
||||||
runtimeOnly("com.h2database:h2:1.4.199")
|
runtimeOnly("com.h2database:h2:1.4.199")
|
||||||
runtimeOnly("mysql:mysql-connector-java:8.0.22")
|
runtimeOnly("mysql:mysql-connector-java:8.0.22")
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
version: "3.1"
|
version: "3.1"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
frontend:
|
cre.frontend:
|
||||||
image: fyloz.dev:5443/color-recipes-explorer/frontend:latest
|
image: fyloz.dev:5443/color-recipes-explorer/frontend:latest
|
||||||
ports:
|
ports:
|
||||||
- 4200:80
|
- "4200:80"
|
||||||
database:
|
cre.database:
|
||||||
image: mysql
|
image: mysql
|
||||||
command: --default-authentication-plugin=mysql_native_password
|
command: --default-authentication-plugin=mysql_native_password
|
||||||
environment:
|
environment:
|
||||||
MYSQL_ROOT_PASSWORD: "pass"
|
MYSQL_ROOT_PASSWORD: "pass"
|
||||||
MYSQL_DATABASE: "cre"
|
MYSQL_DATABASE: "cre"
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- "3306:3306"
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.config
|
package dev.fyloz.colorrecipesexplorer.config
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||||
|
import dev.fyloz.colorrecipesexplorer.config.initializers.AbstractInitializer
|
||||||
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
import dev.fyloz.colorrecipesexplorer.config.properties.CreProperties
|
||||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
|
||||||
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
import dev.fyloz.colorrecipesexplorer.emergencyMode
|
||||||
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
||||||
import dev.fyloz.colorrecipesexplorer.restartApplication
|
import dev.fyloz.colorrecipesexplorer.restartApplication
|
||||||
import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService
|
|
||||||
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
|
import dev.fyloz.colorrecipesexplorer.service.config.ConfigurationService
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
|
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent
|
|
||||||
import org.springframework.context.ApplicationListener
|
import org.springframework.context.ApplicationListener
|
||||||
import org.springframework.context.annotation.Configuration
|
import org.springframework.context.annotation.Configuration
|
||||||
import org.springframework.context.annotation.Profile
|
|
||||||
import org.springframework.core.Ordered
|
import org.springframework.core.Ordered
|
||||||
import org.springframework.core.annotation.Order
|
import org.springframework.core.annotation.Order
|
||||||
import javax.annotation.PostConstruct
|
import javax.annotation.PostConstruct
|
||||||
|
@ -20,15 +18,13 @@ import kotlin.concurrent.thread
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||||
@Profile("!emergency")
|
@RequireDatabase
|
||||||
class ApplicationReadyListener(
|
class ApplicationReadyListener(
|
||||||
private val materialTypeService: MaterialTypeService,
|
|
||||||
private val configurationService: ConfigurationService,
|
private val configurationService: ConfigurationService,
|
||||||
private val materialTypeProperties: MaterialTypeProperties,
|
|
||||||
private val creProperties: CreProperties,
|
private val creProperties: CreProperties,
|
||||||
private val logger: Logger
|
private val logger: Logger
|
||||||
) : ApplicationListener<ApplicationReadyEvent> {
|
) : AbstractInitializer() {
|
||||||
override fun onApplicationEvent(event: ApplicationReadyEvent) {
|
override fun initialize() {
|
||||||
if (emergencyMode) {
|
if (emergencyMode) {
|
||||||
logger.error("Emergency mode is enabled, default material types will not be created")
|
logger.error("Emergency mode is enabled, default material types will not be created")
|
||||||
thread {
|
thread {
|
||||||
|
@ -40,15 +36,9 @@ class ApplicationReadyListener(
|
||||||
}
|
}
|
||||||
|
|
||||||
initDatabaseConfigurations()
|
initDatabaseConfigurations()
|
||||||
initMaterialTypes()
|
|
||||||
CRE_PROPERTIES = creProperties
|
CRE_PROPERTIES = creProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initMaterialTypes() {
|
|
||||||
logger.info("Initializing system material types")
|
|
||||||
materialTypeService.saveSystemTypes(materialTypeProperties.systemTypes)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initDatabaseConfigurations() {
|
private fun initDatabaseConfigurations() {
|
||||||
configurationService.initializeProperties { !it.file }
|
configurationService.initializeProperties { !it.file }
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package dev.fyloz.colorrecipesexplorer.config.annotations
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Profile
|
||||||
|
import java.lang.annotation.Inherited
|
||||||
|
|
||||||
|
@Profile("!emergency")
|
||||||
|
@Inherited
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
annotation class RequireDatabase
|
|
@ -0,0 +1,12 @@
|
||||||
|
package dev.fyloz.colorrecipesexplorer.config.initializers
|
||||||
|
|
||||||
|
import org.springframework.boot.context.event.ApplicationReadyEvent
|
||||||
|
import org.springframework.context.ApplicationListener
|
||||||
|
|
||||||
|
abstract class AbstractInitializer : ApplicationListener<ApplicationReadyEvent> {
|
||||||
|
override fun onApplicationEvent(event: ApplicationReadyEvent) {
|
||||||
|
initialize()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun initialize()
|
||||||
|
}
|
|
@ -0,0 +1,57 @@
|
||||||
|
package dev.fyloz.colorrecipesexplorer.config.initializers
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||||
|
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
||||||
|
import dev.fyloz.colorrecipesexplorer.model.MaterialType
|
||||||
|
import dev.fyloz.colorrecipesexplorer.model.materialType
|
||||||
|
import dev.fyloz.colorrecipesexplorer.service.MaterialTypeService
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequireDatabase
|
||||||
|
class MaterialTypeInitializer(
|
||||||
|
private val materialTypeService: MaterialTypeService,
|
||||||
|
private val materialTypeProperties: MaterialTypeProperties
|
||||||
|
) : AbstractInitializer() {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
override fun initialize() {
|
||||||
|
logger.debug("Executing material type initializer...")
|
||||||
|
ensureSystemMaterialTypesExists()
|
||||||
|
logger.debug("System material types are up to date!")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ensureSystemMaterialTypesExists() {
|
||||||
|
val systemTypes = materialTypeProperties.systemTypes.map { it.toMaterialType() }
|
||||||
|
val oldSystemTypes = materialTypeService.getAllSystemTypes().toMutableSet()
|
||||||
|
|
||||||
|
fun saveOrUpdateSystemType(type: MaterialType) {
|
||||||
|
if (materialTypeService.existsByName(type.name)) {
|
||||||
|
with(materialTypeService.getByName(type.name)) {
|
||||||
|
if (!this.systemType) {
|
||||||
|
logger.info("Material type '${type.name}' already exists and will be flagged as a system type")
|
||||||
|
materialTypeService.update(this.copy(systemType = true))
|
||||||
|
} else {
|
||||||
|
logger.debug("System material type '${type.name}' already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info("System material type '${type.name}' will be created")
|
||||||
|
materialTypeService.save(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save new system types
|
||||||
|
systemTypes.forEach {
|
||||||
|
saveOrUpdateSystemType(it)
|
||||||
|
oldSystemTypes.removeIf { c -> c.name == it.name }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove old system types
|
||||||
|
oldSystemTypes.forEach {
|
||||||
|
logger.info("Material type '${it.name}' is not a system type anymore")
|
||||||
|
materialTypeService.update(materialType(it, newSystemType = false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package dev.fyloz.colorrecipesexplorer.config.initializers
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||||
|
import dev.fyloz.colorrecipesexplorer.model.Mix
|
||||||
|
import dev.fyloz.colorrecipesexplorer.model.MixMaterial
|
||||||
|
import dev.fyloz.colorrecipesexplorer.service.MixService
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.merge
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequireDatabase
|
||||||
|
class MixInitializer(
|
||||||
|
private val mixService: MixService
|
||||||
|
) : AbstractInitializer() {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
override fun initialize() {
|
||||||
|
logger.debug("Executing mix initializer...")
|
||||||
|
fixAllPositions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixAllPositions() {
|
||||||
|
logger.debug("Validating mix materials positions...")
|
||||||
|
|
||||||
|
mixService.getAll()
|
||||||
|
.filter { mix -> mix.mixMaterials.any { it.position == 0 } }
|
||||||
|
.forEach(this::fixMixPositions)
|
||||||
|
|
||||||
|
logger.debug("Mix materials positions are valid!")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixMixPositions(mix: Mix) {
|
||||||
|
val maxPosition = mix.mixMaterials.maxOf { it.position }
|
||||||
|
|
||||||
|
logger.warn("Mix ${mix.id} (${mix.mixType.name}, ${mix.recipe.name}) has invalid positions:")
|
||||||
|
|
||||||
|
val invalidMixMaterials: Collection<MixMaterial> = with(mix.mixMaterials.filter { it.position == 0 }) {
|
||||||
|
if (maxPosition == 0 && this.size > 1) {
|
||||||
|
orderMixMaterials(this)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fixedMixMaterials = increaseMixMaterialsPosition(invalidMixMaterials, maxPosition + 1)
|
||||||
|
val updatedMixMaterials = mix.mixMaterials.merge(fixedMixMaterials)
|
||||||
|
|
||||||
|
with(mix.copy(mixMaterials = updatedMixMaterials.toMutableSet())) {
|
||||||
|
mixService.update(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun increaseMixMaterialsPosition(mixMaterials: Iterable<MixMaterial>, firstPosition: Int) =
|
||||||
|
mixMaterials
|
||||||
|
.mapIndexed { index, mixMaterial -> mixMaterial.copy(position = firstPosition + index) }
|
||||||
|
.onEach {
|
||||||
|
logger.info("\tPosition of material ${it.material.id} (${it.material.name}) has been set to ${it.position}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun orderMixMaterials(mixMaterials: Collection<MixMaterial>) =
|
||||||
|
LinkedList(mixMaterials).apply {
|
||||||
|
while (this.peek().material.materialType?.usePercentages == true) {
|
||||||
|
// The first mix material can't use percents, so move it to the end of the queue
|
||||||
|
val pop = this.pop()
|
||||||
|
this.add(pop)
|
||||||
|
logger.debug("\tMaterial ${pop.material.id} (${pop.material.name}) uses percents, moving to the end of the queue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,72 @@
|
||||||
|
package dev.fyloz.colorrecipesexplorer.config.initializers
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.config.annotations.RequireDatabase
|
||||||
|
import dev.fyloz.colorrecipesexplorer.model.Recipe
|
||||||
|
import dev.fyloz.colorrecipesexplorer.model.RecipeGroupInformation
|
||||||
|
import dev.fyloz.colorrecipesexplorer.model.RecipeStep
|
||||||
|
import dev.fyloz.colorrecipesexplorer.service.RecipeService
|
||||||
|
import dev.fyloz.colorrecipesexplorer.utils.merge
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequireDatabase
|
||||||
|
class RecipeInitializer(
|
||||||
|
private val recipeService: RecipeService
|
||||||
|
) : AbstractInitializer() {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
override fun initialize() {
|
||||||
|
logger.debug("Executing recipe initializer...")
|
||||||
|
fixAllPositions()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixAllPositions() {
|
||||||
|
logger.debug("Validating recipes steps positions...")
|
||||||
|
|
||||||
|
recipeService.getAll()
|
||||||
|
.forEach(this::fixRecipePositions)
|
||||||
|
|
||||||
|
logger.debug("Recipes steps positions are valid!")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixRecipePositions(recipe: Recipe) {
|
||||||
|
val fixedGroupInformation = recipe.groupsInformation
|
||||||
|
.filter { it.steps != null }
|
||||||
|
.filter { groupInfo -> groupInfo.steps!!.any { it.position == 0 } }
|
||||||
|
.map { fixGroupInformationPositions(recipe, it) }
|
||||||
|
|
||||||
|
val updatedGroupInformation = recipe.groupsInformation.merge(fixedGroupInformation)
|
||||||
|
|
||||||
|
with(recipe.copy(groupsInformation = updatedGroupInformation.toMutableSet())) {
|
||||||
|
recipeService.update(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fixGroupInformationPositions(
|
||||||
|
recipe: Recipe,
|
||||||
|
groupInformation: RecipeGroupInformation
|
||||||
|
): RecipeGroupInformation {
|
||||||
|
val steps = groupInformation.steps!!
|
||||||
|
val maxPosition = steps.maxOf { it.position }
|
||||||
|
|
||||||
|
logger.warn("Recipe ${recipe.id} (${recipe.name}) has invalid positions:")
|
||||||
|
|
||||||
|
val invalidRecipeSteps = steps.filter { it.position == 0 }
|
||||||
|
val fixedRecipeSteps = increaseRecipeStepsPosition(groupInformation, invalidRecipeSteps, maxPosition + 1)
|
||||||
|
val updatedRecipeSteps = steps.merge(fixedRecipeSteps)
|
||||||
|
|
||||||
|
return groupInformation.copy(steps = updatedRecipeSteps.toMutableSet())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun increaseRecipeStepsPosition(
|
||||||
|
groupInformation: RecipeGroupInformation,
|
||||||
|
recipeSteps: Iterable<RecipeStep>,
|
||||||
|
firstPosition: Int
|
||||||
|
) =
|
||||||
|
recipeSteps
|
||||||
|
.mapIndexed { index, recipeStep -> recipeStep.copy(position = firstPosition + index) }
|
||||||
|
.onEach {
|
||||||
|
logger.info("\tPosition of step ${it.id} (group: ${groupInformation.group.name}) has been set to ${it.position}")
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ class JwtAuthenticationFilter(
|
||||||
|
|
||||||
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
|
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
|
||||||
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequest::class.java)
|
val loginRequest = jacksonObjectMapper().readValue(request.inputStream, UserLoginRequest::class.java)
|
||||||
|
logger.debug("Login attempt for user ${loginRequest.id}...")
|
||||||
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
|
return authManager.authenticate(UsernamePasswordAuthenticationToken(loginRequest.id, loginRequest.password))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,6 +54,10 @@ class JwtAuthenticationFilter(
|
||||||
val userDetails = auth.principal as UserDetails
|
val userDetails = auth.principal as UserDetails
|
||||||
val token = jwtService.buildJwt(userDetails)
|
val token = jwtService.buildJwt(userDetails)
|
||||||
|
|
||||||
|
with(userDetails.user) {
|
||||||
|
logger.info("User ${this.id} (${this.firstName} ${this.lastName}) has logged in successfully")
|
||||||
|
}
|
||||||
|
|
||||||
response.addHeader("Access-Control-Expose-Headers", authorizationCookieName)
|
response.addHeader("Access-Control-Expose-Headers", authorizationCookieName)
|
||||||
response.addHeader(authorizationCookieName, "Bearer $token")
|
response.addHeader(authorizationCookieName, "Bearer $token")
|
||||||
response.addCookie(authorizationCookieName, "Bearer$token") {
|
response.addCookie(authorizationCookieName, "Bearer$token") {
|
||||||
|
|
|
@ -7,6 +7,8 @@ import dev.fyloz.colorrecipesexplorer.model.account.User
|
||||||
import dev.fyloz.colorrecipesexplorer.service.users.JwtService
|
import dev.fyloz.colorrecipesexplorer.service.users.JwtService
|
||||||
import dev.fyloz.colorrecipesexplorer.service.users.UserDetailsService
|
import dev.fyloz.colorrecipesexplorer.service.users.UserDetailsService
|
||||||
import dev.fyloz.colorrecipesexplorer.service.users.UserService
|
import dev.fyloz.colorrecipesexplorer.service.users.UserService
|
||||||
|
import mu.KLogger
|
||||||
|
import mu.KotlinLogging
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.context.annotation.Bean
|
import org.springframework.context.annotation.Bean
|
||||||
|
@ -39,9 +41,10 @@ abstract class BaseSecurityConfig(
|
||||||
private val userDetailsService: UserDetailsService,
|
private val userDetailsService: UserDetailsService,
|
||||||
private val jwtService: JwtService,
|
private val jwtService: JwtService,
|
||||||
private val environment: Environment,
|
private val environment: Environment,
|
||||||
protected val logger: Logger,
|
|
||||||
protected val securityProperties: CreSecurityProperties
|
protected val securityProperties: CreSecurityProperties
|
||||||
) : WebSecurityConfigurerAdapter() {
|
) : WebSecurityConfigurerAdapter() {
|
||||||
|
protected abstract val logger: Logger
|
||||||
|
|
||||||
protected val passwordEncoder = BCryptPasswordEncoder()
|
protected val passwordEncoder = BCryptPasswordEncoder()
|
||||||
var debugMode = false
|
var debugMode = false
|
||||||
|
|
||||||
|
@ -119,9 +122,10 @@ class SecurityConfig(
|
||||||
@Lazy private val userService: UserService,
|
@Lazy private val userService: UserService,
|
||||||
jwtService: JwtService,
|
jwtService: JwtService,
|
||||||
environment: Environment,
|
environment: Environment,
|
||||||
logger: Logger,
|
|
||||||
securityProperties: CreSecurityProperties
|
securityProperties: CreSecurityProperties
|
||||||
) : BaseSecurityConfig(userDetailsService, jwtService, environment, logger, securityProperties) {
|
) : BaseSecurityConfig(userDetailsService, jwtService, environment, securityProperties) {
|
||||||
|
override val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@PostConstruct
|
@PostConstruct
|
||||||
fun initWebSecurity() {
|
fun initWebSecurity() {
|
||||||
if (emergencyMode) {
|
if (emergencyMode) {
|
||||||
|
@ -165,9 +169,10 @@ class EmergencySecurityConfig(
|
||||||
userDetailsService: UserDetailsService,
|
userDetailsService: UserDetailsService,
|
||||||
jwtService: JwtService,
|
jwtService: JwtService,
|
||||||
environment: Environment,
|
environment: Environment,
|
||||||
logger: Logger,
|
|
||||||
securityProperties: CreSecurityProperties
|
securityProperties: CreSecurityProperties
|
||||||
) : BaseSecurityConfig(userDetailsService, jwtService, environment, logger, securityProperties) {
|
) : BaseSecurityConfig(userDetailsService, jwtService, environment, securityProperties) {
|
||||||
|
override val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
emergencyMode = true
|
emergencyMode = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
||||||
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
import dev.fyloz.colorrecipesexplorer.exception.NotFoundException
|
||||||
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
import dev.fyloz.colorrecipesexplorer.model.account.Group
|
||||||
import dev.fyloz.colorrecipesexplorer.model.account.group
|
import dev.fyloz.colorrecipesexplorer.model.account.group
|
||||||
import dev.fyloz.colorrecipesexplorer.rest.CRE_PROPERTIES
|
|
||||||
import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH
|
import dev.fyloz.colorrecipesexplorer.rest.FILE_CONTROLLER_PATH
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
import java.nio.charset.StandardCharsets
|
||||||
|
@ -158,7 +157,7 @@ data class RecipeOutputDto(
|
||||||
data class RecipeGroupInformation(
|
data class RecipeGroupInformation(
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
val id: Long?,
|
override val id: Long?,
|
||||||
|
|
||||||
@ManyToOne
|
@ManyToOne
|
||||||
@JoinColumn(name = "group_id")
|
@JoinColumn(name = "group_id")
|
||||||
|
@ -169,7 +168,7 @@ data class RecipeGroupInformation(
|
||||||
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.EAGER, orphanRemoval = true)
|
||||||
@JoinColumn(name = "recipe_group_information_id")
|
@JoinColumn(name = "recipe_group_information_id")
|
||||||
var steps: MutableSet<RecipeStep>?
|
var steps: MutableSet<RecipeStep>?
|
||||||
)
|
) : Model
|
||||||
|
|
||||||
data class RecipeStepsDto(
|
data class RecipeStepsDto(
|
||||||
val groupId: Long,
|
val groupId: Long,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import dev.fyloz.colorrecipesexplorer.config.annotations.PreAuthorizeViewUsers
|
||||||
import dev.fyloz.colorrecipesexplorer.model.account.*
|
import dev.fyloz.colorrecipesexplorer.model.account.*
|
||||||
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
|
import dev.fyloz.colorrecipesexplorer.service.users.GroupService
|
||||||
import dev.fyloz.colorrecipesexplorer.service.users.UserService
|
import dev.fyloz.colorrecipesexplorer.service.users.UserService
|
||||||
|
import mu.KotlinLogging
|
||||||
import org.springframework.context.annotation.Profile
|
import org.springframework.context.annotation.Profile
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.security.access.prepost.PreAuthorize
|
import org.springframework.security.access.prepost.PreAuthorize
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.service
|
package dev.fyloz.colorrecipesexplorer.service
|
||||||
|
|
||||||
import dev.fyloz.colorrecipesexplorer.config.properties.MaterialTypeProperties
|
|
||||||
import dev.fyloz.colorrecipesexplorer.exception.AlreadyExistsException
|
|
||||||
import dev.fyloz.colorrecipesexplorer.model.*
|
import dev.fyloz.colorrecipesexplorer.model.*
|
||||||
import dev.fyloz.colorrecipesexplorer.model.validation.isNotNullAndNotBlank
|
import dev.fyloz.colorrecipesexplorer.model.validation.isNotNullAndNotBlank
|
||||||
import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
|
import dev.fyloz.colorrecipesexplorer.repository.MaterialTypeRepository
|
||||||
|
@ -21,9 +19,6 @@ interface MaterialTypeService :
|
||||||
|
|
||||||
/** Gets all material types who are not a system type. */
|
/** Gets all material types who are not a system type. */
|
||||||
fun getAllNonSystemType(): Collection<MaterialType>
|
fun getAllNonSystemType(): Collection<MaterialType>
|
||||||
|
|
||||||
/** Saves and update the system material types. */
|
|
||||||
fun saveSystemTypes(systemTypeProperties: Collection<MaterialTypeProperties.MaterialTypeProperty>)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@ -71,34 +66,11 @@ class MaterialTypeServiceImpl(repository: MaterialTypeRepository, private val ma
|
||||||
throw materialTypePrefixAlreadyExistsException(entity.prefix)
|
throw materialTypePrefixAlreadyExistsException(entity.prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
return super<AbstractExternalNamedModelService>.update(entity)
|
return super.update(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun delete(entity: MaterialType) {
|
override fun delete(entity: MaterialType) {
|
||||||
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialTypeException(entity)
|
if (!repository.canBeDeleted(entity.id!!)) throw cannotDeleteMaterialTypeException(entity)
|
||||||
super.delete(entity)
|
super.delete(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun saveSystemTypes(systemTypeProperties: Collection<MaterialTypeProperties.MaterialTypeProperty>) {
|
|
||||||
val systemTypes = systemTypeProperties.map { it.toMaterialType() }
|
|
||||||
val oldSystemTypes = getAllSystemTypes().toMutableSet()
|
|
||||||
|
|
||||||
fun saveOrUpdateSystemType(type: MaterialType) {
|
|
||||||
if (existsByName(type.name)) {
|
|
||||||
val persistedMaterialType = getByName(type.name)
|
|
||||||
update(materialType(type, newId = persistedMaterialType.id, newSystemType = true))
|
|
||||||
} else {
|
|
||||||
save(type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save new system types
|
|
||||||
systemTypes.forEach {
|
|
||||||
saveOrUpdateSystemType(it)
|
|
||||||
oldSystemTypes.removeIf { c -> c.name == it.name }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove old system types
|
|
||||||
oldSystemTypes.forEach { update(materialType(it, newSystemType = false)) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.service.jobs
|
package dev.fyloz.colorrecipesexplorer.service.jobs
|
||||||
|
|
||||||
import dev.fyloz.colorrecipesexplorer.service.TouchUpKitService
|
import dev.fyloz.colorrecipesexplorer.service.TouchUpKitService
|
||||||
import org.slf4j.Logger
|
import mu.KotlinLogging
|
||||||
import org.springframework.context.annotation.Profile
|
import org.springframework.context.annotation.Profile
|
||||||
import org.springframework.scheduling.annotation.Scheduled
|
import org.springframework.scheduling.annotation.Scheduled
|
||||||
import org.springframework.stereotype.Component
|
import org.springframework.stereotype.Component
|
||||||
|
@ -9,18 +9,22 @@ import org.springframework.stereotype.Component
|
||||||
@Component
|
@Component
|
||||||
@Profile("!emergency")
|
@Profile("!emergency")
|
||||||
class TouchUpKitRemover(
|
class TouchUpKitRemover(
|
||||||
private val touchUpKitService: TouchUpKitService,
|
private val touchUpKitService: TouchUpKitService
|
||||||
private val logger: Logger
|
|
||||||
) {
|
) {
|
||||||
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
@Scheduled(cron = "0 0 0 * * *")
|
@Scheduled(cron = "0 0 0 * * *")
|
||||||
fun execute() {
|
fun execute() {
|
||||||
|
logger.debug("Executing expired touch up kits removal job... ")
|
||||||
removeExpiredKits()
|
removeExpiredKits()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeExpiredKits() {
|
private fun removeExpiredKits() {
|
||||||
logger.info("Removing expired touch up kits...")
|
|
||||||
with(touchUpKitService.getAll().filter(touchUpKitService::isExpired)) {
|
with(touchUpKitService.getAll().filter(touchUpKitService::isExpired)) {
|
||||||
this.forEach(touchUpKitService::delete)
|
this.forEach {
|
||||||
|
logger.debug("Removed expired touch up kit ${it.id} (${it.project} ${it.buggy})")
|
||||||
|
touchUpKitService.delete(it)
|
||||||
|
}
|
||||||
logger.info("Removed ${this.size} expired touch up kits")
|
logger.info("Removed ${this.size} expired touch up kits")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package dev.fyloz.colorrecipesexplorer.utils
|
package dev.fyloz.colorrecipesexplorer.utils
|
||||||
|
|
||||||
|
import dev.fyloz.colorrecipesexplorer.model.Model
|
||||||
|
|
||||||
/** 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]. */
|
/** 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(
|
inline fun <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
|
||||||
throwableConsumer: (E) -> Unit = {},
|
throwableConsumer: (E) -> Unit = {},
|
||||||
transform: (T) -> R
|
transform: (T) -> R
|
||||||
): List<R> = this.mapNotNull {
|
): List<R> = this.mapNotNull {
|
||||||
try {
|
try {
|
||||||
transform(it)
|
transform(it)
|
||||||
|
@ -19,17 +21,17 @@ inline fun <T, R, reified E : Throwable> Iterable<T>.mapMayThrow(
|
||||||
|
|
||||||
/** Find duplicated in the given [Iterable] from keys obtained from the given [keySelector]. */
|
/** Find duplicated in the given [Iterable] from keys obtained from the given [keySelector]. */
|
||||||
inline fun <T, K> Iterable<T>.findDuplicated(keySelector: (T) -> K) =
|
inline fun <T, K> Iterable<T>.findDuplicated(keySelector: (T) -> K) =
|
||||||
this.groupBy(keySelector)
|
this.groupBy(keySelector)
|
||||||
.filter { it.value.count() > 1 }
|
.filter { it.value.count() > 1 }
|
||||||
.map { it.key }
|
.map { it.key }
|
||||||
|
|
||||||
/** Check if the given [Iterable] has gaps between each items, using keys obtained from the given [keySelector]. */
|
/** Check if the given [Iterable] has gaps between each items, using keys obtained from the given [keySelector]. */
|
||||||
inline fun <T> Iterable<T>.hasGaps(keySelector: (T) -> Int) =
|
inline fun <T> Iterable<T>.hasGaps(keySelector: (T) -> Int) =
|
||||||
this.map(keySelector)
|
this.map(keySelector)
|
||||||
.toIntArray()
|
.toIntArray()
|
||||||
.sorted()
|
.sorted()
|
||||||
.filterIndexed { index, it -> it != index + 1 }
|
.filterIndexed { index, it -> it != index + 1 }
|
||||||
.isNotEmpty()
|
.isNotEmpty()
|
||||||
|
|
||||||
/** Clears and fills the given [MutableCollection] with the given [elements]. */
|
/** Clears and fills the given [MutableCollection] with the given [elements]. */
|
||||||
fun <T> MutableCollection<T>.setAll(elements: Collection<T>) {
|
fun <T> MutableCollection<T>.setAll(elements: Collection<T>) {
|
||||||
|
@ -40,6 +42,12 @@ fun <T> MutableCollection<T>.setAll(elements: Collection<T>) {
|
||||||
/** Removes and returns all elements of a [MutableCollection] matching the given [predicate]. */
|
/** Removes and returns all elements of a [MutableCollection] matching the given [predicate]. */
|
||||||
inline fun <T> MutableCollection<T>.excludeAll(predicate: (T) -> Boolean): Iterable<T> {
|
inline fun <T> MutableCollection<T>.excludeAll(predicate: (T) -> Boolean): Iterable<T> {
|
||||||
val matching = this.filter(predicate)
|
val matching = this.filter(predicate)
|
||||||
this.removeAll(matching)
|
this.removeAll(matching.toSet())
|
||||||
return matching
|
return matching
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Merge to [Model] [Iterable]s and prevent id duplication. */
|
||||||
|
fun <T : Model> Iterable<T>.merge(other: Iterable<T>) =
|
||||||
|
this
|
||||||
|
.filter { model -> other.all { it.id != model.id } }
|
||||||
|
.plus(other)
|
||||||
|
|
|
@ -31,4 +31,4 @@ spring.jackson.deserialization.fail-on-null-for-primitives=true
|
||||||
spring.jackson.default-property-inclusion=non_null
|
spring.jackson.default-property-inclusion=non_null
|
||||||
spring.profiles.active=@spring.profiles.active@
|
spring.profiles.active=@spring.profiles.active@
|
||||||
|
|
||||||
spring.datasource.continue-on-error=true
|
spring.sql.init.continue-on-error=true
|
||||||
|
|
|
@ -5,9 +5,12 @@
|
||||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
<layout class="ch.qos.logback.classic.PatternLayout">
|
<layout class="ch.qos.logback.classic.PatternLayout">
|
||||||
<Pattern>
|
<Pattern>
|
||||||
%black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{36}): %msg%n%throwable
|
%green(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{36}): %msg%n%throwable
|
||||||
</Pattern>
|
</Pattern>
|
||||||
</layout>
|
</layout>
|
||||||
|
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
|
||||||
|
<level>INFO</level>
|
||||||
|
</filter>
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
<appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
<appender name="LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
@ -21,12 +24,12 @@
|
||||||
|
|
||||||
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
|
||||||
<Pattern>
|
<Pattern>
|
||||||
%d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] %-5level %logger.%M - %msg%n
|
%d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%M - %msg%n
|
||||||
</Pattern>
|
</Pattern>
|
||||||
</encoder>
|
</encoder>
|
||||||
|
|
||||||
</appender>
|
</appender>
|
||||||
|
|
||||||
|
<logger name="dev.fyloz.colorrecipesexplorer" level="debug"/>
|
||||||
<root level="info">
|
<root level="info">
|
||||||
<appender-ref ref="CONSOLE"/>
|
<appender-ref ref="CONSOLE"/>
|
||||||
<appender-ref ref="LOG_FILE"/>
|
<appender-ref ref="LOG_FILE"/>
|
||||||
|
|
Loading…
Reference in New Issue