diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/backendDevTest.iml b/.idea/backendDevTest.iml new file mode 100644 index 00000000..d6ebd480 --- /dev/null +++ b/.idea/backendDevTest.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 00000000..2b6035b0 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..bbb515b6 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 00000000..712ab9d9 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..8130ddda --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..59be48c8 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/JavaSpringBoot/demo/.gitattributes b/JavaSpringBoot/demo/.gitattributes new file mode 100644 index 00000000..3b41682a --- /dev/null +++ b/JavaSpringBoot/demo/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/JavaSpringBoot/demo/.gitignore b/JavaSpringBoot/demo/.gitignore new file mode 100644 index 00000000..667aaef0 --- /dev/null +++ b/JavaSpringBoot/demo/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/JavaSpringBoot/demo/.mvn/wrapper/maven-wrapper.properties b/JavaSpringBoot/demo/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..c0bcafe9 --- /dev/null +++ b/JavaSpringBoot/demo/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/JavaSpringBoot/demo/mvnw b/JavaSpringBoot/demo/mvnw new file mode 100644 index 00000000..bd8896bf --- /dev/null +++ b/JavaSpringBoot/demo/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# http://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/JavaSpringBoot/demo/mvnw.cmd b/JavaSpringBoot/demo/mvnw.cmd new file mode 100644 index 00000000..92450f93 --- /dev/null +++ b/JavaSpringBoot/demo/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/JavaSpringBoot/demo/pom.xml b/JavaSpringBoot/demo/pom.xml new file mode 100644 index 00000000..bf58ef6e --- /dev/null +++ b/JavaSpringBoot/demo/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.7 + + + com.example + demo + 0.0.1-SNAPSHOT + demo + Demo project for Spring Boot + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + + + jakarta.validation + jakarta.validation-api + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/DemoApplication.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 00000000..64b538a1 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/crud/getSimilar/GetSimilarProducts.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/crud/getSimilar/GetSimilarProducts.java new file mode 100644 index 00000000..fdbb2753 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/crud/getSimilar/GetSimilarProducts.java @@ -0,0 +1,10 @@ +package com.example.demo.products.application.crud.getSimilar; + +import com.example.demo.products.application.dto.ProductApplication; + +import java.util.List; + +public interface GetSimilarProducts { + List getSimilarProductsById(String productId); +} + diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/crud/getSimilar/GetSimilarProductsImp.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/crud/getSimilar/GetSimilarProductsImp.java new file mode 100644 index 00000000..7f5a0875 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/crud/getSimilar/GetSimilarProductsImp.java @@ -0,0 +1,66 @@ +package com.example.demo.products.application.crud.getSimilar; + +import com.example.demo.products.application.dto.ProductApplication; +import com.example.demo.products.infrastructure.http_outputAdapter.ProductApiClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class GetSimilarProductsImp implements GetSimilarProducts { + + private final ProductApiClient productApiClient; + + @Override + public List getSimilarProductsById(String productId) { + log.info("Fetching similar products for productId: {}", productId); + + try { + //1 Obtener IDs de productos similares (bloqueando para simplificar) + //Spring WebFlux -> reactivo -> usa Mono -> necesario blokear + List similarIds = productApiClient.getSimilarProductIds(productId) + .block(); + + if (similarIds == null || similarIds.isEmpty()) { + log.info("No similar products found for productId: {}", productId); + return List.of(); + } + + log.debug("Found {} similar product IDs for productId: {}", similarIds.size(), productId); + + // 2. Obtener detalles de cada producto en paralelo + Flux productsFlux = productApiClient.getProductDetails(similarIds); + + // 3. Recopilar todos los productos (bloqueando para simplificar) + List products = productsFlux + .collectList() + .block(); + + if (products == null) { + log.warn("Failed to fetch product details for productId: {}", productId); + return List.of(); + } + + // 4. Filtrar productos nulos (por si algún producto falló) + List validProducts = products.stream() + .filter(product -> product != null) + .toList(); + + log.info("Successfully retrieved {} similar products for productId: {}", + validProducts.size(), productId); + + // Los productos ya vienen ordenados por similitud según el mock server + return validProducts; + + } catch (Exception e) { + log.error("Error retrieving similar products for productId: {}", productId, e); + throw new RuntimeException("Error retrieving similar products: " + e.getMessage(), e); + } + } +} + diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/dto/ProductApplication.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/dto/ProductApplication.java new file mode 100644 index 00000000..e69c3c01 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/dto/ProductApplication.java @@ -0,0 +1,17 @@ +package com.example.demo.products.application.dto; + +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +@ToString +public class ProductApplication { + private String id; + private String name; + private Double price; + private Boolean availability; +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/mapper/ProductMapperApplication.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/mapper/ProductMapperApplication.java new file mode 100644 index 00000000..723770c1 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/application/mapper/ProductMapperApplication.java @@ -0,0 +1,71 @@ +package com.example.demo.products.application.mapper; + +import com.example.demo.products.application.dto.ProductApplication; +import com.example.demo.products.domain.dto.ProductDomain; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class ProductMapperApplication { + + private static ProductMapperApplication instance; + + private ProductMapperApplication(){} + + public static ProductMapperApplication getInstance() { + if(instance == null) { + instance = new ProductMapperApplication(); + } + return instance; + } + + public ProductDomain applicationToDomain(ProductApplication application){ + if(application==null) {return null;} + + ProductDomain.ProductDomainBuilder builder = ProductDomain.builder(); + + builder.id(application.getId()); + builder.name(application.getName()); + builder.price(application.getPrice()); + builder.availability(application.getAvailability()); + + return builder.build(); + } + + public ProductApplication domainToApplication(ProductDomain domain){ + if(domain==null) {return null;} + + ProductApplication.ProductApplicationBuilder builder = ProductApplication.builder(); + + builder.id(domain.getId()); + builder.name(domain.getName()); + builder.price(domain.getPrice()); + builder.availability(domain.getAvailability()); + + return builder.build(); + } + + public List listDomainToApplicationList(List domainList) { + if(domainList == null) return Collections.emptyList(); + return domainList.stream().map(this::domainToApplication).collect(Collectors.toList()); + } + + public List listApplicationToDomainList(List applicationList) { + if(applicationList == null) return Collections.emptyList(); + return applicationList.stream().map(this::applicationToDomain).collect(Collectors.toList()); + } + + public Set setDomainToApplicationSet(Set domainSet) { + if(domainSet == null) return Collections.emptySet(); + return domainSet.stream().map(this::domainToApplication).collect(Collectors.toSet()); + } + + public Set setApplicationToDomainSet(Set applicationSet) { + if(applicationSet == null) return Collections.emptySet(); + return applicationSet.stream().map(this::applicationToDomain).collect(Collectors.toSet()); + } +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/domain/dto/ProductDomain.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/domain/dto/ProductDomain.java new file mode 100644 index 00000000..ec8578cf --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/domain/dto/ProductDomain.java @@ -0,0 +1,18 @@ +package com.example.demo.products.domain.dto; + +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +@ToString + +public class ProductDomain { + private String id; + private String name; + private Double price; + private Boolean availability; +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/config/WebClientConfig.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/config/WebClientConfig.java new file mode 100644 index 00000000..d9aaa1c8 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/config/WebClientConfig.java @@ -0,0 +1,30 @@ +package com.example.demo.products.infrastructure.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.client.WebClient; + +@Configuration +public class WebClientConfig { + + @Value("${external.api.base-url}") + private String baseUrl; + + @Value("${external.api.timeout.connect:2000}") + private int connectTimeout; + + @Value("${external.api.timeout.read:5000}") + private int readTimeout; + + @Bean + public WebClient webClient() { + return WebClient.builder() + .baseUrl(baseUrl) + .codecs(configurer -> configurer + .defaultCodecs() + .maxInMemorySize(1024 * 1024)) + .build(); + } +} + diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/controller_inputAdapter/ProductControllerImp.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/controller_inputAdapter/ProductControllerImp.java new file mode 100644 index 00000000..6b9d5df9 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/controller_inputAdapter/ProductControllerImp.java @@ -0,0 +1,39 @@ +package com.example.demo.products.infrastructure.controller_inputAdapter; + +import com.example.demo.products.application.crud.getSimilar.GetSimilarProducts; +import com.example.demo.products.application.dto.ProductApplication; +import com.example.demo.products.infrastructure.controller_inputPort.ProductController; +import com.example.demo.shared.service.ResponseHandlerService; +import com.example.demo.shared.valueObject.ResponseDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/product") +@CrossOrigin +@Slf4j +public class ProductControllerImp implements ProductController { + + private final ResponseHandlerService responseHandler; + private final GetSimilarProducts getSimilarProducts; + + @Autowired + public ProductControllerImp( + ResponseHandlerService responseHandler, + GetSimilarProducts getSimilarProducts){ + this.responseHandler = responseHandler; + this.getSimilarProducts = getSimilarProducts; + } + + @GetMapping("/{productId}/similar") + @Override + public ResponseDTO> getSimilarProducts(@PathVariable String productId) { + log.info("Received request for similar products of productId: {}", productId); + return responseHandler.execute("GET_SIMILAR_PRODUCTS", + () -> getSimilarProducts.getSimilarProductsById(productId)); + } + +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/controller_inputPort/ProductController.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/controller_inputPort/ProductController.java new file mode 100644 index 00000000..0e196f15 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/controller_inputPort/ProductController.java @@ -0,0 +1,11 @@ +package com.example.demo.products.infrastructure.controller_inputPort; + +import com.example.demo.products.application.dto.ProductApplication; +import com.example.demo.shared.valueObject.ResponseDTO; + +import java.util.List; + +public interface ProductController { + + ResponseDTO> getSimilarProducts(String productId); +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/entity/ProductEntity.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/entity/ProductEntity.java new file mode 100644 index 00000000..5af04ef8 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/entity/ProductEntity.java @@ -0,0 +1,43 @@ +package com.example.demo.products.infrastructure.entity; + + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.*; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode +@ToString + +//@Entity +//@Table(name="products", schema = "mydb") +public class ProductEntity { + + //@Id + @NotBlank(message = "ID is required and cannot be empty") + @Size(min = 1, message = "ID must have at least 1 character") + //@Column(unique = true, nullable = false, length = 255) + private String id; + + @NotBlank(message = "Name is required and cannot be empty") + @Size(min = 1, message = "Name must have at least 1 character") + //@Column(nullable = false, length = 255) + private String name; + + @NotNull(message = "Price is required") + @DecimalMin(value = "0.0", inclusive = true, message = "Price must be positive") + //@Column(nullable = false) + private Double price; + + @NotNull(message = "Availability is required") + //@Column(nullable = false) + private Boolean availability; + + +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/http_outputAdapter/ProductApiClient.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/http_outputAdapter/ProductApiClient.java new file mode 100644 index 00000000..d7b59f9f --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/http_outputAdapter/ProductApiClient.java @@ -0,0 +1,96 @@ +package com.example.demo.products.infrastructure.http_outputAdapter; + +import com.example.demo.products.application.dto.ProductApplication; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.reactive.function.client.WebClientResponseException; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +import java.time.Duration; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ProductApiClient { + + private final WebClient webClient; + + @Value("${external.api.timeout.read:5000}") + private int readTimeout; + + + //Spring WebFlux -> reactivo -> usar Mono -> queremos 1 unico valor que es la lista + public Mono> getSimilarProductIds(String productId) { + log.debug("Fetching similar product IDs for productId: {}", productId); + return webClient.get() + .uri("/product/{productId}/similarids", productId) + .retrieve() + .bodyToFlux(String.class) + .collectList() + .timeout(Duration.ofMillis(readTimeout)) + .retryWhen(Retry.backoff(2, Duration.ofMillis(100)) + .filter(throwable -> { + if (throwable instanceof WebClientResponseException webClientError) { + return webClientError.getStatusCode().is5xxServerError(); + } + return false; + }) + .doBeforeRetry(retrySignal -> log.warn("Retrying request for similar IDs, productId: {}", productId))) + .doOnSuccess(ids -> log.debug("Successfully fetched {} similar product IDs for productId: {}", ids.size(), productId)) + .doOnError(error -> log.error("Error fetching similar product IDs for productId: {}", productId, error)) + .onErrorMap(this::mapToRuntimeException); + } + + //Spring WebFlux -> reactivo -> usar Mono -> queremos 1 unico valor + public Mono getProductDetail(String productId) { + log.debug("Fetching product detail for productId: {}", productId); + return webClient.get() + .uri("/product/{productId}", productId) + .retrieve() + .bodyToMono(ProductApplication.class) + .timeout(Duration.ofMillis(readTimeout)) + .retryWhen(Retry.backoff(2, Duration.ofMillis(100)) + .filter(throwable -> { + if (throwable instanceof WebClientResponseException webClientError) { + return webClientError.getStatusCode().is5xxServerError(); + } + return false; + }) + .doBeforeRetry(retrySignal -> log.warn("Retrying request for product detail, productId: {}", productId))) + .doOnSuccess(product -> log.debug("Successfully fetched product detail for productId: {}", productId)) + .doOnError(error -> log.error("Error fetching product detail for productId: {}", productId, error)) + .onErrorMap(this::mapToRuntimeException); + } + + //Spring WebFlux -> reactivo -> usar Flux -> queremos multiples valores distintos + public Flux getProductDetails(List productIds) { + log.debug("Fetching product details for {} products", productIds.size()); + return Flux.fromIterable(productIds) + .flatMap(this::getProductDetail, 5) // Concurrencia de 5 + .onErrorContinue((error, obj) -> log.warn("Failed to fetch product detail for: {}", obj, error)); + } + + //Centralizar logica para manejar excepciones + private Throwable mapToRuntimeException(Throwable error) { + if (error instanceof WebClientResponseException webClientError) { + if (webClientError.getStatusCode() == HttpStatus.NOT_FOUND) { + log.warn("Product not found in external API: {}", webClientError.getMessage()); + return new RuntimeException("Product not found in external API: " + webClientError.getMessage()); + } + return new RuntimeException("External API error: " + webClientError.getStatusCode(), error); + } + if (error instanceof java.util.concurrent.TimeoutException) { + log.warn("Timeout calling external API: {}", error.getMessage()); + return new RuntimeException("External API timeout", error); + } + return new RuntimeException("Error calling external API", error); + } +} + diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/mapper/ProductMapperInfrastructure.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/mapper/ProductMapperInfrastructure.java new file mode 100644 index 00000000..5247c165 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/products/infrastructure/mapper/ProductMapperInfrastructure.java @@ -0,0 +1,72 @@ +package com.example.demo.products.infrastructure.mapper; + +import com.example.demo.products.application.dto.ProductApplication; +import com.example.demo.products.infrastructure.entity.ProductEntity; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class ProductMapperInfrastructure { + private static ProductMapperInfrastructure instance; + + private ProductMapperInfrastructure(){} + + public static ProductMapperInfrastructure getInstance() { + if(instance == null) { + instance = new ProductMapperInfrastructure(); + } + return instance; + } + + public ProductApplication entityToApplication(ProductEntity entity){ + if(entity==null) {return null;} + + ProductApplication.ProductApplicationBuilder builder = ProductApplication.builder(); + + builder.id(entity.getId()); + builder.name(entity.getName()); + builder.price(entity.getPrice()); + builder.availability(entity.getAvailability()); + + return builder.build(); + } + + public ProductEntity applicationToEntity(ProductApplication application){ + if(application==null) {return null;} + + ProductEntity.ProductEntityBuilder builder = ProductEntity.builder(); + + builder.id(application.getId()); + builder.name(application.getName()); + builder.price(application.getPrice()); + builder.availability(application.getAvailability()); + + return builder.build(); + } + + public List listApplicationToEntityList(List applicationList) { + if(applicationList == null) return Collections.emptyList(); + return applicationList.stream().map(this::applicationToEntity).collect(Collectors.toList()); + } + + public List listEntityToApplicationList(List entityList) { + if(entityList == null) return Collections.emptyList(); + return entityList.stream().map(this::entityToApplication).collect(Collectors.toList()); + } + + public Set setApplicationToEntitySet(Set applicationSet) { + if(applicationSet == null) return Collections.emptySet(); + return applicationSet.stream().map(this::applicationToEntity).collect(Collectors.toSet()); + } + + public Set setEntityToApplicationSet(Set entitySet) { + if(entitySet == null) return Collections.emptySet(); + return entitySet.stream().map(this::entityToApplication).collect(Collectors.toSet()); + } + +} + diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/DateParserdFromDateToStringException.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/DateParserdFromDateToStringException.java new file mode 100644 index 00000000..e4600a59 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/DateParserdFromDateToStringException.java @@ -0,0 +1,9 @@ +package com.example.demo.shared.exceptions; + +import java.util.Date; + +public class DateParserdFromDateToStringException extends GenericException { + public DateParserdFromDateToStringException(Date date, String pattern){ + super("Cannot parse " + date + ". format this date with this pattern: " + pattern); + } +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/DateParserdFromStringToDateException.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/DateParserdFromStringToDateException.java new file mode 100644 index 00000000..66049708 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/DateParserdFromStringToDateException.java @@ -0,0 +1,7 @@ +package com.example.demo.shared.exceptions; + +public class DateParserdFromStringToDateException extends GenericException { + public DateParserdFromStringToDateException(String dateString, String pattern){ + super("Cannot parse " + dateString + ". format this date with this pattern: " + pattern); + } +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/ExceptionXAlreadyExistException.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/ExceptionXAlreadyExistException.java new file mode 100644 index 00000000..2109aef3 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/ExceptionXAlreadyExistException.java @@ -0,0 +1,10 @@ +package com.example.demo.shared.exceptions; + +import com.example.demo.shared.exceptions.GenericException; + +public class ExceptionXAlreadyExistException extends GenericException { + public ExceptionXAlreadyExistException(String msg){ + super(msg + " already exist"); + } +} + diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/ExceptionXNotExistException.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/ExceptionXNotExistException.java new file mode 100644 index 00000000..c30471d1 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/ExceptionXNotExistException.java @@ -0,0 +1,10 @@ +package com.example.demo.shared.exceptions; + +import com.example.demo.shared.exceptions.GenericException; + +public class ExceptionXNotExistException extends GenericException { + public ExceptionXNotExistException(String msg){ + super(msg+" not exist"); + } +} + diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/GenericException.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/GenericException.java new file mode 100644 index 00000000..f0ba4b0a --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/GenericException.java @@ -0,0 +1,53 @@ +package com.example.demo.shared.exceptions; + +import com.example.demo.shared.valueObject.I18nMessage; + +import java.util.*; + +public class GenericException extends RuntimeException { + private final List errorMessages = new ArrayList(); + + public GenericException(){ + } + + public GenericException(String key, Object... params){ + super(key); + this.addError(key,params); + } + + public GenericException(Throwable t, String key, Object... params){ + super(key, t); + this.addError(key,params); + } + + /** @deprecated **/ + @Deprecated + public Map> getErrorMap(){ + Map> map = new HashMap<>(); + Iterator iterator = this.errorMessages.iterator(); + + while (iterator.hasNext()){ + I18nMessage msg = (I18nMessage) iterator.next(); + map.put(msg.getKey(), msg.getParams()); + } + + return map; + } + + public void addErrors(Map> errorMap){ + Iterator iterator = errorMap.entrySet().iterator(); + + while (iterator.hasNext()){ + Map.Entry> e = (Map.Entry) iterator.next(); + this.addError((String) e.getKey(), e.getValue()); + } + } + + public void addError(String key, Object... params){ + this.errorMessages.add(new I18nMessage(key, Arrays.asList(params))); + } + + public List getErrorMessages(){return this.errorMessages;} + + public Boolean containsError(String key){return this.errorMessages.contains(new I18nMessage(key));} +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/ModuleException.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/ModuleException.java new file mode 100644 index 00000000..1ab71aab --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/exceptions/ModuleException.java @@ -0,0 +1,10 @@ +package com.example.demo.shared.exceptions; + +public class ModuleException extends GenericException { + public ModuleException(){ + } + + public ModuleException(String key, Object... params){super(key,params);} + + public ModuleException(Throwable t, String key, Object... params){super(key,params);} +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/service/ResponseHandlerService.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/service/ResponseHandlerService.java new file mode 100644 index 00000000..e236ea9a --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/service/ResponseHandlerService.java @@ -0,0 +1,36 @@ +package com.example.demo.shared.service; + +import com.example.demo.shared.exceptions.GenericException; +import com.example.demo.shared.valueObject.ResponseDTO; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.function.Supplier; + +@Service +@Slf4j +public class ResponseHandlerService { + + /** + * Ejecuta una acción y devuelve un ResponseDTO con manejo de excepciones y logging. + * @param action nombre de la acción (para logs e infoMessages) + * @param supplier lógica de negocio que devuelve datos + * @param tipo de dato de la respuesta + * @return ResponseDTO + */ + public ResponseDTO execute(String action, Supplier supplier) { + ResponseDTO response = new ResponseDTO<>(); + try { + T data = supplier.get(); + response.setData(data); + response.addInfo(action + "_OK"); + } catch (GenericException e) { + response.setErrorMessages(e.getErrorMessages()); + log.warn("{} failed with business exception", action, e); + } catch (Exception e) { + response.addError(e.getMessage()); + log.error("{} failed", action, e); + } + return response; + } +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/GenericDTO.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/GenericDTO.java new file mode 100644 index 00000000..0c569d61 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/GenericDTO.java @@ -0,0 +1,63 @@ +package com.example.demo.shared.valueObject; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class GenericDTO implements Serializable { + protected List errorMessages = new ArrayList<>(); + protected List warningMessages = new ArrayList<>(); + protected List infoMessages = new ArrayList<>(); + + public GenericDTO() { + } + + public void addInfo(String key, Object... params) { + this.infoMessages.add(new I18nMessage(key, Arrays.asList(params))); + } + + public void addError(String key, Object... params) { + this.errorMessages.add(new I18nMessage(key, Arrays.asList(params))); + } + + public void addWarning(String key, Object... params) { + this.warningMessages.add(new I18nMessage(key, Arrays.asList(params))); + } + + public boolean hasErrors(){ + return !this.errorMessages.isEmpty(); + } + + public List getErrorMessages() { + return errorMessages; + } + + public void setErrorMessages(List errorMessages) { + this.errorMessages = errorMessages; + } + + public List getWarningMessages() { + return warningMessages; + } + + public void setWarningMessages(List warningMessages) { + this.warningMessages = warningMessages; + } + + public List getInfoMessages() { + return infoMessages; + } + + public void setInfoMessages(List infoMessages) { + this.infoMessages = infoMessages; + } + + public boolean hasObjectNotFoundException(){ + return this.getErrorMessages().stream().anyMatch((em) -> { + return em.getKey().equalsIgnoreCase("general.objectnotfound"); + }); + } + +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/I18nMessage.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/I18nMessage.java new file mode 100644 index 00000000..06a31925 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/I18nMessage.java @@ -0,0 +1,80 @@ +package com.example.demo.shared.valueObject; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +public class I18nMessage implements Serializable { + private String key; + private List params; + private String message; + + public I18nMessage(){ + this.key=""; + this.params= Collections.emptyList(); + } + + public I18nMessage(String key, List params){ + this.key=key; + this.params=params; + } + + public I18nMessage(String key){ + this.key=key; + this.params=Collections.emptyList(); + } + + public I18nMessage(String key, List params, String translatedMessage){ + this.key = key; + this.params = params; + this.message = translatedMessage; + } + + public String getKey() {return this.key;} + + public void setKey(String key) { + this.key = key; + } + + public List getParams() { + return params; + } + + public void setParams(List params) { + this.params = params; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int hashCode(){ + int result = 1; + result = 31 * result + (this.key== null ? 0:this.key.hashCode()); + return result; + } + + public boolean equals(Object obj){ + if(this == obj){ + return true; + } else if (obj == null) { + return false; + } else if (!(obj instanceof I18nMessage)) { + return false; + }else { + I18nMessage other = (I18nMessage) obj; + if(this.key == null) { + if(other.getKey() != null){ + return false; + } + } else if (!this.key.equals(other.getKey())) { + return false; + } + return true; + } + } +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/JwtDTO.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/JwtDTO.java new file mode 100644 index 00000000..f2864d15 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/JwtDTO.java @@ -0,0 +1,15 @@ +package com.example.demo.shared.valueObject; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class JwtDTO { + private String token; + +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/MensajeDTO.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/MensajeDTO.java new file mode 100644 index 00000000..a43039ba --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/MensajeDTO.java @@ -0,0 +1,14 @@ +package com.example.demo.shared.valueObject; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class MensajeDTO { + private String mensaje; +} diff --git a/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/ResponseDTO.java b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/ResponseDTO.java new file mode 100644 index 00000000..45ada195 --- /dev/null +++ b/JavaSpringBoot/demo/src/main/java/com/example/demo/shared/valueObject/ResponseDTO.java @@ -0,0 +1,16 @@ +package com.example.demo.shared.valueObject; + + +public class ResponseDTO extends GenericDTO { + private T data; + public ResponseDTO(){ + } + + public ResponseDTO(T data){this.data=data;} + + public T getData(){return this.data;} + + public void setData(T data) { + this.data = data; + } +} diff --git a/JavaSpringBoot/demo/src/main/resources/application.properties b/JavaSpringBoot/demo/src/main/resources/application.properties new file mode 100644 index 00000000..79f4b83c --- /dev/null +++ b/JavaSpringBoot/demo/src/main/resources/application.properties @@ -0,0 +1,12 @@ +spring.application.name=demo +server.port=5000 + +# External API configuration +external.api.base-url=http://localhost:3001 +external.api.timeout.connect=2000 +external.api.timeout.read=5000 + +# Resilience configuration +resilience.circuit-breaker.failure-rate-threshold=50 +resilience.circuit-breaker.wait-duration=10000 +resilience.retry.max-attempts=3 diff --git a/JavaSpringBoot/demo/src/test/java/com/example/demo/DemoApplicationTests.java b/JavaSpringBoot/demo/src/test/java/com/example/demo/DemoApplicationTests.java new file mode 100644 index 00000000..2778a6a7 --- /dev/null +++ b/JavaSpringBoot/demo/src/test/java/com/example/demo/DemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DemoApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/JavaSpringBoot/readmeSolutionAlvaro.md b/JavaSpringBoot/readmeSolutionAlvaro.md new file mode 100644 index 00000000..18f79d2c --- /dev/null +++ b/JavaSpringBoot/readmeSolutionAlvaro.md @@ -0,0 +1,16 @@ +Total time 3h aprox + +# To execute the test run in cmd the following: + +Start Java Spring boot application main + +docker-compose up -d simulado influxdb grafana + +curl http://localhost:3001/product/1/similarids + +curl http://localhost:5000/product/1/similar + +docker-compose run --rm k6 run /scripts/test.js + +0% failed, 100% success + diff --git a/JavaSpringBootAlvaro.zip b/JavaSpringBootAlvaro.zip new file mode 100644 index 00000000..5b63fd5b Binary files /dev/null and b/JavaSpringBootAlvaro.zip differ diff --git a/docker-compose.yaml b/docker-compose.yaml index 2b20a5d9..86164530 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,35 +1,40 @@ -version: "3.3" +version: '4.1' services: - influxdb: - image: influxdb:1.8.2 - ports: - - "8086:8086" - environment: - - INFLUXDB_DB=k6 - grafana: - image: grafana/grafana:8.1.2 - ports: - - "3000:3000" - environment: - - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin - - GF_AUTH_ANONYMOUS_ENABLED=true - - GF_AUTH_BASIC_ENABLED=false - volumes: - - ./shared/grafana:/etc/grafana/provisioning/ - simulado: - image : ldabiralai/simulado:latest - ports: - - "3001:80" - volumes: - - ./shared/simulado:/app - command: ./bin/simulado -f /app/mocks.json - k6: - image: loadimpact/k6:0.28.0 - ports: - - "6565:6565" - volumes: - - ./shared/k6:/scripts - environment: - - K6_OUT=influxdb=http://influxdb:8086/k6 - extra_hosts: - - "host.docker.internal:host-gateway" + influxdb: + image: influxdb:1.8.2 + ports: + - '8086:8086' + environment: + - INFLUXDB_DB=k6 + grafana: + image: grafana/grafana:8.1.2 + ports: + - '3000:3000' + environment: + - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin + - GF_AUTH_ANONYMOUS_ENABLED=true + - GF_AUTH_BASIC_ENABLED=false + volumes: + - ./shared/grafana:/etc/grafana/provisioning/ + mock-server: + image: node:18-alpine + ports: + - '3001:3001' + working_dir: /app + volumes: + - ./mock-server:/app + - ./shared/simulado:/shared/simulado + command: sh -c "npm install && npm start" + environment: + - NODE_ENV=production + k6: + # Usando grafana/k6 -> version oficial de Grafana + image: grafana/k6:latest + ports: + - '6565:6565' + volumes: + - ./shared/k6:/scripts + environment: + - K6_OUT=influxdb=http://influxdb:8086/k6 + extra_hosts: + - 'host.docker.internal:host-gateway' diff --git a/mock-server/.gitignore b/mock-server/.gitignore new file mode 100644 index 00000000..d6665a5c --- /dev/null +++ b/mock-server/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +package-lock.json +*.log + diff --git a/mock-server/README.md b/mock-server/README.md new file mode 100644 index 00000000..e498d6ab --- /dev/null +++ b/mock-server/README.md @@ -0,0 +1,16 @@ +# IMPORTANTE LEER: + +Por motivos de seguridad ya que desconozco que contiene simulado, he remplzado totalmente el contenedor simulado por este nuevo contenedor que es un express + +Se inicia en: `http://localhost:3001` +Simula las APIs según `shared/simulado/mocks.json`. + +Verificación + +```bash +curl http://localhost:3001/product/1/similarids +#[2,3,4] + +curl http://localhost:3001/product/1 +#{"id":"1","name":"Shirt","price":9.99,"availability":true} +``` diff --git a/mock-server/package.json b/mock-server/package.json new file mode 100644 index 00000000..de191445 --- /dev/null +++ b/mock-server/package.json @@ -0,0 +1,12 @@ +{ + "name": "mockServer", + "version": "1.0.0", + "description": "Mock server que remplaza a simulado para garantizar la seguridad", + "main": "server.js", + "scripts": { + "start": "node server.js" + }, + "dependencies": { + "express": "^4.18.2" + } +} diff --git a/mock-server/server.js b/mock-server/server.js new file mode 100644 index 00000000..db111a16 --- /dev/null +++ b/mock-server/server.js @@ -0,0 +1,93 @@ +/** + * MockServer -> Remplazo de simulado + * Ver mocks.json + */ + +const express = require('express'); +const fs = require('fs'); +const path = require('path'); + +const app = express(); +const PORT = 3001; + +// Cargar mocks desde el archivo JSON +// Funciona tanto en Docker como en ejecución local +const mocksPath = + process.env.MOCKS_PATH || path.join(__dirname, '..', 'shared', 'simulado', 'mocks.json'); +// Si estamos en Docker, usar la ruta montada +const dockerMocksPath = '/shared/simulado/mocks.json'; +const finalMocksPath = require('fs').existsSync(dockerMocksPath) ? dockerMocksPath : mocksPath; + +let mocks = []; + +try { + const mocksData = fs.readFileSync(finalMocksPath, 'utf8'); + mocks = JSON.parse(mocksData); + console.log(`✓ Cargados ${mocks.length} mocks desde ${finalMocksPath}`); +} catch (error) { + console.error('✗ Error cargando mocks:', error.message); + console.error(` Intentó cargar desde: ${finalMocksPath}`); + process.exit(1); +} + +// Middleware para parsear JSON +app.use(express.json()); + +// Función para encontrar un mock por path +function findMock(path) { + return mocks.find((mock) => mock.path === path); +} + +// Endpoint genérico que maneja todos los paths +app.get('*', (req, res) => { + const mock = findMock(req.path); + + if (!mock) { + console.log(`⚠️ Mock no encontrado para: ${req.path}`); + return res.status(404).json({ error: 'Not found' }); + } + + // Aplicar delay si existe + if (mock.delay) { + setTimeout(() => { + sendResponse(mock, res); + }, mock.delay); + } else { + sendResponse(mock, res); + } +}); + +function sendResponse(mock, res) { + // Establecer headers + if (mock.headers) { + Object.keys(mock.headers).forEach((key) => { + res.setHeader(key, mock.headers[key]); + }); + } + + // Establecer status code (default 200) + const status = mock.status || 200; + + // Enviar respuesta + if (mock.body) { + // Si el body es un string JSON, parsearlo primero + try { + const body = typeof mock.body === 'string' ? JSON.parse(mock.body) : mock.body; + res.status(status).json(body); + } catch (e) { + // Si no es JSON válido, enviar como string + res.status(status).send(mock.body); + } + } else { + res.status(status).end(); + } +} + +// Iniciar servidor +app.listen(PORT, () => { + console.log(`🚀 Mock Server seguro corriendo en http://localhost:${PORT}`); + console.log(`📋 Endpoints disponibles:`); + mocks.forEach((mock) => { + console.log(` ${mock.path}${mock.delay ? ` (delay: ${mock.delay}ms)` : ''}`); + }); +}); diff --git a/readme.md b/readme.md index bc0c5cc7..724365d7 100644 --- a/readme.md +++ b/readme.md @@ -1,33 +1,43 @@ # Backend dev technical test + We want to offer a new feature to our customers showing similar products to the one they are currently seeing. To do this we agreed with our front-end applications to create a new REST API operation that will provide them the product detail of the similar products for a given one. [Here](./similarProducts.yaml) is the contract we agreed. We already have an endpoint that provides the product Ids similar for a given one. We also have another endpoint that returns the product detail by product Id. [Here](./existingApis.yaml) is the documentation of the existing APIs. **Create a Spring boot application that exposes the agreed REST API on port 5000.** -![Diagram](./assets/diagram.jpg "Diagram") +![Diagram](./assets/diagram.jpg 'Diagram') Note that _Test_ and _Mocks_ components are given, you must only implement _yourApp_. ## Testing and Self-evaluation + You can run the same test we will put through your application. You just need to have docker installed. First of all, you may need to enable file sharing for the `shared` folder on your docker dashboard -> settings -> resources -> file sharing. Then you can start the mocks and other needed infrastructure with the following command. + ``` docker-compose up -d simulado influxdb grafana ``` + Check that mocks are working with a sample request to [http://localhost:3001/product/1/similarids](http://localhost:3001/product/1/similarids). +**Note:** Deleted simulado and created mock server that uses a secure Node.js for security reasons. + To execute the test run: + ``` docker-compose run --rm k6 run scripts/test.js ``` + Browse [http://localhost:3000/d/Le2Ku9NMk/k6-performance-test](http://localhost:3000/d/Le2Ku9NMk/k6-performance-test) to view the results. ## Evaluation + The following topics will be considered: -- Code clarity and maintainability -- Performance -- Resilience + +- Code clarity and maintainability +- Performance +- Resilience diff --git a/readmeSolutionAlvaro.md b/readmeSolutionAlvaro.md new file mode 100644 index 00000000..2a280abb --- /dev/null +++ b/readmeSolutionAlvaro.md @@ -0,0 +1,17 @@ +# Backend dev technical test +**Note:** Deleted simulado and created mock server that uses a secure Node.js for security reasons. +I'm not gona to execute an image that i dont know that does and have and it's not official. + +# To execute the test run in cmd the following: + +Start Java Spring boot application main + +docker-compose up -d mock-server influxdb grafana + +curl http://localhost:3001/product/1/similarids + +curl http://localhost:5000/product/1/similar + +docker-compose run --rm k6 run /scripts/test.js + +0% failed, 100% success \ No newline at end of file