commit 710bd5b0f4c4cd97aad465507bd99ffd72bb93fb Author: Stranni15k Date: Tue Dec 17 00:08:36 2024 +0400 Первый коммит diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..de090a5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: '3.8' + +services: + db: + image: postgres:latest + container_name: parking_db + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: parking + ports: + - "5432:5432" + volumes: + - db_data:/var/lib/postgresql/data + +volumes: + db_data: diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..19529dd --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/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.2 +# +# 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:]' +} + +# 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 <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.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${0##*/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 +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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.2 +@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) { "/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_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::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 +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -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/pom.xml b/pom.xml new file mode 100644 index 0000000..2fc77b8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.4 + + + com.example + demo + 0.0.1-SNAPSHOT + demo + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.postgresql + postgresql + runtime + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + com.zaxxer + HikariCP + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + + + org.springframework.boot + spring-boot-starter-mail + + + + org.springframework.boot + spring-boot-starter-security + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + 21 + + + + + + diff --git a/src/main/java/com/example/demo/DemoApplication.java b/src/main/java/com/example/demo/DemoApplication.java new file mode 100644 index 0000000..1ce6f9a --- /dev/null +++ b/src/main/java/com/example/demo/DemoApplication.java @@ -0,0 +1,15 @@ +package com.example.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; + +@SpringBootApplication +@EnableScheduling +public class DemoApplication { + + public static void main(String[] args) { + SpringApplication.run(DemoApplication.class, args); + } + +} diff --git a/src/main/java/com/example/demo/config/CorsConfig.java b/src/main/java/com/example/demo/config/CorsConfig.java new file mode 100644 index 0000000..d76b96b --- /dev/null +++ b/src/main/java/com/example/demo/config/CorsConfig.java @@ -0,0 +1,26 @@ +package com.example.demo.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; + +@Configuration +public class CorsConfig { + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("http://localhost:3000")); // Укажите URL фронтенда + configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowCredentials(true); + configuration.setAllowedHeaders(List.of("Authorization", "Cache-Control", "Content-Type")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/src/main/java/com/example/demo/config/DataLoader.java b/src/main/java/com/example/demo/config/DataLoader.java new file mode 100644 index 0000000..a4718f5 --- /dev/null +++ b/src/main/java/com/example/demo/config/DataLoader.java @@ -0,0 +1,37 @@ +package com.example.demo.config; + +import com.example.demo.models.UserEntity; +import com.example.demo.repositories.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.CommandLineRunner; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +public class DataLoader implements CommandLineRunner { + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Value("${admin.password}") + private String adminPassword; + + @Override + public void run(String... args) throws Exception { + if (userRepository.findByUsername("admin") == null) { + if (adminPassword == null || adminPassword.isEmpty()) { + throw new IllegalStateException("Admin password is not set in properties!"); + } + + UserEntity admin = new UserEntity(); + admin.setUsername("admin"); + admin.setPassword(passwordEncoder.encode(adminPassword)); // Зашифрованный пароль + admin.setRoles("ADMIN"); + userRepository.save(admin); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/config/SecurityConfiguration.java b/src/main/java/com/example/demo/config/SecurityConfiguration.java new file mode 100644 index 0000000..eed30e5 --- /dev/null +++ b/src/main/java/com/example/demo/config/SecurityConfiguration.java @@ -0,0 +1,55 @@ +package com.example.demo.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.web.cors.CorsConfigurationSource; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) // Включаем поддержку аннотаций @PreAuthorize +public class SecurityConfiguration { + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { + return authenticationConfiguration.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http, CorsConfigurationSource corsConfigurationSource) throws Exception { + http + .securityContext(securityContext -> securityContext + .securityContextRepository(new HttpSessionSecurityContextRepository()) + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) + ) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/admin/**").hasAuthority("ROLE_ADMIN") // Настройка доступа к админским маршрутам + .anyRequest().permitAll() + ) + .logout(logout -> logout.permitAll()) + .csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource)); // Подключение CORS конфигурации + + return http.build(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/config/SecurityContextConfig.java b/src/main/java/com/example/demo/config/SecurityContextConfig.java new file mode 100644 index 0000000..a9be85c --- /dev/null +++ b/src/main/java/com/example/demo/config/SecurityContextConfig.java @@ -0,0 +1,15 @@ +package com.example.demo.config; + +import jakarta.annotation.PostConstruct; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.core.context.SecurityContextHolder; + +@Configuration +public class SecurityContextConfig { + + @PostConstruct + public void init() { + SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL ); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/config/SwaggerConfig.java b/src/main/java/com/example/demo/config/SwaggerConfig.java new file mode 100644 index 0000000..11f9d7c --- /dev/null +++ b/src/main/java/com/example/demo/config/SwaggerConfig.java @@ -0,0 +1,19 @@ +package com.example.demo.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("Parking Management API") + .version("1.0") + .description("API для управления парковками и прогнозирования состояний парковок")); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/config/WebConfig.java b/src/main/java/com/example/demo/config/WebConfig.java new file mode 100644 index 0000000..f03f11a --- /dev/null +++ b/src/main/java/com/example/demo/config/WebConfig.java @@ -0,0 +1,17 @@ +//package com.example.demo.config; +// +//import org.springframework.context.annotation.Configuration; +//import org.springframework.web.servlet.config.annotation.CorsRegistry; +//import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +// +//@Configuration +//public class WebConfig implements WebMvcConfigurer { +// +// @Override +// public void addCorsMappings(CorsRegistry registry) { +// registry.addMapping("/**") // Разрешить все запросы к любым конечным точкам +// .allowedOriginPatterns("http://localhost:3000") // Замените на точный URL фронтенда// Разрешить запросы с любого источника +// .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") +// .allowCredentials(true); // Поддержка куки +// } +//} diff --git a/src/main/java/com/example/demo/controllers/AuthController.java b/src/main/java/com/example/demo/controllers/AuthController.java new file mode 100644 index 0000000..2a7155f --- /dev/null +++ b/src/main/java/com/example/demo/controllers/AuthController.java @@ -0,0 +1,110 @@ +package com.example.demo.controllers; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpSession; + +import com.example.demo.dtos.AuthRequest; +import com.example.demo.dtos.ChangePasswordRequest; +import com.example.demo.models.UserEntity; +import com.example.demo.repositories.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + @Autowired + private UserRepository userRepository; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private AuthenticationManager authenticationManager; + + @PostMapping("/register") + public String registerUser(@RequestBody UserEntity userEntity) { + if (userRepository.findByUsername(userEntity.getUsername()) != null) { + return "Пользователь с таким именем уже существует!"; + } + userEntity.setPassword(passwordEncoder.encode(userEntity.getPassword())); // Шифрование пароля + userEntity.setRoles("ADMIN"); // Установка роли по умолчанию + userRepository.save(userEntity); + return "Регистрация успешна!"; + } + + @PostMapping("/login") + public ResponseEntity loginUser(@RequestBody AuthRequest authRequest, HttpServletRequest request) { + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword()) + ); + + // Устанавливаем аутентификацию в SecurityContext + SecurityContextHolder.getContext().setAuthentication(authentication); + + // Явно создаем и сохраняем сессию + HttpSession session = request.getSession(true); + session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext()); + + return ResponseEntity.ok("Вход успешен для пользователя: " + authRequest.getUsername()); + } catch (AuthenticationException e) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Ошибка аутентификации: " + e.getMessage()); + } + } + + @GetMapping("/auth-status") + public ResponseEntity authStatus() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication != null && authentication.isAuthenticated() && !"anonymousUser".equals(authentication.getName())) { + return ResponseEntity.ok("Пользователь аутентифицирован: " + authentication.getName()); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Пользователь не аутентифицирован"); + } + } + + @PostMapping("/change-password") + public ResponseEntity changePassword(@RequestBody ChangePasswordRequest changePasswordRequest) { + // Получаем текущего аутентифицированного пользователя + Authentication currentAuthentication = SecurityContextHolder.getContext().getAuthentication(); + String currentUsername = currentAuthentication.getName(); + + UserEntity userEntity = userRepository.findByUsername(currentUsername); + if (userEntity == null) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Пользователь не найден."); + } + + // Проверяем старый пароль + if (!passwordEncoder.matches(changePasswordRequest.getOldPassword(), userEntity.getPassword())) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Старый пароль введен неверно."); + } + + // Устанавливаем и сохраняем новый зашифрованный пароль + userEntity.setPassword(passwordEncoder.encode(changePasswordRequest.getNewPassword())); + userRepository.save(userEntity); + + // Обновляем Authentication в SecurityContext, чтобы не сбрасывать сессию + Authentication newAuthentication = new UsernamePasswordAuthenticationToken( + userEntity.getUsername(), + changePasswordRequest.getNewPassword(), + currentAuthentication.getAuthorities() + ); + SecurityContextHolder.getContext().setAuthentication(newAuthentication); + + return ResponseEntity.ok("Пароль успешно изменен."); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/controllers/CityController.java b/src/main/java/com/example/demo/controllers/CityController.java new file mode 100644 index 0000000..7c24897 --- /dev/null +++ b/src/main/java/com/example/demo/controllers/CityController.java @@ -0,0 +1,28 @@ +package com.example.demo.controllers; + +import java.util.List; + +import com.example.demo.dtos.CityDTO; +import com.example.demo.models.City; +import com.example.demo.services.CityService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/cities") +public class CityController { + + private final CityService cityService; + + public CityController(CityService cityService) { + this.cityService = cityService; + } + + @GetMapping + public ResponseEntity> getAllCities() { + List cities = cityService.getAllCities(); + return ResponseEntity.ok(cities); + } +} diff --git a/src/main/java/com/example/demo/controllers/ParkingCompanyController.java b/src/main/java/com/example/demo/controllers/ParkingCompanyController.java new file mode 100644 index 0000000..78ce6ed --- /dev/null +++ b/src/main/java/com/example/demo/controllers/ParkingCompanyController.java @@ -0,0 +1,46 @@ +package com.example.demo.controllers; + +import java.util.List; + +import com.example.demo.dtos.ParkingCompanyRequest; +import com.example.demo.services.ParkingCompanyService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin/company") +public class ParkingCompanyController { + + @Autowired + private ParkingCompanyService parkingCompanyService; + + + //получение списка компаний + @GetMapping("/list") + public List getAllParkingCompanys() { + SecurityContextHolder.getContext().getAuthentication(); + return parkingCompanyService.findAllForRequest(); + } + + // Добавление новой компании + + @PostMapping("/add") + public ParkingCompanyRequest addParkingCompany(@RequestBody ParkingCompanyRequest parkingCompany) { + return parkingCompanyService.saveAsRequest(parkingCompany); + } + + // Удаление компании по ID + @DeleteMapping("/delete/{id}") + public String deleteParkingCompany(@PathVariable Long id) { + parkingCompanyService.deleteById(id); + return "Компания с ID " + id + " удалена."; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/controllers/ParkingForecastController.java b/src/main/java/com/example/demo/controllers/ParkingForecastController.java new file mode 100644 index 0000000..2b56ecb --- /dev/null +++ b/src/main/java/com/example/demo/controllers/ParkingForecastController.java @@ -0,0 +1,31 @@ +package com.example.demo.controllers; + +import java.time.LocalDateTime; + +import com.example.demo.dtos.ParkingForecastRequestDTO; +import com.example.demo.dtos.ParkingLotForecastDTO; +import com.example.demo.services.ParkingLotForecastService; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/forecast") +public class ParkingForecastController { + + private final ParkingLotForecastService parkingLotForecastService; + + public ParkingForecastController(ParkingLotForecastService parkingLotForecastService) { + this.parkingLotForecastService = parkingLotForecastService; + } + + // Используем @PostMapping для принятия JSON через POST запрос + @PostMapping("/parking") + public ParkingLotForecastDTO getParkingForecast(@RequestBody ParkingForecastRequestDTO request) { + // Преобразуем строку даты и времени в LocalDateTime + LocalDateTime targetDateTime = LocalDateTime.parse(request.getDateTime()); + // Вызываем сервис для получения прогноза парковок + return parkingLotForecastService.forecastForDateTime(request.getCityName(), targetDateTime); + } +} diff --git a/src/main/java/com/example/demo/controllers/ParkingLotController.java b/src/main/java/com/example/demo/controllers/ParkingLotController.java new file mode 100644 index 0000000..3a83197 --- /dev/null +++ b/src/main/java/com/example/demo/controllers/ParkingLotController.java @@ -0,0 +1,68 @@ +package com.example.demo.controllers; + +import java.util.List; + +import com.example.demo.dtos.ParkingLotDTO; +import com.example.demo.models.ParkingLot; +import com.example.demo.models.ParkingStatus; +import com.example.demo.services.ParkingLotService; +import com.example.demo.services.ParkingNotificationService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/parkings") +public class ParkingLotController { + + private final ParkingLotService parkingLotService; + private final ParkingNotificationService parkingNotificationService; + + public ParkingLotController(ParkingLotService parkingLotService, ParkingNotificationService parkingNotificationService) { + this.parkingLotService = parkingLotService; + this.parkingNotificationService = parkingNotificationService; + } + + // Получение всех парковок с последними статусами + @GetMapping + public ResponseEntity> getAllParkingLotsWithLatestStatus() { + List parkingLots = parkingLotService.findAllWithLatestStatus(); + return ResponseEntity.ok(parkingLots); + } + + // Получение списка парковок по городу и их последних состояний + @GetMapping("/city/{cityName}") + public ResponseEntity> getParkingLotsByCityWithLatestStatus(@PathVariable String cityName) { + List parkingLotDTOs = parkingLotService.findByCityWithLatestStatus(cityName); + return ResponseEntity.ok(parkingLotDTOs); + } + + @PostMapping("/subscribe") + public ResponseEntity subscribeForNotification( + @RequestParam Long parkingLotId, + @RequestParam String email) { + + // Поиск парковки по ID + ParkingLot parkingLot = parkingLotService.findById(parkingLotId); + if (parkingLot == null) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Parking lot not found."); + } + + try { + // Попытка создать подписку + parkingNotificationService.createNotification(parkingLot, email); + } catch (IllegalStateException e) { + // Если подписка уже существует, возвращаем сообщение об этом + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage()); + } + + // Подписка создана успешно + return ResponseEntity.ok("You have successfully subscribed to notifications for parking lot ID: " + parkingLotId); + } + +} diff --git a/src/main/java/com/example/demo/dataProviders/ExternalParkingDataProvider.java b/src/main/java/com/example/demo/dataProviders/ExternalParkingDataProvider.java new file mode 100644 index 0000000..1354c76 --- /dev/null +++ b/src/main/java/com/example/demo/dataProviders/ExternalParkingDataProvider.java @@ -0,0 +1,85 @@ +package com.example.demo.dataProviders; + +import lombok.Getter; +import lombok.Setter; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.example.demo.dtos.ParkingResponse; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Component; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +public class ExternalParkingDataProvider implements ParkingDataProvider { + + private final String parkingDataUrl; + private final String sourcePrefix; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + public ExternalParkingDataProvider(String parkingDataUrl, String sourcePrefix, RestTemplate restTemplate, ObjectMapper objectMapper) { + this.parkingDataUrl = parkingDataUrl; + this.sourcePrefix = sourcePrefix; + this.restTemplate = restTemplate; + this.objectMapper = objectMapper; + } + + @Override + public List getParkingData() { + List parkingResponses = new ArrayList<>(); + + try { + String jsonResponse = restTemplate.getForObject(parkingDataUrl, String.class); + + List externalParkingResponses = objectMapper.readValue(jsonResponse, new TypeReference>() { + }); + + for (ExternalParkingResponse externalResponse : externalParkingResponses) { + ParkingResponse response = new ParkingResponse(); + response.setExternalParkingId((long) (parkingResponses.size() + 1)); + response.setParkingName(externalResponse.getName()); + response.setAddress(externalResponse.getAddress()); + response.setLocation(externalResponse.getLocation()); + response.setCityName(externalResponse.getCity()); + response.setTotalSpots(externalResponse.getTotalSpaces()); + response.setOccupiedSpots(externalResponse.getOccupiedSpaces()); + + parkingResponses.add(response); + } + + } catch (IOException e) { + e.printStackTrace(); + } + + return parkingResponses; + } + + @Override + public String getSourcePrefix() { + return sourcePrefix; + } + + @Getter + @Setter + private static class ExternalParkingResponse { + private String address; + private String city; + private String name; + private String location; + + @JsonProperty("free_spaces") + private int freeSpaces; + + @JsonProperty("occupied_spaces") + private int occupiedSpaces; + + @JsonProperty("total_spaces") + private int totalSpaces; + } + +} diff --git a/src/main/java/com/example/demo/dataProviders/ParkingDataProvider.java b/src/main/java/com/example/demo/dataProviders/ParkingDataProvider.java new file mode 100644 index 0000000..1d424ac --- /dev/null +++ b/src/main/java/com/example/demo/dataProviders/ParkingDataProvider.java @@ -0,0 +1,10 @@ +package com.example.demo.dataProviders; + +import java.util.List; + +import com.example.demo.dtos.ParkingResponse; + +public interface ParkingDataProvider { + List getParkingData(); + String getSourcePrefix(); +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/dataProviders/ParkingDataProviderFactory.java b/src/main/java/com/example/demo/dataProviders/ParkingDataProviderFactory.java new file mode 100644 index 0000000..fa9f2c8 --- /dev/null +++ b/src/main/java/com/example/demo/dataProviders/ParkingDataProviderFactory.java @@ -0,0 +1,76 @@ +package com.example.demo.dataProviders; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.example.demo.models.ParkingCompany; +import com.example.demo.repositories.ParkingCompanyRepository; +import com.example.demo.services.ParkingCompanyService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +@Component +public class ParkingDataProviderFactory { + + private final Map providers = new HashMap<>(); + private final ParkingCompanyService parkingCompanyService; + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + public ParkingDataProviderFactory(VaryImportantParkingDataProviderSimulator simulator, + ParkingCompanyService parkingCompanyService) { + this.parkingCompanyService = parkingCompanyService; + + addProvider(simulator.getSourcePrefix(), simulator); + + addProvidersFromDatabase(); + } + + private void addProvidersFromDatabase() { + List companies = parkingCompanyService.findAll(); + + for (ParkingCompany company : companies) { + String prefix = company.getPrefix(); + + if (!providers.containsKey(prefix)) { + if (company.getDataSourceUrl() != null) { + ExternalParkingDataProvider externalProvider = new ExternalParkingDataProvider( + company.getDataSourceUrl(), + prefix, + restTemplate, + objectMapper + ); + addProvider(prefix, externalProvider); + } else { + System.out.println("Company with prefix " + prefix + " has no data source URL."); + } + } + } + } + + public Collection getAllProviders() { + return providers.values(); + } + + public void addProvider(String prefix, ParkingDataProvider provider) { + checkAndAddCompany(provider); + + providers.put(prefix, provider); + } + + public void checkAndAddCompany(ParkingDataProvider provider) { + String prefix = provider.getSourcePrefix(); + if (!parkingCompanyService.existsByPrefix(prefix)) { + ParkingCompany newCompany = new ParkingCompany(); + newCompany.setCompanyName("Company for " + prefix); + newCompany.setPrefix(prefix); + newCompany.setDataSourceUrl(null); + parkingCompanyService.save(newCompany); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/dataProviders/VaryImportantParkingDataProviderSimulator.java b/src/main/java/com/example/demo/dataProviders/VaryImportantParkingDataProviderSimulator.java new file mode 100644 index 0000000..a9521fd --- /dev/null +++ b/src/main/java/com/example/demo/dataProviders/VaryImportantParkingDataProviderSimulator.java @@ -0,0 +1,50 @@ +package com.example.demo.dataProviders; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import com.example.demo.dtos.ParkingResponse; +import org.springframework.stereotype.Service; + +@Service +public class VaryImportantParkingDataProviderSimulator implements ParkingDataProvider { + + private final Random random = new Random(); + + // Фиксированный набор геолокаций (широта, долгота) + private final List fixedLocations = List.of( + "55.7558, 37.6176", // Москва + "55.7575, 37.6159", // Рядом с Красной площадью + "55.7580, 37.6200", // Еще одна точка в центре + "55.7600, 37.6205", // Немного севернее + "55.7520, 37.6175" // Немного южнее + ); + + @Override + public List getParkingData() { + List parkingResponses = new ArrayList<>(); + + for (long i = 1; i <= 5; i++) { + ParkingResponse response = new ParkingResponse(); + response.setExternalParkingId(i); + response.setParkingName("Симулятор Парковка " + i); + response.setAddress("Улица Симулятора, " + random.nextInt(100)); + response.setCityName("СимуляторСити"); + response.setTotalSpots(100); + response.setOccupiedSpots(random.nextInt(101)); + + String location = fixedLocations.get((int) (i - 1)); + response.setLocation(location); + + parkingResponses.add(response); + } + + return parkingResponses; + } + + @Override + public String getSourcePrefix() { + return "very_important_"; + } +} diff --git a/src/main/java/com/example/demo/dtos/AuthRequest.java b/src/main/java/com/example/demo/dtos/AuthRequest.java new file mode 100644 index 0000000..df190e3 --- /dev/null +++ b/src/main/java/com/example/demo/dtos/AuthRequest.java @@ -0,0 +1,9 @@ +package com.example.demo.dtos; + +import lombok.Data; + +@Data +public class AuthRequest { + private String username; + private String password; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/dtos/ChangePasswordRequest.java b/src/main/java/com/example/demo/dtos/ChangePasswordRequest.java new file mode 100644 index 0000000..1afe3f5 --- /dev/null +++ b/src/main/java/com/example/demo/dtos/ChangePasswordRequest.java @@ -0,0 +1,9 @@ +package com.example.demo.dtos; + +import lombok.Data; + +@Data +public class ChangePasswordRequest { + private String oldPassword; + private String newPassword; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/dtos/CityDTO.java b/src/main/java/com/example/demo/dtos/CityDTO.java new file mode 100644 index 0000000..d543bc0 --- /dev/null +++ b/src/main/java/com/example/demo/dtos/CityDTO.java @@ -0,0 +1,11 @@ +package com.example.demo.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class CityDTO { + private Long id; + private String name; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/dtos/ParkingCompanyDto.java b/src/main/java/com/example/demo/dtos/ParkingCompanyDto.java new file mode 100644 index 0000000..953e903 --- /dev/null +++ b/src/main/java/com/example/demo/dtos/ParkingCompanyDto.java @@ -0,0 +1,15 @@ +package com.example.demo.dtos; + +import lombok.Data; + +@Data +public class ParkingCompanyDto { + + private Long companyId; + private String companyName; + + public ParkingCompanyDto(Long companyId, String companyName) { + this.companyId = companyId; + this.companyName = companyName; + } +} diff --git a/src/main/java/com/example/demo/dtos/ParkingCompanyRequest.java b/src/main/java/com/example/demo/dtos/ParkingCompanyRequest.java new file mode 100644 index 0000000..4a9852f --- /dev/null +++ b/src/main/java/com/example/demo/dtos/ParkingCompanyRequest.java @@ -0,0 +1,13 @@ +package com.example.demo.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ParkingCompanyRequest { + private Long companyId; + private String companyName; + private String prefix; + private String dataSourceUrl; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/dtos/ParkingForecastRequestDTO.java b/src/main/java/com/example/demo/dtos/ParkingForecastRequestDTO.java new file mode 100644 index 0000000..8b0c2f9 --- /dev/null +++ b/src/main/java/com/example/demo/dtos/ParkingForecastRequestDTO.java @@ -0,0 +1,9 @@ +package com.example.demo.dtos; + +import lombok.Data; + +@Data +public class ParkingForecastRequestDTO { + private String cityName; + private String dateTime; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/dtos/ParkingLotDTO.java b/src/main/java/com/example/demo/dtos/ParkingLotDTO.java new file mode 100644 index 0000000..f2fe193 --- /dev/null +++ b/src/main/java/com/example/demo/dtos/ParkingLotDTO.java @@ -0,0 +1,21 @@ +package com.example.demo.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.LocalDateTime; + +import com.example.demo.models.ParkingStatus; + +@Data +@AllArgsConstructor +public class ParkingLotDTO { + private Long parkingId; + private String cityName; + private String parkingName; + private String address; + private String location; + private Integer totalSpots; + private Integer availableSpots; + private LocalDateTime timestamp; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/dtos/ParkingLotForecastDTO.java b/src/main/java/com/example/demo/dtos/ParkingLotForecastDTO.java new file mode 100644 index 0000000..02db08a --- /dev/null +++ b/src/main/java/com/example/demo/dtos/ParkingLotForecastDTO.java @@ -0,0 +1,26 @@ +package com.example.demo.dtos; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@AllArgsConstructor +public class ParkingLotForecastDTO { + + private LocalDateTime forecastDateTime; + private List parkingLotForecasts; + + @Data + @AllArgsConstructor + public static class ParkingLotForecast { + private Long parkingId; + private String parkingName; + private String address; + private String location; + private Integer totalSpots; + private Integer predictedAvailableSpots; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/dtos/ParkingResponse.java b/src/main/java/com/example/demo/dtos/ParkingResponse.java new file mode 100644 index 0000000..113d689 --- /dev/null +++ b/src/main/java/com/example/demo/dtos/ParkingResponse.java @@ -0,0 +1,20 @@ +package com.example.demo.dtos; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ParkingResponse { + private Long externalParkingId; + private String parkingName; + private String address; + private String location; + private String cityName; + private Integer totalSpots; + private Integer occupiedSpots; + + public boolean hasFreeSpots() { + return totalSpots > occupiedSpots; + } +} diff --git a/src/main/java/com/example/demo/models/City.java b/src/main/java/com/example/demo/models/City.java new file mode 100644 index 0000000..5b4e89d --- /dev/null +++ b/src/main/java/com/example/demo/models/City.java @@ -0,0 +1,25 @@ +package com.example.demo.models; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; + +@Entity +@Table(name = "cities") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class City { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long cityId; + + @Column(nullable = false, unique = true) + private String cityName; + + @OneToMany(mappedBy = "city", cascade = CascadeType.ALL, orphanRemoval = true) + private List parkingLots; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/models/ParkingCompany.java b/src/main/java/com/example/demo/models/ParkingCompany.java new file mode 100644 index 0000000..555147c --- /dev/null +++ b/src/main/java/com/example/demo/models/ParkingCompany.java @@ -0,0 +1,40 @@ +package com.example.demo.models; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Entity +@Table(name = "parking_companies") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ParkingCompany { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long companyId; + + @Column(nullable = false, unique = true) + private String companyName; + + @Column(nullable = false, unique = true) + private String prefix; + + @OneToMany(mappedBy = "parkingCompany", cascade = CascadeType.ALL, orphanRemoval = true) + private List parkingLots; + + @Column(nullable = true) + private String dataSourceUrl; + +} diff --git a/src/main/java/com/example/demo/models/ParkingLot.java b/src/main/java/com/example/demo/models/ParkingLot.java new file mode 100644 index 0000000..93d0ba9 --- /dev/null +++ b/src/main/java/com/example/demo/models/ParkingLot.java @@ -0,0 +1,44 @@ +package com.example.demo.models; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.List; + +@Entity +@Table(name = "parking_lots") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ParkingLot { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long parkingId; + + @Column(nullable = false, unique = true) + private String sourceParkingId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "city_id", nullable = false) + private City city; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "company_id", nullable = false) + private ParkingCompany parkingCompany; + + @Column(nullable = false) + private String parkingName; + + @Column(nullable = false) + private String address; + + @Column(nullable = false) + private String location; + + @Column(nullable = false) + private Integer totalSpots; + + @OneToMany(mappedBy = "parkingLot", cascade = CascadeType.ALL, orphanRemoval = true) + private List statuses; +} diff --git a/src/main/java/com/example/demo/models/ParkingNotification.java b/src/main/java/com/example/demo/models/ParkingNotification.java new file mode 100644 index 0000000..cabe6ce --- /dev/null +++ b/src/main/java/com/example/demo/models/ParkingNotification.java @@ -0,0 +1,36 @@ +package com.example.demo.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "parking_notifications") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ParkingNotification { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parking_lot_id", nullable = false) + private ParkingLot parkingLot; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private Boolean notified = false; +} diff --git a/src/main/java/com/example/demo/models/ParkingStatus.java b/src/main/java/com/example/demo/models/ParkingStatus.java new file mode 100644 index 0000000..49ff9df --- /dev/null +++ b/src/main/java/com/example/demo/models/ParkingStatus.java @@ -0,0 +1,33 @@ +package com.example.demo.models; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "parking_statuses") +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ParkingStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long statusId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parking_id", nullable = false) + private ParkingLot parkingLot; + + @Column(nullable = false) + private Integer availableSpots; + + @Column(nullable = false) + private LocalDateTime timestamp; + + public ParkingStatus(ParkingLot parkingLot, Integer availableSpots, LocalDateTime timestamp) { + this.parkingLot = parkingLot; + this.availableSpots = availableSpots; + this.timestamp = timestamp; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/models/UserEntity.java b/src/main/java/com/example/demo/models/UserEntity.java new file mode 100644 index 0000000..e7fa649 --- /dev/null +++ b/src/main/java/com/example/demo/models/UserEntity.java @@ -0,0 +1,14 @@ +package com.example.demo.models; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.Data; + +@Data +@Entity +public class UserEntity { + @Id + private String username; + private String password; + private String roles; +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/repositories/CityRepository.java b/src/main/java/com/example/demo/repositories/CityRepository.java new file mode 100644 index 0000000..5c59f94 --- /dev/null +++ b/src/main/java/com/example/demo/repositories/CityRepository.java @@ -0,0 +1,8 @@ +package com.example.demo.repositories; + +import com.example.demo.models.City; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CityRepository extends JpaRepository { + City findByCityName(String cityName); +} diff --git a/src/main/java/com/example/demo/repositories/ParkingCompanyRepository.java b/src/main/java/com/example/demo/repositories/ParkingCompanyRepository.java new file mode 100644 index 0000000..3bc5ac2 --- /dev/null +++ b/src/main/java/com/example/demo/repositories/ParkingCompanyRepository.java @@ -0,0 +1,13 @@ +package com.example.demo.repositories; + +import java.util.Optional; + +import com.example.demo.models.ParkingCompany; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ParkingCompanyRepository extends JpaRepository { + boolean existsByPrefix(String prefix); + Optional findByPrefix(String prefix); +} diff --git a/src/main/java/com/example/demo/repositories/ParkingLotRepository.java b/src/main/java/com/example/demo/repositories/ParkingLotRepository.java new file mode 100644 index 0000000..f64988a --- /dev/null +++ b/src/main/java/com/example/demo/repositories/ParkingLotRepository.java @@ -0,0 +1,33 @@ +package com.example.demo.repositories; + +import java.util.List; +import java.util.Optional; + +import com.example.demo.models.ParkingLot; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ParkingLotRepository extends JpaRepository { + Optional findBySourceParkingId(String sourceParkingId); + + @Query("SELECT pl FROM ParkingLot pl " + + "JOIN FETCH pl.statuses ps " + + "WHERE ps.timestamp = " + + "(SELECT MAX(ps2.timestamp) FROM ParkingStatus ps2 WHERE ps2.parkingLot = pl)") + List findAllWithLatestStatus(); + + @Query("SELECT pl FROM ParkingLot pl " + + "JOIN FETCH pl.statuses ps " + + "WHERE pl.city.cityName = :cityName AND ps.timestamp = " + + "(SELECT MAX(ps2.timestamp) FROM ParkingStatus ps2 WHERE ps2.parkingLot = pl)") + List findByCityWithLatestStatus(@Param("cityName") String cityName); + + Optional findById(Long id); + + @Query("SELECT pl FROM ParkingLot pl JOIN FETCH pl.statuses WHERE pl.city.cityName = :cityName") + List findByCityWithLatestStatusForecast(String cityName); + +} diff --git a/src/main/java/com/example/demo/repositories/ParkingNotificationRepository.java b/src/main/java/com/example/demo/repositories/ParkingNotificationRepository.java new file mode 100644 index 0000000..8e2e903 --- /dev/null +++ b/src/main/java/com/example/demo/repositories/ParkingNotificationRepository.java @@ -0,0 +1,15 @@ +package com.example.demo.repositories; + +import java.util.List; +import java.util.Optional; + +import com.example.demo.models.ParkingLot; +import com.example.demo.models.ParkingNotification; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ParkingNotificationRepository extends JpaRepository { + List findByParkingLotAndNotifiedFalse(ParkingLot parkingLot); + Optional findByParkingLotAndEmailAndNotifiedFalse(ParkingLot parkingLot, String email); +} diff --git a/src/main/java/com/example/demo/repositories/ParkingStatusRepository.java b/src/main/java/com/example/demo/repositories/ParkingStatusRepository.java new file mode 100644 index 0000000..b267408 --- /dev/null +++ b/src/main/java/com/example/demo/repositories/ParkingStatusRepository.java @@ -0,0 +1,26 @@ +package com.example.demo.repositories; + +import java.time.LocalDateTime; +import java.util.List; + +import com.example.demo.models.ParkingStatus; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface ParkingStatusRepository extends JpaRepository { + + @Query("SELECT ps FROM ParkingStatus ps WHERE ps.timestamp = :dateTime") + List findAllByDateTime(@Param("dateTime") LocalDateTime dateTime); + + @Query("SELECT ps FROM ParkingStatus ps WHERE ps.parkingLot.id = :parkingId AND ps.timestamp = :dateTime") + ParkingStatus findByParkingLotAndDateTime(@Param("parkingId") Long parkingId, @Param("dateTime") LocalDateTime dateTime); + + @Query("SELECT ps FROM ParkingStatus ps WHERE ps.parkingLot.city.cityName = :cityName AND ps.timestamp = :dateTime") + List findByCityAndDateTime(@Param("cityName") String cityName, @Param("dateTime") LocalDateTime dateTime); + + @Query("SELECT ps FROM ParkingStatus ps WHERE ps.parkingLot.parkingCompany.id = :companyId AND ps.timestamp = :dateTime") + List findByCompanyAndDateTime(@Param("companyId") Long companyId, @Param("dateTime") LocalDateTime dateTime); +} diff --git a/src/main/java/com/example/demo/repositories/UserRepository.java b/src/main/java/com/example/demo/repositories/UserRepository.java new file mode 100644 index 0000000..3b6d031 --- /dev/null +++ b/src/main/java/com/example/demo/repositories/UserRepository.java @@ -0,0 +1,8 @@ +package com.example.demo.repositories; + +import com.example.demo.models.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + UserEntity findByUsername(String username); +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/services/CityService.java b/src/main/java/com/example/demo/services/CityService.java new file mode 100644 index 0000000..cd06a81 --- /dev/null +++ b/src/main/java/com/example/demo/services/CityService.java @@ -0,0 +1,37 @@ +package com.example.demo.services; + +import java.util.List; +import java.util.stream.Collectors; + +import com.example.demo.dtos.CityDTO; +import com.example.demo.models.City; +import com.example.demo.repositories.CityRepository; +import org.springframework.stereotype.Service; + +@Service +public class CityService { + + private final CityRepository cityRepository; + + public CityService(CityRepository cityRepository) { + this.cityRepository = cityRepository; + } + + public City findByCityName(String cityName) { + return cityRepository.findByCityName(cityName); + } + + public City save(City city) { + return cityRepository.save(city); + } + + public List getAllCities() { + List cities = cityRepository.findAll(); + + List cityDTOs = cities.stream() + .map(city -> new CityDTO(city.getCityId(), city.getCityName())) + .collect(Collectors.toList()); + + return cityDTOs; + } +} diff --git a/src/main/java/com/example/demo/services/CustomUserDetailsService.java b/src/main/java/com/example/demo/services/CustomUserDetailsService.java new file mode 100644 index 0000000..ff87279 --- /dev/null +++ b/src/main/java/com/example/demo/services/CustomUserDetailsService.java @@ -0,0 +1,39 @@ +package com.example.demo.services; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import com.example.demo.models.UserEntity; +import com.example.demo.repositories.UserRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + @Autowired + private UserRepository userRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + UserEntity user = userRepository.findByUsername(username); + if (user == null) { + throw new UsernameNotFoundException("User not found"); + } + + // Добавляем префикс ROLE_ для каждой роли перед созданием авторитетов + List authorities = Arrays.stream(user.getRoles().split(",")) + .map(role -> new SimpleGrantedAuthority("ROLE_" + role.trim())) + .collect(Collectors.toList()); + + // Возвращаем Spring Security User с добавленным префиксом в ролях + return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities); + } +} diff --git a/src/main/java/com/example/demo/services/EmailService.java b/src/main/java/com/example/demo/services/EmailService.java new file mode 100644 index 0000000..5b5a5f6 --- /dev/null +++ b/src/main/java/com/example/demo/services/EmailService.java @@ -0,0 +1,28 @@ +package com.example.demo.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +public class EmailService { + + @Autowired + private JavaMailSender javaMailSender; + + private final String companyName = "Digital parking"; + + @Value("${spring.mail.username}") + private String fromEmail; + + public void sendEmail(String to, String subject, String text) { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom("Digital parking <" + fromEmail + ">"); + message.setTo(to); + message.setSubject(subject); + message.setText(text + "\n\nС уважением, команда " + companyName); + javaMailSender.send(message); + } +} diff --git a/src/main/java/com/example/demo/services/ParkingCompanyService.java b/src/main/java/com/example/demo/services/ParkingCompanyService.java new file mode 100644 index 0000000..8f2c166 --- /dev/null +++ b/src/main/java/com/example/demo/services/ParkingCompanyService.java @@ -0,0 +1,79 @@ +package com.example.demo.services; + +import java.util.List; +import java.util.stream.Collectors; + +import com.example.demo.dtos.ParkingCompanyDto; +import com.example.demo.dtos.ParkingCompanyRequest; +import com.example.demo.models.ParkingCompany; +import com.example.demo.repositories.ParkingCompanyRepository; +import org.springframework.stereotype.Service; + +@Service +public class ParkingCompanyService { + + private final ParkingCompanyRepository parkingCompanyRepository; + + public ParkingCompanyService(ParkingCompanyRepository parkingCompanyRepository) { + this.parkingCompanyRepository = parkingCompanyRepository; + } + + public boolean existsByPrefix(String prefix) { + return parkingCompanyRepository.existsByPrefix(prefix); + } + + public ParkingCompany findByPrefix(String prefix) { + return parkingCompanyRepository.findByPrefix(prefix) + .orElse(null); + } + + public List findAll() { + return parkingCompanyRepository.findAll(); + } + public List findAllForRequest() { + return parkingCompanyRepository.findAll().stream() + .map(parkingCompany -> new ParkingCompanyRequest( + parkingCompany.getCompanyId(), + parkingCompany.getCompanyName(), + parkingCompany.getPrefix(), + parkingCompany.getDataSourceUrl() + )) + .collect(Collectors.toList()); + } + + + public ParkingCompany save(ParkingCompany parkingCompany) { + return parkingCompanyRepository.save(parkingCompany); + } + public ParkingCompanyRequest saveAsRequest(ParkingCompanyRequest parkingCompanyRequest) { + // Преобразуем PerkingCompanyRequest в ParkingCompany + ParkingCompany parkingCompany = new ParkingCompany(); + parkingCompany.setCompanyId(parkingCompanyRequest.getCompanyId()); + parkingCompany.setCompanyName(parkingCompanyRequest.getCompanyName()); + parkingCompany.setPrefix(parkingCompanyRequest.getPrefix()); + parkingCompany.setDataSourceUrl(parkingCompanyRequest.getDataSourceUrl()); + + // Сохраняем объект ParkingCompany в базе данных + ParkingCompany savedCompany = parkingCompanyRepository.save(parkingCompany); + + // Преобразуем обратно в PerkingCompanyRequest для возврата + return new ParkingCompanyRequest( + savedCompany.getCompanyId(), + savedCompany.getCompanyName(), + savedCompany.getPrefix(), + savedCompany.getDataSourceUrl() + ); + } + + public List getAllCompanies() { + List companies = parkingCompanyRepository.findAll(); + + return companies.stream() + .map(company -> new ParkingCompanyDto(company.getCompanyId(), company.getCompanyName())) + .collect(Collectors.toList()); + } + + public void deleteById(Long id) { + parkingCompanyRepository.deleteById(id); + } +} diff --git a/src/main/java/com/example/demo/services/ParkingLotForecastService.java b/src/main/java/com/example/demo/services/ParkingLotForecastService.java new file mode 100644 index 0000000..a4f5c00 --- /dev/null +++ b/src/main/java/com/example/demo/services/ParkingLotForecastService.java @@ -0,0 +1,104 @@ +package com.example.demo.services; + +import com.example.demo.dtos.ParkingLotForecastDTO; +import com.example.demo.models.ParkingLot; +import com.example.demo.models.ParkingStatus; +import com.example.demo.repositories.ParkingLotRepository; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.time.DayOfWeek; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import java.util.stream.Collectors; + +@Service +public class ParkingLotForecastService { + + private final ParkingLotRepository parkingLotRepository; + private final RestTemplate restTemplate; + + public ParkingLotForecastService(ParkingLotRepository parkingLotRepository) { + this.parkingLotRepository = parkingLotRepository; + this.restTemplate = new RestTemplate(); + } + + public ParkingLotForecastDTO forecastForDateTime(String cityName, LocalDateTime targetDateTime) { + // Получаем все парковки в нужном городе + List parkingLots = parkingLotRepository.findByCityWithLatestStatusForecast(cityName); + + // Проверка, является ли дата праздничным или выходным днем + boolean isHoliday = isHoliday(targetDateTime); + + // Прогноз для каждой парковки + List forecasts = parkingLots.stream() + .map(parkingLot -> { + // Получаем исторические данные для анализа + List statuses = parkingLot.getStatuses(); + + // Прогнозируем доступные места (упрощенный метод, реальный может включать ML модели) + Integer predictedSpots = predictAvailableSpots(statuses, targetDateTime, isHoliday); + + return new ParkingLotForecastDTO.ParkingLotForecast( + parkingLot.getParkingId(), + parkingLot.getParkingName(), + parkingLot.getAddress(), + parkingLot.getLocation(), + parkingLot.getTotalSpots(), + predictedSpots + ); + }) + .collect(Collectors.toList()); + + return new ParkingLotForecastDTO(targetDateTime, forecasts); + } + + private boolean isHoliday(LocalDateTime date) { + // Форматируем дату для запроса + String url = String.format("https://isdayoff.ru/api/getdata?year=%d&month=%02d&day=%02d", + date.getYear(), date.getMonthValue(), date.getDayOfMonth()); + + try { + String response = restTemplate.getForObject(url, String.class); + return "1".equals(response) || "2".equals(response); // 1 - нерабочий день, 2 - сокращенный день + } catch (Exception e) { + // Логирование ошибки или возврат дефолтного значения + return false; + } + } + + private Integer predictAvailableSpots(List statuses, LocalDateTime targetDateTime, boolean isHoliday) { + int targetDayOfWeek = targetDateTime.getDayOfWeek().getValue(); + LocalTime targetTime = targetDateTime.toLocalTime(); + + // Если это праздничный день, используем данные за все субботы и воскресенья + if (isHoliday) { + List weekendStatuses = statuses.stream() + .filter(status -> { + DayOfWeek dayOfWeek = status.getTimestamp().getDayOfWeek(); + return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY; + }) + .filter(status -> status.getTimestamp().toLocalTime().isBefore(targetTime.plusHours(1)) + && status.getTimestamp().toLocalTime().isAfter(targetTime.minusHours(1))) + .collect(Collectors.toList()); + + return (int) weekendStatuses.stream() + .mapToInt(ParkingStatus::getAvailableSpots) + .average() + .orElse(0.0); + } + + // Если это обычный рабочий день, используем данные по аналогичным дням недели + List similarTimeStatuses = statuses.stream() + .filter(status -> status.getTimestamp().getDayOfWeek().getValue() == targetDayOfWeek) + .filter(status -> status.getTimestamp().toLocalTime().isBefore(targetTime.plusHours(1)) + && status.getTimestamp().toLocalTime().isAfter(targetTime.minusHours(1))) + .collect(Collectors.toList()); + + return (int) similarTimeStatuses.stream() + .mapToInt(ParkingStatus::getAvailableSpots) + .average() + .orElse(0.0); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/services/ParkingLotService.java b/src/main/java/com/example/demo/services/ParkingLotService.java new file mode 100644 index 0000000..c6c4124 --- /dev/null +++ b/src/main/java/com/example/demo/services/ParkingLotService.java @@ -0,0 +1,94 @@ +package com.example.demo.services; + +import jakarta.persistence.EntityNotFoundException; + +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.example.demo.dtos.ParkingLotDTO; +import com.example.demo.models.ParkingLot; +import com.example.demo.models.ParkingStatus; +import com.example.demo.repositories.ParkingLotRepository; +import org.springframework.stereotype.Service; + +@Service +public class ParkingLotService { + + private final ParkingLotRepository parkingLotRepository; + + public ParkingLotService(ParkingLotRepository parkingLotRepository) { + this.parkingLotRepository = parkingLotRepository; + } + + public ParkingLot findBySourceParkingId(String sourceParkingId) { + Optional parkingLot = parkingLotRepository.findBySourceParkingId(sourceParkingId); + if (parkingLot.isEmpty()) { + System.out.println("Parking lot with external ID " + sourceParkingId + " not found"); + return null; + } + return parkingLot.get(); + } + + public ParkingLot save(ParkingLot parkingLot) { + return parkingLotRepository.save(parkingLot); + } + + public ParkingLot findById(Long parkingLotId) { + return parkingLotRepository.findById(parkingLotId) + .orElseThrow(() -> new EntityNotFoundException("Parking lot with ID " + parkingLotId + " not found")); + } + + public List findAllWithLatestStatus() { + List parkingLots = parkingLotRepository.findAllWithLatestStatus(); + + List parkingLotDTOs = parkingLots.stream() + .map(parkingLot -> { + ParkingStatus latestStatus = parkingLot.getStatuses().stream() + .max(Comparator.comparing(ParkingStatus::getTimestamp)) + .orElse(null); + + return new ParkingLotDTO( + parkingLot.getParkingId(), + parkingLot.getCity().getCityName(), + parkingLot.getParkingName(), + parkingLot.getAddress(), + parkingLot.getLocation(), + parkingLot.getTotalSpots(), + latestStatus.getAvailableSpots(), + latestStatus.getTimestamp() + ); + }) + .collect(Collectors.toList()); + + return parkingLotDTOs; + } + + public List findByCityWithLatestStatus(String cityName) { + List parkingLots = parkingLotRepository.findByCityWithLatestStatus(cityName); + + List parkingLotDTOs = parkingLots.stream() + .map(parkingLot -> { + ParkingStatus latestStatus = parkingLot.getStatuses().stream() + .max(Comparator.comparing(ParkingStatus::getTimestamp)) + .orElse(null); + + return new ParkingLotDTO( + parkingLot.getParkingId(), + parkingLot.getCity().getCityName(), + parkingLot.getParkingName(), + parkingLot.getAddress(), + parkingLot.getLocation(), + parkingLot.getTotalSpots(), + latestStatus.getAvailableSpots(), + latestStatus.getTimestamp() + ); + }) + .collect(Collectors.toList()); + + return parkingLotDTOs; + } + + +} diff --git a/src/main/java/com/example/demo/services/ParkingNotificationService.java b/src/main/java/com/example/demo/services/ParkingNotificationService.java new file mode 100644 index 0000000..2db955c --- /dev/null +++ b/src/main/java/com/example/demo/services/ParkingNotificationService.java @@ -0,0 +1,79 @@ +package com.example.demo.services; + +import jakarta.transaction.Transactional; + +import java.util.List; +import java.util.Optional; + +import com.example.demo.models.ParkingLot; +import com.example.demo.models.ParkingNotification; +import com.example.demo.repositories.ParkingNotificationRepository; +import org.springframework.stereotype.Service; + +@Service +public class ParkingNotificationService { + private final ParkingNotificationRepository notificationRepository; + private final EmailService emailService; + private final ParkingLotService parkingLotService; + + public ParkingNotificationService(ParkingNotificationRepository notificationRepository, EmailService emailService, ParkingLotService parkingLotService) { + this.notificationRepository = notificationRepository; + this.emailService = emailService; + this.parkingLotService = parkingLotService; + } + + @Transactional + public void checkAndSendNotifications(String sourceParkingId, int countAvailableSpots) { + ParkingLot parkingLot = parkingLotService.findBySourceParkingId(sourceParkingId); + + if (parkingLot != null && countAvailableSpots>0) { + List notifications = + notificationRepository.findByParkingLotAndNotifiedFalse(parkingLot); + + for (ParkingNotification notification : notifications) { + // Доступ к лениво загруженному полю cityName + String cityName = parkingLot.getCity().getCityName(); + + // Формируем текст письма + String emailText = String.format( + "Уважаемый пользователь!\n\n" + + "На парковке по адресу: %s, город %s, появились свободные места.\n" + + "Свободных мест: %d из %d.\n\n" + + "Обратите внимание, что это письмо отправлено одноразово, и ваша электронная почта не будет сохранена в системе.\n" + + "Спасибо, что пользуетесь нашими услугами!", + parkingLot.getAddress(), + cityName, + countAvailableSpots, + parkingLot.getTotalSpots() + ); + + // Отправляем письмо + emailService.sendEmail( + notification.getEmail(), + "Уведомление о свободных местах на парковке", + emailText + ); + + // Удаляем запись после отправки уведомления + notificationRepository.delete(notification); + } + } + } + + // Метод для создания уведомления, с проверкой на уникальность сочетания парковка + email + public void createNotification(ParkingLot parkingLot, String email) { + // Проверка, есть ли уже такая запись + Optional existingNotification = + notificationRepository.findByParkingLotAndEmailAndNotifiedFalse(parkingLot, email); + + if (existingNotification.isPresent()) { + throw new IllegalStateException("You have already subscribed to notifications for this parking lot."); + } + + // Если записи нет, создаем новое уведомление + ParkingNotification notification = new ParkingNotification(); + notification.setParkingLot(parkingLot); + notification.setEmail(email); + notificationRepository.save(notification); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/services/ParkingStatusScheduler.java b/src/main/java/com/example/demo/services/ParkingStatusScheduler.java new file mode 100644 index 0000000..09148de --- /dev/null +++ b/src/main/java/com/example/demo/services/ParkingStatusScheduler.java @@ -0,0 +1,46 @@ +package com.example.demo.services; + +import java.util.Collection; +import java.util.List; + +import com.example.demo.dataProviders.ParkingDataProvider; +import com.example.demo.dataProviders.ParkingDataProviderFactory; +import com.example.demo.dtos.ParkingResponse; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +public class ParkingStatusScheduler { + + private final ParkingSyncService syncService; + private final ParkingDataProviderFactory providerFactory; + private final ParkingNotificationService parkingNotificationService; + + public ParkingStatusScheduler(ParkingSyncService syncService, ParkingDataProviderFactory providerFactory, + ParkingNotificationService parkingNotificationService) { + this.syncService = syncService; + this.providerFactory = providerFactory; + this.parkingNotificationService = parkingNotificationService; + } + + @Scheduled(fixedRate = 100000) + public void updateParkingStatuses() { + System.out.println("Scheduled task is running"); + + Collection providers = providerFactory.getAllProviders(); + + for (ParkingDataProvider provider : providers) { + List parkingResponses = provider.getParkingData(); + + for (ParkingResponse response : parkingResponses) { + syncService.processParkingData(response, provider.getSourcePrefix()); + + String sourceParkingId = syncService.generateSourceParkingId(provider.getSourcePrefix(), response.getExternalParkingId()); + + // Проверка и отправка уведомлений о свободных местах на основе sourceParkingId + parkingNotificationService.checkAndSendNotifications(sourceParkingId, response.getTotalSpots()-response.getOccupiedSpots()); + } + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/services/ParkingStatusService.java b/src/main/java/com/example/demo/services/ParkingStatusService.java new file mode 100644 index 0000000..b5c0eac --- /dev/null +++ b/src/main/java/com/example/demo/services/ParkingStatusService.java @@ -0,0 +1,48 @@ +package com.example.demo.services; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; + +import com.example.demo.dataProviders.ParkingDataProvider; +import com.example.demo.dataProviders.ParkingDataProviderFactory; +import com.example.demo.dtos.ParkingResponse; +import com.example.demo.models.ParkingCompany; +import com.example.demo.models.ParkingStatus; +import com.example.demo.repositories.ParkingStatusRepository; +import com.example.demo.services.ParkingSyncService; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +@Service +public class ParkingStatusService { + + private final ParkingStatusRepository parkingStatusRepository; + + public ParkingStatusService(ParkingStatusRepository parkingStatusRepository) { + this.parkingStatusRepository = parkingStatusRepository; + } + + public ParkingStatus save(ParkingStatus parkingStatus) { + return parkingStatusRepository.save(parkingStatus); + } + // Получение всех парковок по дате и времени + public List findAllByDateTime(LocalDateTime dateTime) { + return parkingStatusRepository.findAllByDateTime(dateTime); + } + + // Получение статуса одной парковки по дате и времени + public ParkingStatus findByParkingLotAndDateTime(Long parkingId, LocalDateTime dateTime) { + return parkingStatusRepository.findByParkingLotAndDateTime(parkingId, dateTime); + } + + // Получение парковок по городу и дате + public List findByCityAndDateTime(String cityName, LocalDateTime dateTime) { + return parkingStatusRepository.findByCityAndDateTime(cityName, dateTime); + } + + // Получение парковок по компании и дате + public List findByCompanyAndDateTime(Long companyId, LocalDateTime dateTime) { + return parkingStatusRepository.findByCompanyAndDateTime(companyId, dateTime); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/services/ParkingSyncService.java b/src/main/java/com/example/demo/services/ParkingSyncService.java new file mode 100644 index 0000000..e5e19aa --- /dev/null +++ b/src/main/java/com/example/demo/services/ParkingSyncService.java @@ -0,0 +1,76 @@ +package com.example.demo.services; + +import java.time.LocalDateTime; + +import com.example.demo.dtos.ParkingResponse; +import com.example.demo.models.City; +import com.example.demo.models.ParkingCompany; +import com.example.demo.models.ParkingLot; +import com.example.demo.models.ParkingStatus; +import com.example.demo.repositories.CityRepository; +import com.example.demo.repositories.ParkingLotRepository; +import com.example.demo.repositories.ParkingStatusRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class ParkingSyncService { + + private final ParkingLotService parkingLotService; + private final ParkingStatusService parkingStatusService; + private final CityService cityService; + private final ParkingCompanyService parkingCompanyService; + + public ParkingSyncService(ParkingLotService parkingLotService, + ParkingStatusService parkingStatusService, + CityService cityService, + ParkingCompanyService parkingCompanyService) { + this.parkingLotService = parkingLotService; + this.parkingStatusService = parkingStatusService; + this.cityService = cityService; + this.parkingCompanyService = parkingCompanyService; + } + + public void processParkingData(ParkingResponse response, String sourcePrefix) { + String sourceParkingId = generateSourceParkingId(sourcePrefix, response.getExternalParkingId()); + + ParkingLot parkingLot = parkingLotService.findBySourceParkingId(sourceParkingId); + + if (parkingLot == null) { + ParkingCompany company = parkingCompanyService.findByPrefix(sourcePrefix); + if (company == null) { + throw new IllegalArgumentException("Company with prefix " + sourcePrefix + " not found."); + } + + City city = cityService.findByCityName(response.getCityName()); + if (city == null) { + city = new City(); + city.setCityName(response.getCityName()); + cityService.save(city); + } + + parkingLot = new ParkingLot(); + parkingLot.setSourceParkingId(sourceParkingId); + parkingLot.setParkingName(response.getParkingName()); + parkingLot.setAddress(response.getAddress()); + parkingLot.setLocation(response.getLocation()); + parkingLot.setCity(city); + parkingLot.setTotalSpots(response.getTotalSpots()); + parkingLot.setParkingCompany(company); + parkingLotService.save(parkingLot); + } + + ParkingStatus status = new ParkingStatus( + parkingLot, + response.getTotalSpots() - response.getOccupiedSpots(), + LocalDateTime.now() + ); + + parkingStatusService.save(status); + + } + + public String generateSourceParkingId(String sourcePrefix, Long externalParkingId) { + return sourcePrefix + externalParkingId; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..4ba893b --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,33 @@ +spring.application.name=demo +# URL ??????????? ? ???? ?????? PostgreSQL +spring.datasource.url=jdbc:postgresql://localhost:5432/parking +spring.datasource.username=user +spring.datasource.password=password + +# ??????? ???? ?????? PostgreSQL +spring.datasource.driver-class-name=org.postgresql.Driver + +# ?????? ????????? JPA +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +# ??????? ??????????? ??? SQL-???????? (???????????) +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true +server.port=8082 + +springdoc.api-docs.path=/v3/api-docs +springdoc.swagger-ui.path=/swagger-ui.html +springdoc.swagger-ui.enabled=true + +# ????????? ?????? ??? ???????????? +springdoc.packages-to-scan=com.example.demo.controllers + +spring.mail.host=smtp.mail.ru +spring.mail.port=587 +spring.mail.username=stranni19k@mail.ru +spring.mail.password=1UuhuHw5KxjxU7r34Z7B +spring.mail.properties.mail.smtp.auth=true +spring.mail.properties.mail.smtp.starttls.enable=true + +admin.password=admin diff --git a/src/test/java/com/example/demo/DemoApplicationTests.java b/src/test/java/com/example/demo/DemoApplicationTests.java new file mode 100644 index 0000000..2778a6a --- /dev/null +++ b/src/test/java/com/example/demo/DemoApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.demo; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class DemoApplicationTests { + + @Test + void contextLoads() { + } + +}