From 1e701e320a03ccb65d0b0fbdd858a8bbfba8e498 Mon Sep 17 00:00:00 2001 From: Yana Pechenenko Date: Thu, 28 May 2026 15:02:34 +0200 Subject: [PATCH 1/7] Move Spring Boot project into task-1 folder --- task-1/API_DESIGN.md | 0 task-1/HELP.md | 28 ++ task-1/mvnw | 295 ++++++++++++++++++ task-1/mvnw.cmd | 189 +++++++++++ task-1/pom.xml | 117 +++++++ .../AnalyticsAPI/AnalyticsApiApplication.java | 13 + .../featureModule/FeatureModelController.java | 4 + .../featureModule/FeatureModelService.java | 4 + .../dto/request/FeatureModelRequest.java | 4 + .../dto/response/FeatureModelResponse.java | 4 + task-1/src/main/resources/application.yaml | 3 + .../AnalyticsApiApplicationTests.java | 13 + 12 files changed, 674 insertions(+) create mode 100644 task-1/API_DESIGN.md create mode 100644 task-1/HELP.md create mode 100755 task-1/mvnw create mode 100644 task-1/mvnw.cmd create mode 100644 task-1/pom.xml create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplication.java create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelController.java create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelService.java create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/request/FeatureModelRequest.java create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/response/FeatureModelResponse.java create mode 100644 task-1/src/main/resources/application.yaml create mode 100644 task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplicationTests.java diff --git a/task-1/API_DESIGN.md b/task-1/API_DESIGN.md new file mode 100644 index 0000000..e69de29 diff --git a/task-1/HELP.md b/task-1/HELP.md new file mode 100644 index 0000000..bc934c9 --- /dev/null +++ b/task-1/HELP.md @@ -0,0 +1,28 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/4.0.6/maven-plugin) +* [Create an OCI image](https://docs.spring.io/spring-boot/4.0.6/maven-plugin/build-image.html) +* [Spring Web](https://docs.spring.io/spring-boot/4.0.6/reference/web/servlet.html) +* [Validation](https://docs.spring.io/spring-boot/4.0.6/reference/io/validation.html) +* [SpringDoc OpenAPI](https://springdoc.org/) + +### Guides +The following guides illustrate how to use some features concretely: + +* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) +* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) +* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) +* [Validation](https://spring.io/guides/gs/validating-form-input/) +* [SpringDoc OpenAPI](https://github.com/springdoc/springdoc-openapi-demos/) + +### Maven Parent overrides + +Due to Maven's design, elements are inherited from the parent POM to the project POM. +While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent. +To prevent this, the project POM contains empty overrides for these elements. +If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. + diff --git a/task-1/mvnw b/task-1/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/task-1/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/task-1/mvnw.cmd b/task-1/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/task-1/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/task-1/pom.xml b/task-1/pom.xml new file mode 100644 index 0000000..31e66b6 --- /dev/null +++ b/task-1/pom.xml @@ -0,0 +1,117 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.6 + + + net.hackyourfuture + AnalyticsAPI + 0.0.1-SNAPSHOT + + + + + + + + + + + + + + + + + 25 + + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-webmvc + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 3.0.2 + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-validation-test + test + + + org.springframework.boot + spring-boot-starter-webmvc-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + compile + + compile + + + + + org.projectlombok + lombok + + + + + + default-testCompile + test-compile + + testCompile + + + + + org.projectlombok + lombok + + + + + + + + + + diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplication.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplication.java new file mode 100644 index 0000000..e062e55 --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplication.java @@ -0,0 +1,13 @@ +package net.hackyourfuture.AnalyticsAPI; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AnalyticsApiApplication { + + public static void main(String[] args) { + SpringApplication.run(AnalyticsApiApplication.class, args); + } + +} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelController.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelController.java new file mode 100644 index 0000000..be3f70e --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelController.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.AnalyticsAPI.featureModule; + +public class FeatureModelController { +} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelService.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelService.java new file mode 100644 index 0000000..896681a --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelService.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.AnalyticsAPI.featureModule; + +public class FeatureModelService { +} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/request/FeatureModelRequest.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/request/FeatureModelRequest.java new file mode 100644 index 0000000..ff3796a --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/request/FeatureModelRequest.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.AnalyticsAPI.featureModule.dto.request; + +public class FeatureModelRequest { +} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/response/FeatureModelResponse.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/response/FeatureModelResponse.java new file mode 100644 index 0000000..dea4c2f --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/response/FeatureModelResponse.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.AnalyticsAPI.featureModule.dto.response; + +public class FeatureModelResponse { +} diff --git a/task-1/src/main/resources/application.yaml b/task-1/src/main/resources/application.yaml new file mode 100644 index 0000000..d1a18fb --- /dev/null +++ b/task-1/src/main/resources/application.yaml @@ -0,0 +1,3 @@ +spring: + application: + name: AnalyticsAPI diff --git a/task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplicationTests.java b/task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplicationTests.java new file mode 100644 index 0000000..0af358f --- /dev/null +++ b/task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplicationTests.java @@ -0,0 +1,13 @@ +package net.hackyourfuture.AnalyticsAPI; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AnalyticsApiApplicationTests { + + @Test + void contextLoads() { + } + +} From e858cdc111c11722bd3e103c5dab224e9991ad61 Mon Sep 17 00:00:00 2001 From: Yana Pechenenko Date: Thu, 28 May 2026 18:16:05 +0200 Subject: [PATCH 2/7] Add get method with filtration and get by id --- .../AnalyticsRecordController.java | 107 ++++++++++++++++++ .../AnalyticsRecordService.java | 4 + .../dto/request/AnalyticsRecordRequest.java | 24 ++++ .../dto/response/AnalyticsRecordResponse.java | 7 ++ .../dto/response/SummaryResponse.java | 6 + .../models/AnalyticsRecord.java | 21 ++++ .../featureModule/FeatureModelController.java | 4 - .../featureModule/FeatureModelService.java | 4 - .../dto/request/FeatureModelRequest.java | 4 - .../dto/response/FeatureModelResponse.java | 4 - task-1/target/classes/application.yaml | 3 + .../AnalyticsApiApplication.class | Bin 0 -> 787 bytes .../AnalyticsRecordController.class | Bin 0 -> 7647 bytes .../AnalyticsRecordService.class | Bin 0 -> 387 bytes .../dto/request/AnalyticsRecordRequest.class | Bin 0 -> 3211 bytes .../response/AnalyticsRecordResponse.class | Bin 0 -> 2386 bytes .../dto/response/SummaryResponse.class | Bin 0 -> 2220 bytes .../models/AnalyticsRecord.class | Bin 0 -> 2002 bytes 18 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.java create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/request/AnalyticsRecordRequest.java create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/AnalyticsRecordResponse.java create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/SummaryResponse.java create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/models/AnalyticsRecord.java delete mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelController.java delete mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelService.java delete mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/request/FeatureModelRequest.java delete mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/response/FeatureModelResponse.java create mode 100644 task-1/target/classes/application.yaml create mode 100644 task-1/target/classes/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplication.class create mode 100644 task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.class create mode 100644 task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.class create mode 100644 task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/request/AnalyticsRecordRequest.class create mode 100644 task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/AnalyticsRecordResponse.class create mode 100644 task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/SummaryResponse.class create mode 100644 task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/models/AnalyticsRecord.class diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java new file mode 100644 index 0000000..5394025 --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java @@ -0,0 +1,107 @@ +package net.hackyourfuture.AnalyticsAPI.analyticsRecord; + + +import net.hackyourfuture.AnalyticsAPI.analyticsRecord.dto.response.AnalyticsRecordResponse; +import net.hackyourfuture.AnalyticsAPI.analyticsRecord.models.AnalyticsRecord; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/records") +public class AnalyticsRecordController { + private final List analyticsRecords = new ArrayList<>(List.of( + new AnalyticsRecord( + Instant.parse("2024-05-28T10:15:30Z"), + "PAGE_VIEW", + "frontend", + "session12345", + UUID.fromString("11111111-1111-1111-1111-111111111111") + ), + new AnalyticsRecord( + Instant.parse("2024-05-28T13:16:10Z"), + "BUTTON_CLICK", + "frontend", + "session12345", + UUID.fromString("22222222-2222-2222-2222-222222222222") + ), + new AnalyticsRecord( + Instant.parse("2024-05-28T17:05:00Z"), + "LOGIN_ATTEMPT", + "mobile", + "mobileSession98765", + UUID.fromString("33333333-3333-3333-3333-333333333333") + ), + new AnalyticsRecord( + Instant.parse("2024-05-28T21:30:45Z"), + "NAVIGATION", + "frontend", + "session55555", + UUID.fromString("44444444-4444-4444-4444-444444444444") + ) + + )); + + @GetMapping + public ResponseEntity> getAllAnalyticsRecords( + @RequestParam(required=false) Instant timeStart, + @RequestParam(required=false) Instant timeEnd, + @RequestParam(required=false) String eventType, + @RequestParam(required=false) String eventSource, + @RequestParam(required=false) String sessionId){ +List filteredAnalyticsReports = analyticsRecords.stream() + .filter(record -> timeStart == null || record.getTimestamp().compareTo(timeStart) >= 0) + .filter(record -> timeEnd == null || record.getTimestamp().compareTo(timeEnd) <=0) + .filter(record -> eventType == null || record.getEventType().equalsIgnoreCase(eventType)) + .filter(record -> eventSource == null || record.getEventSource().equalsIgnoreCase(eventSource)) + .filter(record -> sessionId == null || record.getSessionId().equals(sessionId)) + .map(record -> new AnalyticsRecordResponse( + record.getTimestamp(), + record.getEventType(), + record.getEventSource(), + record.getSessionId(), + record.getTraceId() + + )).toList(); + +return ResponseEntity.ok(filteredAnalyticsReports); + } + + @GetMapping ("/{traceId}") + public ResponseEntity getAnalyticsRecordByTraceId(@PathVariable UUID traceId){ + return analyticsRecords.stream() + .filter(record -> traceId.equals(record.getTraceId())) + .findFirst() + .map(record -> new AnalyticsRecordResponse( + record.getTimestamp(), + record.getEventType(), + record.getEventSource(), + record.getSessionId(), + record.getTraceId() + )) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + +} + + +//POST +//POST create record + + +//PUT +//PUT replace record +///records/{traceId} +// +//DELETE +//DELETE record +///records/{traceId} +// +//GET summary +//GET summary +///records/summary diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.java new file mode 100644 index 0000000..9c601bc --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.AnalyticsAPI.analyticsRecord; + +public class AnalyticsRecordService { +} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/request/AnalyticsRecordRequest.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/request/AnalyticsRecordRequest.java new file mode 100644 index 0000000..afb50e3 --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/request/AnalyticsRecordRequest.java @@ -0,0 +1,24 @@ +package net.hackyourfuture.AnalyticsAPI.analyticsRecord.dto.request; + + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + + +import java.time.Instant; + +public record AnalyticsRecordRequest ( + @NotNull(message = "Timestamp is required.") + Instant timestamp, + + @NotBlank(message = "Event type is required.") + String eventType, + + @NotBlank(message = "Event source is required.") + String eventSource, + + @NotBlank(message = "Session id is required.") + @Size(min = 6, max = 64, message = "Session id must be between 6 and 64 symbols.") + String sessionId) { +} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/AnalyticsRecordResponse.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/AnalyticsRecordResponse.java new file mode 100644 index 0000000..459190d --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/AnalyticsRecordResponse.java @@ -0,0 +1,7 @@ +package net.hackyourfuture.AnalyticsAPI.analyticsRecord.dto.response; + +import java.time.Instant; +import java.util.UUID; + +public record AnalyticsRecordResponse (Instant timestamp, String eventType, String eventSource, String sessionId, UUID traceId){ +} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/SummaryResponse.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/SummaryResponse.java new file mode 100644 index 0000000..f7076a1 --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/SummaryResponse.java @@ -0,0 +1,6 @@ +package net.hackyourfuture.AnalyticsAPI.analyticsRecord.dto.response; + +import java.util.Map; + +public record SummaryResponse(long totalRecords, Map eventsByType, long uniqueSessions ) { +} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/models/AnalyticsRecord.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/models/AnalyticsRecord.java new file mode 100644 index 0000000..cc4c0e8 --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/models/AnalyticsRecord.java @@ -0,0 +1,21 @@ +package net.hackyourfuture.AnalyticsAPI.analyticsRecord.models; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +import java.time.Instant; +import java.util.UUID; + +@Getter +@Setter +@AllArgsConstructor + +public class AnalyticsRecord { + private Instant timestamp; + private String eventType; + private String eventSource; + private String sessionId; + private UUID traceId; +} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelController.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelController.java deleted file mode 100644 index be3f70e..0000000 --- a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelController.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.hackyourfuture.AnalyticsAPI.featureModule; - -public class FeatureModelController { -} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelService.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelService.java deleted file mode 100644 index 896681a..0000000 --- a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/FeatureModelService.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.hackyourfuture.AnalyticsAPI.featureModule; - -public class FeatureModelService { -} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/request/FeatureModelRequest.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/request/FeatureModelRequest.java deleted file mode 100644 index ff3796a..0000000 --- a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/request/FeatureModelRequest.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.hackyourfuture.AnalyticsAPI.featureModule.dto.request; - -public class FeatureModelRequest { -} diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/response/FeatureModelResponse.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/response/FeatureModelResponse.java deleted file mode 100644 index dea4c2f..0000000 --- a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/featureModule/dto/response/FeatureModelResponse.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.hackyourfuture.AnalyticsAPI.featureModule.dto.response; - -public class FeatureModelResponse { -} diff --git a/task-1/target/classes/application.yaml b/task-1/target/classes/application.yaml new file mode 100644 index 0000000..d1a18fb --- /dev/null +++ b/task-1/target/classes/application.yaml @@ -0,0 +1,3 @@ +spring: + application: + name: AnalyticsAPI diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplication.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplication.class new file mode 100644 index 0000000000000000000000000000000000000000..a6e9f50c41035a3ab5b8f9a837af3a4b68b714c1 GIT binary patch literal 787 zcmbVKO>Yx15PeRQZoZ%;rGY{@a7%NjZ&X!Kr8H`#N()p3g##z=*4en-*pb%@^k;Db zao`8^M_*}@T!RlHUd=Js9}>~`&xVzJQX_OAEIj+ zJBH1-N-KB5u+cl11gJ9{Y3cY(#5cDlx6|C^R`Q`1>8(?7HoUwj1q(G?EK(HJ%$vxQYUnOZTI-;BNCkui>si)5*jhTB{-M7?lz!BHx*Z1AO&89NU) zI0)Av$3{E(-SLspQk}~lN3L2E)+ek+gtaR3Dt#7XvEH%GAZ^>x|zAU6rMsSEgfu*NtaljD}U+z zl`;>+IP^p^?0w9&Q*${{nev2aZCoK1;he1Hu~10l&Jdvmk#ecmDM?o%bb!|gXuJGZ z0rUGsc8elTo{_Khe}ere%G+dp8PxKGZ2SHII@qD8fv4Cd5a0Z!00Zisp#KYv&#QjF i%Ages&?;fn(Zw^$Kc~M&B$KOPudpxhlDr?g5Bvj(Hqx;G literal 0 HcmV?d00001 diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.class new file mode 100644 index 0000000000000000000000000000000000000000..6c1c63e91eb72bbbeb6173ae56bc42dc4d49016d GIT binary patch literal 7647 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 x>rV@Vvv}xhAx&>Rf~{VAy#7be*ktQu>b%7 literal 0 HcmV?d00001 diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.class new file mode 100644 index 0000000000000000000000000000000000000000..59c16b24da0913fc7592f6af773a0f7a78ba3a0d GIT binary patch literal 387 zcmb`D%}N6?6ot>7pH3aCEpEGVr!LF`C_)iSg+awi_n9=>)G-5TrqIW7CAjbbe5m40 z{lmQpoSX05#mznM>(@5`7wF}P5NAj_NYNtn=5i@TCGCT_F6UZ#LhIC6ICb^O=n_b+!LD9eD z2RkF<3?KXf{wT+DcQ>02q=1ghuDc9Zj#Bcb zhDl5@WTX?=U}(%%{{Jvb#f#o5{I@jR!Ck^{aIdlM)G4kPH-leRa0LzbF-=9Leu^V(*!K|N?%v^=NYyK<B-VFKGKh`22sJYG`2K6$2P;g&c1hp zVKjnJLPXlI)wUB1uj1J-?PKRq7=hv@b($r-2NlOTY#o&@?Vs?X8p0>StGVWpblmur zb&GpmoDpvG?_y?qpm(j*s32O|wuS4P6kZy;3^SdWdsu15kj6I*cQ03^S_J`Pkv=GW zG?7W<)R&Ryx9Kzlh6nV_cz6fz5@Q%_3>{WZeZ*qAvw?0grW>(x>MM3pSHGyMlMnP| z3w7_(J2Wzfc_i?#j0G5Fyr69zQw-}5Pm$FplcyMeMbbVj(f@wBsdDil{a*@hVHqFMiSGN^Qu3)V7m8Ams{JEPee;J+9vde!bT9`*=hk3SGTb zOJ?-BGu(UK{d}Mk6(obt@R%G>DNivvfDS|Woa&*B8gx=2rS#vCzAT_rXz_JpFy5&d z862UUT?82ZHvmzo!Z{;=h(*66dje$t13)xz9RQso$oB*ozb?Qvr?i7PM`VvCPGW_s z(}z{8(fb#gR<(^-Y)M4jtyWK55M@hEUk`!q4`U#>&cawH2XbWSOIiRKJi$|ZflZY0 OHSq_Dzk@yOWAY!B{=U-y literal 0 HcmV?d00001 diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/AnalyticsRecordResponse.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/AnalyticsRecordResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..5d8c7ed1bc6e6973fe03829678f7089bea2031b7 GIT binary patch literal 2386 zcmcIlUvC>l5dUpr`yBT|V#i4^l-866>`U7t{D%uIX+m35w^VH!3Fynl-qx2mcjj&# zDH0!v2P$D9<$({theFKmo}II5BERHeZ+2(r&u?aD=KlHj^lt#)<52-Kn6;3xkwuPS z@qiz5x66Z9?o-hUqc%fs(+_-kn_;$A-1`%g~KP+zW#^C2207CVYlD=^u$$@}nNZQd2`x9Jdh=GmxG_ z@s^DRERw7^7J=M7=?R9Ck=5{@$;c00Y0;96B_(=Ei#{W$Eul;-+gQ;iViCuF7&O|7 z`?ie=-eIt0#9N|48CW!C`_k{a&!0DTyb|T^vW@p}g&{9PJ zkP=N^e0t8=gVy&LGA4x!O+OHi`$zjC+U5IQT~%R=clUVYt8>E4$c|4L*t?h)rax_8+ESP86V!L8NH+d7pNC$&C@E< zD$`nB$YHC1yV$m{W78bF$8hy*bVdC@M`eOdywj-I9|B@{oz9mZ9EXR(HG&U#(C&)( zdNT|U`#ta6a!M4{^w|+{EAo3XjM6IGT^`42MpC4|q|8q6KGyQ1C$k$tAR=v2#1`n) z*Un;|U}YGCg@+7R&KIS3GXZ0RX5b8Udln@%rPO=3j98m`Wik?QnX1_1(J8xPw~P3>8}`>Q7ZC;`V#`6Ld;Sl z5utw?K20zwY$~Lz^CztH0?H;98D(ggkF@gqbjnK;m5b9UuS`@fPp4eLXA@#xoklr> z&nZPS_yS)Nty0055;p~I%G(sTDebRFs!YzlgGFueH9TzMTPo5!xKB3)x_N*f&_v}w D@@pX$ literal 0 HcmV?d00001 diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/SummaryResponse.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/dto/response/SummaryResponse.class new file mode 100644 index 0000000000000000000000000000000000000000..0de7d50ca7e2343edb7ac58288aefcbdbc5f9376 GIT binary patch literal 2220 zcmb_dZFAE`5PmLpEXOD~HgPB|O`rv8ix)vDEv?efBtUV-yc9F}(0)4GM=T=ijC9J3 z|5H0tW}4|tKJ*9lM|HX<$%z~clMne|-QC{qKF{v$uK)e-;2!{w&@5pFIRkkUvnVhu zyy1J?_PD=ezZ6{=^cV`OuJ5WfhFra|J%=LZ43tcqfyvPFg|hp+yE~Fm@H$daAnbLY zdn4s`!}S+Un~x8PH@l~#9f)u!{ZQELXfWWx=w*6lFqKr?OH@M!wx!|DnkZv|F2$bk zRk$&FH5v+rax119Dc7@~@}Z+ut0vBAT4y8QeH)3k2t${UsuXI?#1hUk6qRhN!1Z?+ zI`vbf?=%udaqrXBrUv@l#4=Xs!Nl8$dz8QKDZ^yq zd)|2?y2@$%$}rbQlO1Xd^OIX7L z13#Hq#|Fd7QNTguD|aC5B=8iBO0qM?XLy&*o$K$(U12ARPq^RnM0lko$HOoyNPYa4aXSTlVrU#bpV{<%5yUPTwpWzhL}9 zmmdmfHuXsViSUofUn8u+*AgYlQ78RZtUh8+@tj(Jzr8=la-a! f)wiUf>ur_Fq=@hEJ$`_LO_G*K`Vf!t1Q-4XAyN_; literal 0 HcmV?d00001 diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/models/AnalyticsRecord.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/models/AnalyticsRecord.class new file mode 100644 index 0000000000000000000000000000000000000000..55c8934d891c39130cb0590dd12b66f4a02f4d1b GIT binary patch literal 2002 zcmb`IS#KIK5XZ*^LV*n-TrEB6O*Y5&erT#lQJbVlw2g8-^~HdjYy#eB*F=ic57kOV zmHN;R&<|C0#>)bMmEfU5!uBxp^Y}kA;?Lh-zX4zqUL_y`GjWI-5QA9;TT&?Nm^V%@ zoR0gYqdKmzHY9IdD7)cp>{Klt9Ck&+ahq1#X^NIN@bS$I63T81Px1B{gIw)|pL2^W zR#l==sx`XziUA2oqOUj?Qte-y2?kl;x5ed=wWnNL9%)y@fH`taY1chWt|169Z$L&P zJmGn^BdblaW(~+eoydkPh1{ojQQFhBZIHkRd+pXw)~ zp%~1*v8Aon8O&4`4;dIoLhTRURH!V5yoy2eEegV5zGh4DvD2;#cc0f=`e9Ckw+^{$ z)46Ais$<(@@V@qcWM5-&W49wo=&C%oJsWS@kkV0Hp*DC?w$^gmb?4OD7E-uei6(~7 z%QgoU$OkVQt^tE|;N^?&hwTNbGf0v?f!x`EF%TRByh{oeD73VF5+;;aK)L#U4+I&uQ&sG#h%wIb~&yoz`Dat^t**83r`H!nE3c!;6(pJIGoJ7Z42r Date: Thu, 28 May 2026 20:37:02 +0200 Subject: [PATCH 3/7] Refactor analytics API: extract business logic to service layer, simplify controller, implement CRUD and summary endpoint --- .../AnalyticsRecordController.java | 131 +++++++++++------- .../AnalyticsRecordService.java | 81 +++++++++++ .../AnalyticsRecordController.class | Bin 7647 -> 10616 bytes 3 files changed, 163 insertions(+), 49 deletions(-) diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java index 5394025..a93ef80 100644 --- a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java @@ -1,50 +1,28 @@ package net.hackyourfuture.AnalyticsAPI.analyticsRecord; +import jakarta.validation.Valid; +import net.hackyourfuture.AnalyticsAPI.analyticsRecord.dto.request.AnalyticsRecordRequest; import net.hackyourfuture.AnalyticsAPI.analyticsRecord.dto.response.AnalyticsRecordResponse; +import net.hackyourfuture.AnalyticsAPI.analyticsRecord.dto.response.SummaryResponse; import net.hackyourfuture.AnalyticsAPI.analyticsRecord.models.AnalyticsRecord; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.net.URI; import java.time.Instant; -import java.util.ArrayList; import java.util.List; import java.util.UUID; @RestController @RequestMapping("/records") public class AnalyticsRecordController { - private final List analyticsRecords = new ArrayList<>(List.of( - new AnalyticsRecord( - Instant.parse("2024-05-28T10:15:30Z"), - "PAGE_VIEW", - "frontend", - "session12345", - UUID.fromString("11111111-1111-1111-1111-111111111111") - ), - new AnalyticsRecord( - Instant.parse("2024-05-28T13:16:10Z"), - "BUTTON_CLICK", - "frontend", - "session12345", - UUID.fromString("22222222-2222-2222-2222-222222222222") - ), - new AnalyticsRecord( - Instant.parse("2024-05-28T17:05:00Z"), - "LOGIN_ATTEMPT", - "mobile", - "mobileSession98765", - UUID.fromString("33333333-3333-3333-3333-333333333333") - ), - new AnalyticsRecord( - Instant.parse("2024-05-28T21:30:45Z"), - "NAVIGATION", - "frontend", - "session55555", - UUID.fromString("44444444-4444-4444-4444-444444444444") - ) - - )); + +private final AnalyticsRecordService service; + +public AnalyticsRecordController(AnalyticsRecordService service){ + this.service = service; +} @GetMapping public ResponseEntity> getAllAnalyticsRecords( @@ -53,7 +31,7 @@ public ResponseEntity> getAllAnalyticsRecords( @RequestParam(required=false) String eventType, @RequestParam(required=false) String eventSource, @RequestParam(required=false) String sessionId){ -List filteredAnalyticsReports = analyticsRecords.stream() +List filteredAnalyticsReports = service.getAll().stream() .filter(record -> timeStart == null || record.getTimestamp().compareTo(timeStart) >= 0) .filter(record -> timeEnd == null || record.getTimestamp().compareTo(timeEnd) <=0) .filter(record -> eventType == null || record.getEventType().equalsIgnoreCase(eventType)) @@ -73,9 +51,7 @@ public ResponseEntity> getAllAnalyticsRecords( @GetMapping ("/{traceId}") public ResponseEntity getAnalyticsRecordByTraceId(@PathVariable UUID traceId){ - return analyticsRecords.stream() - .filter(record -> traceId.equals(record.getTraceId())) - .findFirst() + return service.findByTraceId(traceId) .map(record -> new AnalyticsRecordResponse( record.getTimestamp(), record.getEventType(), @@ -87,21 +63,78 @@ public ResponseEntity getAnalyticsRecordByTraceId(@Path .orElse(ResponseEntity.notFound().build()); } + @PostMapping + public ResponseEntity createAnalyticsRecord(@Valid @RequestBody AnalyticsRecordRequest record){ + + UUID newTraceId = UUID.randomUUID(); + + AnalyticsRecord newRecord = new AnalyticsRecord( + record.timestamp(), + record.eventType(), + record.eventSource(), + record.sessionId(), + newTraceId + ); + + service.add(newRecord); + + URI location = URI.create("records/" + newTraceId); + + return ResponseEntity.created(location) + .body( + new AnalyticsRecordResponse( + newRecord.getTimestamp(), + newRecord.getEventType(), + newRecord.getEventSource(), + newRecord.getSessionId(), + newRecord.getTraceId() + ) + ); + } + + @PutMapping("/{traceId}") + public ResponseEntity replaceAnalyticsReport( @PathVariable UUID traceId, @Valid @RequestBody AnalyticsRecordRequest updatedRecord){ + AnalyticsRecord newRecord = new AnalyticsRecord( + updatedRecord.timestamp(), + updatedRecord.eventType(), + updatedRecord.eventSource(), + updatedRecord.sessionId(), + traceId + ); + + AnalyticsRecord replaced = service.replace(traceId, newRecord); + + if(replaced == null) { + return ResponseEntity.notFound().build(); + } + + return ResponseEntity.ok( + new AnalyticsRecordResponse( + replaced.getTimestamp(), + replaced.getEventType(), + replaced.getEventSource(), + replaced.getSessionId(), + replaced.getTraceId() + ) + ); + }; + + + @DeleteMapping ("/{traceId}") + public ResponseEntity deleteAnalyticsReport(@PathVariable UUID traceId){ + + return service.delete(traceId) + ? ResponseEntity.noContent().build() + : ResponseEntity.notFound().build(); + } + + @GetMapping("/summary") + public ResponseEntity getSummary(){ + return ResponseEntity.ok(service.getSummary()); + } + } -//POST -//POST create record -//PUT -//PUT replace record -///records/{traceId} -// -//DELETE -//DELETE record -///records/{traceId} -// -//GET summary -//GET summary -///records/summary diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.java index 9c601bc..ab5a59c 100644 --- a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.java +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordService.java @@ -1,4 +1,85 @@ package net.hackyourfuture.AnalyticsAPI.analyticsRecord; +import net.hackyourfuture.AnalyticsAPI.analyticsRecord.dto.response.SummaryResponse; +import net.hackyourfuture.AnalyticsAPI.analyticsRecord.models.AnalyticsRecord; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.*; +import java.util.stream.Collectors; + +@Service public class AnalyticsRecordService { + private final List analyticsRecords = new ArrayList<>(List.of( + new AnalyticsRecord( + Instant.parse("2024-05-28T10:15:30Z"), + "PAGE_VIEW", + "frontend", + "session12345", + UUID.fromString("11111111-1111-1111-1111-111111111111") + ), + new AnalyticsRecord( + Instant.parse("2024-05-28T13:16:10Z"), + "BUTTON_CLICK", + "frontend", + "session12345", + UUID.fromString("22222222-2222-2222-2222-222222222222") + ), + new AnalyticsRecord( + Instant.parse("2024-05-28T17:05:00Z"), + "LOGIN_ATTEMPT", + "mobile", + "mobileSession98765", + UUID.fromString("33333333-3333-3333-3333-333333333333") + ), + new AnalyticsRecord( + Instant.parse("2024-05-28T21:30:45Z"), + "NAVIGATION", + "frontend", + "session55555", + UUID.fromString("44444444-4444-4444-4444-444444444444") + ) + + )); + + public List getAll(){ + return analyticsRecords; + } + + public Optional findByTraceId(UUID traceId){ + return analyticsRecords.stream() + .filter(record -> record.getTraceId().equals(traceId)) + .findFirst(); + } + + public AnalyticsRecord add(AnalyticsRecord record){ + analyticsRecords.add(record); + return record; + } + + public AnalyticsRecord replace(UUID traceId, AnalyticsRecord newReport){ + for(int i=0; i < analyticsRecords.size(); i++){ + if(analyticsRecords.get(i).getTraceId().equals(traceId)){ + analyticsRecords.set(i, newReport); + return newReport; + } + } + return null; + } + + public boolean delete(UUID traceId){ + return analyticsRecords.removeIf(report -> report.getTraceId().equals(traceId)); + } + + public SummaryResponse getSummary(){ + long totalRecords = analyticsRecords.size(); + + Map eventsByType = analyticsRecords.stream(). + collect(Collectors.groupingBy(AnalyticsRecord::getEventType, Collectors.counting())); + + + long uniqueSessions = analyticsRecords.stream().map(AnalyticsRecord::getSessionId).distinct().count(); + + return new SummaryResponse(totalRecords, eventsByType, uniqueSessions); + } } diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.class index 6c1c63e91eb72bbbeb6173ae56bc42dc4d49016d..ccd469aca8930dfee5876bd3f694d3355e357149 100644 GIT binary patch delta 3796 zcmb7H3v^Uh72RjPnaRta2~8j=q#=X=9VlND5=ck_;nUD05Cd3v$vj}lFcW4b(Xgyj zR*Of-wSIj|qaeR>#+se_ zfbQ>)2Sa}4tMTFxOgBEP;UoB{pv|0eOrsb)e|-JxFIMbTA8A6?AjDuYIcnqC(RQXn-aqsMiimib|c;q z+(jn!U432#eMZBV6bovgNfFdB*hhLicvQn4I!d8Y20o)|kr;OORjmALO|_&$C>M=ZF9e9BWlF@s9MsvLG5Oq&6(y9 zoPP^3?~?`Q-YlQFGJC?Bb&*(nt=`wC?unqJtv?*60yhL>!S0Zu~?ccUc;Ltxq(1D zxCKOQW*F}Z#^MZFAVD@>ysZ8{hGVeisLI2BdtC47<;gm99z^iMo8N7pd0lpucU}J| zFXr6br_A}e}SO=eiVM%8Q5WV%%5;sA$NDo2YZ6>?3kTtmkity_IW}LU!=TgOPr-CV!feg}KF7H6=sVv)(2J8uJSQI=%9C>2k|P zO*YBqp}T!HsiFHuuWXU6ZqZpOrCU&LRW_@91;Yc8UL#c$lf|=H?}M@zt0&&v`bYY& zMJB%ma9{#*P>5pwRGnJUUW!+25NgNmcMJyNoO|O>V(xmG@biG8E&q+iO3w#Q}*AIN@fFvjX#mlr6 zBNzGm6}H7rcK*lq>PEcw)=XXuaact37qwVKlv;;tB@ydL-3=&38_DUy3~a`oDNek)c&t5~x+bdei5H$h_JvSN0tofVBD8RFq_E&vI#2rwtHrs!qn@l9!Asb5q!di&RN6w)M9cS=J*X)xx z#dezQEZaG@ubjfys+~oSVSK&FIf8H5_&h6hsl3gX+E}!S2B5M2R4gFW^6=SE?JUIK`ca- zI$U$OG;wOjErih+Jq7(ew$##V8o|$C2+G(v) zr2Wi}PDO8#ll-dBY4tg)KIhcO;@DN>8ba^gWoz%rC;tNYr328nKV6dmlQnADeL-biALL_yBrw05Kfo@ZQ#QGpf$<{RU!i>w123nN z;;|_)ODRW_d6_aQXqkFR(CPe8)1&#xv!(Q%&Qh*dm1C-cO2>`Nkt#@&6{l6ClEmNck4>8aus zQaOK+ouvF#83Av)0w_!*XIbR1508Z<#NspaMuA@;?s~@Ez_^z)?ncJl#JHOocm3<) z&PoQ(M^0^KK0c{noucB|!*Dx}!(mrpCzX^8ds#AUD{3DrxguDRse~V_&9D zz;`qasyP1Dc%Vx!168Gad{92sY0%Se1X_0)sCpU32jyP_X;3AL-y9w1__ddTs)uKM zQ2y~T3YxG$kBkGlXbQOYSY%Tw6#2CHOmTFm0G+y?YlnZYnB_NP2R!nI;4ji6;m3wA|wHqhyQeh O#Bh0Jo9vLCDEdFq%16@x delta 1983 zcmbVMYfx2H6#n*ooa;Gk?!7!Npn@oq3{Vr(k0ul$dQi|{7#C{Lrn=$DQ08KAamHWA zX8M_xonEH)QnLpZb_+}M(F=9wkr`{YYxJUkj@u+|#~q1WtsT{8 zSUT>KSgmq?uB%;UMq}a5o~}02fQ~g1Ye$!d&F=2@&S?EYHF1x`y|_=nP}p6OHnV=A zKxRp4!{W%YNO@1Jy`#LPrT+X%11nKvARp@`HejQGW?t75>F5>+l{jnd9K&Bc-@qoU z_u(Odpaakmi7qM+$GY01i`3L35|5(SJ|fGt$0Qy{U%W8=urPE}scl}q)U-$Zxp`?$ zm=G_ZcaQ3gX9ot{c4OY4OY5?aW;M}0C0;ezEBxuJu-Ekg&$PzM~P1`#1%iQ=og7YILPQX75y&pu@n8FqCX`*#K9nL zVr#m$1()oUZvqmcUGVe?hLjqA(~z_Nv0FtSvkkE<;XN+wpeM^ElpadS2pacn%kFe4bfp@z+G^1*TqPD&!?1hz0DR?V|0b zC2H~#2I#Ab?BVxiT7{RvE40D+c=bQJui!RtDjSQX!{pHbmL7&vrjPb7Q1Mu^Fu!l zSh#}Zs!#dbv9h(SA2-u%+MTq!Y3sJ){yxsTpn8^&+DQ5H*u6`TjwW_Hj1ap&m9kIt zJso1B`1PwdG!)}-K=HGx7s8{;Uf@%`Y7@hRtA_{_OOne;wq z`~_JP_vlM}#rSgQLTAUn#u0p*^u8bSej4+B9rKPQy Date: Thu, 28 May 2026 21:01:11 +0200 Subject: [PATCH 4/7] Implement GlobalExceptionHandler for validation errors and fallback exception handling --- .../exception/GlobalExceptionHandler.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.java diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..871f8e2 --- /dev/null +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.java @@ -0,0 +1,31 @@ +package net.hackyourfuture.AnalyticsAPI.shared.exception; + +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.HashMap; +import java.util.Map; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationErrors(MethodArgumentNotValidException ex) { + Map errors = new HashMap<>(); + + for (FieldError error : ex.getBindingResult().getFieldErrors()) { + errors.put(error.getField(), error.getDefaultMessage()); + } + + return ResponseEntity.badRequest().body(errors); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGeneralErrors(Exception ex) { + return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage())); + } + +} \ No newline at end of file From e04c7adc6f97dbd7fb231a3ad280b46acb9adf40 Mon Sep 17 00:00:00 2001 From: Yana Pechenenko Date: Thu, 28 May 2026 21:41:26 +0200 Subject: [PATCH 5/7] Add unit tests for AnalyticsRecordService covering CRUD and summary logic --- .../AnalyticsApiApplicationTests.java | 13 -- .../AnalyticsRecordServiceTest.java | 111 ++++++++++++++++++ .../AnalyticsRecordController.class | Bin 10616 -> 10347 bytes .../AnalyticsRecordService.class | Bin 387 -> 5829 bytes .../exception/GlobalExceptionHandler.class | Bin 0 -> 2818 bytes .../AnalyticsRecordServiceTest.class | Bin 0 -> 4173 bytes 6 files changed, 111 insertions(+), 13 deletions(-) delete mode 100644 task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplicationTests.java create mode 100644 task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordServiceTest.java create mode 100644 task-1/target/classes/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.class create mode 100644 task-1/target/test-classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordServiceTest.class diff --git a/task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplicationTests.java b/task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplicationTests.java deleted file mode 100644 index 0af358f..0000000 --- a/task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/AnalyticsApiApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.hackyourfuture.AnalyticsAPI; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class AnalyticsApiApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordServiceTest.java b/task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordServiceTest.java new file mode 100644 index 0000000..5f9a7c3 --- /dev/null +++ b/task-1/src/test/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordServiceTest.java @@ -0,0 +1,111 @@ +package net.hackyourfuture.AnalyticsAPI.analyticsRecord; + +import net.hackyourfuture.AnalyticsAPI.analyticsRecord.dto.response.SummaryResponse; +import net.hackyourfuture.AnalyticsAPI.analyticsRecord.models.AnalyticsRecord; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +class AnalyticsRecordServiceTest { + private AnalyticsRecordService service; + + @BeforeEach + void setUp(){ + service = new AnalyticsRecordService(); + } + + private AnalyticsRecord createTestRecord(UUID traceId) { + return new AnalyticsRecord( + Instant.now(), + "TEST_EVENT", + "backend", + "session123", + traceId + ); + } + + + @Test + void testAddRecord() { + AnalyticsRecord record = createTestRecord(UUID.randomUUID()); + + service.add(record); + + assertTrue(service.getAll().contains(record)); + } + + @Test + void testFindRecordByTraceId() { + UUID traceId = UUID.randomUUID(); + AnalyticsRecord record = createTestRecord(traceId); + + service.add(record); + + Optional foundedRecord = service.findByTraceId(traceId); + assertTrue(foundedRecord.isPresent()); + assertEquals("TEST_EVENT", foundedRecord.get().getEventType()); + } + + + + @Test + void testReplaceRecord() { + UUID traceId = UUID.randomUUID(); + + AnalyticsRecord original = createTestRecord(traceId); + + service.add(original); + + AnalyticsRecord updated = new AnalyticsRecord( + Instant.now(), + "NEW_EVENT", + "backend", + "session2", + traceId + ); + + AnalyticsRecord result = service.replace(traceId, updated); + + assertNotNull(result); + assertEquals("NEW_EVENT", result.getEventType()); + assertEquals("backend", result.getEventSource()); + + } + + @Test + void testReplaceRecordNotFound() { + AnalyticsRecord updated = createTestRecord(UUID.randomUUID()); + + AnalyticsRecord result = service.replace(UUID.randomUUID(), updated); + + assertNull(result); + } + + @Test + void testDeleteRecord() { + UUID traceId = UUID.randomUUID(); + + AnalyticsRecord record = createTestRecord(traceId); + + service.add(record); + + boolean deleted = service.delete(traceId); + + assertTrue(deleted); + assertFalse(service.findByTraceId(traceId).isPresent()); + } + + @Test + void getSummary() { + SummaryResponse summary = service.getSummary(); + + assertTrue(summary.totalRecords() >= 0); + assertNotNull(summary.eventsByType()); + assertTrue(summary.uniqueSessions() >= 0); + } +} \ No newline at end of file diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.class index ccd469aca8930dfee5876bd3f694d3355e357149..aed665e408158e3688340dbe11d4a66949871609 100644 GIT binary patch literal 10347 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 literal 10616 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 diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.class new file mode 100644 index 0000000000000000000000000000000000000000..531b4f9ef6d9e79e7d5ceb85ce67b3e3ac06a06a GIT binary patch literal 2818 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 Date: Thu, 28 May 2026 21:57:57 +0200 Subject: [PATCH 6/7] Finish API_DESIGN --- task-1/API_DESIGN.md | 188 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) diff --git a/task-1/API_DESIGN.md b/task-1/API_DESIGN.md index e69de29..3f24fa5 100644 --- a/task-1/API_DESIGN.md +++ b/task-1/API_DESIGN.md @@ -0,0 +1,188 @@ +# API_DESIGN.md + +# Analytics API — Design Document + +This document describes the design of the Analytics API, including resources, supported actions, request bodies, response bodies, status codes, validation rules, and error responses. + +--- + +## 1. Resource Model + +AnalyticsRecord contains: +- timestamp (Instant) +- eventType (String) +- eventSource (String) +- sessionId (String) +- traceId (UUID, generated by the server) + +--- + +## 2. Endpoints + +Base path: /records + +--- + +## 2.1 Create a new analytics record +POST /records + +Request body: +{ +"timestamp": "2024-01-01T12:00:00Z", +"eventType": "CLICK", +"eventSource": "frontend", +"sessionId": "abc123xyz" +} + +Validation rules: +- timestamp: required, not null +- eventType: required, not blank +- eventSource: required, not blank +- sessionId: required, 6–64 characters + +Responses: +201 Created +Body: +{ +"timestamp": "...", +"eventType": "...", +"eventSource": "...", +"sessionId": "...", +"traceId": "generated-uuid" +} +Header: Location: /records/{traceId} + +400 Bad Request (validation errors): +{ +"eventType": "Event type is required.", +"sessionId": "Session id must be between 6 and 64 symbols." +} + +--- + +## 2.2 List analytics records +GET /records + +Optional filters: +- timeStart (Instant) +- timeEnd (Instant) +- eventType (String) +- eventSource (String) +- sessionId (String) + +Response 200 OK: +[ +{ +"timestamp": "...", +"eventType": "...", +"eventSource": "...", +"sessionId": "...", +"traceId": "..." +} +] + +--- + +## 2.3 Fetch a single record +GET /records/{traceId} + +Responses: +200 OK: +{ +"timestamp": "...", +"eventType": "...", +"eventSource": "...", +"sessionId": "...", +"traceId": "..." +} + +404 Not Found: +{} + +--- + +## 2.4 Replace a record +PUT /records/{traceId} + +Request body: same as POST. + +Responses: +200 OK: +{ +"timestamp": "...", +"eventType": "...", +"eventSource": "...", +"sessionId": "...", +"traceId": "..." +} + +404 Not Found: +{} + +400 Bad Request (validation errors): +{ +"eventType": "Event type is required.", +"sessionId": "Session id must be between 6 and 64 symbols." +} + +--- + +## 2.5 Delete a record +DELETE /records/{traceId} + +Responses: +204 No Content +(no body) + +404 Not Found: +{} + +--- + +## 2.6 Summary +GET /records/summary + +Response 200 OK: +{ +"totalRecords": 42, +"eventsByType": { +"CLICK": 20, +"LOGIN": 22 +}, +"uniqueSessions": 12 +} + +--- + +## 3. Error Handling + +Validation errors → 400 Bad Request: +{ +"timestamp": "Timestamp is required.", +"eventType": "Event type is required." +} + +General errors → 400 Bad Request: +{ +"error": "Some error message" +} + +--- + +## 4. Status Code Summary + +POST /records → 201 or 400 +GET /records → 200 +GET /records/{id} → 200 or 404 +PUT /records/{id} → 200 or 400 or 404 +DELETE /records/{id} → 204 or 404 +GET /records/summary → 200 + +--- + +## 5. Notes + +- Controllers are thin and delegate logic to the service layer. +- Validation uses Bean Validation annotations. +- Errors are returned as structured JSON via GlobalExceptionHandler. + From 49c57297f9305f3f25bb0945a1c8b64787d907e1 Mon Sep 17 00:00:00 2001 From: Yana Pechenenko Date: Thu, 28 May 2026 23:45:11 +0200 Subject: [PATCH 7/7] finish with structure --- .../AnalyticsRecordController.java | 2 ++ .../exception/GlobalExceptionHandler.java | 6 +++--- .../AnalyticsRecordController.class | Bin 10347 -> 10347 bytes .../exception/GlobalExceptionHandler.class | Bin 2818 -> 2822 bytes 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java index a93ef80..19cb391 100644 --- a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.java @@ -14,8 +14,10 @@ import java.util.List; import java.util.UUID; + @RestController @RequestMapping("/records") + public class AnalyticsRecordController { private final AnalyticsRecordService service; diff --git a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.java b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.java index 871f8e2..f54b59b 100644 --- a/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.java +++ b/task-1/src/main/java/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.java @@ -3,13 +3,13 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.HashMap; import java.util.Map; -@ControllerAdvice +@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) @@ -28,4 +28,4 @@ public ResponseEntity> handleGeneralErrors(Exception ex) { return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage())); } -} \ No newline at end of file +} diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/analyticsRecord/AnalyticsRecordController.class index aed665e408158e3688340dbe11d4a66949871609..ab85ecba980e7fbf064e0241a52252328e654855 100644 GIT binary patch delta 330 zcmWO0J1>J_7>DueO&A)fq!Q)T(6rvZJ)n)l(4q|nb!aJ7Q58d-202JjFtso+bkh8^7Cae%y+=(=0|S=1a2`eftM4q& delta 328 zcmWO0y-Pw-7{~GNg&HIj46H6uqF(RS-lPa$DocXOO42ed3xceMD3IP(Yl)_&^Cz@4 z2K^J!($-Ya?%L4Uk24*<&-t9EQ|VM*+_0~oE(Y*nn^gEY5~Y_ILpUUeGmT3Dk2T%O zq^L9b`WLpXU>ZLeLd@&wf`-y28u`dCiCPg=R9z*gXCc;wl{K*`tg87si|nw>u4;n~ z_Sn{GP&E_L-`pYHG$kBKIF@jN&8e0-Bgr{aw8(Nno=YiL`s`K?n;vfI zsX3`LQfIZ-9D`)cLA+`wRFkSBs-vo7s^h8&^BymmFE?Nu bw`LZ+fO&9hR@{8MP0KL_uW7~1llS%yZ%#Vl diff --git a/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.class b/task-1/target/classes/net/hackyourfuture/AnalyticsAPI/shared/exception/GlobalExceptionHandler.class index 531b4f9ef6d9e79e7d5ceb85ce67b3e3ac06a06a..eff973794088070081ef7df89ad1225c2789fcd0 100644 GIT binary patch delta 34 pcmZn?YZKdWgoDv;vLUM+e_~!=64(-OaRA^3#|YE delta 30 lcmZn@YZBXVgoDv)vLUM+cVb>%eo10UW`3Uj=1&|VOaPhW3JU-L