Technical exercise ITX v1.0

This commit is contained in:
José Antonio Zamora 2026-01-12 11:33:54 +01:00
commit 8cd737ef31
29 changed files with 1951 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

51
.gitignore vendored Normal file
View File

@ -0,0 +1,51 @@
######################
# Eclipse
######################
.project
.metadata
tmp/
tmp/**/*
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
.factorypath
######################
# Maven
######################
/log/
/target/
######################
# Package Files
######################
*.jar
*.war
*.ear
*.db
######################
# Windows
######################
# Windows image file caches
Thumbs.db
# Folder config file
Desktop.ini
######################
# Directories
######################
/bin/
/deploy/
######################
# Logs
######################
*.log*

3
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View File

@ -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.12/apache-maven-3.9.12-bin.zip

98
README.md Normal file
View File

@ -0,0 +1,98 @@
# Price Service Technical Exercise
## Description
Spring Boot service that exposes a REST endpoint to retrieve the applicable price for a product, brand and application date.
The service uses:
- Spring Boot
- Spring Data JPA
- H2 in-memory database
- Liquibase for schema and data initialization
- Maven
- Integration tests with MockMvc
The pricing logic follows the specification provided in the exercise, including date ranges and priority handling.
---
## Data Model
The service is initialized with sample data equivalent to the provided `PRICES` table, including:
- Brand identifier
- Product identifier
- Price list
- Application date range (start / end)
- Priority
- Final price
If multiple prices apply for the same date range, the one with the highest priority is selected.
---
## REST Endpoint
### Request
GET /api/price/{brandId}/{productId}?applicationDate={applicationDate}
**Path parameters:**
- `brandId`: brand identifier
- `productId`: product identifier
**Request parameters:**
- `applicationDate`: date and time of application (ISO-8601 format)
**Example:**
/api/price/1/35455?applicationDate=2020-06-14T10:00:00
---
### Response
The service returns:
- product identifier
- brand identifier
- applicable price list
- start date
- end date
- final price
---
## Running the Application
To start the application locally:
```bash
mvn spring-boot:run
```
The service will be available on port 8080.
---
## Running Tests
The project includes integration tests that validate the scenarios described in the exercise.
To execute the tests:
```bash
mvn test
```
The tests:
- Load the Spring context
- Initialize the in-memory H2 database via Liquibase
- Validate the REST endpoint responses
---
## Notes
The database is fully in-memory and requires no external setup.
Liquibase is used to manage database schema and initial data.

295
mvnw vendored Normal file
View File

@ -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-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -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 "$@"

189
mvnw.cmd vendored Normal file
View File

@ -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-<version>,maven-mvnd-<version>-<platform>}/<hash>
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"

144
pom.xml Normal file
View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.jzamoram</groupId>
<artifactId>itx</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>itx</name>
<description>itx example</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
<spring.profiles.active>dev</spring.profiles.active>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>5.0.0</version>
<scope>provided</scope>
<classifier>jakarta</classifier>
</dependency>
<dependency>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-jpa</artifactId>
<version>5.0.0</version>
<classifier>jakarta</classifier>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>com.mysema.maven</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.1.3</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>process</goal>
</goals>
<configuration>
<outputDirectory>target/generated-sources</outputDirectory>
<processor>
com.mysema.query.apt.jpa.JPAAnnotationProcessor</processor>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<spring.profiles.active>dev</spring.profiles.active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<spring.profiles.active>prod</spring.profiles.active>
</properties>
</profile>
</profiles>
</project>

View File

@ -0,0 +1,65 @@
package com.jzamoram.itx;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;
import java.util.TimeZone;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.core.env.Environment;
import com.jzamoram.itx.config.ApplicationProperties;
import com.jzamoram.itx.config.Constants;
@SpringBootApplication(exclude = { H2ConsoleAutoConfiguration.class })
@EnableConfigurationProperties({ LiquibaseProperties.class, ApplicationProperties.class })
public class ItxApplication {
private static final Logger LOG = LoggerFactory.getLogger(ItxApplication.class);
public ItxApplication() {
}
/**
* Main method, used to run the application.
*
* @param args the command line arguments.
*/
public static void main(String[] args) {
TimeZone.setDefault(TimeZone.getTimeZone(Constants.DEFAULT_TIMEZONE));
SpringApplication app = new SpringApplication(ItxApplication.class);
Environment env = app.run(args).getEnvironment();
logApplicationStartup(env);
}
private static void logApplicationStartup(Environment env) {
String protocol = Optional.ofNullable(env.getProperty("server.ssl.key-store")).map(key -> "https")
.orElse("http");
String applicationName = env.getProperty("spring.application.name");
String serverPort = env.getProperty("server.port");
String contextPath = Optional.ofNullable(env.getProperty("server.servlet.context-path"))
.filter(StringUtils::isNotBlank).orElse("/");
String hostAddress = "localhost";
try {
hostAddress = InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
LOG.warn("The host name could not be determined, using `localhost` as fallback");
}
LOG.info("""
----------------------------------------------------------
\tApplication '{}' is running! Access URLs:
\tLocal: \t\t{}://localhost:{}{}
\tExternal: \t{}://{}:{}{}
\tProfile(s): \t{}
----------------------------------------------------------""", applicationName, protocol, serverPort,
contextPath, protocol, hostAddress, serverPort, contextPath,
env.getActiveProfiles().length == 0 ? env.getDefaultProfiles() : env.getActiveProfiles());
}
}

View File

@ -0,0 +1,28 @@
package com.jzamoram.itx.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "application", ignoreUnknownFields = false)
public class ApplicationProperties {
private final Liquibase liquibase = new Liquibase();
public Liquibase getLiquibase() {
return liquibase;
}
public static class Liquibase {
private Boolean asyncStart = true;
public Boolean getAsyncStart() {
return asyncStart;
}
public void setAsyncStart(Boolean asyncStart) {
this.asyncStart = asyncStart;
}
}
}

View File

@ -0,0 +1,15 @@
package com.jzamoram.itx.config;
/**
* Application constants.
*/
public final class Constants {
public static final String SYSTEM = "system";
public static final String DEFAULT_LANGUAGE = "es";
public static final String DEFAULT_TIMEZONE = "UTC";
public static final String PROFILE_DEVELOPMENT = "dev";
private Constants() {}
}

View File

@ -0,0 +1,58 @@
package com.jzamoram.itx.config;
import java.sql.SQLException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.h2.H2ConsoleProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import com.jzamoram.itx.config.h2.H2ConfigurationHelper;
@Configuration
@EnableJpaRepositories({"com.jzamoram.itx.repository"})
@EnableTransactionManagement
@EnableConfigurationProperties(H2ConsoleProperties.class)
public class DatabaseConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(DatabaseConfiguration.class);
private final Environment env;
public DatabaseConfiguration(Environment env) {
this.env = env;
}
/**
* Open the TCP port for the H2 database, so it is available remotely.
*
* @return the H2 database TCP server.
* @throws SQLException if the server failed to start.
*/
@Bean(initMethod = "start", destroyMethod = "stop")
@ConditionalOnProperty(prefix = "spring.h2.console", name = "enabled", havingValue = "true")
public Object h2TCPServer() throws SQLException {
String port = getValidPortForH2();
LOG.info("H2 database is available on port {}", port);
return H2ConfigurationHelper.createServer(port);
}
private String getValidPortForH2() {
int port = Integer.parseInt(env.getProperty("server.port"));
if (port < 10000) {
port = 10000 + port;
} else {
if (port < 63536) {
port = port + 2000;
} else {
port = port - 2000;
}
}
return String.valueOf(port);
}
}

View File

@ -0,0 +1,20 @@
package com.jzamoram.itx.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.datetime.standard.DateTimeFormatterRegistrar;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Configure the converters to use the ISO format for dates by default.
*/
@Configuration
public class DateTimeFormatConfiguration implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}

View File

@ -0,0 +1,83 @@
package com.jzamoram.itx.config;
import java.util.concurrent.Executor;
import javax.sql.DataSource;
import liquibase.integration.spring.SpringLiquibase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseDataSource;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.jzamoram.itx.config.liquibase.SpringLiquibaseUtil;
@Configuration
public class LiquibaseConfiguration {
private static final Logger LOG = LoggerFactory.getLogger(LiquibaseConfiguration.class);
private final Environment env;
public LiquibaseConfiguration(Environment env) {
this.env = env;
}
@Bean
public SpringLiquibase liquibase(
@Qualifier("taskExecutor") Executor executor,
LiquibaseProperties liquibaseProperties,
@LiquibaseDataSource ObjectProvider<DataSource> liquibaseDataSource,
ObjectProvider<DataSource> dataSource,
ApplicationProperties applicationProperties,
DataSourceProperties dataSourceProperties
) {
SpringLiquibase liquibase;
if (Boolean.TRUE.equals(applicationProperties.getLiquibase().getAsyncStart())) {
liquibase = SpringLiquibaseUtil.createAsyncSpringLiquibase(
this.env,
executor,
liquibaseDataSource.getIfAvailable(),
liquibaseProperties,
dataSource.getIfUnique(),
dataSourceProperties
);
} else {
liquibase = SpringLiquibaseUtil.createSpringLiquibase(
liquibaseDataSource.getIfAvailable(),
liquibaseProperties,
dataSource.getIfUnique(),
dataSourceProperties
);
}
liquibase.setChangeLog("classpath:liquibase/master.xml");
if (!CollectionUtils.isEmpty(liquibaseProperties.getContexts())) {
liquibase.setContexts(StringUtils.collectionToCommaDelimitedString(liquibaseProperties.getContexts()));
}
liquibase.setDefaultSchema(liquibaseProperties.getDefaultSchema());
liquibase.setLiquibaseSchema(liquibaseProperties.getLiquibaseSchema());
liquibase.setLiquibaseTablespace(liquibaseProperties.getLiquibaseTablespace());
liquibase.setDatabaseChangeLogLockTable(liquibaseProperties.getDatabaseChangeLogLockTable());
liquibase.setDatabaseChangeLogTable(liquibaseProperties.getDatabaseChangeLogTable());
liquibase.setDropFirst(liquibaseProperties.isDropFirst());
if (!CollectionUtils.isEmpty(liquibaseProperties.getLabelFilter())) {
liquibase.setLabelFilter(StringUtils.collectionToCommaDelimitedString(liquibaseProperties.getLabelFilter()));
}
liquibase.setChangeLogParameters(liquibaseProperties.getParameters());
liquibase.setRollbackFile(liquibaseProperties.getRollbackFile());
liquibase.setTestRollbackOnUpdate(liquibaseProperties.isTestRollbackOnUpdate());
if (env.matchesProfiles("no-liquibase")) {
liquibase.setShouldRun(false);
} else {
liquibase.setShouldRun(liquibaseProperties.isEnabled());
LOG.debug("Configuring Liquibase");
}
return liquibase;
}
}

View File

@ -0,0 +1,49 @@
package com.jzamoram.itx.config;
import jakarta.servlet.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import com.jzamoram.itx.config.h2.H2ConfigurationHelper;
@Configuration
public class WebConfigurer implements ServletContextInitializer {
private static final Logger LOG = LoggerFactory.getLogger(WebConfigurer.class);
private final Environment env;
public WebConfigurer(Environment env) {
this.env = env;
}
@Override
public void onStartup(ServletContext servletContext) {
if (env.getActiveProfiles().length != 0) {
LOG.info("Web application configuration, using profiles: {}", (Object[]) env.getActiveProfiles());
}
if (h2ConsoleIsEnabled(env)) {
initH2Console(servletContext);
}
LOG.info("Web application fully configured");
}
private boolean h2ConsoleIsEnabled(Environment env) {
return (
env.acceptsProfiles(Profiles.of(Constants.PROFILE_DEVELOPMENT)) &&
"true".equals(env.getProperty("spring.h2.console.enabled"))
);
}
private void initH2Console(ServletContext servletContext) {
LOG.info("Initialize H2 console");
H2ConfigurationHelper.initH2Console(servletContext);
}
}

View File

@ -0,0 +1,113 @@
package com.jzamoram.itx.config.h2;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletRegistration;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
/**
* Utility class to configure H2 in development.
* <p>
* We don't want to include H2 when we are packaging for the "prod" profile and won't
* actually need it, so we have to load / invoke things at runtime through reflection.
*/
public class H2ConfigurationHelper {
private H2ConfigurationHelper() {
throw new AssertionError("The class should not be instantiated");
}
/**
* <p>createServer.</p>
*
* @return a {@link java.lang.Object} object.
* @throws java.sql.SQLException if any.
*/
public static Object createServer() throws SQLException {
return createServer("9092");
}
/**
* <p>createServer.</p>
*
* @param port a {@link java.lang.String} object.
* @return a {@link java.lang.Object} object.
* @throws java.sql.SQLException if any.
*/
public static Object createServer(String port) throws SQLException {
try {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> serverClass = Class.forName("org.h2.tools.Server", true, loader);
Method createServer = serverClass.getMethod("createTcpServer", String[].class);
return createServer.invoke(null, new Object[] { new String[] { "-tcp", "-tcpAllowOthers", "-tcpPort", port } });
} catch (ClassNotFoundException | LinkageError e) {
throw new RuntimeException("Failed to load and initialize org.h2.tools.Server", e);
} catch (SecurityException | NoSuchMethodException e) {
throw new RuntimeException("Failed to get method org.h2.tools.Server.createTcpServer()", e);
} catch (IllegalAccessException | IllegalArgumentException e) {
throw new RuntimeException("Failed to invoke org.h2.tools.Server.createTcpServer()", e);
} catch (InvocationTargetException e) {
Throwable t = e.getTargetException();
if (t instanceof SQLException) {
throw (SQLException) t;
}
throw new RuntimeException("Unchecked exception in org.h2.tools.Server.createTcpServer()", t);
}
}
/**
* Init the H2 console via H2's webserver when no servletContext {@link jakarta.servlet.ServletContext}
* is available.
*/
public static void initH2Console() {
initH2Console("src/main/resources");
}
/**
* Init the H2 console via H2's webserver when no servletContext {@link jakarta.servlet.ServletContext}
* is available.
*
* @param propertiesLocation the location where to find .h2.server.properties
*/
static void initH2Console(String propertiesLocation) {
try {
// We don't want to include H2 when we are packaging for the "prod" profile and won't
// actually need it, so we have to load / invoke things at runtime through reflection.
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> serverClass = Class.forName("org.h2.tools.Server", true, loader);
Method createWebServer = serverClass.getMethod("createWebServer", String[].class);
Method start = serverClass.getMethod("start");
Object server = createWebServer.invoke(null, new Object[] { new String[] { "-properties", propertiesLocation } });
start.invoke(server);
} catch (Exception e) {
throw new RuntimeException("Failed to start h2 webserver console", e);
}
}
/**
* <p>initH2Console.</p>
*
* @param servletContext a {@link jakarta.servlet.ServletContext} object.
*/
public static void initH2Console(ServletContext servletContext) {
try {
// We don't want to include H2 when we are packaging for the "prod" profile and won't
// actually need it, so we have to load / invoke things at runtime through reflection.
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class<?> servletClass = Class.forName("org.h2.server.web.JakartaWebServlet", true, loader);
Servlet servlet = (Servlet) servletClass.getDeclaredConstructor().newInstance();
ServletRegistration.Dynamic h2ConsoleServlet = servletContext.addServlet("H2Console", servlet);
h2ConsoleServlet.addMapping("/h2-console/*");
h2ConsoleServlet.setInitParameter("-properties", "src/main/resources/");
h2ConsoleServlet.setLoadOnStartup(1);
} catch (ClassNotFoundException | LinkageError | NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException("Failed to load and initialize org.h2.server.web.JakartaWebServlet", e);
} catch (IllegalAccessException | InstantiationException e) {
throw new RuntimeException("Failed to instantiate org.h2.server.web.JakartaWebServlet", e);
}
}
}

View File

@ -0,0 +1,116 @@
package com.jzamoram.itx.config.liquibase;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.concurrent.Executor;
import liquibase.exception.LiquibaseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.liquibase.DataSourceClosingSpringLiquibase;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.util.StopWatch;
/**
* Specific liquibase.integration.spring.SpringLiquibase that will update the database asynchronously and close
* DataSource if necessary. <p> By default, this asynchronous version only works when using the "dev" profile.<p> The standard
* liquibase.integration.spring.SpringLiquibase starts Liquibase in the current thread: <ul> <li>This is needed if you
* want to do some database requests at startup</li> <li>This ensure that the database is ready when the application
* starts</li> </ul> But as this is a rather slow process, we use this asynchronous version to speed up our start-up
* time: <ul> <li>On a recent MacBook Pro, start-up time is down from 14 seconds to 8 seconds</li> <li>In production,
* this can help your application run on platforms like Heroku, where it must start/restart very quickly</li> </ul>
*/
public class AsyncSpringLiquibase extends DataSourceClosingSpringLiquibase {
/** Constant <code>DISABLED_MESSAGE="Liquibase is disabled"</code> */
public static final String DISABLED_MESSAGE = "Liquibase is disabled";
/** Constant <code>STARTING_ASYNC_MESSAGE="Starting Liquibase asynchronously, your"{trunked}</code> */
public static final String STARTING_ASYNC_MESSAGE = "Starting Liquibase asynchronously, your database might not be ready at startup!";
/** Constant <code>STARTING_SYNC_MESSAGE="Starting Liquibase synchronously"</code> */
public static final String STARTING_SYNC_MESSAGE = "Starting Liquibase synchronously";
/** Constant <code>STARTED_MESSAGE="Liquibase has updated your database in "{trunked}</code> */
public static final String STARTED_MESSAGE = "Liquibase has updated your database in {} ms";
/** Constant <code>EXCEPTION_MESSAGE="Liquibase could not start correctly, yo"{trunked}</code> */
public static final String EXCEPTION_MESSAGE = "Liquibase could not start correctly, your database is NOT ready: {}";
/** Constant <code>SLOWNESS_THRESHOLD=5</code> */
public static final long SLOWNESS_THRESHOLD = 5; // seconds
/** Constant <code>SLOWNESS_MESSAGE="Warning, Liquibase took more than {} se"{trunked}</code> */
public static final String SLOWNESS_MESSAGE = "Warning, Liquibase took more than {} seconds to start up!";
// named "logger" because there is already a field called "log" in "SpringLiquibase"
private final Logger logger = LoggerFactory.getLogger(AsyncSpringLiquibase.class);
private final Executor executor;
private final Environment env;
/**
* <p>Constructor for AsyncSpringLiquibase.</p>
*
* @param executor a {@link java.util.concurrent.Executor} object.
* @param env a {@link org.springframework.core.env.Environment} object.
*/
public AsyncSpringLiquibase(Executor executor, Environment env) {
this.executor = executor;
this.env = env;
}
/** {@inheritDoc} */
@Override
public void afterPropertiesSet() throws LiquibaseException {
if (isLiquibaseDisabled()) {
logger.debug(DISABLED_MESSAGE);
return;
}
if (isAsyncProfileActive()) {
handleAsyncExecution();
} else {
logger.debug(STARTING_SYNC_MESSAGE);
initDb();
}
}
private boolean isLiquibaseDisabled() {
return env.acceptsProfiles(Profiles.of("no-liquibase"));
}
private boolean isAsyncProfileActive() {
return env.acceptsProfiles(Profiles.of("dev" + "|" + "heroku"));
}
private void handleAsyncExecution() {
// Prevent Thread Lock with spring-cloud-context GenericScope
// https://github.com/spring-cloud/spring-cloud-commons/commit/aaa7288bae3bb4d6fdbef1041691223238d77b7b#diff-afa0715eafc2b0154475fe672dab70e4R328
try (Connection connection = getDataSource().getConnection()) {
executor.execute(() -> {
try {
logger.warn(STARTING_ASYNC_MESSAGE);
initDb();
} catch (LiquibaseException e) {
logger.error(EXCEPTION_MESSAGE, e.getMessage(), e);
}
});
} catch (SQLException e) {
logger.error(EXCEPTION_MESSAGE, e.getMessage(), e);
}
}
/**
* <p>initDb.</p>
*
* @throws liquibase.exception.LiquibaseException if any.
*/
protected void initDb() throws LiquibaseException {
StopWatch watch = new StopWatch();
watch.start();
super.afterPropertiesSet();
watch.stop();
logger.debug(STARTED_MESSAGE, watch.getTotalTimeMillis());
boolean isExecutionTimeLong = watch.getTotalTimeMillis() > SLOWNESS_THRESHOLD * 1000L;
if (isExecutionTimeLong) {
logger.warn(SLOWNESS_MESSAGE, SLOWNESS_THRESHOLD);
}
}
}

View File

@ -0,0 +1,106 @@
package com.jzamoram.itx.config.liquibase;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.function.Supplier;
import javax.sql.DataSource;
import liquibase.integration.spring.SpringLiquibase;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.autoconfigure.liquibase.DataSourceClosingSpringLiquibase;
import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.core.env.Environment;
/**
* Utility class for handling SpringLiquibase.
*
* <p>
* It follows implementation of
* <a href="https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfiguration.java">LiquibaseAutoConfiguration</a>.
*/
public final class SpringLiquibaseUtil {
private SpringLiquibaseUtil() {}
/**
* <p>createSpringLiquibase.</p>
*
* @param liquibaseDatasource a {@link javax.sql.DataSource} object.
* @param liquibaseProperties a {@link org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties} object.
* @param dataSource a {@link javax.sql.DataSource} object.
* @param dataSourceProperties a {@link org.springframework.boot.autoconfigure.jdbc.DataSourceProperties} object.
* @return a {@link liquibase.integration.spring.SpringLiquibase} object.
*/
public static SpringLiquibase createSpringLiquibase(
DataSource liquibaseDatasource,
LiquibaseProperties liquibaseProperties,
DataSource dataSource,
DataSourceProperties dataSourceProperties
) {
SpringLiquibase liquibase;
DataSource liquibaseDataSource = getDataSource(liquibaseDatasource, liquibaseProperties, dataSource);
if (liquibaseDataSource != null) {
liquibase = new SpringLiquibase();
liquibase.setDataSource(liquibaseDataSource);
return liquibase;
}
liquibase = new DataSourceClosingSpringLiquibase();
liquibase.setDataSource(createNewDataSource(liquibaseProperties, dataSourceProperties));
return liquibase;
}
/**
* <p>createAsyncSpringLiquibase.</p>
*
* @param env a {@link org.springframework.core.env.Environment} object.
* @param executor a {@link java.util.concurrent.Executor} object.
* @param liquibaseDatasource a {@link javax.sql.DataSource} object.
* @param liquibaseProperties a {@link org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties} object.
* @param dataSource a {@link javax.sql.DataSource} object.
* @param dataSourceProperties a {@link org.springframework.boot.autoconfigure.jdbc.DataSourceProperties} object.
* @return a {@link AsyncSpringLiquibase} object.
*/
public static AsyncSpringLiquibase createAsyncSpringLiquibase(
Environment env,
Executor executor,
DataSource liquibaseDatasource,
LiquibaseProperties liquibaseProperties,
DataSource dataSource,
DataSourceProperties dataSourceProperties
) {
AsyncSpringLiquibase liquibase = new AsyncSpringLiquibase(executor, env);
DataSource liquibaseDataSource = getDataSource(liquibaseDatasource, liquibaseProperties, dataSource);
if (liquibaseDataSource != null) {
liquibase.setCloseDataSourceOnceMigrated(false);
liquibase.setDataSource(liquibaseDataSource);
} else {
liquibase.setDataSource(createNewDataSource(liquibaseProperties, dataSourceProperties));
}
return liquibase;
}
private static DataSource getDataSource(
DataSource liquibaseDataSource,
LiquibaseProperties liquibaseProperties,
DataSource dataSource
) {
if (liquibaseDataSource != null) {
return liquibaseDataSource;
}
if (liquibaseProperties.getUrl() == null && liquibaseProperties.getUser() == null) {
return dataSource;
}
return null;
}
private static DataSource createNewDataSource(LiquibaseProperties liquibaseProperties, DataSourceProperties dataSourceProperties) {
String url = getProperty(liquibaseProperties::getUrl, dataSourceProperties::determineUrl);
String user = getProperty(liquibaseProperties::getUser, dataSourceProperties::determineUsername);
String password = getProperty(liquibaseProperties::getPassword, dataSourceProperties::determinePassword);
return DataSourceBuilder.create().url(url).username(user).password(password).build();
}
private static String getProperty(Supplier<String> property, Supplier<String> defaultValue) {
return Optional.of(property).map(Supplier::get).orElseGet(defaultValue);
}
}

View File

@ -0,0 +1,158 @@
package com.jzamoram.itx.domain;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import java.io.Serializable;
import java.time.Instant;
@Entity
@Table(name = "prices")
public class Price implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
@SequenceGenerator(name = "sequenceGenerator")
@Column(name = "id")
@JsonIgnore
private Long id;
@Column(name = "brand_id")
private Long brandId;
@Column(name = "start_date")
private Instant startDate;
@Column(name = "end_date")
private Instant endDate;
@Column(name = "price_list")
private Long priceList;
@Column(name = "product_id")
private Long productId;
@Column(name = "priority")
@JsonIgnore
private Long priority;
@Column(name = "price")
private Double price;
@Column(name = "curr")
@JsonIgnore
private String curr;
public Long getId() {
return this.id;
}
public Price id(Long id) {
this.setId(id);
return this;
}
public void setId(Long id) {
this.id = id;
}
public Long getBrandId() {
return brandId;
}
public void setBrandId(Long brandId) {
this.brandId = brandId;
}
public Instant getStartDate() {
return startDate;
}
public void setStartDate(Instant startDate) {
this.startDate = startDate;
}
public Instant getEndDate() {
return endDate;
}
public void setEndDate(Instant endDate) {
this.endDate = endDate;
}
public Long getPriceList() {
return priceList;
}
public void setPriceList(Long priceList) {
this.priceList = priceList;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public Long getPriority() {
return priority;
}
public void setPriority(Long priority) {
this.priority = priority;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
public String getCurr() {
return curr;
}
public void setCurr(String curr) {
this.curr = curr;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Price)) {
return false;
}
return getId() != null && getId().equals(((Price) o).getId());
}
@Override
public int hashCode() {
// see https://vladmihalcea.com/how-to-implement-equals-and-hashcode-using-the-jpa-entity-identifier/
return getClass().hashCode();
}
// prettier-ignore
@Override
public String toString() {
return "Price{" +
"id=" + getId() +
", brandId=" + getBrandId() +
", startDate='" + getStartDate() + "'" +
", endDate='" + getEndDate() + "'" +
", price_list=" + getPriceList() +
", product_id=" + getProductId() +
", priority=" + getPriority() +
", price=" + getPrice() +
", curr='" + getCurr() + "'" +
"}";
}
}

View File

@ -0,0 +1,10 @@
package com.jzamoram.itx.repository;
import com.jzamoram.itx.domain.Price;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.querydsl.QuerydslPredicateExecutor;
import org.springframework.stereotype.Repository;
@Repository
public interface PriceRepository extends JpaRepository<Price, Long>, QuerydslPredicateExecutor<Price> {}

View File

@ -0,0 +1,15 @@
package com.jzamoram.itx.service;
import com.jzamoram.itx.domain.Price;
import java.time.Instant;
import java.util.List;
/**
* Service Interface for managing {@link com.jzamoram.itx.domain.Price}.
*/
public interface PriceService {
List<Price> query(Long brandId, Long productId, Instant applicationDate);
}

View File

@ -0,0 +1,45 @@
package com.jzamoram.itx.service.impl;
import com.jzamoram.itx.domain.Price;
import com.jzamoram.itx.domain.QPrice;
import com.jzamoram.itx.repository.PriceRepository;
import com.jzamoram.itx.service.PriceService;
import com.querydsl.core.types.dsl.BooleanExpression;
import java.time.Instant;
import java.util.List;
import org.apache.commons.collections4.IterableUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class PriceServiceImpl implements PriceService {
private static final Logger LOG = LoggerFactory.getLogger(PriceServiceImpl.class);
private final PriceRepository priceRepository;
public PriceServiceImpl(PriceRepository priceRepository) {
this.priceRepository = priceRepository;
}
@Override
@Transactional(readOnly = true)
public List<Price> query(Long brandId, Long productId, Instant applicationDate) {
LOG.debug("Request to query Price : productId {}, brandId {}, applicationDate {}", productId, brandId,
applicationDate);
QPrice priceFilter = QPrice.price1;
BooleanExpression productIdBE = priceFilter.productId.eq(productId);
BooleanExpression brandIdBE = priceFilter.brandId.eq(brandId);
BooleanExpression applicationDateGteBE = priceFilter.startDate.lt(applicationDate).or(priceFilter.startDate.eq(applicationDate));
BooleanExpression applicationDateLteBE = priceFilter.endDate.gt(applicationDate).or(priceFilter.endDate.eq(applicationDate));
return IterableUtils.toList(
priceRepository.findAll(productIdBE.and(brandIdBE).and(applicationDateGteBE).and(applicationDateLteBE), Sort.by(Sort.Direction.DESC, "priority")));
}
}

View File

@ -0,0 +1,38 @@
package com.jzamoram.itx.web.rest;
import com.jzamoram.itx.domain.Price;
import com.jzamoram.itx.repository.PriceRepository;
import com.jzamoram.itx.service.PriceService;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/price")
public class PriceResource {
private static final Logger LOG = LoggerFactory.getLogger(PriceResource.class);
private final PriceService priceService;
public PriceResource(PriceService priceService, PriceRepository priceRepository) {
this.priceService = priceService;
}
@GetMapping(value = "/{brandId}/{productId}", params = "applicationDate")
public ResponseEntity<Price> getPriceFilter(@PathVariable("brandId") Long brandId,
@PathVariable("productId") Long productId,
@RequestParam("applicationDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime applicationDate) {
LOG.debug("REST request to get Price for brandId {}, productId {} and applyDate {}", brandId, productId,
applicationDate);
List<Price> price = priceService.query(brandId, productId,
applicationDate.toInstant(OffsetDateTime.now().getOffset()));
return ResponseUtil.getFirstElement(price, null);
}
}

View File

@ -0,0 +1,22 @@
package com.jzamoram.itx.web.rest;
import java.util.List;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.server.ResponseStatusException;
public interface ResponseUtil {
static <X> ResponseEntity<X> getFirstElement(List<X> maybeResponse, HttpHeaders header) {
if(maybeResponse!=null && maybeResponse.size()>0)
{
return ResponseEntity.ok().headers(header).body(maybeResponse.get(0));
}
else {
throw new ResponseStatusException(HttpStatus.NOT_FOUND);
}
}
}

View File

@ -0,0 +1,27 @@
#H2 Server Properties
#Thu Jan 08 14:07:46 CET 2026
0=Generic JNDI Data Source|javax.naming.InitialContext|java\:comp/env/jdbc/Test|sa
1=Generic Teradata|com.teradata.jdbc.TeraDriver|jdbc\:teradata\://whomooz/|
10=Generic DB2|com.ibm.db2.jcc.DB2Driver|jdbc\:db2\://localhost/test|
11=Generic Oracle|oracle.jdbc.driver.OracleDriver|jdbc\:oracle\:thin\:@localhost\:1521\:XE|sa
12=Generic MS SQL Server 2000|com.microsoft.jdbc.sqlserver.SQLServerDriver|jdbc\:microsoft\:sqlserver\://localhost\:1433;DatabaseName\=sqlexpress|sa
13=Generic MS SQL Server 2005|com.microsoft.sqlserver.jdbc.SQLServerDriver|jdbc\:sqlserver\://localhost;DatabaseName\=test|sa
14=Generic PostgreSQL|org.postgresql.Driver|jdbc\:postgresql\:test|
15=Generic MySQL|com.mysql.cj.jdbc.Driver|jdbc\:mysql\://localhost\:3306/test|
16=Generic MariaDB|org.mariadb.jdbc.Driver|jdbc\:mariadb\://localhost\:3306/test|
17=Generic HSQLDB|org.hsqldb.jdbcDriver|jdbc\:hsqldb\:test;hsqldb.default_table_type\=cached|sa
18=Generic Derby (Server)|org.apache.derby.client.ClientAutoloadedDriver|jdbc\:derby\://localhost\:1527/test;create\=true|sa
19=Generic Derby (Embedded)|org.apache.derby.iapi.jdbc.AutoloadedDriver|jdbc\:derby\:test;create\=true|sa
2=Generic Snowflake|com.snowflake.client.jdbc.SnowflakeDriver|jdbc\:snowflake\://accountName.snowflakecomputing.com|
20=Generic H2 (Server)|org.h2.Driver|jdbc\:h2\:tcp\://localhost/~/test|sa
21=Generic H2 (Embedded)|org.h2.Driver|jdbc\:h2\:mem\:itx;DB_CLOSE_DELAY\=-1|itx
3=Generic Redshift|com.amazon.redshift.jdbc42.Driver|jdbc\:redshift\://endpoint\:5439/database|
4=Generic Impala|org.cloudera.impala.jdbc41.Driver|jdbc\:impala\://clustername\:21050/default|
5=Generic Hive 2|org.apache.hive.jdbc.HiveDriver|jdbc\:hive2\://clustername\:10000/default|
6=Generic Hive|org.apache.hadoop.hive.jdbc.HiveDriver|jdbc\:hive\://clustername\:10000/default|
7=Generic Azure SQL|com.microsoft.sqlserver.jdbc.SQLServerDriver|jdbc\:sqlserver\://name.database.windows.net\:1433|
8=Generic Firebird Server|org.firebirdsql.jdbc.FBDriver|jdbc\:firebirdsql\:localhost\:c\:/temp/firebird/test|sysdba
9=Generic SQLite|org.sqlite.JDBC|jdbc\:sqlite\:test|sa
webAllowOthers=false
webPort=8082
webSSL=false

View File

@ -0,0 +1,36 @@
logging:
level:
ROOT: INFO
org.hibernate.SQL: DEBUG
com.jzamoram: DEBUG
spring:
application:
name: itx
jackson:
serialization:
indent-output: true
datasource:
type: com.zaxxer.hikari.HikariDataSource
url: jdbc:h2:mem:itx;DB_CLOSE_DELAY=-1
username: itx
password:
hikari:
poolName: Hikari
auto-commit: false
h2:
console:
enabled: true
path: /h2-console
liquibase:
# Remove 'faker' if you do not want the sample data to be loaded automatically
contexts: dev, faker
main:
allow-bean-definition-overriding: true
profiles:
active: '@spring.profiles.active@'
server:
port: 8080
forward-headers-strategy: native

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="20260103" author="jzamoram">
<createTable tableName="prices">
<column name="id" type="bigint">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="brand_id" type="bigint">
<constraints nullable="false"/>
</column>
<column name="start_date" type="${datetimeType}">
<constraints nullable="false" />
</column>
<column name="end_date" type="${datetimeType}">
<constraints nullable="false" />
</column>
<column name="price_list" type="bigint">
<constraints nullable="false" />
</column>
<column name="product_id" type="bigint">
<constraints nullable="false" />
</column>
<column name="priority" type="bigint">
<constraints nullable="false" />
</column>
<column name="price" type="double">
<constraints nullable="false" />
</column>
<column name="curr" type="varchar(3)">
<constraints nullable="false" />
</column>
</createTable>
<dropDefaultValue tableName="prices" columnName="start_date" columnDataType="${datetimeType}"/>
<dropDefaultValue tableName="prices" columnName="end_date" columnDataType="${datetimeType}"/>
</changeSet>
<!--
Load sample data generated with Faker.js
- This data can be easily edited using a CSV editor (or even MS Excel) and
is located in the 'src/main/resources/config/liquibase/fake-data' directory
- This can be customized by adding or removing 'faker' in the 'spring.liquibase.contexts'
Spring Boot configuration key.
-->
<changeSet id="20260103-1-data" author="jzamoram" context="faker">
<loadData
file="liquibase/fake-data/prices.csv"
separator=";"
tableName="prices"
usePreparedStatements="true">
<column name="brand_id" type="numeric"/>
<column name="start_date" type="date"/>
<column name="end_date" type="date"/>
<column name="price_list" type="numeric"/>
<column name="product_id" type="numeric"/>
<column name="priority" type="numeric"/>
<column name="price" type="numeric"/>
<column name="curr" type="string"/>
</loadData>
</changeSet>
</databaseChangeLog>

View File

@ -0,0 +1,5 @@
id;brand_id;start_date;end_date;price_list;product_id;priority;price;curr
1;1;2020-06-14T00:00:00;2020-12-31T23:59:59;1;35455;0;35.50;EUR
2;1;2020-06-14T15:00:00;2020-06-14T18:30:00;2;35455;1;25.45;EUR
3;1;2020-06-15T00:00:00;2020-06-15T11:00:00;3;35455;1;30.50;EUR
4;1;2020-06-15T16:00:00;2020-12-31T23:59:59;4;35455;1;38.95;EUR
1 id brand_id start_date end_date price_list product_id priority price curr
2 1 1 2020-06-14T00:00:00 2020-12-31T23:59:59 1 35455 0 35.50 EUR
3 2 1 2020-06-14T15:00:00 2020-06-14T18:30:00 2 35455 1 25.45 EUR
4 3 1 2020-06-15T00:00:00 2020-06-15T11:00:00 3 35455 1 30.50 EUR
5 4 1 2020-06-15T16:00:00 2020-12-31T23:59:59 4 35455 1 38.95 EUR

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<property name="now" value="now()" dbms="h2"/>
<property name="floatType" value="float4" dbms="h2"/>
<property name="uuidType" value="uuid" dbms="h2"/>
<property name="datetimeType" value="datetime(6)" dbms="h2"/>
<property name="timeType" value="time(6)" dbms="h2"/>
<property name="clobType" value="longvarchar" dbms="h2"/>
<property name="blobType" value="blob" dbms="h2"/>
<property name="now" value="current_timestamp" dbms="postgresql"/>
<property name="floatType" value="float4" dbms="postgresql"/>
<property name="clobType" value="clob" dbms="postgresql"/>
<property name="blobType" value="blob" dbms="postgresql"/>
<property name="uuidType" value="uuid" dbms="postgresql"/>
<property name="datetimeType" value="datetime" dbms="postgresql"/>
<property name="timeType" value="time(6)" dbms="postgresql"/>
<include file="liquibase/changelog/20260103_added_entity_Prices.xml" relativeToChangelogFile="false"/>
</databaseChangeLog>

View File

@ -0,0 +1,71 @@
package com.jzamoram.itx;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* Integration tests for the {@link PricesResource} REST controller.
*/
@SpringBootTest
@AutoConfigureMockMvc
class PricesResourceTest {
@Autowired
private MockMvc mockMvc;
@Test
void test1_priceAt10OnDay14() throws Exception {
StringBuffer sbUrl = new StringBuffer();
sbUrl.append("/api/price").append("/1").append("/35455");
mockMvc.perform(get(sbUrl.toString()).param("applicationDate", "2020-06-14T10:00:00"))
.andExpect(status().isOk()).andExpect(jsonPath("$.productId").value(35455))
.andExpect(jsonPath("$.brandId").value(1)).andExpect(jsonPath("$.priceList").value(1))
.andExpect(jsonPath("$.price").value(35.50));
}
@Test
void test2_priceAt16OnDay14() throws Exception {
StringBuffer sbUrl = new StringBuffer();
sbUrl.append("/api/price").append("/1").append("/35455");
mockMvc.perform(get(sbUrl.toString()).param("applicationDate", "2020-06-14T16:00:00"))
.andExpect(status().isOk()).andExpect(jsonPath("$.productId").value(35455))
.andExpect(jsonPath("$.brandId").value(1)).andExpect(jsonPath("$.priceList").value(2))
.andExpect(jsonPath("$.price").value(25.45));
}
@Test
void test3_priceAt21OnDay14() throws Exception {
StringBuffer sbUrl = new StringBuffer();
sbUrl.append("/api/price").append("/1").append("/35455");
mockMvc.perform(get(sbUrl.toString()).param("applicationDate", "2020-06-14T21:00:00"))
.andExpect(status().isOk()).andExpect(jsonPath("$.productId").value(35455))
.andExpect(jsonPath("$.brandId").value(1)).andExpect(jsonPath("$.priceList").value(1))
.andExpect(jsonPath("$.price").value(35.50));
}
@Test
void test4_priceAt10OnDay15() throws Exception {
StringBuffer sbUrl = new StringBuffer();
sbUrl.append("/api/price").append("/1").append("/35455");
mockMvc.perform(get(sbUrl.toString()).param("applicationDate", "2020-06-15T10:00:00"))
.andExpect(status().isOk()).andExpect(jsonPath("$.productId").value(35455))
.andExpect(jsonPath("$.brandId").value(1)).andExpect(jsonPath("$.priceList").value(3))
.andExpect(jsonPath("$.price").value(30.50));
}
@Test
void test5_priceAt21OnDay16() throws Exception {
StringBuffer sbUrl = new StringBuffer();
sbUrl.append("/api/price").append("/1").append("/35455");
mockMvc.perform(get(sbUrl.toString()).param("applicationDate", "2020-06-16T21:00:00"))
.andExpect(status().isOk()).andExpect(jsonPath("$.productId").value(35455))
.andExpect(jsonPath("$.brandId").value(1)).andExpect(jsonPath("$.priceList").value(4))
.andExpect(jsonPath("$.price").value(38.95));
}
}