Technical exercise ITX v1.0
This commit is contained in:
commit
8cd737ef31
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/mvnw text eol=lf
|
||||||
|
*.cmd text eol=crlf
|
||||||
51
.gitignore
vendored
Normal file
51
.gitignore
vendored
Normal 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
3
.mvn/wrapper/maven-wrapper.properties
vendored
Normal 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
98
README.md
Normal 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
295
mvnw
vendored
Normal 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
189
mvnw.cmd
vendored
Normal 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
144
pom.xml
Normal 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>
|
||||||
65
src/main/java/com/jzamoram/itx/ItxApplication.java
Normal file
65
src/main/java/com/jzamoram/itx/ItxApplication.java
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/main/java/com/jzamoram/itx/config/Constants.java
Normal file
15
src/main/java/com/jzamoram/itx/config/Constants.java
Normal 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() {}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/main/java/com/jzamoram/itx/config/WebConfigurer.java
Normal file
49
src/main/java/com/jzamoram/itx/config/WebConfigurer.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
158
src/main/java/com/jzamoram/itx/domain/Price.java
Normal file
158
src/main/java/com/jzamoram/itx/domain/Price.java
Normal 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() + "'" +
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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> {}
|
||||||
15
src/main/java/com/jzamoram/itx/service/PriceService.java
Normal file
15
src/main/java/com/jzamoram/itx/service/PriceService.java
Normal 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);
|
||||||
|
|
||||||
|
}
|
||||||
@ -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")));
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/main/java/com/jzamoram/itx/web/rest/PriceResource.java
Normal file
38
src/main/java/com/jzamoram/itx/web/rest/PriceResource.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
22
src/main/java/com/jzamoram/itx/web/rest/ResponseUtil.java
Normal file
22
src/main/java/com/jzamoram/itx/web/rest/ResponseUtil.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
27
src/main/resources/.h2.server.properties
Normal file
27
src/main/resources/.h2.server.properties
Normal 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
|
||||||
36
src/main/resources/application.yaml
Normal file
36
src/main/resources/application.yaml
Normal 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
|
||||||
@ -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>
|
||||||
5
src/main/resources/liquibase/fake-data/prices.csv
Normal file
5
src/main/resources/liquibase/fake-data/prices.csv
Normal 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
|
||||||
|
23
src/main/resources/liquibase/master.xml
Normal file
23
src/main/resources/liquibase/master.xml
Normal 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>
|
||||||
71
src/test/java/com/jzamoram/itx/PricesResourceTest.java
Normal file
71
src/test/java/com/jzamoram/itx/PricesResourceTest.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user