Merge branch 'base-functionalities' into 'master'

Ajout des fonctionnalitées de bases de l'utilitaire

Closes #1, #2, and #3

See merge request color-recipes-explorer/database-manager!1
This commit is contained in:
William Nolin 2021-03-04 05:48:54 +00:00
commit 5e7fd754c0
16 changed files with 1253 additions and 12 deletions

1
.gitignore vendored
View File

@ -10,3 +10,4 @@ gradlew*
build/
logs/
dokka/
workdir/*

View File

@ -1,12 +1,13 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.4.30"
}
group = "dev.fyloz.colorrecipesexplorer"
version = "1.0"
plugins {
kotlin("jvm") version "1.4.30"
id("com.github.johnrengelman.shadow") version "6.1.0"
}
repositories {
mavenCentral()
}
@ -14,9 +15,17 @@ repositories {
dependencies {
implementation("org.liquibase:liquibase-core:4.3.1")
testImplementation(kotlin("test-junit5"))
testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
// Logging
implementation("io.github.microutils:kotlin-logging:1.12.0")
implementation("org.slf4j:slf4j-api:1.7.30")
implementation("ch.qos.logback:logback-classic:1.0.13")
// Database drivers
implementation("mysql:mysql-connector-java:8.0.22")
testImplementation("io.mockk:mockk:1.10.6")
testImplementation("io.kotest:kotest-runner-junit5:4.4.1")
implementation(kotlin("stdlib-jdk8"))
}
tasks.test {
@ -25,4 +34,11 @@ tasks.test {
tasks.withType<KotlinCompile>() {
kotlinOptions.jvmTarget = "11"
kotlinOptions.useIR = true
}
tasks.withType<Jar> {
manifest {
attributes["Main-Class"] = "dev.fyloz.colorrecipesexplorer.databasemanager.DatabaseUpdaterKt"
}
}

View File

@ -1,3 +1,3 @@
rootProject.name = "DatabaseUpdater"
rootProject.name = "DatabaseManager"

View File

@ -0,0 +1,158 @@
package dev.fyloz.colorrecipesexplorer.databasemanager
import liquibase.Contexts
import liquibase.Liquibase
import liquibase.database.DatabaseFactory
import liquibase.database.jvm.JdbcConnection
import liquibase.resource.ClassLoaderResourceAccessor
import mu.KotlinLogging
import org.slf4j.Logger
import java.sql.Connection
import java.sql.DriverManager
internal const val DATABASE_VERSION_NOT_INITIALIZED = 0
internal const val METADATA_TABLE_NAME = "updater_metadata"
internal const val COLUMN_KEY_NAME = "metadata_key"
internal const val COLUMN_VALUE_NAME = "metadata_value"
internal const val ROW_VERSION_NAME = "version"
internal const val QUERY_SELECT_VERSION =
"SELECT $COLUMN_VALUE_NAME FROM $METADATA_TABLE_NAME WHERE $COLUMN_KEY_NAME='$ROW_VERSION_NAME'"
/** [CreDatabase]'s context. Contains data needed by the utility like the [properties] and the [logger]. */
data class DatabaseContext(
val properties: DatabaseUpdaterProperties,
val logger: Logger
)
/** DSL for [DatabaseContext]. */
fun databaseContext(
properties: DatabaseUpdaterProperties,
logger: Logger = KotlinLogging.logger { },
op: DatabaseContext.() -> Unit = {}
) = DatabaseContext(properties, logger).apply(op)
class CreDatabase(
context: DatabaseContext
) {
private val properties = context.properties
private val logger = context.logger
/** Connection to the database server */
val serverConnection = tryConnect(properties.url)
private var databaseConnectionOpen = false
/** Connection to the database */
val databaseConnection by lazy {
tryConnect(properties.url + properties.dbName)
.apply { databaseConnectionOpen = true }
}
private var databaseOpen = false
val database by lazy {
DatabaseFactory.getInstance().findCorrectDatabaseImplementation(JdbcConnection(databaseConnection))
.apply { databaseOpen = true }
}
/** If the database exists */
val exists: Boolean by lazy {
with(serverConnection.metaData.catalogs) {
while (next()) {
if (properties.dbName == getString(1)) {
return@with true
}
}
// If no catalog has the name of the targeted database, it does not exist
false
}
}
/** If the updater's metadata table exists in the database */
val metadataTableExists by lazy {
with(
serverConnection.metaData.getTables(properties.dbName, null, METADATA_TABLE_NAME, arrayOf("TABLE"))
) {
// If there is no result, the table does not exist
this.next()
}
}
/** The version of the database */
val version by lazy { fetchVersion() }
/** Updates the database to the target version. */
fun update() {
val targetVersion = properties.targetVersion
if (version >= targetVersion) {
logger.warn("Database version is superior or equals to the target version. No changes will be made.")
return
}
logger.info("Updating database from version $version to $targetVersion...")
val liquibase =
Liquibase("/changelogs/changelog.master.$targetVersion.xml", ClassLoaderResourceAccessor(), database)
liquibase.update(Contexts())
logger.debug("Update to version $targetVersion completed!")
liquibase.close()
logger.info("Update completed!")
}
/** Gets the database version. */
private fun fetchVersion(): Int {
fun getVersionFromDatabase(): Int {
val statement = databaseConnection.createStatement()
val results = statement.executeQuery(QUERY_SELECT_VERSION)
if (!results.next()) {
logger.debug("The version row in the metadata table was not found, assuming version $DATABASE_VERSION_NOT_INITIALIZED")
return DATABASE_VERSION_NOT_INITIALIZED
}
return results.getInt(COLUMN_VALUE_NAME)
}
// Check if the database exists
if (!exists) {
throw CreDatabaseException.UnknownDatabase(properties.dbName)
}
// Check if the metadata table exists
if (!metadataTableExists) {
logger.debug("The metadata table was not found, assuming version $DATABASE_VERSION_NOT_INITIALIZED")
return DATABASE_VERSION_NOT_INITIALIZED
}
return getVersionFromDatabase()
}
/** Try to create a [Connection] with the given [url]. */
private fun tryConnect(url: String): Connection =
try {
DriverManager.getConnection(url, properties.username, properties.password)
} catch (ex: Exception) {
throw CreDatabaseException.Connection(url, ex)
}
/** Closes all connections to the database. */
fun close() {
serverConnection.close()
if (databaseConnectionOpen) databaseConnection.close()
}
}
sealed class CreDatabaseException(message: String, exception: Exception? = null) : Exception(message, exception) {
/** Thrown when an error occurs while creating a connection to the database at the given [url]. */
class Connection(val url: String, exception: Exception) :
CreDatabaseException("Could not create a connection to the database at '$url'", exception)
/** Thrown when the database with the given [name] cannot be found. */
class UnknownDatabase(val name: String) :
CreDatabaseException("Unknown database '$name'")
}

View File

@ -0,0 +1,116 @@
package dev.fyloz.colorrecipesexplorer.databasemanager
import mu.KotlinLogging
import java.io.File
import kotlin.system.exitProcess
private const val ACTION_ARG_INDEX = 0
private const val PROPERTIES_PATH_ARG_INDEX = 1
private const val ACTION_VERSION_CHECK = "versionCheck"
private const val ACTION_UPDATE = "update"
private var propertiesPath: String? = null
private var database: CreDatabase? = null
private val logger = KotlinLogging.logger { }
// Lazy because propertiesPath is needed to load the properties file
private val properties by lazy { loadProperties(propertiesPath) }
internal fun main(args: Array<String>) {
logger.info("Starting Color Recipes Explorer database updater...")
if (args.isEmpty() || args.size < 2) {
logger.error("No enough parameters were specified. ")
exitLogUsage()
}
val action = args[ACTION_ARG_INDEX]
propertiesPath = args[PROPERTIES_PATH_ARG_INDEX]
try {
database = CreDatabase(DatabaseContext(properties, logger))
// Run the specified action
when (action) {
ACTION_VERSION_CHECK -> executeVersionCheck(database!!)
ACTION_UPDATE -> executeUpdate(database!!)
else -> {
logger.error("Invalid action '$action'")
exitLogUsage()
}
}
} catch (ex: CreDatabaseException) {
if (ex is CreDatabaseException.Connection) {
logger.error(ex.message, ex)
} else {
logger.error(ex.message)
}
exitError()
}
exit()
}
private fun executeVersionCheck(database: CreDatabase) {
logger.info("Performing a version check...")
logger.info("Target version: ${properties.targetVersion}")
val databaseVersion = database.version
logger.info("Database version: $databaseVersion")
when (databaseVersion.compareTo(properties.targetVersion)) {
1 -> logger.info("Database version is higher than target version!")
0 -> logger.info("Database version matches the target version!")
-1 -> logger.info("Database version is lower than target version!")
}
}
private fun executeUpdate(database: CreDatabase) {
logger.info("Performing an update...")
database.update()
}
/** Load properties from the given [path]. */
private fun loadProperties(path: String?): DatabaseUpdaterProperties {
// Stop the utility if propertiesPath is null
if (path == null) {
logger.error("No path to a properties file was specified")
exitError()
}
val file = File(path)
logger.debug("Loading properties from ${file.absolutePath}")
try {
val properties = databaseUpdaterProperties(file)
logger.debug("Loaded properties: $properties")
return properties
} catch (exception: InvalidDatabaseUpdaterPropertiesException) {
logger.error(exception.message)
exitError()
}
}
/** Exits the utility. */
private fun exit(): Nothing {
database?.close()
logger.info("Exiting Color Recipes Explorer database updater without error")
exitProcess(0)
}
/** Exits the utility with an error code and a warning. */
private fun exitError(): Nothing {
database?.close()
logger.warn("Exiting Color Recipes Explorer database updater with an error")
exitProcess(1)
}
/** Exits the utility and logs how to use it. */
private fun exitLogUsage(): Nothing {
logger.error("Usage: java -jar DatabaseUpdater.jar <action> <path to properties>")
logger.error("Actions: ")
logger.error("\t- versionCheck: Compare the database's version to the target version specified in the properties")
logger.error("\t- update: Update the database to the target version specified in the properties")
exitError()
}

View File

@ -0,0 +1,117 @@
package dev.fyloz.colorrecipesexplorer.databasemanager
import java.io.File
import java.io.FileInputStream
import java.util.*
/** The latest version of the database supported by the utility. The [DatabaseUpdaterProperties] target version cannot be higher than this. */
internal const val LATEST_DATABASE_VERSION = 2
/** The key of the target version property */
internal const val PROPERTY_TARGET_VERSION = "database.target-version"
/** The key of the database server's url property */
internal const val PROPERTY_URL = "database.url"
/** The key of the database's name property */
internal const val PROPERTY_DB_NAME = "database.name"
/** The key of the database's username property */
internal const val PROPERTY_USERNAME = "database.username"
/** The key of the database's password property */
internal const val PROPERTY_PASSWORD = "database.password"
/** The regular expression to validate a database url */
internal val DATABASE_URL_REGEX =
Regex("^jdbc:\\w+://(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])(:\\d+)?/\$")
/** The properties used by the utility */
data class DatabaseUpdaterProperties(
/** The target version of the database */
val targetVersion: Int,
/** The url of the database server */
val url: String,
/** The name of the database */
val dbName: String,
/** The username of the database's user */
val username: String,
/** The password of the database's user */
val password: String
) {
init {
if (targetVersion < 0 || targetVersion > LATEST_DATABASE_VERSION)
throw InvalidDatabaseUpdaterPropertiesException.InvalidTargetVersion(targetVersion)
if (!url.matches(DATABASE_URL_REGEX))
throw InvalidDatabaseUpdaterPropertiesException.InvalidUrl(url)
}
}
/** DSL for creating an instance of [DatabaseUpdaterProperties]. */
fun databaseUpdaterProperties(
targetVersion: Int = 0,
url: String = "jdbc:driver://host:1234/",
dbName: String = "database",
username: String = "user",
password: String = "pass",
op: DatabaseUpdaterProperties.() -> Unit = {}
) = DatabaseUpdaterProperties(targetVersion, url, dbName, username, password).apply(op)
/** DSL for creating an instance of [DatabaseUpdaterProperties] from the given [properties]. */
internal fun databaseUpdaterProperties(
properties: Properties,
op: DatabaseUpdaterProperties.() -> Unit = {}
) = with(properties) {
/** Gets a [property] from the given [properties]. Calls exitMissingProperty() when the value is null. */
fun property(property: String): String =
getProperty(property) ?: throw InvalidDatabaseUpdaterPropertiesException.MissingProperty(property)
databaseUpdaterProperties(
targetVersion = with(property(PROPERTY_TARGET_VERSION)) {
toIntOrNull() ?: throw InvalidDatabaseUpdaterPropertiesException.TargetVersionFormat(this)
},
url = property(PROPERTY_URL),
dbName = property(PROPERTY_DB_NAME),
username = property(PROPERTY_USERNAME),
password = property(PROPERTY_PASSWORD),
op
)
}
/** DSL for creating an instance of [DatabaseUpdaterProperties] from the given [file]. */
internal fun databaseUpdaterProperties(
file: File,
op: DatabaseUpdaterProperties.() -> Unit = {}
) = databaseUpdaterProperties(
properties = Properties().apply {
if (!file.exists() || !file.isFile)
throw InvalidDatabaseUpdaterPropertiesException.PropertiesNotFound(file.absolutePath)
load(FileInputStream(file))
},
op
)
/** An exception representing an invalid value in a [DatabaseUpdaterProperties] instance. */
internal sealed class InvalidDatabaseUpdaterPropertiesException(message: String) : Exception(message) {
/** Thrown when the properties file does not exists. */
class PropertiesNotFound(val path: String) :
InvalidDatabaseUpdaterPropertiesException("Could not find properties file at '$path'")
/** Thrown when the target version property cannot be represented as an [Int]. */
class TargetVersionFormat(val value: String) :
InvalidDatabaseUpdaterPropertiesException("Invalid database target version format for value '$value'")
/** Thrown when the target version property is not in the range {0, [LATEST_DATABASE_VERSION]}. */
class InvalidTargetVersion(val targetVersion: Int) :
InvalidDatabaseUpdaterPropertiesException("Invalid database target version '$targetVersion'. The database version must be between 0 and $LATEST_DATABASE_VERSION")
/** Thrown when the url property does not matches the pattern 'jdbc:{driver}://{host}{:port?}/'. */
class InvalidUrl(val url: String) :
InvalidDatabaseUpdaterPropertiesException("Invalid database url '$url'. The proper format is: jdbc:{driver}://{host}{:port?}/")
/** Thrown when the given [property] is not defined. */
class MissingProperty(val property: String) :
InvalidDatabaseUpdaterPropertiesException("The property $property is not defined")
}

View File

@ -1,4 +0,0 @@
package dev.fyloz.colorrecipesexplorer.databaseupdater

View File

@ -0,0 +1,250 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-3.9.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.9.xsd">
<changeSet author="william" id="1">
<createTable tableName="company">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="VARCHAR(50)">
<constraints nullable="false" unique="true"/>
</column>
</createTable>
</changeSet>
<changeSet author="william" id="2">
<createTable tableName="material">
<column name="id" type="bigint">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="inventory_quantity" type="DOUBLE">
<constraints nullable="false"/>
</column>
<column name="is_mix_type" type="BIT(1)">
<constraints nullable="false"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints nullable="false" unique="true"/>
</column>
<column name="material_type_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="william" id="3">
<createTable tableName="material_type">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints nullable="false" unique="true"/>
</column>
<column name="prefix" type="VARCHAR(255)">
<constraints nullable="false" unique="true"/>
</column>
<column defaultValueBoolean="false" name="use_percentages" type="BIT(1)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="william" id="4">
<createTable tableName="mix">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="location" type="VARCHAR(255)"/>
<column name="mix_type_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="recipe_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="william" id="5">
<createTable tableName="mix_quantity">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="quantity" type="DOUBLE">
<constraints nullable="false"/>
</column>
<column name="material_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="mix_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="mix" type="BIGINT"/>
</createTable>
</changeSet>
<changeSet author="william" id="6">
<createTable tableName="mix_type">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints unique="true" nullable="false"/>
</column>
<column name="material_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="william" id="7">
<createTable tableName="recipe">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="approbation_date" type="VARCHAR(255)"/>
<column name="description" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="note" type="VARCHAR(255)"/>
<column name="remark" type="VARCHAR(255)"/>
<column name="sample" type="INT">
<constraints nullable="false"/>
</column>
<column name="company_id" type="BIGINT">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="william" id="8">
<createTable tableName="recipe_step">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="message" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="recipe_id" type="BIGINT"/>
</createTable>
</changeSet>
<changeSet author="william" id="9">
<createTable tableName="updater_metadata">
<column name="metadata_key" type="VARCHAR(255)">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="metadata_value" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
</changeSet>
<changeSet author="william" id="10">
<insert tableName="updater_metadata">
<column name="metadata_key" value="version"/>
<column name="metadata_value" value="1"/>
</insert>
<rollback>
<delete tableName="updater_metadata">
<where>metadata_key='version'</where>
</delete>
</rollback>
</changeSet>
<changeSet author="william" id="11">
<createIndex indexName="ix_mix_quantity_mix" tableName="mix_quantity">
<column name="mix"/>
</createIndex>
</changeSet>
<changeSet author="william" id="12">
<createIndex indexName="ix_material_material_type_id" tableName="material">
<column name="material_type_id"/>
</createIndex>
</changeSet>
<changeSet author="william" id="13">
<createIndex indexName="ix_mix_quantity_mix_id" tableName="mix_quantity">
<column name="mix_id"/>
</createIndex>
</changeSet>
<changeSet author="william" id="14">
<createIndex indexName="ix_recipe_company_Id" tableName="recipe">
<column name="company_id"/>
</createIndex>
</changeSet>
<changeSet author="william" id="15">
<createIndex indexName="ix_mix_type_material_id" tableName="mix_type">
<column name="material_id"/>
</createIndex>
</changeSet>
<changeSet author="william" id="16">
<createIndex indexName="ix_mix_recipe_id" tableName="mix">
<column name="recipe_id"/>
</createIndex>
</changeSet>
<changeSet author="william" id="17">
<createIndex indexName="ix_mix_mix_type_id" tableName="mix">
<column name="mix_type_id"/>
</createIndex>
</changeSet>
<changeSet author="william" id="18">
<createIndex indexName="ix_recipe_step_recipe_id" tableName="recipe_step">
<column name="recipe_id"/>
</createIndex>
</changeSet>
<changeSet author="william" id="19">
<createIndex indexName="ix_mix_quantity_material_id" tableName="mix_quantity">
<column name="material_id"/>
</createIndex>
</changeSet>
<changeSet author="william" id="20">
<addForeignKeyConstraint baseColumnNames="mix" baseTableName="mix_quantity"
constraintName="fk_mix_quantity_mix_mix" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="mix" validate="true"/>
</changeSet>
<changeSet author="william" id="21">
<addForeignKeyConstraint baseColumnNames="material_type_id" baseTableName="material"
constraintName="fk_material_material_type_material_type_id" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="material_type" validate="true"/>
</changeSet>
<changeSet author="william" id="22">
<addForeignKeyConstraint baseColumnNames="mix_id" baseTableName="mix_quantity"
constraintName="fk_mix_quantity_mix_mix_id" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="mix" validate="true"/>
</changeSet>
<changeSet author="william" id="23">
<addForeignKeyConstraint baseColumnNames="company_id" baseTableName="recipe"
constraintName="fk_recipe_company_company_id" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="company" validate="true"/>
</changeSet>
<changeSet author="william" id="24">
<addForeignKeyConstraint baseColumnNames="material_id" baseTableName="mix_type"
constraintName="fk_mix_type_material_material_id" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="material" validate="true"/>
</changeSet>
<changeSet author="william" id="25">
<addForeignKeyConstraint baseColumnNames="recipe_id" baseTableName="mix"
constraintName="fk_mix_recipe_recipe_id" deferrable="false" initiallyDeferred="false"
onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="id"
referencedTableName="recipe" validate="true"/>
</changeSet>
<changeSet author="william" id="26">
<addForeignKeyConstraint baseColumnNames="mix_type_id" baseTableName="mix"
constraintName="fk_mix_mix_type_mix_type_id" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="mix_type" validate="true"/>
</changeSet>
<changeSet author="william" id="27">
<addForeignKeyConstraint baseColumnNames="recipe_id" baseTableName="recipe_step"
constraintName="fk_recipe_step_recipe_recipe_id" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="recipe" validate="true"/>
</changeSet>
<changeSet author="william" id="28">
<addForeignKeyConstraint baseColumnNames="material_id" baseTableName="mix_quantity"
constraintName="fk_mix_quantity_material_material_id" deferrable="false"
initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT"
referencedColumnNames="id" referencedTableName="material" validate="true"/>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,182 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/pro http://www.liquibase.org/xml/ns/pro/liquibase-pro-3.9.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.9.xsd">
<!-- Employees -->
<changeSet id="1" author="william">
<createTable tableName="employee_group">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="name" type="VARCHAR(255)">
<constraints nullable="false" unique="true"/>
</column>
</createTable>
</changeSet>
<changeSet id="2" author="william">
<createTable tableName="employee">
<column name="id" type="BIGINT">
<constraints nullable="false" primaryKey="true"/>
</column>
<column name="first_name" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="last_name" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="password" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="default_group_user" type="BIT(1)" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="system_user" type="BIT(1)" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
<column name="group_id" type="BIGINT"/>
<column name="last_login_time" type="datetime"/>
</createTable>
<addForeignKeyConstraint baseTableName="employee" baseColumnNames="group_id"
constraintName="fk_employee_employee_group_group_id"
referencedTableName="employee_group"
referencedColumnNames="id"/>
<createIndex tableName="employee" indexName="ix_employee_group_id">
<column name="group_id"/>
</createIndex>
</changeSet>
<changeSet id="3" author="william">
<createTable tableName="employee_permission">
<column name="employee_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="permission" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseTableName="employee_permission" baseColumnNames="employee_id"
constraintName="fk_employee_permission_employee_employee_id"
referencedTableName="employee"
referencedColumnNames="id"/>
<createIndex tableName="employee_permission" indexName="ix_employee_permission_employee_id">
<column name="employee_id"/>
</createIndex>
</changeSet>
<changeSet id="4" author="william">
<createTable tableName="group_permission">
<column name="group_id" type="BIGINT">
<constraints nullable="false"/>
</column>
<column name="permission" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<addForeignKeyConstraint baseTableName="group_permission" baseColumnNames="group_id"
constraintName="fk_group_permission_employee_group_group_id"
referencedTableName="employee_group"
referencedColumnNames="id"/>
<createIndex tableName="group_permission" indexName="ix_group_permission_group_id">
<column name="group_id"/>
</createIndex>
</changeSet>
<!-- Modèle -->
<changeSet id="5" author="william">
<dropForeignKeyConstraint baseTableName="mix_quantity" constraintName="fk_mix_quantity_material_material_id"/>
<dropForeignKeyConstraint baseTableName="mix_quantity" constraintName="fk_mix_quantity_mix_mix_id"/>
<dropForeignKeyConstraint baseTableName="mix_quantity" constraintName="fk_mix_quantity_mix_mix"/>
<dropIndex tableName="mix_quantity" indexName="ix_mix_quantity_material_id"/>
<dropIndex tableName="mix_quantity" indexName="ix_mix_quantity_mix_id"/>
<dropIndex tableName="mix_quantity" indexName="ix_mix_quantity_mix"/>
<dropColumn tableName="mix_quantity" columnName="mix"/>
<renameTable oldTableName="mix_quantity" newTableName="mix_material"/>
<addForeignKeyConstraint baseTableName="mix_material" baseColumnNames="material_id"
constraintName="fk_mix_material_material_material_id"
referencedTableName="material"
referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="mix_material" baseColumnNames="mix_id"
constraintName="fk_mix_material_mix_mix_id"
referencedTableName="mix"
referencedColumnNames="id"/>
<createIndex tableName="mix_material" indexName="ix_mix_material_material_id">
<column name="material_id"/>
</createIndex>
<createIndex tableName="mix_material" indexName="ix_mix_material_mix_id">
<column name="mix_id"/>
</createIndex>
</changeSet>
<changeSet id="6" author="william">
<renameColumn tableName="material" oldColumnName="is_mix_type" newColumnName="mix_type"
columnDataType="BIT(10)"/>
<modifyDataType tableName="material" columnName="inventory_quantity" newDataType="FLOAT(12)"/>
<addNotNullConstraint tableName="material" columnName="inventory_quantity" columnDataType="FLOAT(12)"/>
</changeSet>
<changeSet id="7" author="william">
<modifyDataType tableName="recipe" columnName="approbation_date" newDataType="DATE(10)"/>
</changeSet>
<changeSet id="8" author="william">
<addColumn tableName="material_type">
<column name="system_type" type="BIT(1)" defaultValueBoolean="false">
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet id="9" author="william">
<modifyDataType tableName="company" columnName="name" newDataType="VARCHAR(255)"/>
<addNotNullConstraint tableName="company" columnName="name" columnDataType="VARCHAR(255)"/>
</changeSet>
<changeSet id="10" author="william">
<dropForeignKeyConstraint baseTableName="mix_type" constraintName="fk_mix_type_material_material_id"/>
<dropForeignKeyConstraint baseTableName="mix" constraintName="fk_mix_recipe_recipe_id"/>
<dropForeignKeyConstraint baseTableName="mix" constraintName="fk_mix_mix_type_mix_type_id"/>
<dropForeignKeyConstraint baseTableName="recipe_step" constraintName="fk_recipe_step_recipe_recipe_id"/>
<dropForeignKeyConstraint baseTableName="employee" constraintName="fk_employee_employee_group_group_id"/>
<dropForeignKeyConstraint baseTableName="employee_permission" constraintName="fk_employee_permission_employee_employee_id"/>
<dropForeignKeyConstraint baseTableName="group_permission" constraintName="fk_group_permission_employee_group_group_id"/>
<dropForeignKeyConstraint baseTableName="mix_material" constraintName="fk_mix_material_material_material_id"/>
<dropForeignKeyConstraint baseTableName="mix_material" constraintName="fk_mix_material_mix_mix_id"/>
<dropForeignKeyConstraint baseTableName="material" constraintName="fk_material_material_type_material_type_id"/>
<dropForeignKeyConstraint baseTableName="recipe" constraintName="fk_recipe_company_company_id"/>
</changeSet>
<changeSet id="11" author="william">
<addAutoIncrement tableName="employee" columnName="id" columnDataType="BIGINT"/>
<addAutoIncrement tableName="employee_group" columnName="id" columnDataType="BIGINT"/>
<addAutoIncrement tableName="company" columnName="id" columnDataType="BIGINT"/>
<addAutoIncrement tableName="material" columnName="id" columnDataType="BIGINT"/>
<addAutoIncrement tableName="material_type" columnName="id" columnDataType="BIGINT"/>
<addAutoIncrement tableName="mix" columnName="id" columnDataType="BIGINT"/>
<addAutoIncrement tableName="mix_material" columnName="id" columnDataType="BIGINT"/>
<addAutoIncrement tableName="mix_type" columnName="id" columnDataType="BIGINT"/>
<addAutoIncrement tableName="recipe" columnName="id" columnDataType="BIGINT"/>
<addAutoIncrement tableName="recipe_step" columnName="id" columnDataType="BIGINT"/>
</changeSet>
<changeSet id="12" author="william">
<addForeignKeyConstraint baseTableName="mix_type" baseColumnNames="material_id" constraintName="fk_mix_type_material_material_id" referencedTableName="material" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="mix" baseColumnNames="recipe_id" constraintName="fk_mix_recipe_recipe_id" referencedTableName="recipe" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="mix" baseColumnNames="mix_type_id" constraintName="fk_mix_mix_type_mix_type_id" referencedTableName="mix_type" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="recipe_step" baseColumnNames="recipe_id" constraintName="fk_recipe_step_recipe_recipe_id" referencedTableName="recipe" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="employee" baseColumnNames="group_id" constraintName="fk_employee_employee_group_group_id" referencedTableName="employee_group" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="employee_permission" baseColumnNames="employee_id" constraintName="fk_employee_permission_employee_employee_id" referencedTableName="employee" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="group_permission" baseColumnNames="group_id" constraintName="fk_group_permission_employee_group_group_id" referencedTableName="employee_group" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="mix_material" baseColumnNames="material_id" constraintName="fk_mix_material_material_material_id" referencedTableName="material" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="mix_material" baseColumnNames="mix_id" constraintName="fk_mix_material_mix_mix_id" referencedTableName="mix" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="material" baseColumnNames="material_type_id" constraintName="fk_material_material_type_material_type_id" referencedTableName="material_type" referencedColumnNames="id"/>
<addForeignKeyConstraint baseTableName="recipe" baseColumnNames="company_id" constraintName="fk_recipe_company_company_id" referencedTableName="company" referencedColumnNames="id"/>
</changeSet>
<changeSet id="13" author="william">
<update tableName="updater_metadata">
<column name="metadata_value" value="2"/>
<where>metadata_key='version'</where>
</update>
<rollback>
<update tableName="updater_metadata">
<column name="metadata_value" value="1"/>
<where>metadata_key='version'</where>
</update>
</rollback>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
<include file="/changelogs/changelog.1.xml"/>
</databaseChangeLog>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd">
<include file="/changelogs/changelog.1.xml"/>
<include file="/changelogs/changelog.2.xml"/>
</databaseChangeLog>

View File

@ -0,0 +1,5 @@
url: jdbc:mysql://172.66.1.1/updater
username: root
password: pass
changeLogFile: changelog.master.xml
classpath: /home/william/.m2/repository/mysql/mysql-connector-java/8.0.22/mysql-connector-java-8.0.22.jar

View File

@ -0,0 +1,34 @@
<configuration>
<statusListener class="ch.qos.logback.core.status.NopStatusListener"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
<encoder>
<pattern>%black(%d{ISO8601}) %highlight(%-5level) [%blue(%t)] %yellow(%C{36}): %msg%n%throwable</pattern>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/latest.log</file>
<append>true</append>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/du-%d{dd-MM-yyyy}.log.zip</fileNamePattern>
<maxHistory>10</maxHistory>
<totalSizeCap>100MB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{dd-MM-yyyy HH:mm:ss.SSS} [%thread] %-5level %logger.%M - %msg%n</pattern>
</encoder>
</appender>
<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>
</configuration>

View File

@ -0,0 +1,186 @@
package dev.fyloz.colorrecipesexplorer.databasemanager
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.datatest.forAll
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.matchers.shouldBe
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.spyk
import org.slf4j.Logger
import java.sql.Connection
import java.sql.DriverManager
import java.sql.SQLException
class DatabaseTest : ShouldSpec({
val logger = mockk<Logger> {
every { debug(any()) } answers {}
}
val properties = databaseUpdaterProperties()
val context = databaseContext(properties, logger)
fun connectionMock(url: String) = every {
DriverManager.getConnection(url, properties.username, properties.password)
}
fun serverConnectionMock() = connectionMock(properties.url)
fun databaseConnectionMock() = connectionMock(properties.url + properties.dbName)
context("CreDatabase") {
mockkStatic(DriverManager::class.qualifiedName!!) {
fun withDatabase(test: CreDatabase.() -> Unit) {
serverConnectionMock() returns mockk()
CreDatabase(context).test()
}
context("::new") {
should("throws Connection exception when the url is invalid") {
serverConnectionMock() throws SQLException()
val exception = shouldThrow<CreDatabaseException.Connection> { CreDatabase(context) }
exception.url shouldBe properties.url
}
should("initialize a connection to the database's server") {
val connection = mockk<Connection>()
serverConnectionMock() returns connection
val database = CreDatabase(context)
database.serverConnection shouldBe connection
}
}
context("::databaseConnection") {
val databaseConnectionUrl = properties.url + properties.dbName
should("throws Connection exception when the url is invalid") {
withDatabase {
databaseConnectionMock() throws SQLException()
val exception = shouldThrow<CreDatabaseException.Connection> { databaseConnection }
exception.url shouldBe databaseConnectionUrl
}
}
should("initialize a connection to the database") {
withDatabase {
val connection = mockk<Connection>()
databaseConnectionMock() returns connection
databaseConnection shouldBe connection
}
}
}
context("::exists should be") {
fun withDatabaseCatalog(catalog: String?, test: CreDatabase.() -> Unit) {
withDatabase {
every { serverConnection.metaData } returns mockk {
every { catalogs } returns mockk {
every { next() } answers { catalog != null } andThen false
every { getString(1) } answers { catalog } andThen null
}
}
test()
}
}
should("be true when database's catalogs contains database's name") {
withDatabaseCatalog(properties.dbName) {
exists shouldBe true
}
}
should("be false when database's catalogs does not contain given database's name") {
withDatabaseCatalog("anotherName") {
exists shouldBe false
}
}
should("be false when database's catalogs are empty") {
withDatabaseCatalog(null) {
exists shouldBe false
}
}
}
context("::metadataTableExists should be") {
forAll<Boolean>(
"true when query returns a table" to true,
"false when query returns nothing" to false
) { exists ->
withDatabase {
every { serverConnection.metaData } returns mockk {
every {
getTables(properties.dbName, null, METADATA_TABLE_NAME, arrayOf("TABLE"))
} returns mockk {
every { next() } answers { exists } andThen false
}
}
metadataTableExists shouldBe exists
}
}
}
context("::version") {
fun withMockedDatabase(
databaseExists: Boolean,
metadataTableExists: Boolean,
versionRowExists: Boolean,
test: CreDatabase.() -> Unit
) {
withDatabase {
// Mocking lazy properties does not seems to work, so we need to mock everything
every { serverConnection.metaData } returns mockk {
every { catalogs } returns mockk {
every { next() } answers { databaseExists } andThen false
every { getString(1) } answers { properties.dbName }
}
every {
getTables(properties.dbName, null, METADATA_TABLE_NAME, arrayOf("TABLE"))
} returns mockk {
every { next() } returns metadataTableExists
}
}
databaseConnectionMock() returns mockk {
every { createStatement() } returns mockk {
every { executeQuery(QUERY_SELECT_VERSION) } returns mockk {
every { next() } answers { versionRowExists } andThen false
every { getInt(COLUMN_VALUE_NAME) } returns properties.targetVersion
}
}
}
test()
}
}
should("equals database's version") {
withMockedDatabase(databaseExists = true, metadataTableExists = true, versionRowExists = true) {
version shouldBe properties.targetVersion
}
}
should("throw UnknownDatabase exception when database doesn't exists") {
withMockedDatabase(databaseExists = false, metadataTableExists = true, versionRowExists = true) {
val exception = shouldThrow<CreDatabaseException.UnknownDatabase> { version }
exception.name shouldBe properties.dbName
}
}
should("be 0 when the metadata table does not exist") {
withMockedDatabase(databaseExists = true, metadataTableExists = false, versionRowExists = true) {
version shouldBe 0
}
}
should("be 0 when the version row does not exist") {
withMockedDatabase(databaseExists = true, metadataTableExists = true, versionRowExists = false) {
version shouldBe 0
}
}
}
}
}
})

View File

@ -0,0 +1,154 @@
package dev.fyloz.colorrecipesexplorer.databasemanager
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.datatest.forAll
import io.kotest.core.spec.style.ShouldSpec
import io.kotest.core.test.TestContext
import io.kotest.matchers.shouldBe
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import java.io.File
import java.util.*
class PropertiesTest : ShouldSpec({
val targetVersion = 0
val url = "jdbc:driver://url:3306/"
val dbName = "database"
val username = "user"
val password = "pass"
context("database's url regular expression") {
context("matches valid urls") {
forAll(
"jdbc:mysql://host:9999/",
"jdbc:h2://host:9999/",
"jdbc:mysql://127.0.0.1:9999/",
"jdbc:mysql://host:9999/",
"jdbc:mysql://host/"
) { url ->
url.matches(DATABASE_URL_REGEX) shouldBe true
}
}
context("does not match invalid urls") {
forAll(
"mysql://host:9999/",
"jdbc://host:9999/",
"jdbc:mysql:host:9999/",
"jdbc:mysql/",
"host:9999",
"jdbc:mysql://host:9999/database",
"jdbc:mysql://host"
) { url ->
url.matches(DATABASE_URL_REGEX) shouldBe false
}
}
should("not matches empty url") {
"".matches(DATABASE_URL_REGEX) shouldBe false
}
}
context("DatabaseUpdaterProperties::new throws") {
context("InvalidTargetVersion exception with invalid database target versions") {
forAll<Int>(
"negative" to -1,
"too large" to LATEST_DATABASE_VERSION + 1
) { targetVersion ->
val exception =
shouldThrow<InvalidDatabaseUpdaterPropertiesException.InvalidTargetVersion> {
DatabaseUpdaterProperties(targetVersion, url, dbName, username, password)
}
exception.targetVersion shouldBe targetVersion
}
}
context("InvalidUrl exception with invalid database urls") {
forAll(
"mysql://host:9999/",
"jdbc:mysql://:9999"
) { url ->
val exception =
shouldThrow<InvalidDatabaseUpdaterPropertiesException.InvalidUrl> {
DatabaseUpdaterProperties(targetVersion, url, dbName, username, password)
}
exception.url shouldBe url
}
}
}
context("DatabaseUpdaterProperties DSL from properties") {
val properties = mockk<Properties>()
every { properties.getProperty(PROPERTY_URL) } returns url
every { properties.getProperty(PROPERTY_DB_NAME) } returns dbName
every { properties.getProperty(PROPERTY_USERNAME) } returns username
every { properties.getProperty(PROPERTY_PASSWORD) } returns password
fun withTargetVersion(version: String?, test: () -> Unit) {
every { properties.getProperty(PROPERTY_TARGET_VERSION) } returns version
test()
}
fun withTargetVersion(version: Int, test: () -> Unit) = withTargetVersion(version.toString(), test)
should("throw TargetVersionFormat with non-numeric target version") {
val invalidTargetVersion = "zero"
withTargetVersion(invalidTargetVersion) {
val exception = shouldThrow<InvalidDatabaseUpdaterPropertiesException.TargetVersionFormat> {
databaseUpdaterProperties(properties)
}
exception.value shouldBe invalidTargetVersion
}
}
should("return instance with correct values") {
withTargetVersion(targetVersion) {
with(databaseUpdaterProperties(properties)) {
this.targetVersion shouldBe targetVersion
this.url shouldBe url
this.dbName shouldBe dbName
this.username shouldBe username
this.password shouldBe password
}
}
}
should("throw MissingProperty when a property is not defined") {
withTargetVersion(null) {
val exception = shouldThrow<InvalidDatabaseUpdaterPropertiesException.MissingProperty> {
databaseUpdaterProperties(properties)
}
exception.property shouldBe PROPERTY_TARGET_VERSION
}
}
afterEach {
clearMocks(properties)
}
}
context("DatabaseUpdaterProperties DSL from file") {
val file = mockk<File>()
val fileAbsolutePath = "/config/du.properties"
every { file.absolutePath } returns fileAbsolutePath
should("throw PropertiesNotFound when the file does not exists") {
every { file.exists() } returns false
every { file.isFile } returns true
val exception = shouldThrow<InvalidDatabaseUpdaterPropertiesException.PropertiesNotFound> {
databaseUpdaterProperties(file)
}
exception.path shouldBe fileAbsolutePath
}
should("throw PropertiesNotFound when the file is not a file") {
every { file.exists() } returns true
every { file.isFile } returns false
val exception = shouldThrow<InvalidDatabaseUpdaterPropertiesException.PropertiesNotFound> {
databaseUpdaterProperties(file)
}
exception.path shouldBe fileAbsolutePath
}
}
})

View File

@ -0,0 +1,9 @@
package dev.fyloz.colorrecipesexplorer.databasemanager
import io.kotest.core.config.AbstractProjectConfig
import io.kotest.core.spec.IsolationMode
import io.kotest.core.test.AssertionMode
object TestConfig : AbstractProjectConfig() {
override val assertionMode = AssertionMode.Warn
}