commit eeeda509e10472a38ec0c2099274f5ba311bf307 Author: Jari Date: Mon Oct 20 21:51:00 2025 +0200 base project : WIP diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e9e444 --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Kotlin ### +.kotlin + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +.idea/* +.env +data/* +testSuite/* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..58fb3eb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,31 @@ +# Step 1: Build the application using Eclipse Temurin JDK 21 +FROM maven:3.9.8-eclipse-temurin-21 AS build + +WORKDIR /app + +# Copy Gradle wrapper and dependencies for caching +COPY gradle gradle +COPY gradlew . +COPY build.gradle.kts . +COPY settings.gradle.kts . +COPY src src + +# Give execution permission to Gradle wrapper +RUN chmod +x ./gradlew + +# Build the JAR file +RUN ./gradlew bootJar + +# Step 2: Create a minimal runtime image (JDK is required for Spring Boot) + +FROM openjdk:21 +WORKDIR /app + +# Copy the built JAR from the builder stage +COPY --from=build /app/build/libs/*.jar app.jar + +# Expose port 8080 +EXPOSE 8080 + +# Run the application +ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..9edf316 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# Planning-BE + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..633ec19 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,80 @@ +plugins { + kotlin("jvm") version "1.9.25" + kotlin("plugin.spring") version "1.9.25" + id("org.springframework.boot") version "3.4.3" + id("io.spring.dependency-management") version "1.1.7" + kotlin("plugin.jpa") version "1.9.25" + kotlin("kapt") version "1.9.25" +} + +group = "org.octopus.internal" +version = "1.0-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom(configurations.annotationProcessor.get()) + } +} + +kapt { + correctErrorTypes = true +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-mail") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-actuator") + developmentOnly("org.springframework.boot:spring-boot-devtools") + implementation("org.jetbrains.kotlin:kotlin-reflect") + compileOnly("org.projectlombok:lombok") + annotationProcessor("org.projectlombok:lombok") + + runtimeOnly("com.h2database:h2") + + implementation("org.mapstruct:mapstruct:1.6.0") + kapt("org.mapstruct:mapstruct-processor:1.6.0") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.jetbrains.kotlin:kotlin-reflect") + kapt("org.springframework.boot:spring-boot-configuration-processor") + + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.0") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + +// implementation("io.jsonwebtoken:jjwt-api:0.12.6") +// runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6") +// runtimeOnly("io.jsonwebtoken:jjwt-gson:0.12.6") +} + +kotlin { + compilerOptions { + freeCompilerArgs.addAll("-Xjsr305=strict") + } +} + +allOpen { + annotation("jakarta.persistence.Entity") + annotation("jakarta.persistence.MappedSuperclass") + annotation("jakarta.persistence.Embeddable") +} + +tasks.withType { + useJUnitPlatform() +} + +tasks.register("testJeko") { + commandLine("docker", "build", "-t", "be-planning-local:$version", ".") +} \ No newline at end of file diff --git a/compose.yaml b/compose.yaml new file mode 100755 index 0000000..e2d3940 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,8 @@ +services: + app: + image: be-planning-local + container_name: be + ports: + - "8080:8081" + volumes: + - ./data:/app/data \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e18bc25 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1b6c787 --- /dev/null +++ b/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..c5bd36a --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "OctopusMyRecipesApplication" + diff --git a/src/main/kotlin/org/octopus/internal/OctopusPlanningApplication.kt b/src/main/kotlin/org/octopus/internal/OctopusPlanningApplication.kt new file mode 100755 index 0000000..f3e090b --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/OctopusPlanningApplication.kt @@ -0,0 +1,11 @@ +package org.octopus.internal + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication(scanBasePackages = ["org.octopus.internal"]) +class OctopusPlanningApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/src/main/kotlin/org/octopus/internal/common/Extensions.kt b/src/main/kotlin/org/octopus/internal/common/Extensions.kt new file mode 100644 index 0000000..112c0a0 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/Extensions.kt @@ -0,0 +1 @@ +package org.octopus.internal.common diff --git a/src/main/kotlin/org/octopus/internal/common/enums/EBusinessException.kt b/src/main/kotlin/org/octopus/internal/common/enums/EBusinessException.kt new file mode 100755 index 0000000..6351988 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/enums/EBusinessException.kt @@ -0,0 +1,6 @@ +package org.octopus.internal.common.enums + +enum class EBusinessException(val msg: String) { + ENTITY_WITH_ID_NOT_FOUND("%s with id %s not found"), + INVALID_REQUEST("Invalid request for %s with reason: %s") +} \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/common/enums/EFoodCategory.kt b/src/main/kotlin/org/octopus/internal/common/enums/EFoodCategory.kt new file mode 100644 index 0000000..6047c55 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/enums/EFoodCategory.kt @@ -0,0 +1,21 @@ +package org.octopus.internal.common.enums + +enum class EFoodCategory(val type: String) { + CARBOHYDRATES("CARBOIDRATI"), + PROTEINS("PROTEINE"), + FIBERS("FIBRE"), + VEGETABLE_FATS("GRASSI VEGETALI"), + ANIMAL_FATS("GRASSI ANIMALI"), + FRUIT("FRUTTA"), + VEGETABLE("VERDURA"), + DAIRY("LATTICINI"), + SPICES_AND_HERBS("SPEZIE ED ERBE"), + SAUCES_AND_CONDIMENTS("SALSE E CONDIMENTI"), + NUTS_AND_SEEDS("FRUTTA SECCA E SEMI"), + LEGUMES("LEGUMI"), + CEREALS_AND_GRAINS("CEREALI E GRANI"), + SWEETENERS("DOLCIFICANTI"), + BEVERAGES("BEVANDE"), + OILS_AND_VINEGARS("OLI E ACETI"), + OTHER("ALTRO") +} diff --git a/src/main/kotlin/org/octopus/internal/common/enums/EFoodGroup.kt b/src/main/kotlin/org/octopus/internal/common/enums/EFoodGroup.kt new file mode 100644 index 0000000..7a0cf5f --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/enums/EFoodGroup.kt @@ -0,0 +1,19 @@ +package org.octopus.internal.common.enums + +enum class EFoodGroup(val type: String) { + CEREALS_AND_GRAINS("CEREALI E GRANI"), + MEAT("CARNE"), + FISH_AND_SEAFOOD("PESCE E FRUTTI DI MARE"), + EGGS("UOVA"), + LEGUMES("LEGUMI"), + NUTS_AND_SEEDS("FRUTTA SECCA E SEMI"), + DAIRY("LATTICINI"), + VEGETABLE("VERDURA"), + FRUIT("FRUTTA"), + SPICES_AND_HERBS("SPEZIE ED ERBE"), + OILS_AND_VINEGARS("OLI E ACETI"), + SAUCES_AND_CONDIMENTS("SALSE E CONDIMENTI"), + SWEETENERS("DOLCIFICANTI"), + BEVERAGES("BEVANDE"), + OTHER("ALTRO") +} diff --git a/src/main/kotlin/org/octopus/internal/common/enums/EFoodNutrient.kt b/src/main/kotlin/org/octopus/internal/common/enums/EFoodNutrient.kt new file mode 100644 index 0000000..a10b4c6 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/enums/EFoodNutrient.kt @@ -0,0 +1,10 @@ +package org.octopus.internal.common.enums + +enum class EFoodNutrient(val type: String) { + CARBOHYDRATES("CARBOIDRATI"), + PROTEINS("PROTEINE"), + FIBERS("FIBRE"), + VEGETABLE_FATS("GRASSI VEGETALI"), + ANIMAL_FATS("GRASSI ANIMALI"), + OTHER("ALTRO") +} diff --git a/src/main/kotlin/org/octopus/internal/common/enums/ERecipeCategory.kt b/src/main/kotlin/org/octopus/internal/common/enums/ERecipeCategory.kt new file mode 100644 index 0000000..710816c --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/enums/ERecipeCategory.kt @@ -0,0 +1,8 @@ +package org.octopus.internal.common.enums + +enum class ERecipeCategory(val type: String) { + VEG("VEGETARIANO/VEGANO"), + CARNE("CARNE"), + PESCE("PESCE"), + MIX("MISTA") +} \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/common/enums/ERecipeTag.kt b/src/main/kotlin/org/octopus/internal/common/enums/ERecipeTag.kt new file mode 100644 index 0000000..a611e6a --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/enums/ERecipeTag.kt @@ -0,0 +1,8 @@ +package org.octopus.internal.common.enums + +enum class ERecipeTag(val type: String) { + LIGHT("LEGGERA"), + EVENT("EVENTO"), + DAY("PRANZO"), + NIGHT("CENA") +} \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/common/exceptions/OctopusPlanningException.kt b/src/main/kotlin/org/octopus/internal/common/exceptions/OctopusPlanningException.kt new file mode 100755 index 0000000..68c9e34 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/exceptions/OctopusPlanningException.kt @@ -0,0 +1,13 @@ +package org.octopus.internal.common.exceptions + +import org.octopus.internal.common.enums.EBusinessException + +class OctopusPlanningException(val ex: EBusinessException, override val message: String) : Exception() { + + companion object { + fun create(exc: EBusinessException, vararg args: Any): OctopusPlanningException { + return OctopusPlanningException(exc, exc.msg.format(*args)) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/common/mappers/CookingTriesMapper.kt b/src/main/kotlin/org/octopus/internal/common/mappers/CookingTriesMapper.kt new file mode 100644 index 0000000..659edb3 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/mappers/CookingTriesMapper.kt @@ -0,0 +1,9 @@ +package org.octopus.internal.common.mappers + +import org.octopus.internal.common.models.CookingTries +import org.octopus.internal.db.entities.CookingTriesEntity +import org.mapstruct.Mapper + + +@Mapper(componentModel = "spring", uses = [RecipeMapper::class]) +interface CookingTriesMapper : GenericMapper diff --git a/src/main/kotlin/org/octopus/internal/common/mappers/FoodMapper.kt b/src/main/kotlin/org/octopus/internal/common/mappers/FoodMapper.kt new file mode 100644 index 0000000..3506d13 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/mappers/FoodMapper.kt @@ -0,0 +1,8 @@ +package org.octopus.internal.common.mappers + +import org.octopus.internal.common.models.Food +import org.octopus.internal.db.entities.FoodEntity +import org.mapstruct.Mapper + +@Mapper(componentModel = "spring") +interface FoodMapper : GenericMapper diff --git a/src/main/kotlin/org/octopus/internal/common/mappers/GenericMapper.kt b/src/main/kotlin/org/octopus/internal/common/mappers/GenericMapper.kt new file mode 100755 index 0000000..3635ca2 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/mappers/GenericMapper.kt @@ -0,0 +1,13 @@ +package org.octopus.internal.common.mappers + +interface GenericMapper { + + fun toEntity(model: M): E + fun toModel(entity: E): M + + fun toEntities(models: MutableList): MutableList = + models.map { toEntity(it) }.toMutableList() + + fun toModels(entities: MutableList): MutableList = + entities.map { toModel(it) }.toMutableList() +} \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/common/mappers/RecipeMapper.kt b/src/main/kotlin/org/octopus/internal/common/mappers/RecipeMapper.kt new file mode 100644 index 0000000..27fcac7 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/mappers/RecipeMapper.kt @@ -0,0 +1,13 @@ +package org.octopus.internal.common.mappers + +import org.octopus.internal.common.models.Recipe +import org.octopus.internal.db.entities.RecipeEntity +import org.mapstruct.Mapper +import org.mapstruct.Mapping + +@Mapper(componentModel = "spring", uses = [FoodMapper::class, CookingTriesMapper::class]) +interface RecipeMapper : GenericMapper { + + @Mapping(target = "cookingTries.recipe", ignore = true) + override fun toModel(entity: RecipeEntity): Recipe +} diff --git a/src/main/kotlin/org/octopus/internal/common/models/CookingTries.kt b/src/main/kotlin/org/octopus/internal/common/models/CookingTries.kt new file mode 100644 index 0000000..792a170 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/models/CookingTries.kt @@ -0,0 +1,13 @@ +package org.octopus.internal.common.models + +import java.time.LocalDate + +data class CookingTries( + val id: Long, + val recipe: Recipe, + val date: LocalDate, + val score: Int, + val notes: String, + val modifiedMainIngredients: List = mutableListOf(), + val modifiedOtherIngredients: List = mutableListOf() +) diff --git a/src/main/kotlin/org/octopus/internal/common/models/Food.kt b/src/main/kotlin/org/octopus/internal/common/models/Food.kt new file mode 100644 index 0000000..bc8ae38 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/models/Food.kt @@ -0,0 +1,11 @@ +package org.octopus.internal.common.models + +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient + +data class Food( + val id: Long, + val name: String, + val nutrient: EFoodNutrient, + val group: EFoodGroup +) diff --git a/src/main/kotlin/org/octopus/internal/common/models/Recipe.kt b/src/main/kotlin/org/octopus/internal/common/models/Recipe.kt new file mode 100644 index 0000000..85f80b0 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/common/models/Recipe.kt @@ -0,0 +1,15 @@ +package org.octopus.internal.common.models + +import org.octopus.internal.common.enums.ERecipeCategory +import org.octopus.internal.common.enums.ERecipeTag + +data class Recipe( + val id: Long, + val name: String, + val category: ERecipeCategory, + val tags: List, + val mainIngredients: List, + val otherIngredients: List, + val complexity: Int, + val cookingTries: List +) diff --git a/src/main/kotlin/org/octopus/internal/db/CookingTriesJpa.kt b/src/main/kotlin/org/octopus/internal/db/CookingTriesJpa.kt new file mode 100644 index 0000000..52c99c0 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/db/CookingTriesJpa.kt @@ -0,0 +1,19 @@ +package org.octopus.internal.db + +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.db.entities.CookingTriesEntity +import org.octopus.internal.db.entities.FoodEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param + +interface CookingTriesJpa : JpaRepository { + @Query("SELECT DISTINCT ct FROM cooking_tries ct LEFT JOIN ct.modifiedMainIngredients mmi LEFT JOIN ct.modifiedOtherIngredients moi WHERE mmi.nutrient = :nutrient OR moi.nutrient = :nutrient") + fun findByFoodNutrient(@Param("nutrient") nutrient: EFoodNutrient): MutableList + + @Query("SELECT DISTINCT ct FROM cooking_tries ct LEFT JOIN ct.modifiedMainIngredients mmi LEFT JOIN ct.modifiedOtherIngredients moi WHERE mmi.group = :group OR moi.group = :group") + fun findByFoodGroup(@Param("group") group: EFoodGroup): MutableList + + fun findAllByModifiedMainIngredientsInOrModifiedOtherIngredientsIn(mainIngredients: List, otherIngredients: List): MutableList +} diff --git a/src/main/kotlin/org/octopus/internal/db/FoodJpa.kt b/src/main/kotlin/org/octopus/internal/db/FoodJpa.kt new file mode 100644 index 0000000..f599a04 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/db/FoodJpa.kt @@ -0,0 +1,11 @@ +package org.octopus.internal.db + +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.db.entities.FoodEntity +import org.springframework.data.jpa.repository.JpaRepository + +interface FoodJpa : JpaRepository { + fun findAllByNutrient(nutrient: EFoodNutrient): MutableList + fun findAllByGroup(group: EFoodGroup): MutableList +} diff --git a/src/main/kotlin/org/octopus/internal/db/RecipeJpa.kt b/src/main/kotlin/org/octopus/internal/db/RecipeJpa.kt new file mode 100755 index 0000000..e57699b --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/db/RecipeJpa.kt @@ -0,0 +1,23 @@ +package org.octopus.internal.db + +import org.octopus.internal.common.enums.ERecipeCategory +import org.octopus.internal.common.enums.ERecipeTag +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.db.entities.FoodEntity +import org.octopus.internal.db.entities.RecipeEntity +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.data.jpa.repository.Query +import org.springframework.data.repository.query.Param + +interface RecipeJpa : JpaRepository { + fun findAllByCategory(category: ERecipeCategory): MutableList + fun findAllByTagsContaining(tag: ERecipeTag): MutableList + fun findAllByMainIngredientsInOrOtherIngredientsIn(mainIngredients: List, otherIngredients: List): MutableList + + @Query("SELECT DISTINCT r FROM recipes r LEFT JOIN r.mainIngredients mi LEFT JOIN r.otherIngredients oi WHERE mi.nutrient = :nutrient OR oi.nutrient = :nutrient") + fun findByFoodNutrient(@Param("nutrient") nutrient: EFoodNutrient): MutableList + + @Query("SELECT DISTINCT r FROM recipes r LEFT JOIN r.mainIngredients mi LEFT JOIN r.otherIngredients oi WHERE mi.group = :group OR oi.group = :group") + fun findByFoodGroup(@Param("group") group: EFoodGroup): MutableList +} diff --git a/src/main/kotlin/org/octopus/internal/db/converters/MonthListConverter.kt b/src/main/kotlin/org/octopus/internal/db/converters/MonthListConverter.kt new file mode 100644 index 0000000..e27f8be --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/db/converters/MonthListConverter.kt @@ -0,0 +1,19 @@ +package org.octopus.internal.db.converters + +import jakarta.persistence.AttributeConverter +import jakarta.persistence.Converter +import java.time.Month + +@Converter +class MonthListConverter : AttributeConverter, String> { + + override fun convertToDatabaseColumn(attribute: List?): String? { + return attribute?.joinToString(separator = ",") { it.name } + } + + override fun convertToEntityAttribute(dbData: String?): List? { + return dbData?.split(",") + ?.filter { it.isNotBlank() } + ?.mapNotNull { runCatching { Month.valueOf(it.trim()) }.getOrNull() } + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/db/entities/CookingTriesEntity.kt b/src/main/kotlin/org/octopus/internal/db/entities/CookingTriesEntity.kt new file mode 100644 index 0000000..333f634 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/db/entities/CookingTriesEntity.kt @@ -0,0 +1,43 @@ +package org.octopus.internal.db.entities + +import jakarta.persistence.CascadeType +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.JoinTable +import jakarta.persistence.ManyToMany +import jakarta.persistence.ManyToOne +import java.time.LocalDate + +@Entity(name = "cooking_tries") +data class CookingTriesEntity( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long = 0L, + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipe_id", nullable = false) + val recipe: RecipeEntity, + + val date: LocalDate, + val score: Int, + val notes: String, + + @ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @JoinTable( + name = "cooking_tries_modified_main_ingredients", + joinColumns = [JoinColumn(name = "cooking_try_id")], + inverseJoinColumns = [JoinColumn(name = "food_id")] + ) + val modifiedMainIngredients: List = mutableListOf(), + + @ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @JoinTable( + name = "cooking_tries_modified_other_ingredients", + joinColumns = [JoinColumn(name = "cooking_try_id")], + inverseJoinColumns = [JoinColumn(name = "food_id")] + ) + val modifiedOtherIngredients: List = mutableListOf() +) diff --git a/src/main/kotlin/org/octopus/internal/db/entities/FoodEntity.kt b/src/main/kotlin/org/octopus/internal/db/entities/FoodEntity.kt new file mode 100644 index 0000000..2c17eda --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/db/entities/FoodEntity.kt @@ -0,0 +1,21 @@ +package org.octopus.internal.db.entities + +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient + +@Entity(name = "foods") +data class FoodEntity( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long = 0L, + val name: String, + @Enumerated(EnumType.STRING) + val nutrient: EFoodNutrient, + @Enumerated(EnumType.STRING) + val group: EFoodGroup +) diff --git a/src/main/kotlin/org/octopus/internal/db/entities/RecipeEntity.kt b/src/main/kotlin/org/octopus/internal/db/entities/RecipeEntity.kt new file mode 100644 index 0000000..a0e7cd7 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/db/entities/RecipeEntity.kt @@ -0,0 +1,45 @@ +package org.octopus.internal.db.entities + +import jakarta.persistence.CascadeType +import jakarta.persistence.Entity +import jakarta.persistence.FetchType +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.JoinColumn +import jakarta.persistence.JoinTable +import jakarta.persistence.ManyToMany +import jakarta.persistence.OneToMany +import org.octopus.internal.common.enums.ERecipeCategory +import org.octopus.internal.common.enums.ERecipeTag + +@Entity(name = "recipes") +data class RecipeEntity( + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long = 0L, + val name: String, + val category: ERecipeCategory, + val tags: List, + + @ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @JoinTable( + name = "recipe_main_ingredients", + joinColumns = [JoinColumn(name = "recipe_id")], + inverseJoinColumns = [JoinColumn(name = "food_id")] + ) + val mainIngredients: List, + + @ManyToMany(fetch = FetchType.LAZY, cascade = [CascadeType.ALL]) + @JoinTable( + name = "recipe_other_ingredients", + joinColumns = [JoinColumn(name = "recipe_id")], + inverseJoinColumns = [JoinColumn(name = "food_id")] + ) + val otherIngredients: List, + + val complexity: Int, + + @OneToMany(cascade = [CascadeType.ALL], orphanRemoval = true) + @JoinColumn(name = "recipe_id") + val cookingTries: List +) diff --git a/src/main/kotlin/org/octopus/internal/repositories/CookingTriesRepository.kt b/src/main/kotlin/org/octopus/internal/repositories/CookingTriesRepository.kt new file mode 100644 index 0000000..1770399 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/repositories/CookingTriesRepository.kt @@ -0,0 +1,16 @@ +package org.octopus.internal.repositories + +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.models.CookingTries +import org.octopus.internal.common.models.Food + +interface CookingTriesRepository { + fun getById(id: Long): CookingTries? + fun getAll(): MutableList + fun deleteById(id: Long) + fun deleteAll() + fun findByFoodNutrient(nutrient: EFoodNutrient): MutableList + fun findByFoodGroup(group: EFoodGroup): MutableList + fun findByFoods(foods: List): MutableList +} diff --git a/src/main/kotlin/org/octopus/internal/repositories/FoodRepository.kt b/src/main/kotlin/org/octopus/internal/repositories/FoodRepository.kt new file mode 100644 index 0000000..e22d443 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/repositories/FoodRepository.kt @@ -0,0 +1,14 @@ +package org.octopus.internal.repositories + +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.models.Food + +interface FoodRepository { + fun getById(id: Long): Food? + fun getAll(): MutableList + fun deleteById(id: Long) + fun deleteAll() + fun findByNutrient(nutrient: EFoodNutrient): MutableList + fun findByGroup(group: EFoodGroup): MutableList +} diff --git a/src/main/kotlin/org/octopus/internal/repositories/RecipeRepository.kt b/src/main/kotlin/org/octopus/internal/repositories/RecipeRepository.kt new file mode 100644 index 0000000..d4b5ca7 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/repositories/RecipeRepository.kt @@ -0,0 +1,20 @@ +package org.octopus.internal.repositories + +import org.octopus.internal.common.enums.ERecipeCategory +import org.octopus.internal.common.enums.ERecipeTag +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.models.Food +import org.octopus.internal.common.models.Recipe + +interface RecipeRepository { + fun getById(id: Long): Recipe? + fun getAll(): MutableList + fun deleteById(id: Long) + fun deleteAll() + fun findByCategory(category: ERecipeCategory): MutableList + fun findByTag(tag: ERecipeTag): MutableList + fun findByFoods(foods: List): MutableList + fun findByFoodNutrient(nutrient: EFoodNutrient): MutableList + fun findByFoodGroup(group: EFoodGroup): MutableList +} diff --git a/src/main/kotlin/org/octopus/internal/repositories/impl/CookingTriesRepositoryImpl.kt b/src/main/kotlin/org/octopus/internal/repositories/impl/CookingTriesRepositoryImpl.kt new file mode 100644 index 0000000..69e2e37 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/repositories/impl/CookingTriesRepositoryImpl.kt @@ -0,0 +1,48 @@ +package org.octopus.internal.repositories.impl + +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.mappers.CookingTriesMapper +import org.octopus.internal.common.mappers.FoodMapper +import org.octopus.internal.common.models.CookingTries +import org.octopus.internal.common.models.Food +import org.octopus.internal.db.CookingTriesJpa +import org.octopus.internal.repositories.CookingTriesRepository +import org.springframework.stereotype.Repository + +@Repository +class CookingTriesRepositoryImpl( + private val jpa: CookingTriesJpa, + private val mapper: CookingTriesMapper, + private val foodMapper: FoodMapper +) : CookingTriesRepository { + override fun getById(id: Long): CookingTries? { + return jpa.findById(id).map { mapper.toModel(it) }.orElse(null) + } + + override fun getAll(): MutableList { + return jpa.findAll().map { mapper.toModel(it) }.toMutableList() + } + + override fun deleteById(id: Long) { + jpa.deleteById(id) + } + + override fun deleteAll() { + jpa.deleteAll() + } + + override fun findByFoodNutrient(nutrient: EFoodNutrient): MutableList { + return jpa.findByFoodNutrient(nutrient).map { mapper.toModel(it) }.toMutableList() + } + + override fun findByFoodGroup(group: EFoodGroup): MutableList { + return jpa.findByFoodGroup(group).map { mapper.toModel(it) }.toMutableList() + } + + override fun findByFoods(foods: List): MutableList { + val foodEntities = foods.map { foodMapper.toEntity(it) } + return jpa.findAllByModifiedMainIngredientsInOrModifiedOtherIngredientsIn(foodEntities, foodEntities) + .map { mapper.toModel(it) }.toMutableList() + } +} diff --git a/src/main/kotlin/org/octopus/internal/repositories/impl/FoodRepositoryImpl.kt b/src/main/kotlin/org/octopus/internal/repositories/impl/FoodRepositoryImpl.kt new file mode 100644 index 0000000..b173fd5 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/repositories/impl/FoodRepositoryImpl.kt @@ -0,0 +1,39 @@ +package org.octopus.internal.repositories.impl + +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.mappers.FoodMapper +import org.octopus.internal.common.models.Food +import org.octopus.internal.db.FoodJpa +import org.octopus.internal.repositories.FoodRepository +import org.springframework.stereotype.Repository + +@Repository +class FoodRepositoryImpl( + private val jpa: FoodJpa, + private val mapper: FoodMapper +) : FoodRepository { + override fun getById(id: Long): Food? { + return jpa.findById(id).map { mapper.toModel(it) }.orElse(null) + } + + override fun getAll(): MutableList { + return jpa.findAll().map { mapper.toModel(it) }.toMutableList() + } + + override fun deleteById(id: Long) { + jpa.deleteById(id) + } + + override fun deleteAll() { + jpa.deleteAll() + } + + override fun findByNutrient(nutrient: EFoodNutrient): MutableList { + return jpa.findAllByNutrient(nutrient).map { mapper.toModel(it) }.toMutableList() + } + + override fun findByGroup(group: EFoodGroup): MutableList { + return jpa.findAllByGroup(group).map { mapper.toModel(it) }.toMutableList() + } +} diff --git a/src/main/kotlin/org/octopus/internal/repositories/impl/RecipeRepositoryImpl.kt b/src/main/kotlin/org/octopus/internal/repositories/impl/RecipeRepositoryImpl.kt new file mode 100644 index 0000000..85ff848 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/repositories/impl/RecipeRepositoryImpl.kt @@ -0,0 +1,59 @@ +package org.octopus.internal.repositories.impl + +import org.octopus.internal.common.enums.ERecipeCategory +import org.octopus.internal.common.enums.ERecipeTag +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.mappers.FoodMapper +import org.octopus.internal.common.mappers.RecipeMapper +import org.octopus.internal.common.models.Food +import org.octopus.internal.common.models.Recipe +import org.octopus.internal.db.RecipeJpa +import org.octopus.internal.repositories.RecipeRepository +import org.springframework.stereotype.Repository + +@Repository +class RecipeRepositoryImpl( + private val jpa: RecipeJpa, + private val mapper: RecipeMapper, + private val foodMapper: FoodMapper +) : RecipeRepository { + + override fun getById(id: Long): Recipe? { + return jpa.findById(id).map { mapper.toModel(it) }.orElse(null) + } + + override fun getAll(): MutableList { + return jpa.findAll().map { mapper.toModel(it) }.toMutableList() + } + + override fun deleteById(id: Long) { + jpa.deleteById(id) + } + + override fun deleteAll() { + jpa.deleteAll() + } + + override fun findByCategory(category: ERecipeCategory): MutableList { + return jpa.findAllByCategory(category).map { mapper.toModel(it) }.toMutableList() + } + + override fun findByTag(tag: ERecipeTag): MutableList { + return jpa.findAllByTagsContaining(tag).map { mapper.toModel(it) }.toMutableList() + } + + override fun findByFoods(foods: List): MutableList { + val foodEntities = foods.map { foodMapper.toEntity(it) } + return jpa.findAllByMainIngredientsInOrOtherIngredientsIn(foodEntities, foodEntities) + .map { mapper.toModel(it) }.toMutableList() + } + + override fun findByFoodNutrient(nutrient: EFoodNutrient): MutableList { + return jpa.findByFoodNutrient(nutrient).map { mapper.toModel(it) }.toMutableList() + } + + override fun findByFoodGroup(group: EFoodGroup): MutableList { + return jpa.findByFoodGroup(group).map { mapper.toModel(it) }.toMutableList() + } +} diff --git a/src/main/kotlin/org/octopus/internal/services/CookingTriesService.kt b/src/main/kotlin/org/octopus/internal/services/CookingTriesService.kt new file mode 100644 index 0000000..cc5379f --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/services/CookingTriesService.kt @@ -0,0 +1,16 @@ +package org.octopus.internal.services + +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.models.CookingTries +import org.octopus.internal.common.models.Food + +interface CookingTriesService { + fun getById(id: Long): CookingTries + fun getAll(): MutableList + fun deleteById(id: Long) + fun deleteAll() + fun findByFoodNutrient(nutrient: EFoodNutrient): MutableList + fun findByFoodGroup(group: EFoodGroup): MutableList + fun findByFoods(foods: List): MutableList +} diff --git a/src/main/kotlin/org/octopus/internal/services/FoodService.kt b/src/main/kotlin/org/octopus/internal/services/FoodService.kt new file mode 100644 index 0000000..1daec6d --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/services/FoodService.kt @@ -0,0 +1,14 @@ +package org.octopus.internal.services + +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.models.Food + +interface FoodService { + fun getById(id: Long): Food + fun getAll(): MutableList + fun deleteById(id: Long) + fun deleteAll() + fun findByNutrient(nutrient: EFoodNutrient): MutableList + fun findByGroup(group: EFoodGroup): MutableList +} diff --git a/src/main/kotlin/org/octopus/internal/services/RecipeService.kt b/src/main/kotlin/org/octopus/internal/services/RecipeService.kt new file mode 100644 index 0000000..499610d --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/services/RecipeService.kt @@ -0,0 +1,20 @@ +package org.octopus.internal.services + +import org.octopus.internal.common.enums.ERecipeCategory +import org.octopus.internal.common.enums.ERecipeTag +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.models.Food +import org.octopus.internal.common.models.Recipe + +interface RecipeService { + fun getById(id: Long): Recipe + fun getAll(): MutableList + fun deleteById(id: Long) + fun deleteAll() + fun findByCategory(category: ERecipeCategory): MutableList + fun findByTag(tag: ERecipeTag): MutableList + fun findByFoods(foods: List): MutableList + fun findByFoodNutrient(nutrient: EFoodNutrient): MutableList + fun findByFoodGroup(group: EFoodGroup): MutableList +} diff --git a/src/main/kotlin/org/octopus/internal/services/impl/CookingTriesServiceImpl.kt b/src/main/kotlin/org/octopus/internal/services/impl/CookingTriesServiceImpl.kt new file mode 100644 index 0000000..a9ea991 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/services/impl/CookingTriesServiceImpl.kt @@ -0,0 +1,46 @@ +package org.octopus.internal.services.impl + +import org.octopus.internal.common.enums.EBusinessException +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.exceptions.OctopusPlanningException +import org.octopus.internal.common.models.CookingTries +import org.octopus.internal.common.models.Food +import org.octopus.internal.common.models.Recipe +import org.octopus.internal.repositories.CookingTriesRepository +import org.octopus.internal.services.CookingTriesService +import org.springframework.stereotype.Service + +@Service +class CookingTriesServiceImpl( + private val cookingTriesRepository: CookingTriesRepository +) : CookingTriesService { + override fun getById(id: Long): CookingTries { + return cookingTriesRepository.getById(id) + ?: throw OctopusPlanningException.create(EBusinessException.ENTITY_WITH_ID_NOT_FOUND, CookingTries::class.java.simpleName, id) + } + + override fun getAll(): MutableList { + return cookingTriesRepository.getAll() + } + + override fun deleteById(id: Long) { + cookingTriesRepository.deleteById(id) + } + + override fun deleteAll() { + cookingTriesRepository.deleteAll() + } + + override fun findByFoodNutrient(nutrient: EFoodNutrient): MutableList { + return cookingTriesRepository.findByFoodNutrient(nutrient) + } + + override fun findByFoodGroup(group: EFoodGroup): MutableList { + return cookingTriesRepository.findByFoodGroup(group) + } + + override fun findByFoods(foods: List): MutableList { + return cookingTriesRepository.findByFoods(foods) + } +} diff --git a/src/main/kotlin/org/octopus/internal/services/impl/FoodServiceImpl.kt b/src/main/kotlin/org/octopus/internal/services/impl/FoodServiceImpl.kt new file mode 100644 index 0000000..5a878cf --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/services/impl/FoodServiceImpl.kt @@ -0,0 +1,41 @@ +package org.octopus.internal.services.impl + +import org.octopus.internal.common.enums.EBusinessException +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.exceptions.OctopusPlanningException +import org.octopus.internal.common.models.Food +import org.octopus.internal.common.models.Recipe +import org.octopus.internal.repositories.FoodRepository +import org.octopus.internal.services.FoodService +import org.springframework.stereotype.Service + +@Service +class FoodServiceImpl( + private val foodRepository: FoodRepository +) : FoodService { + override fun getById(id: Long): Food { + return foodRepository.getById(id) + ?: throw OctopusPlanningException.create(EBusinessException.ENTITY_WITH_ID_NOT_FOUND, Food::class.java.simpleName, id) + } + + override fun getAll(): MutableList { + return foodRepository.getAll() + } + + override fun deleteById(id: Long) { + foodRepository.deleteById(id) + } + + override fun deleteAll() { + foodRepository.deleteAll() + } + + override fun findByNutrient(nutrient: EFoodNutrient): MutableList { + return foodRepository.findByNutrient(nutrient) + } + + override fun findByGroup(group: EFoodGroup): MutableList { + return foodRepository.findByGroup(group) + } +} diff --git a/src/main/kotlin/org/octopus/internal/services/impl/RecipeServiceImpl.kt b/src/main/kotlin/org/octopus/internal/services/impl/RecipeServiceImpl.kt new file mode 100644 index 0000000..a7ea334 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/services/impl/RecipeServiceImpl.kt @@ -0,0 +1,93 @@ +package org.octopus.internal.services.impl + +import org.octopus.internal.common.enums.EBusinessException +import org.octopus.internal.common.enums.ERecipeCategory +import org.octopus.internal.common.enums.ERecipeTag +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.exceptions.OctopusPlanningException +import org.octopus.internal.common.models.Food +import org.octopus.internal.common.models.Recipe +import org.octopus.internal.repositories.CookingTriesRepository +import org.octopus.internal.repositories.RecipeRepository +import org.octopus.internal.services.RecipeService +import org.springframework.stereotype.Service + +@Service +class RecipeServiceImpl( + private val recipeRepository: RecipeRepository, + private val cookingTriesRepository: CookingTriesRepository +) : RecipeService { + + override fun getById(id: Long): Recipe { + return recipeRepository.getById(id) ?: throw OctopusPlanningException.create(EBusinessException.ENTITY_WITH_ID_NOT_FOUND, Recipe::class.java.simpleName, id) + } + + override fun getAll(): MutableList { + return recipeRepository.getAll() + } + + override fun deleteById(id: Long) { + recipeRepository.deleteById(id) + } + + override fun deleteAll() { + recipeRepository.deleteAll() + } + + override fun findByCategory(category: ERecipeCategory): MutableList { + return recipeRepository.findByCategory(category) + } + + override fun findByTag(tag: ERecipeTag): MutableList { + return recipeRepository.findByTag(tag) + } + + override fun findByFoods(foods: List): MutableList { + val standardRecipes = recipeRepository.findByFoods(foods) + val tries = cookingTriesRepository.findByFoods(foods) + + val virtualRecipes = tries.map { tryItem -> + val originalRecipe = tryItem.recipe + originalRecipe.copy( + name = "${originalRecipe.name} (Variante: ${tryItem.notes})", + mainIngredients = tryItem.modifiedMainIngredients.ifEmpty { originalRecipe.mainIngredients }, + otherIngredients = tryItem.modifiedOtherIngredients.ifEmpty { originalRecipe.otherIngredients }, + ) + } + + return (standardRecipes + virtualRecipes).distinctBy { it.id to it.name }.toMutableList() + } + + override fun findByFoodNutrient(nutrient: EFoodNutrient): MutableList { + val standardRecipes = recipeRepository.findByFoodNutrient(nutrient) + val tries = cookingTriesRepository.findByFoodNutrient(nutrient) + + val virtualRecipes = tries.map { tryItem -> + val originalRecipe = tryItem.recipe + originalRecipe.copy( + name = "${originalRecipe.name} (Variante: ${tryItem.notes})", + mainIngredients = tryItem.modifiedMainIngredients.ifEmpty { originalRecipe.mainIngredients }, + otherIngredients = tryItem.modifiedOtherIngredients.ifEmpty { originalRecipe.otherIngredients }, + ) + } + + return (standardRecipes + virtualRecipes).distinctBy { it.id to it.name }.toMutableList() + } + + override fun findByFoodGroup(group: EFoodGroup): MutableList { + val standardRecipes = recipeRepository.findByFoodGroup(group) + val tries = cookingTriesRepository.findByFoodGroup(group) + + val virtualRecipes = tries.map { tryItem -> + val originalRecipe = tryItem.recipe + originalRecipe.copy( + name = "${originalRecipe.name} (Variante: ${tryItem.notes})", + mainIngredients = tryItem.modifiedMainIngredients.ifEmpty { originalRecipe.mainIngredients }, + otherIngredients = tryItem.modifiedOtherIngredients.ifEmpty { originalRecipe.otherIngredients }, + ) + } + + return (standardRecipes + virtualRecipes).distinctBy { it.id to it.name }.toMutableList() + } +} diff --git a/src/main/kotlin/org/octopus/internal/web/controllers/CookingTriesController.kt b/src/main/kotlin/org/octopus/internal/web/controllers/CookingTriesController.kt new file mode 100644 index 0000000..521b7e4 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/web/controllers/CookingTriesController.kt @@ -0,0 +1,63 @@ +package org.octopus.internal.web.controllers + +import org.octopus.internal.common.enums.EBusinessException +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.exceptions.OctopusPlanningException +import org.octopus.internal.common.models.CookingTries +import org.octopus.internal.services.CookingTriesService +import org.octopus.internal.web.utils.responses.WebResponse +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/cooking-tries") +class CookingTriesController( + private val cookingTriesService: CookingTriesService +) { + + @GetMapping + fun getAllCookingTries(): WebResponse> { + return WebResponse.ok(cookingTriesService.getAll()) + } + + @GetMapping("/{id}") + fun getCookingTriesById(@PathVariable id: Long): WebResponse { + return WebResponse.ok(cookingTriesService.getById(id)) + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun createCookingTries(@RequestBody cookingTries: CookingTries): WebResponse { + return WebResponse.ok(cookingTries) + } + + @PutMapping("/{id}") + fun updateCookingTries(@PathVariable id: Long, @RequestBody cookingTries: CookingTries): WebResponse { + return WebResponse.ok(cookingTries) + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + fun deleteCookingTriesById(@PathVariable id: Long): WebResponse { + cookingTriesService.deleteById(id) + return WebResponse.ok() + } + + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + fun deleteAllCookingTries(): WebResponse { + cookingTriesService.deleteAll() + return WebResponse.ok() + } + + @GetMapping("/nutrient/{nutrient}") + fun getCookingTriesByFoodNutrient(@PathVariable nutrient: EFoodNutrient): WebResponse> { + return WebResponse.ok(cookingTriesService.findByFoodNutrient(nutrient)) + } + + @GetMapping("/group/{group}") + fun getCookingTriesByFoodGroup(@PathVariable group: EFoodGroup): WebResponse> { + return WebResponse.ok(cookingTriesService.findByFoodGroup(group)) + } +} diff --git a/src/main/kotlin/org/octopus/internal/web/controllers/FoodController.kt b/src/main/kotlin/org/octopus/internal/web/controllers/FoodController.kt new file mode 100644 index 0000000..75f79ca --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/web/controllers/FoodController.kt @@ -0,0 +1,63 @@ +package org.octopus.internal.web.controllers + +import org.octopus.internal.common.enums.EBusinessException +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.exceptions.OctopusPlanningException +import org.octopus.internal.common.models.Food +import org.octopus.internal.services.FoodService +import org.octopus.internal.web.utils.responses.WebResponse +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/foods") +class FoodController( + private val foodService: FoodService +) { + + @GetMapping + fun getAllFoods(): WebResponse> { + return WebResponse.ok(foodService.getAll()) + } + + @GetMapping("/{id}") + fun getFoodById(@PathVariable id: Long): WebResponse { + return WebResponse.ok(foodService.getById(id)) + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun createFood(@RequestBody food: Food): WebResponse { + return WebResponse.ok(food) + } + + @PutMapping("/{id}") + fun updateFood(@PathVariable id: Long, @RequestBody food: Food): WebResponse { + return WebResponse.ok(food) + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + fun deleteFoodById(@PathVariable id: Long): WebResponse { + foodService.deleteById(id) + return WebResponse.ok() + } + + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + fun deleteAllFoods(): WebResponse { + foodService.deleteAll() + return WebResponse.ok() + } + + @GetMapping("/nutrient/{nutrient}") + fun getFoodsByNutrient(@PathVariable nutrient: EFoodNutrient): WebResponse> { + return WebResponse.ok(foodService.findByNutrient(nutrient)) + } + + @GetMapping("/group/{group}") + fun getFoodsByGroup(@PathVariable group: EFoodGroup): WebResponse> { + return WebResponse.ok(foodService.findByGroup(group)) + } +} diff --git a/src/main/kotlin/org/octopus/internal/web/controllers/RecipeController.kt b/src/main/kotlin/org/octopus/internal/web/controllers/RecipeController.kt new file mode 100644 index 0000000..5f19b59 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/web/controllers/RecipeController.kt @@ -0,0 +1,81 @@ +package org.octopus.internal.web.controllers + +import org.octopus.internal.common.enums.EBusinessException +import org.octopus.internal.common.enums.ERecipeCategory +import org.octopus.internal.common.enums.ERecipeTag +import org.octopus.internal.common.enums.EFoodGroup +import org.octopus.internal.common.enums.EFoodNutrient +import org.octopus.internal.common.exceptions.OctopusPlanningException +import org.octopus.internal.common.models.Food +import org.octopus.internal.common.models.Recipe +import org.octopus.internal.services.RecipeService +import org.octopus.internal.web.utils.responses.WebResponse +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/recipes") +class RecipeController( + private val recipeService: RecipeService +) { + + @GetMapping + fun getAllRecipes(): WebResponse> { + return WebResponse.ok(recipeService.getAll()) + } + + @GetMapping("/{id}") + fun getRecipeById(@PathVariable id: Long): WebResponse { + return WebResponse.ok(recipeService.getById(id)) + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun createRecipe(@RequestBody recipe: Recipe): WebResponse { +// return WebResponse.ok(recipe) + } + + @PutMapping("/{id}") + fun updateRecipe(@PathVariable id: Long, @RequestBody recipe: Recipe): WebResponse { +// return WebResponse.ok(recipe) + } + + @DeleteMapping("/{id}") + @ResponseStatus(HttpStatus.NO_CONTENT) + fun deleteRecipeById(@PathVariable id: Long): WebResponse { + recipeService.deleteById(id) + return WebResponse.ok() + } + + @DeleteMapping + @ResponseStatus(HttpStatus.NO_CONTENT) + fun deleteAllRecipes(): WebResponse { + recipeService.deleteAll() + return WebResponse.ok() + } + + @GetMapping("/category/{category}") + fun getRecipesByCategory(@PathVariable category: ERecipeCategory): WebResponse> { + return WebResponse.ok(recipeService.findByCategory(category)) + } + + @GetMapping("/tag/{tag}") + fun getRecipesByTag(@PathVariable tag: ERecipeTag): WebResponse> { + return WebResponse.ok(recipeService.findByTag(tag)) + } + + @PostMapping("/foods") + fun getRecipesByFoods(@RequestBody foods: List): WebResponse> { + return WebResponse.ok(recipeService.findByFoods(foods)) + } + + @GetMapping("/nutrient/{nutrient}") + fun getRecipesByFoodNutrient(@PathVariable nutrient: EFoodNutrient): WebResponse> { + return WebResponse.ok(recipeService.findByFoodNutrient(nutrient)) + } + + @GetMapping("/group/{group}") + fun getRecipesByFoodGroup(@PathVariable group: EFoodGroup): WebResponse> { + return WebResponse.ok(recipeService.findByFoodGroup(group)) + } +} diff --git a/src/main/kotlin/org/octopus/internal/web/dtos/EntryDto.kt b/src/main/kotlin/org/octopus/internal/web/dtos/EntryDto.kt new file mode 100755 index 0000000..270dcf6 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/web/dtos/EntryDto.kt @@ -0,0 +1,43 @@ +package org.octopus.internal.web.dtos + +import io.swagger.v3.oas.annotations.media.Schema +import jakarta.validation.constraints.DecimalMin +import org.hibernate.validator.constraints.Length +import org.octopus.internal.web.dtos.validators.EntryHexColorValidator +import org.octopus.internal.web.dtos.validators.EntryMonthValidator +import org.octopus.internal.web.dtos.validators.EntryTypeValidator +import java.util.Date + +data class EntryDto( + @field:Schema(description = "The name of the entry (between 1 and 50 characters long).") + @field:Length(min = 1, max = 50, message = "The name must be between 1 and 50 characters long.") + val name: String, + + @field:Schema(description = "The type of entry (INCOME, OUTCOME, INVESTMENT).") + @field:EntryTypeValidator.Validate + val type: String, + + @field:Schema(description = "The amount of the entry (greater than 0.00).") + @field:DecimalMin(value="0.00", inclusive = false, message = "The amount must be greater than 0.00.") + val amount: Double, + + @field:Schema(description = "IF NOT recurrent: when is the entry being counted.") + val fixedDate: Date?, + + @field:Schema(description = "IF recurrent: must be true") + val recurrent: Boolean?, + + @field:Schema(description = "IF recurrent: list of month in which the event is repeated (1=JAN, 12=DEC)") + @field:EntryMonthValidator.Validate + val recurrentMonths: List?, + + @field:Schema(description = "IF recurrent: starting date of the repetition") + val startDate: Date?, + + @field:Schema(description = "IF recurrent: ending date of the repetition") + val endDate: Date?, + + @field:Schema(description = "Color of the entry in the UI") + @field:EntryHexColorValidator.Validate + val color: String = "0xFFFFFF" +) \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/web/dtos/validators/EntryHexColorValidator.kt b/src/main/kotlin/org/octopus/internal/web/dtos/validators/EntryHexColorValidator.kt new file mode 100755 index 0000000..d65425a --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/web/dtos/validators/EntryHexColorValidator.kt @@ -0,0 +1,41 @@ +package org.octopus.internal.web.dtos.validators + +import jakarta.validation.Constraint +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import jakarta.validation.Payload +import org.octopus.internal.common.enums.EEntryType +import kotlin.reflect.KClass + + +class EntryHexColorValidator : ConstraintValidator { + companion object { + private val HEX_COLOR_PATTERN = Regex("^0x([0-9a-fA-F]{6}|[0-9a-fA-F]{3})$") + } + + override fun isValid(value: String?, context: ConstraintValidatorContext): Boolean { + if (value.isNullOrBlank()) { + return false + } + + val isMatched = HEX_COLOR_PATTERN.matches(value) + + if (!isMatched) { + context.disableDefaultConstraintViolation() + context.buildConstraintViolationWithTemplate("Invalid hex color format. Value must be in 0xRRGGBB format.") + .addConstraintViolation() + } + + return isMatched + } + + @Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.RUNTIME) + @MustBeDocumented + @Constraint(validatedBy = [EntryHexColorValidator::class]) + annotation class Validate( + val message: String = "", + val groups: Array> = [], + val payload: Array> = [] + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/web/dtos/validators/EntryMonthValidator.kt b/src/main/kotlin/org/octopus/internal/web/dtos/validators/EntryMonthValidator.kt new file mode 100755 index 0000000..30d4097 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/web/dtos/validators/EntryMonthValidator.kt @@ -0,0 +1,39 @@ +package org.octopus.internal.web.dtos.validators + +import jakarta.validation.Constraint +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import jakarta.validation.Payload +import org.octopus.internal.common.enums.EEntryType +import java.time.Month +import kotlin.reflect.KClass + + +class EntryMonthValidator : ConstraintValidator?> { + override fun isValid(value: List?, context: ConstraintValidatorContext): Boolean { + val monthInt = Month.entries.map { m -> m.value } + if (value == null) { + return true + } + + val allMatched = value.all { v -> monthInt.contains(v) } + + if (!allMatched) { + context.disableDefaultConstraintViolation() + context.buildConstraintViolationWithTemplate("Invalid. '$value' contains at least a value outside [1,12] range.") + .addConstraintViolation() + } + + return allMatched + } + + @Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.RUNTIME) + @MustBeDocumented + @Constraint(validatedBy = [EntryMonthValidator::class]) + annotation class Validate( + val message: String = "", + val groups: Array> = [], + val payload: Array> = [] + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/web/dtos/validators/EntryTypeValidator.kt b/src/main/kotlin/org/octopus/internal/web/dtos/validators/EntryTypeValidator.kt new file mode 100755 index 0000000..b7bfdd0 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/web/dtos/validators/EntryTypeValidator.kt @@ -0,0 +1,34 @@ +package org.octopus.internal.web.dtos.validators + +import jakarta.validation.Constraint +import jakarta.validation.ConstraintValidator +import jakarta.validation.ConstraintValidatorContext +import jakarta.validation.Payload +import org.octopus.internal.common.enums.EEntryType +import kotlin.reflect.KClass + + +class EntryTypeValidator : ConstraintValidator { + override fun isValid(value: String, context: ConstraintValidatorContext): Boolean { + val typeNames = EEntryType.entries.map {e -> e.name} + val matchedType = typeNames.any { e -> e == value } + + if (!matchedType) { + context.disableDefaultConstraintViolation() + context.buildConstraintViolationWithTemplate("Invalid. '$value' is not in accepted values: $typeNames") + .addConstraintViolation() + } + + return matchedType + } + + @Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) + @Retention(AnnotationRetention.RUNTIME) + @MustBeDocumented + @Constraint(validatedBy = [EntryTypeValidator::class]) + annotation class Validate( + val message: String = "", + val groups: Array> = [], + val payload: Array> = [] + ) +} \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/web/utils/BaseAdvice.kt b/src/main/kotlin/org/octopus/internal/web/utils/BaseAdvice.kt new file mode 100755 index 0000000..eea4bd8 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/web/utils/BaseAdvice.kt @@ -0,0 +1,125 @@ +package org.octopus.internal.web.utils + +import org.octopus.internal.common.enums.EBusinessException +import org.octopus.internal.common.exceptions.OctopusPlanningException +import org.octopus.internal.web.utils.responses.WebResponse +import org.springframework.http.HttpStatus +import org.springframework.http.converter.HttpMessageNotReadableException +//import org.springframework.security.authorization.AuthorizationDeniedException +import org.springframework.web.HttpRequestMethodNotSupportedException +import org.springframework.web.bind.MethodArgumentNotValidException +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.RestControllerAdvice +import org.springframework.web.context.request.ServletWebRequest +import org.springframework.web.context.request.WebRequest +import org.springframework.web.servlet.resource.NoResourceFoundException + +@RestControllerAdvice +class BaseAdvice { + + @ExceptionHandler(OctopusPlanningException::class) + fun handleLorcaBusinessException( + ex: OctopusPlanningException, + request: WebRequest + ): WebResponse { + return WebResponse.ko( + deductStatus(ex.ex), + ex.message + ) + } + + private fun deductStatus(ex: EBusinessException): HttpStatus { + return when (ex) { + EBusinessException.ENTITY_WITH_ID_NOT_FOUND -> HttpStatus.NOT_FOUND + EBusinessException.INVALID_REQUEST -> HttpStatus.BAD_REQUEST + } + } + + @ExceptionHandler(NumberFormatException::class) + fun handleNumberFormatException( + ex: NumberFormatException, + request: WebRequest + ): WebResponse { + return WebResponse.ko( + HttpStatus.BAD_REQUEST, + "${HttpStatus.BAD_REQUEST.reasonPhrase}: ${ex.message}" + ) + } + + + @ExceptionHandler(NoResourceFoundException::class) + fun handleNoResourceFoundException( + ex: NoResourceFoundException, + request: WebRequest + ): WebResponse { + return WebResponse.ko( + HttpStatus.NOT_FOUND, + "${HttpStatus.NOT_FOUND.reasonPhrase}: ${(request as ServletWebRequest).request.requestURI}" + + ) + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException::class) + fun handleHttpRequestMethodNotSupportedException( + ex: HttpRequestMethodNotSupportedException, + request: WebRequest + ): WebResponse { + return WebResponse.ko( + HttpStatus.METHOD_NOT_ALLOWED, + "${HttpStatus.METHOD_NOT_ALLOWED.reasonPhrase}: ${(request as ServletWebRequest).request.requestURI}" + + ) + } + + + @ExceptionHandler(MethodArgumentNotValidException::class) + fun handleMethodArgumentNotValidException( + ex: MethodArgumentNotValidException, + request: WebRequest + ): WebResponse { + val errors = + ex.bindingResult.fieldErrors.map { + "${it.field} - ${it.defaultMessage}" + } + return WebResponse.ko( + HttpStatus.NOT_ACCEPTABLE, + errors + ) + } + + + @ExceptionHandler(HttpMessageNotReadableException::class) + fun handleHttpMessageNotReadableException( + ex: HttpMessageNotReadableException, + request: WebRequest + ): WebResponse { + return WebResponse.ko( + HttpStatus.NOT_ACCEPTABLE, + "${HttpStatus.NOT_ACCEPTABLE.reasonPhrase}: JSON parse error. Please verify the body has the correct format.", + ) + } + +// @ExceptionHandler(AuthorizationDeniedException::class) +// fun handleAuthorizationDeniedException( +// ex: AuthorizationDeniedException, +// request: WebRequest +// ): WebResponse { +// return WebResponse.ko( +// HttpStatus.FORBIDDEN, +// "${HttpStatus.FORBIDDEN.reasonPhrase}: ${ex.message}" +// +// ) +// } + + + @ExceptionHandler(Exception::class) + fun handleException( + ex: Exception, + request: WebRequest + ): WebResponse { + return WebResponse.ko( + HttpStatus.INTERNAL_SERVER_ERROR, + ex.message + ) + } +} diff --git a/src/main/kotlin/org/octopus/internal/web/utils/LoggingAspect.kt b/src/main/kotlin/org/octopus/internal/web/utils/LoggingAspect.kt new file mode 100755 index 0000000..59ee777 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/web/utils/LoggingAspect.kt @@ -0,0 +1,39 @@ +package org.octopus.internal.web.utils + + +//@Aspect +//@Component +class LoggingAspect { +// @Around("execution(* org.octopus.lorca_core.web.controllers.*.*(..))") +// @Throws(Throwable::class) +// fun logMethodDetails(proceedingJoinPoint: ProceedingJoinPoint): Any? { +// val methodName = proceedingJoinPoint.signature.toShortString() +// val arguments = proceedingJoinPoint.args +// +// // Log method entry and arguments +// logger.info("Entering method: {} with arguments: {}", methodName, Arrays.toString(arguments)) +// +// var result: Any? = null +// +// try { +// // Proceed with method execution +// result = proceedingJoinPoint.proceed() +// logger.info("Exiting method: {} with result: {}", methodName, result) +// } catch (ex: Throwable) { +// // Handle exception logging +// logger.error( +// "Exception in method: {} with arguments: {} and exception: {}", +// methodName, +// arguments.contentToString(), +// ex.message +// ) +// throw ex // Re-throw the exception so that Spring's exception resolver can handle it +// } +// +// return result +// } +// +// companion object { +// private val logger: Logger = LoggerFactory.getLogger(LoggingAspect::class.java) +// } +} \ No newline at end of file diff --git a/src/main/kotlin/org/octopus/internal/web/utils/exceptions/WebException.kt b/src/main/kotlin/org/octopus/internal/web/utils/exceptions/WebException.kt new file mode 100644 index 0000000..e69de29 diff --git a/src/main/kotlin/org/octopus/internal/web/utils/responses/WebResponse.kt b/src/main/kotlin/org/octopus/internal/web/utils/responses/WebResponse.kt new file mode 100755 index 0000000..ad6c940 --- /dev/null +++ b/src/main/kotlin/org/octopus/internal/web/utils/responses/WebResponse.kt @@ -0,0 +1,30 @@ +package org.octopus.internal.web.utils.responses + +import org.springframework.http.HttpStatus + + +data class WebResponse( + val status: HttpStatus, + val error: String? = null, + val data: T? = null +) { + + companion object { + fun ok(): WebResponse { + return WebResponse(status = HttpStatus.OK) + } + + fun ok(data: T): WebResponse { + return WebResponse(status = HttpStatus.OK, data = data) + } + + fun ko(status: HttpStatus, error: String?): WebResponse { + return WebResponse(status = status, error = error) + } + + fun ko(status: HttpStatus, errors: List): WebResponse { + return WebResponse(status = status, error = errors.toString()) + } + } + +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100755 index 0000000..9ee1f1b --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,17 @@ +server.servlet.context-path=/api +spring.application.name=octopus-planning-application +# +# +# DATABASE +# +spring.datasource.url=jdbc:h2:file:./data/planning-db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true + +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console +