From 222db2a9d8b996a24b3ff2683c0ac954bf8a490f Mon Sep 17 00:00:00 2001 From: RM Date: Sat, 15 Nov 2025 12:54:31 +0300 Subject: [PATCH] first commit --- .gitignore | 33 ++ .mvn/wrapper/maven-wrapper.properties | 19 + mvnw | 259 ++++++++++ mvnw.cmd | 149 ++++++ pom.xml | 140 ++++++ .../com/pledge/torgi/MainApplication.java | 17 + .../DownloadHeadlessController.java | 202 ++++++++ .../controller/FileUploadController.java | 37 ++ .../torgi/controller/HealthController.java | 16 + .../torgi/controller/storage/FileService.java | 10 + .../controller/storage/FileServiceImpl.java | 86 ++++ .../controller/storage/StorageException.java | 12 + .../com/pledge/torgi/enums/IntervalEnum.java | 30 ++ .../java/com/pledge/torgi/model/Configs.java | 23 + .../java/com/pledge/torgi/model/Emails.java | 28 ++ .../com/pledge/torgi/model/TorgiEvent.java | 59 +++ .../torgi/repositories/ConfigsRepository.java | 7 + .../torgi/repositories/EmailsRepository.java | 11 + .../repositories/TorgiEventRepository.java | 15 + .../service/DownloadHeadlessService.java | 237 +++++++++ .../com/pledge/torgi/service/EmailData.java | 13 + .../pledge/torgi/service/EmailService.java | 44 ++ .../java/com/pledge/torgi/service/Lot.java | 48 ++ .../com/pledge/torgi/service/MainService.java | 33 ++ .../torgi/service/ReceiveEmailService.java | 122 +++++ .../torgi/service/SendEmailService.java | 64 +++ .../java/com/pledge/torgi/utils/AppUtils.java | 33 ++ .../com/pledge/torgi/utils/StartParams.java | 19 + src/main/resources/application.properties | 14 + .../db/migration/V1__init_event_table.sql | 22 + .../db/migration/V2__add_configs.sql | 7 + .../resources/db/migration/V3__add_emails.sql | 10 + src/main/resources/schemas/lot-shema.json | 471 ++++++++++++++++++ src/main/resources/schemas/shema.json | 268 ++++++++++ src/main/resources/templates/uploadForm.html | 22 + 35 files changed, 2580 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 src/main/java/com/pledge/torgi/MainApplication.java create mode 100644 src/main/java/com/pledge/torgi/controller/DownloadHeadlessController.java create mode 100644 src/main/java/com/pledge/torgi/controller/FileUploadController.java create mode 100644 src/main/java/com/pledge/torgi/controller/HealthController.java create mode 100644 src/main/java/com/pledge/torgi/controller/storage/FileService.java create mode 100644 src/main/java/com/pledge/torgi/controller/storage/FileServiceImpl.java create mode 100644 src/main/java/com/pledge/torgi/controller/storage/StorageException.java create mode 100644 src/main/java/com/pledge/torgi/enums/IntervalEnum.java create mode 100644 src/main/java/com/pledge/torgi/model/Configs.java create mode 100644 src/main/java/com/pledge/torgi/model/Emails.java create mode 100644 src/main/java/com/pledge/torgi/model/TorgiEvent.java create mode 100644 src/main/java/com/pledge/torgi/repositories/ConfigsRepository.java create mode 100644 src/main/java/com/pledge/torgi/repositories/EmailsRepository.java create mode 100644 src/main/java/com/pledge/torgi/repositories/TorgiEventRepository.java create mode 100644 src/main/java/com/pledge/torgi/service/DownloadHeadlessService.java create mode 100644 src/main/java/com/pledge/torgi/service/EmailData.java create mode 100644 src/main/java/com/pledge/torgi/service/EmailService.java create mode 100644 src/main/java/com/pledge/torgi/service/Lot.java create mode 100644 src/main/java/com/pledge/torgi/service/MainService.java create mode 100644 src/main/java/com/pledge/torgi/service/ReceiveEmailService.java create mode 100644 src/main/java/com/pledge/torgi/service/SendEmailService.java create mode 100644 src/main/java/com/pledge/torgi/utils/AppUtils.java create mode 100644 src/main/java/com/pledge/torgi/utils/StartParams.java create mode 100644 src/main/resources/application.properties create mode 100644 src/main/resources/db/migration/V1__init_event_table.sql create mode 100644 src/main/resources/db/migration/V2__add_configs.sql create mode 100644 src/main/resources/db/migration/V3__add_emails.sql create mode 100644 src/main/resources/schemas/lot-shema.json create mode 100644 src/main/resources/schemas/shema.json create mode 100644 src/main/resources/templates/uploadForm.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..667aaef --- /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/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..12fbe1e --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# 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. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip diff --git a/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..fdd2ad0 --- /dev/null +++ b/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.5 + + + com.pledge + torgi + 0.0.1-SNAPSHOT + Torgi + Torgi + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-postgresql + + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + com.fasterxml.jackson.core + jackson-databind + 2.17.1 + + + org.apache.poi + poi-ooxml + 5.4.0 + + + com.sun.mail + javax.mail + 1.5.5 + + + org.projectlombok + lombok + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.jsonschema2pojo + jsonschema2pojo-maven-plugin + 1.2.1 + + ${basedir}/src/main/resources/schemas + com.pledge.torgi + false + + + + + generate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/pledge/torgi/MainApplication.java b/src/main/java/com/pledge/torgi/MainApplication.java new file mode 100644 index 0000000..ef30111 --- /dev/null +++ b/src/main/java/com/pledge/torgi/MainApplication.java @@ -0,0 +1,17 @@ +package com.pledge.torgi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@SpringBootApplication +@EnableJpaRepositories +@EnableJpaAuditing +@EnableConfigurationProperties +public class MainApplication { + public static void main(String[] args) { + SpringApplication.run(MainApplication.class, args); + } +} diff --git a/src/main/java/com/pledge/torgi/controller/DownloadHeadlessController.java b/src/main/java/com/pledge/torgi/controller/DownloadHeadlessController.java new file mode 100644 index 0000000..8aed7d1 --- /dev/null +++ b/src/main/java/com/pledge/torgi/controller/DownloadHeadlessController.java @@ -0,0 +1,202 @@ +package com.pledge.torgi.controller; + +import com.pledge.torgi.model.Configs; +import com.pledge.torgi.model.TorgiEvent; +import com.pledge.torgi.repositories.ConfigsRepository; +import com.pledge.torgi.repositories.TorgiEventRepository; +import com.pledge.torgi.service.DownloadHeadlessService; +import com.pledge.torgi.service.SendEmailService; +import com.pledge.torgi.service.Lot; +import com.pledge.torgi.utils.StartParams; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static com.pledge.torgi.model.Configs.LAST_RUN_TIME; + +@Service +public class DownloadHeadlessController { + + private final Logger logger = LoggerFactory.getLogger(DownloadHeadlessController.class); + private final Set content = new HashSet<>(); + private final TorgiEventRepository eventRepository; + private final ConfigsRepository configsRepository; + + public DownloadHeadlessController(TorgiEventRepository eventRepository, ConfigsRepository configsRepository) { + this.eventRepository = eventRepository; + this.configsRepository = configsRepository; + eventRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(0, 10000)).forEach(event -> content.add(event.getKey())); + } + + public void process() { + addNewToDiff(); + } + + private void addNewToDiff() { + logger.error("startProcessing"); + DownloadHeadlessService downloadHeadlessService = new DownloadHeadlessService(); + Map currentContent = downloadHeadlessService.downloadExcel(); + List newEvents = new ArrayList<>(); + + for (String key : currentContent.keySet()) { + if (!content.contains(key)) { + newEvents.add(new TorgiEvent(key, currentContent.get(key))); + content.add(key); + logger.error("added new diff content"); + } + } + eventRepository.saveAll(newEvents); + logger.error("completeProcessing"); + } + + public void sendNewContent(StartParams params, List cadastral, String email) { + DownloadHeadlessService downloadHeadlessService = new DownloadHeadlessService(); + LocalDate now = LocalDate.now(); + downloadHeadlessService.createNewDocument(eventRepository, cadastral, now); + createArchive(now); + sendArchive(params, now, email); + removeArchiveAndSource(now); + logger.error("sended archive"); + } + + public void sendNewContent(StartParams params) { + if (alreadyRunnedToday()) { + return; + } + DownloadHeadlessService downloadHeadlessService = new DownloadHeadlessService(); + LocalDate now = LocalDate.now(); + downloadHeadlessService.createNewDocument(eventRepository, now); +// int i = 0; +// while (true) { +// List partition = eventRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(i, 30000)).stream().map(Lot::new).toList(); +// if (partition.isEmpty()) { +// break; +// } +// downloadHeadlessService.createNewDocument(eventRepository, now, i); +// i++; +// } + createArchive(now); + sendArchive(params, now); + removeArchiveAndSource(now); + logger.error("sended archive"); + saveTime(); + } + + private void saveTime() { + List configs = configsRepository.findAll().stream().filter(cfg -> cfg.getKey().equals(LAST_RUN_TIME)).toList(); + if (configs.isEmpty()) { + Configs newValue = new Configs(); + newValue.setId(1L); + newValue.setKey(LAST_RUN_TIME); + newValue.setValue(LocalDateTime.now().toString()); + configsRepository.save(newValue); + } + else { + Configs oldValue = configs.get(0); + oldValue.setValue(LocalDateTime.now().toString()); + configsRepository.save(oldValue); + } + } + + private boolean alreadyRunnedToday() { + List configs = configsRepository.findAll().stream().filter(cfg -> cfg.getKey().equals(LAST_RUN_TIME)).toList(); + if (configs.isEmpty()) { + return false; + } + LocalDateTime fromDB = LocalDateTime.parse(configs.get(0).getValue()); + return fromDB.format(DateTimeFormatter.ISO_LOCAL_DATE).equals(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE)); + } + + private void removeArchiveAndSource(LocalDate now) { + new File(now + ".zip").delete(); + deleteDirectory(new File(now.toString())); + } + + private boolean deleteDirectory(File directoryToBeDeleted) { + File[] allContents = directoryToBeDeleted.listFiles(); + if (allContents != null) { + for (File file : allContents) { + deleteDirectory(file); + } + } + return directoryToBeDeleted.delete(); + } + + private void sendArchive(StartParams params, LocalDate now) { + try { + new SendEmailService("smtp.gmail.com", 587, params.getFrom(), params.getToken()) + .sendMail(params.getFrom(), + params.getTo(),//"m.ryabyh@gmail.com,MEvRyabykh@sberbank.ru", + "Новая Выгрузка по торгам от " + now, + "Новая Выгрузка по торгам от " + now, + new File(now + ".zip")); + } catch (Exception e) { + logger.error(e.getMessage()); + } + } + + private void sendArchive(StartParams params, LocalDate now, String to) { + try { + new SendEmailService("smtp.gmail.com", 587, params.getFrom(), params.getToken()) + .sendMail(params.getFrom(), + to,//"m.ryabyh@gmail.com,MEvRyabykh@sberbank.ru", + "Новая Выгрузка по торгам от " + now, + "Новая Выгрузка по торгам от " + now, + new File(now + ".zip")); + } catch (Exception e) { + logger.error(e.getMessage()); + } + } + + private void createArchive(LocalDate now) { + String sourceFile = now.toString(); + try (FileOutputStream fos = new FileOutputStream(now + ".zip"); + ZipOutputStream zipOut = new ZipOutputStream(fos)) { + File fileToZip = new File(sourceFile); + zipFile(fileToZip, fileToZip.getName(), zipOut); + } catch (Exception ex) { + logger.error("in createArchive" + ex.getMessage()); + } + } + + private static void zipFile(File fileToZip, String fileName, ZipOutputStream zipOut) throws IOException { + if (fileToZip.isHidden()) { + return; + } + if (fileToZip.isDirectory()) { + if (fileName.endsWith("/")) { + zipOut.putNextEntry(new ZipEntry(fileName)); + zipOut.closeEntry(); + } else { + zipOut.putNextEntry(new ZipEntry(fileName + "/")); + zipOut.closeEntry(); + } + File[] children = fileToZip.listFiles(); + for (File childFile : children) { + zipFile(childFile, fileName + "/" + childFile.getName(), zipOut); + } + return; + } + FileInputStream fis = new FileInputStream(fileToZip); + ZipEntry zipEntry = new ZipEntry(fileName); + zipOut.putNextEntry(zipEntry); + byte[] bytes = new byte[1024]; + int length; + while ((length = fis.read(bytes)) >= 0) { + zipOut.write(bytes, 0, length); + } + fis.close(); + } +} diff --git a/src/main/java/com/pledge/torgi/controller/FileUploadController.java b/src/main/java/com/pledge/torgi/controller/FileUploadController.java new file mode 100644 index 0000000..c6e34f1 --- /dev/null +++ b/src/main/java/com/pledge/torgi/controller/FileUploadController.java @@ -0,0 +1,37 @@ +package com.pledge.torgi.controller; + +import com.pledge.torgi.controller.storage.FileService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + + +@Controller +public class FileUploadController { + + private final FileService storageService; + + @Autowired + public FileUploadController(FileService storageService) { + this.storageService = storageService; + } + + @GetMapping("/") + public String listUploadedFiles() { + return "uploadForm"; + } + + @PostMapping("/") + public String handleFileUpload(@RequestParam("file") MultipartFile file, @RequestParam("email") String email, + RedirectAttributes redirectAttributes) { + + String result = storageService.processAsync(file, email); + redirectAttributes.addFlashAttribute("message", + result); + return "redirect:/"; + } +} diff --git a/src/main/java/com/pledge/torgi/controller/HealthController.java b/src/main/java/com/pledge/torgi/controller/HealthController.java new file mode 100644 index 0000000..dd3d69f --- /dev/null +++ b/src/main/java/com/pledge/torgi/controller/HealthController.java @@ -0,0 +1,16 @@ +package com.pledge.torgi.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +public class HealthController { + + @GetMapping("/health") + public ResponseEntity getPerson() { + return ResponseEntity.ok().body("OK"); + } +} diff --git a/src/main/java/com/pledge/torgi/controller/storage/FileService.java b/src/main/java/com/pledge/torgi/controller/storage/FileService.java new file mode 100644 index 0000000..6d1de4f --- /dev/null +++ b/src/main/java/com/pledge/torgi/controller/storage/FileService.java @@ -0,0 +1,10 @@ +package com.pledge.torgi.controller.storage; + +import org.apache.poi.ss.usermodel.Workbook; +import org.springframework.web.multipart.MultipartFile; + +public interface FileService { + + String processAsync(MultipartFile file, String email); + String processSync(Workbook file, String email); +} diff --git a/src/main/java/com/pledge/torgi/controller/storage/FileServiceImpl.java b/src/main/java/com/pledge/torgi/controller/storage/FileServiceImpl.java new file mode 100644 index 0000000..49ba83b --- /dev/null +++ b/src/main/java/com/pledge/torgi/controller/storage/FileServiceImpl.java @@ -0,0 +1,86 @@ +package com.pledge.torgi.controller.storage; + +import com.pledge.torgi.controller.DownloadHeadlessController; +import com.pledge.torgi.utils.StartParams; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@AllArgsConstructor +public class FileServiceImpl implements FileService { + + private final DownloadHeadlessController headlessController; + private final StartParams params; + + @Override + public String processAsync(MultipartFile file, String email) { + try { + log.error("Get request"); + IOUtils.setByteArrayMaxOverride(1000000000); + Workbook wb = new XSSFWorkbook(file.getInputStream()); + List cadastrals = extractCadastral(wb); + log.error("Extract cadastrals: " + cadastrals.size()); + + Thread myThread = new Thread(() -> headlessController.sendNewContent(params, cadastrals, email)); + myThread.start(); + + log.error("Complete request"); + return "Взято в обработку " + cadastrals.size() + " кадастровых номеров"; + } + catch (IOException e) { + throw new StorageException("Failed to store file.", e); + } + } + + @Override + public String processSync(Workbook file, String email) { + log.error("Get request"); + List cadastrals = extractCadastral(file); + log.error("Extract cadastrals: " + cadastrals.size()); + + headlessController.sendNewContent(params, cadastrals, email); + + log.error("Complete request"); + return "Обработано " + cadastrals.size() + " кадастровых номеров"; + } + + private List extractCadastral(Workbook wb) { + List result = new ArrayList<>(); + Sheet firstSheet = wb.getSheetAt(0); + int rowNum = 0; + Row currentRow = firstSheet.getRow(rowNum); + + while(currentRow != null && currentRow.getCell(0) != null) { + try { + String stringCellValue = currentRow.getCell(0).getStringCellValue(); + if (StringUtils.isBlank(stringCellValue)) { + break; + } else { + result.add(stringCellValue); + } + } catch (IllegalStateException e) { + log.error("Error during processing " + rowNum + " " + e); + } + + rowNum++; + currentRow = firstSheet.getRow(rowNum); + } + + return result; + } + +} diff --git a/src/main/java/com/pledge/torgi/controller/storage/StorageException.java b/src/main/java/com/pledge/torgi/controller/storage/StorageException.java new file mode 100644 index 0000000..8678474 --- /dev/null +++ b/src/main/java/com/pledge/torgi/controller/storage/StorageException.java @@ -0,0 +1,12 @@ +package com.pledge.torgi.controller.storage; + +public class StorageException extends RuntimeException { + + public StorageException(String message) { + super(message); + } + + public StorageException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/pledge/torgi/enums/IntervalEnum.java b/src/main/java/com/pledge/torgi/enums/IntervalEnum.java new file mode 100644 index 0000000..5406b9f --- /dev/null +++ b/src/main/java/com/pledge/torgi/enums/IntervalEnum.java @@ -0,0 +1,30 @@ +package com.pledge.torgi.enums; + +import java.util.Arrays; + +public enum IntervalEnum { + PUB("По дате публикации", "pub"), + BIDD_END("По дате окончания срока подачи заявок", "biddEnd"), + AUC_START("По дате проведения торгов", "aucStart"), + ; + + private final String description; + private final String searchParam; + + IntervalEnum(String description, String searchParam) { + this.description = description; + this.searchParam = searchParam; + } + + public static IntervalEnum getByDescription(String description) { + return Arrays.stream(IntervalEnum.values()).filter(el -> el.getDescription().equals(description)).findFirst().orElse(null); + } + + public String getDescription() { + return description; + } + + public String getSearchParam() { + return searchParam; + } +} \ No newline at end of file diff --git a/src/main/java/com/pledge/torgi/model/Configs.java b/src/main/java/com/pledge/torgi/model/Configs.java new file mode 100644 index 0000000..e8eefc5 --- /dev/null +++ b/src/main/java/com/pledge/torgi/model/Configs.java @@ -0,0 +1,23 @@ +package com.pledge.torgi.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@Table(name = "CONFIGS") +public class Configs { + + public static final String LAST_RUN_TIME = "LAST_RUN_TIME"; + + @Id + private Long id; + private String key; + private String value; +} diff --git a/src/main/java/com/pledge/torgi/model/Emails.java b/src/main/java/com/pledge/torgi/model/Emails.java new file mode 100644 index 0000000..d75c882 --- /dev/null +++ b/src/main/java/com/pledge/torgi/model/Emails.java @@ -0,0 +1,28 @@ +package com.pledge.torgi.model; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@EntityListeners(AuditingEntityListener.class) +@AllArgsConstructor +@NoArgsConstructor +public class Emails { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "EmailId") + @SequenceGenerator(name = "EmailId", sequenceName = "EMAILS_SEQ", allocationSize = 1) + private Long id; + + private String mailId; + private String replyTo; + private LocalDateTime processedTime; +} diff --git a/src/main/java/com/pledge/torgi/model/TorgiEvent.java b/src/main/java/com/pledge/torgi/model/TorgiEvent.java new file mode 100644 index 0000000..11a238c --- /dev/null +++ b/src/main/java/com/pledge/torgi/model/TorgiEvent.java @@ -0,0 +1,59 @@ +package com.pledge.torgi.model; + +import com.pledge.torgi.service.Lot; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Entity +@Getter +@Setter +@EntityListeners(AuditingEntityListener.class) +@AllArgsConstructor +@NoArgsConstructor +public class TorgiEvent { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "TorgiId") + @SequenceGenerator(name = "TorgiId", sequenceName = "TORGI_EVENT_SEQ", allocationSize = 1) + private Long id; + private String key; + private String status; + private String bidStart; + private String bidEnd; + private String tradesDate; + private String startPrice; + private String step; + private String initMoney; + private String finalPrice; + private String notice; + private Integer lotNumber; + private String cadastral; + private String type; + private String fullInfo; + @CreatedDate + @Column(name = "created_at") + private LocalDateTime createdAt; + + public TorgiEvent(String key, Lot lotInfo) { + this.key = key; + this.status = lotInfo.getStatus(); + this.bidStart = lotInfo.getBidStart(); + this.bidEnd = lotInfo.getBidEnd(); + this.tradesDate = lotInfo.getTradesDate(); + this.startPrice = lotInfo.getStartPrice(); + this.step = lotInfo.getStep(); + this.initMoney = lotInfo.getInitMoney(); + this.finalPrice = lotInfo.getFinalPrice(); + this.notice = lotInfo.getNotice(); + this.lotNumber = lotInfo.getLotNumber(); + this.cadastral = lotInfo.getCadastral(); + this.type = lotInfo.getType(); + this.fullInfo = lotInfo.getFullInfo(); + } +} diff --git a/src/main/java/com/pledge/torgi/repositories/ConfigsRepository.java b/src/main/java/com/pledge/torgi/repositories/ConfigsRepository.java new file mode 100644 index 0000000..0b894b6 --- /dev/null +++ b/src/main/java/com/pledge/torgi/repositories/ConfigsRepository.java @@ -0,0 +1,7 @@ +package com.pledge.torgi.repositories; + +import com.pledge.torgi.model.Configs; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ConfigsRepository extends JpaRepository { +} diff --git a/src/main/java/com/pledge/torgi/repositories/EmailsRepository.java b/src/main/java/com/pledge/torgi/repositories/EmailsRepository.java new file mode 100644 index 0000000..5ef038c --- /dev/null +++ b/src/main/java/com/pledge/torgi/repositories/EmailsRepository.java @@ -0,0 +1,11 @@ +package com.pledge.torgi.repositories; + +import com.pledge.torgi.model.Emails; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface EmailsRepository extends JpaRepository { + boolean existsByMailId(String mailId); + List findTop10ByOrderByIdDesc(); +} diff --git a/src/main/java/com/pledge/torgi/repositories/TorgiEventRepository.java b/src/main/java/com/pledge/torgi/repositories/TorgiEventRepository.java new file mode 100644 index 0000000..fcac7cf --- /dev/null +++ b/src/main/java/com/pledge/torgi/repositories/TorgiEventRepository.java @@ -0,0 +1,15 @@ +package com.pledge.torgi.repositories; + +import com.pledge.torgi.model.TorgiEvent; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TorgiEventRepository extends JpaRepository { + List findAllByOrderByCreatedAtDesc(); + + Page findAllByOrderByCreatedAtDesc(Pageable pageable); + List findAllByFullInfoLikeOrderByCreatedAtDesc(String cadastral); +} diff --git a/src/main/java/com/pledge/torgi/service/DownloadHeadlessService.java b/src/main/java/com/pledge/torgi/service/DownloadHeadlessService.java new file mode 100644 index 0000000..da73082 --- /dev/null +++ b/src/main/java/com/pledge/torgi/service/DownloadHeadlessService.java @@ -0,0 +1,237 @@ +package com.pledge.torgi.service; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pledge.torgi.model.TorgiEvent; +import com.pledge.torgi.repositories.TorgiEventRepository; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFRow; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.PageRequest; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.FileSystems; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.*; +import java.util.regex.Pattern; + +import static com.pledge.torgi.utils.AppUtils.EMPTY; +import static com.pledge.torgi.utils.AppUtils.EXCEL_LINK; +import static java.util.Objects.nonNull; + +public class DownloadHeadlessService { + + private final Logger logger = LoggerFactory.getLogger(DownloadHeadlessService.class); + private final ObjectMapper objectMapper = new ObjectMapper().configure(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION, true).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).enable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); + Pattern cadastralPattern = Pattern.compile("\\d{1,2}:\\d{1,2}:\\d{1,7}:\\d{1,9}"); + Pattern vinPattern = Pattern.compile("\\b[A-HJ-NPR-Z0-9-]{12,17}\\b"); + + public Map downloadExcel() { + try { + HttpURLConnection connection = (HttpURLConnection) new URL(EXCEL_LINK).openConnection(); + connection.setRequestMethod("GET"); + if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) { + Map data = new HashMap<>(); + Workbook wb = new XSSFWorkbook(connection.getInputStream()); + Sheet sheetWithData = wb.getSheetAt(0); + int currentRow = 2; + while (sheetWithData.getRow(currentRow) != null) { + List cadastrals = extractCadastral(sheetWithData.getRow(currentRow).getCell(29).getStringCellValue()); + if (cadastrals.isEmpty()) { + Lot extractedContent = extractDataFromRow(sheetWithData.getRow(currentRow), ""); + data.put(extractedContent.getNotice()+ "_" + extractedContent.getLotNumber() + "_" + extractedContent.getStatus(), extractedContent); + } else { + for (String cadastral : cadastrals) { + Lot extractedContent = extractDataFromRow(sheetWithData.getRow(currentRow), cadastral); + data.put(extractedContent.getNotice()+ "_" + extractedContent.getLotNumber() + "_" + extractedContent.getStatus() + "_" + cadastral, extractedContent); + } + } + + currentRow++; + } + return data; + } else { + logger.error("Error in line 99 "); + return downloadExcel(); + } + } catch (Exception e) { + logger.error("Error in line 103 " + e); + } + return downloadExcel(); + } + + private Lot extractDataFromRow(Row row, String cadastral) { + Lot lot = new Lot(); + lot.setBidStart(row.getCell(6).getStringCellValue()); + lot.setBidEnd(row.getCell(7).getStringCellValue()); + lot.setCadastral(cadastral); + lot.setLotNumber((int)row.getCell(16).getNumericCellValue()); + lot.setFinalPrice(row.getCell(27).getStringCellValue()); + lot.setNotice(row.getCell(3).getStringCellValue()); + lot.setInitMoney(row.getCell(24).getStringCellValue()); + lot.setStep(row.getCell(25).getStringCellValue()); + lot.setStartPrice(row.getCell(26).getStringCellValue()); + lot.setStatus(row.getCell(17).getStringCellValue()); + lot.setTradesDate(row.getCell(9).getStringCellValue()); + lot.setFullInfo(row.getCell(29).getStringCellValue()); + lot.setType(row.getCell(1).getStringCellValue()); + return lot; + } + + public List extractCadastral(String rawValue) { + String[] split = rawValue.split(";"); + List cadastrals = new ArrayList<>(); + for (String part : split) { + int delimiter = part.indexOf(":"); + if (delimiter == -1) { + continue; + } + + if (part.trim().startsWith("Кадастровый номер") || part.trim().startsWith("VIN") || part.trim().startsWith("Условный номер земельного участка")) { + String value = part.substring(delimiter + 1).trim(); + if (value.contains("|")) { + String[] differentCadastrals = value.split("\\|"); + for (String singleCadastral : differentCadastrals) { + cadastrals.add(singleCadastral.trim()); + } + } else if (value.contains(",")) { + String[] differentCadastrals = value.split(","); + for (String singleCadastral : differentCadastrals) { + cadastrals.add(singleCadastral.trim()); + } + } else { + cadastrals.add(part.substring(delimiter + 1).trim()); + } + } + } + return cadastrals; + } + + public void createNewDocument(TorgiEventRepository eventRepository, LocalDate now) { + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + XSSFSheet sheet = workbook.createSheet("sheet"); + setRowHead(sheet); + logger.error("before write body"); + int i = 0; + int currentRow = 1; + while (true) { + List partition = eventRepository.findAllByOrderByCreatedAtDesc(PageRequest.of(i, 1000)).stream().map(Lot::new).toList(); + if (partition.isEmpty()) { + break; + } + currentRow = setRowBodyNew(sheet, currentRow, partition); + i++; + } + //setRowWith(sheet); + logger.error("before write to file"); + try (FileOutputStream fileOut = new FileOutputStream(getFileToSave(now))) { + workbook.write(fileOut); + } + } catch (Exception ex) { + logger.error("in createNewDocument " + ex.getMessage()); + } + } + + public void createNewDocument(TorgiEventRepository eventRepository, List cadastrals, LocalDate now) { + try (XSSFWorkbook workbook = new XSSFWorkbook()) { + XSSFSheet sheet = workbook.createSheet("sheet"); + setRowHead(sheet); + + List data = extractData(eventRepository, cadastrals); + int currentRow = 1; + setRowBodyNew(sheet, currentRow, data); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + workbook.write(out); + logger.error("before write to file"); + try (FileOutputStream fileOut = new FileOutputStream(getFileToSave(now))) { + workbook.write(fileOut); + } + } catch (Exception ex) { + logger.error("in createNewDocument " + ex.getMessage()); + } + } + + private List extractData(TorgiEventRepository eventRepository, List cadastrals) { + Set events = new LinkedHashSet<>(); + int i = 0; + for (String cadastral : cadastrals) { + i++; + try { + events.addAll(eventRepository.findAllByFullInfoLikeOrderByCreatedAtDesc("% " + cadastral + " %")); + } catch (Exception e) { + logger.error("Error during process cadastral = " + cadastral + " " + e); + } + if (i % 100 == 0) { + logger.error("executed = " + i + " added = " + events.size()); + } + } + return events.stream().map(Lot::new).toList(); + } + + private void setRowHead(XSSFSheet sheet) { + XSSFRow rowHead = sheet.createRow( 0); + rowHead.createCell(0).setCellValue("Статус торгов"); + rowHead.createCell(1).setCellValue("Дата начала подачи заявок"); + rowHead.createCell(2).setCellValue("Дата окончания подачи заявок"); + rowHead.createCell(3).setCellValue("Дата проведения торгов"); + rowHead.createCell(4).setCellValue("Начальная цена"); + rowHead.createCell(5).setCellValue("Шаг аукциона"); + rowHead.createCell(6).setCellValue("Размер задатка"); + rowHead.createCell(7).setCellValue("Итоговая цена"); + rowHead.createCell(8).setCellValue("Извещение"); + rowHead.createCell(9).setCellValue("Лот"); + rowHead.createCell(10).setCellValue("Кадастровый номер"); + rowHead.createCell(11).setCellValue("Общая информация"); + rowHead.createCell(12).setCellValue("Тип активности"); + rowHead.createCell(13).setCellValue("Время получения события"); + } + + private int setRowBodyNew(XSSFSheet sheet, int currentRow, Collection contents) throws JsonProcessingException { + for (Lot lot : contents) { + currentRow = createRow(sheet, currentRow, lot); + } + return currentRow; + } + + private static int createRow(XSSFSheet sheet, int rowCount, Lot lotShema) { + XSSFRow row = sheet.createRow(rowCount); + row.createCell(0).setCellValue(nonNull(lotShema.getStatus()) ? lotShema.getStatus() : EMPTY); // Статус торгов + row.createCell(1).setCellValue(nonNull(lotShema.getBidStart()) ? lotShema.getBidStart() : EMPTY); // Дата начала подачи заявок + row.createCell(2).setCellValue(nonNull(lotShema.getBidEnd()) ? lotShema.getBidEnd() : EMPTY); // Дата окончания подачи заявок + row.createCell(3).setCellValue(nonNull(lotShema.getTradesDate()) ? lotShema.getTradesDate() : EMPTY); // Дата проведения торгов + row.createCell(4).setCellValue(nonNull(lotShema.getStartPrice()) ? lotShema.getStartPrice() : EMPTY); // Начальная цена + row.createCell(5).setCellValue(nonNull(lotShema.getStep()) ? lotShema.getStep() : EMPTY); // Шаг аукциона + row.createCell(6).setCellValue(nonNull(lotShema.getInitMoney()) ? lotShema.getInitMoney() : EMPTY); // Размер задатка + row.createCell(7).setCellValue(nonNull(lotShema.getFinalPrice()) ? lotShema.getFinalPrice() : EMPTY); // Итоговая цена + row.createCell(8).setCellValue(nonNull(lotShema.getNotice()) ? lotShema.getNotice() : EMPTY); // Извещение + row.createCell(9).setCellValue(nonNull(lotShema.getLotNumber()) ? lotShema.getLotNumber() : 0); // Лот + row.createCell(10).setCellValue(lotShema.getCadastral()); + row.createCell(11).setCellValue(nonNull(lotShema.getFullInfo()) ? lotShema.getFullInfo() : EMPTY); + row.createCell(12).setCellValue(nonNull(lotShema.getType()) ? lotShema.getType() : EMPTY); + row.createCell(13).setCellValue(nonNull(lotShema.getGettedAt()) ? lotShema.getGettedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) : EMPTY); + rowCount++; + return rowCount; + } + + + private File getFileToSave(LocalDate now) { + File dir = new File(now.toString()); + if(!dir.exists()) { + dir.mkdir(); + } + return new File(dir.getAbsolutePath() + FileSystems.getDefault().getSeparator() + now + ".xlsx"); + } +} diff --git a/src/main/java/com/pledge/torgi/service/EmailData.java b/src/main/java/com/pledge/torgi/service/EmailData.java new file mode 100644 index 0000000..a11b060 --- /dev/null +++ b/src/main/java/com/pledge/torgi/service/EmailData.java @@ -0,0 +1,13 @@ +package com.pledge.torgi.service; + +import lombok.Getter; +import lombok.Setter; +import org.apache.poi.ss.usermodel.Workbook; + +@Getter +@Setter +public class EmailData { + private String emails; + private String emailId; + private Workbook file; +} diff --git a/src/main/java/com/pledge/torgi/service/EmailService.java b/src/main/java/com/pledge/torgi/service/EmailService.java new file mode 100644 index 0000000..d8eaee0 --- /dev/null +++ b/src/main/java/com/pledge/torgi/service/EmailService.java @@ -0,0 +1,44 @@ +package com.pledge.torgi.service; + +import com.pledge.torgi.controller.storage.FileService; +import com.pledge.torgi.model.Emails; +import com.pledge.torgi.repositories.EmailsRepository; +import com.pledge.torgi.utils.StartParams; +import lombok.AllArgsConstructor; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@AllArgsConstructor +public class EmailService { + + private final FileService fileService; + private final EmailsRepository emailsRepository; + + public void processEmails(StartParams params) { + List alreadyProcessed = emailsRepository.findTop10ByOrderByIdDesc(); + List emailData = new ReceiveEmailService("imap.gmail.com", 993, params.getFrom(), params.getToken(), params.getSpecialEmail()) + .downloadEmails(alreadyProcessed); + for (EmailData data : emailData) { + if (alreadyProcessed(data.getEmailId())) { + continue; + } + fileService.processSync(data.getFile(), data.getEmails()); + saveProcessedEmail(data); + } + } + + private boolean alreadyProcessed(String emailId) { + return emailsRepository.existsByMailId(emailId); + } + + private void saveProcessedEmail(EmailData data) { + Emails processed = new Emails(); + processed.setMailId(data.getEmailId()); + processed.setReplyTo(data.getEmails()); + processed.setProcessedTime(LocalDateTime.now()); + emailsRepository.save(processed); + } +} diff --git a/src/main/java/com/pledge/torgi/service/Lot.java b/src/main/java/com/pledge/torgi/service/Lot.java new file mode 100644 index 0000000..88c300b --- /dev/null +++ b/src/main/java/com/pledge/torgi/service/Lot.java @@ -0,0 +1,48 @@ +package com.pledge.torgi.service; + +import com.pledge.torgi.model.TorgiEvent; +import lombok.Getter; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Setter +@Getter +public class Lot { + + private String status; + private String bidStart; + private String bidEnd; + private String tradesDate; + private String startPrice; + private String step; + private String initMoney; + private String finalPrice; + private String notice; + private Integer lotNumber; + private String cadastral; + private String type; + + private String fullInfo; + private LocalDateTime gettedAt; + + public Lot(TorgiEvent event) { + status = event.getStatus(); + bidStart = event.getBidStart(); + bidEnd = event.getBidEnd(); + tradesDate = event.getTradesDate(); + startPrice = event.getStartPrice(); + step = event.getStep(); + initMoney = event.getInitMoney(); + finalPrice = event.getFinalPrice(); + notice = event.getNotice(); + lotNumber = event.getLotNumber(); + cadastral = event.getCadastral(); + type = event.getType(); + fullInfo = event.getFullInfo(); + gettedAt = event.getCreatedAt(); + } + + public Lot() { + } +} diff --git a/src/main/java/com/pledge/torgi/service/MainService.java b/src/main/java/com/pledge/torgi/service/MainService.java new file mode 100644 index 0000000..e7b7aae --- /dev/null +++ b/src/main/java/com/pledge/torgi/service/MainService.java @@ -0,0 +1,33 @@ +package com.pledge.torgi.service; + +import com.pledge.torgi.controller.DownloadHeadlessController; +import com.pledge.torgi.controller.storage.FileService; +import com.pledge.torgi.utils.StartParams; +import lombok.AllArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +@Service +@EnableScheduling +@AllArgsConstructor +public class MainService { + private final StartParams params; + private final DownloadHeadlessController headlessController; + private final EmailService emailService; + + @Scheduled(timeUnit = TimeUnit.SECONDS, fixedRate = 120) + public void getNewData() { + headlessController.process(); + } + + @Scheduled(timeUnit = TimeUnit.SECONDS, fixedRate = 600) + public void checkEmail() { + emailService.processEmails(params); + } +} \ No newline at end of file diff --git a/src/main/java/com/pledge/torgi/service/ReceiveEmailService.java b/src/main/java/com/pledge/torgi/service/ReceiveEmailService.java new file mode 100644 index 0000000..2877461 --- /dev/null +++ b/src/main/java/com/pledge/torgi/service/ReceiveEmailService.java @@ -0,0 +1,122 @@ +package com.pledge.torgi.service; + +import com.pledge.torgi.model.Emails; +import com.sun.mail.imap.IMAPMessage; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.util.IOUtils; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +import javax.mail.*; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMultipart; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + + +public class ReceiveEmailService { + private String username; + private String password; + private String specialEmail; + private String protocol = "imaps"; + + private final Properties prop; + public ReceiveEmailService(String host, int port, String username, String password, String specialEmail) { + prop = getServerProperties(host, port); + + this.username = username; + this.password = password; + this.specialEmail = specialEmail; + } + + public List downloadEmails(List alreadyProcessed) { + List result = new ArrayList<>(); + Session session = Session.getDefaultInstance(prop); + + try { + // connects to the message store + Store store = session.getStore(protocol); + store.connect(prop.getProperty("mail.imaps.host"), username, password); + + // opens the inbox folder + Folder folderInbox = store.getFolder("INBOX"); + folderInbox.open(Folder.READ_ONLY); + + // fetches new messages from server + int messageCount = folderInbox.getMessageCount(); + Message[] messages = folderInbox.getMessages(messageCount - 50, messageCount); + + for (int i = 0; i < messages.length; i++) { + Message msg = messages[i]; + Address[] toList = msg.getRecipients(Message.RecipientType.TO); + boolean specialAddress = Arrays.stream(toList).anyMatch(addr -> ((InternetAddress) addr).getAddress().equals(specialEmail)); + if (!specialAddress) { + continue; + } + + String messageID = ((IMAPMessage) msg).getMessageID(); + if (alreadyProcessed.stream().anyMatch(processed -> processed.getMailId().equals(messageID))) { + continue; + } + + try { + MimeMultipart content = (MimeMultipart) msg.getDataHandler().getContent(); + if (content.getCount() == 1 || !Part.ATTACHMENT.equalsIgnoreCase(content.getBodyPart(1).getDisposition())) { + continue; + } + InputStream excelIS = content.getBodyPart(1).getInputStream(); + IOUtils.setByteArrayMaxOverride(1000000000); + Workbook wb = new XSSFWorkbook(excelIS); + List replyMails = new ArrayList<>(Arrays.stream(toList).map(addr -> ((InternetAddress) addr).getAddress()) + .filter(email -> !email.equals(specialEmail)) + .toList()); + replyMails.add(((InternetAddress)((IMAPMessage) msg).getSender()).getAddress()); + EmailData data = new EmailData(); + data.setEmailId(messageID); + data.setFile(wb); + data.setEmails(String.join(",", replyMails)); + System.out.println("Emails for reply: " + data.getEmails()); + result.add(data); + } catch (Exception ex) { + System.out.println("No provider for protocol: " + protocol); + ex.printStackTrace(); + } + } + + // disconnect + folderInbox.close(false); + store.close(); + } catch (NoSuchProviderException ex) { + System.out.println("No provider for protocol: " + protocol); + ex.printStackTrace(); + } catch (MessagingException ex) { + System.out.println("Could not connect to the message store"); + ex.printStackTrace(); + } + return result; + } + + private Properties getServerProperties(String host, int port) { + + Properties properties = new Properties(); + + // server setting + properties.put(String.format("mail.%s.host", protocol), host); + properties.put(String.format("mail.%s.port", protocol), port); + + // SSL setting + properties.setProperty( + String.format("mail.%s.socketFactory.class", protocol), + "javax.net.ssl.SSLSocketFactory"); + properties.setProperty( + String.format("mail.%s.socketFactory.fallback", protocol), + "false"); + properties.setProperty( + String.format("mail.%s.socketFactory.port", protocol), + String.valueOf(port)); + + return properties; + } +} diff --git a/src/main/java/com/pledge/torgi/service/SendEmailService.java b/src/main/java/com/pledge/torgi/service/SendEmailService.java new file mode 100644 index 0000000..11bf4e9 --- /dev/null +++ b/src/main/java/com/pledge/torgi/service/SendEmailService.java @@ -0,0 +1,64 @@ +package com.pledge.torgi.service; + +import javax.activation.DataHandler; +import javax.activation.DataSource; +import javax.activation.FileDataSource; +import javax.mail.*; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import java.io.File; +import java.util.Properties; + + +public class SendEmailService { + private String username; + private String password; + + private final Properties prop; + public SendEmailService(String host, int port, String username, String password) { + prop = new Properties(); + prop.put("mail.smtp.auth", true); + prop.put("mail.smtp.starttls.enable", "true"); + prop.put("mail.smtp.host", host); + prop.put("mail.smtp.port", port); + prop.put("mail.smtp.ssl.trust", host); + + this.username = username; + this.password = password; + } + + public void sendMail(String from,String to,String subject, String msg, File archive) throws Exception { + + Session session = Session.getInstance(prop, new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + + Message message = new MimeMessage(session); + message.setFrom(new InternetAddress(from)); + message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(to)); + message.setSubject(subject); + + MimeBodyPart mimeBodyPart = new MimeBodyPart(); + mimeBodyPart.setContent(msg, "text/html; charset=utf-8"); + + + MimeBodyPart attachmentPart = new MimeBodyPart(); + DataSource source = new FileDataSource(archive); + attachmentPart.setDataHandler(new DataHandler(source)); + attachmentPart.setFileName(source.getName()); + + // Combine body and attachment parts + MimeMultipart multipart = new MimeMultipart(); + multipart.addBodyPart(mimeBodyPart); + multipart.addBodyPart(attachmentPart); + + message.setContent(multipart); + + Transport.send(message); + } +} diff --git a/src/main/java/com/pledge/torgi/utils/AppUtils.java b/src/main/java/com/pledge/torgi/utils/AppUtils.java new file mode 100644 index 0000000..f6664ee --- /dev/null +++ b/src/main/java/com/pledge/torgi/utils/AppUtils.java @@ -0,0 +1,33 @@ +package com.pledge.torgi.utils; + +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +public final class AppUtils { + + public final static Map STATUS_TRANSLATE = new HashMap() {{ + put("DETERMINING_WINNER", "Определение победителя"); + put("APPLICATIONS_SUBMISSION", "Прием заявок"); + put("CANCELED", "Отменен"); + put("PUBLISHED", "Опубликован"); + put("SUCCEED", "Состоялся"); + put("FAILED", "Не состоялся"); + }}; + public static final ArrayList CADASTRAL_NUMBER_LIST = new ArrayList() {{ + add("Кадастровый номер"); + add("Кадастровые номера"); + add("Кадастровый номер земельного участка"); + add("VIN номер"); + }}; + public static final int THREAD_SIZE = 16; + public static final String EMPTY = ""; + public static final String XLSX = ".xlsx"; + public static final int MAX_DAY = 7; + public static final String LOT_CARDS_URL = "https://torgi.gov.ru/new/api/public/lotcards/"; //example https://torgi.gov.ru/new/api/public/lotcards/21000000050000000852_1 + public static final String SEARCH_URL = "https://torgi.gov.ru/new/api/public/lotcards/search?"; //example https://torgi.gov.ru/new/api/public/lotcards/search?pubFrom=2025-02-04&pubTo=2025-02-04&byFirstVersion=true&withFacets=true&page=0&size=10&sort=firstVersionPublicationDate,desc + public static final Format DATE_FORMATTER = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + public static final String EXCEL_LINK = "https://torgi.gov.ru/new/api/public/lotcards/export/excel?byFirstVersion=true&sort=updateDate,desc"; +} diff --git a/src/main/java/com/pledge/torgi/utils/StartParams.java b/src/main/java/com/pledge/torgi/utils/StartParams.java new file mode 100644 index 0000000..c6d55f7 --- /dev/null +++ b/src/main/java/com/pledge/torgi/utils/StartParams.java @@ -0,0 +1,19 @@ +package com.pledge.torgi.utils; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "pledge.params") +public class StartParams { + + private String from; + private String token; + private String to; + private String days; + private String specialEmail; +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..2612ccf --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,14 @@ +pledge.params.from=changeit@gmail.com +pledge.params.token=changeit +pledge.params.to=m.ryabyh@gmail.com,AAAtapina@sberbank.ru,Oreshkina.V.V@sberbank.ru,MEvRyabykh@sberbank.ru +pledge.params.days=1 +pledge.params.special-email=m.ryabyh+torgi@gmail.com +spring.datasource.url=jdbc:postgresql://localhost:5432/torgi +spring.datasource.username=torgi +spring.datasource.password=17maxim02 +spring.datasource.driver-class-name=org.postgresql.Driver +spring.servlet.multipart.max-file-size=20MB +spring.servlet.multipart.max-request-size=20MB +server.tomcat.connection-timeout=300000 +server.tomcat.keep-alive-timeout=300000 +server.connection-timeout=300000 \ No newline at end of file diff --git a/src/main/resources/db/migration/V1__init_event_table.sql b/src/main/resources/db/migration/V1__init_event_table.sql new file mode 100644 index 0000000..362354c --- /dev/null +++ b/src/main/resources/db/migration/V1__init_event_table.sql @@ -0,0 +1,22 @@ +CREATE TABLE torgi_event +( + id BIGINT NOT NULL, + key VARCHAR(255), + status VARCHAR(255), + bid_start VARCHAR(255), + bid_end VARCHAR(255), + trades_date VARCHAR(255), + start_price VARCHAR(255), + step VARCHAR(255), + init_money VARCHAR(255), + final_price VARCHAR(255), + notice VARCHAR(255), + lot_number INTEGER, + cadastral VARCHAR(255), + type VARCHAR(255), + created_at TIMESTAMP, + full_info VARCHAR, + CONSTRAINT pk_torgievent PRIMARY KEY (id) +); + +CREATE SEQUENCE TORGI_EVENT_SEQ; \ No newline at end of file diff --git a/src/main/resources/db/migration/V2__add_configs.sql b/src/main/resources/db/migration/V2__add_configs.sql new file mode 100644 index 0000000..77149f9 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_configs.sql @@ -0,0 +1,7 @@ +CREATE TABLE configs +( + id BIGINT NOT NULL, + key VARCHAR(255), + value VARCHAR(255), + CONSTRAINT pk_configs PRIMARY KEY (id) +); diff --git a/src/main/resources/db/migration/V3__add_emails.sql b/src/main/resources/db/migration/V3__add_emails.sql new file mode 100644 index 0000000..3bde0b9 --- /dev/null +++ b/src/main/resources/db/migration/V3__add_emails.sql @@ -0,0 +1,10 @@ +CREATE TABLE emails +( + id BIGINT NOT NULL, + mail_id VARCHAR(255), + processed_time TIMESTAMP, + reply_to VARCHAR(255), + CONSTRAINT pk_emails PRIMARY KEY (id) +); + +CREATE SEQUENCE emails_seq; diff --git a/src/main/resources/schemas/lot-shema.json b/src/main/resources/schemas/lot-shema.json new file mode 100644 index 0000000..96e6028 --- /dev/null +++ b/src/main/resources/schemas/lot-shema.json @@ -0,0 +1,471 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "lotName": { + "type": "string" + }, + "lotStatus": { + "type": "string" + }, + "biddForm": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "noticeNumber": { + "type": "string" + }, + "lotNumber": { + "type": "integer" + }, + "biddType": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "subjectRFCode": { + "type": "string" + }, + "lotDescription": { + "type": "string" + }, + "priceMin": { + "type": "number" + }, + "priceStep": { + "type": "number" + }, + "previousProcedures": { + "type": "array" + }, + "lotImages": { + "type": "array", + "items": { + "type": "string" + } + }, + "lotAttachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fileId": { + "type": "string" + }, + "signatureId": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "fileSize": { + "type": "integer" + }, + "uploadDate": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "attachmentTypeCode": { + "type": "string" + }, + "attachmentTypeName": { + "type": "string" + }, + "inactive": { + "type": "boolean" + }, + "checkResult": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "noticeAttachments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "fileId": { + "type": "string" + }, + "signatureId": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "fileSize": { + "type": "integer" + }, + "uploadDate": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "attachmentTypeCode": { + "type": "string" + }, + "attachmentTypeName": { + "type": "string" + }, + "inactive": { + "type": "boolean" + }, + "checkResult": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "characteristics": { + "type": "array", + "items": { + "type": "object", + "properties": { + "characteristicValue": { + "anyOf": [ + { + "type": [ + "number", + "string" + ] + }, + { + "type": "array", + "items": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "selectNsi": { + "type": "string" + }, + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "actual": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + ] + }, + "name": { + "type": "string" + }, + "code": { + "type": "string" + }, + "type": { + "type": "string" + }, + "unit": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + }, + "currencyCode": { + "type": "string" + }, + "etpCode": { + "type": "string" + }, + "category": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "timeZoneName": { + "type": "string" + }, + "timezoneOffset": { + "type": "string" + }, + "ownershipForm": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "etpUrl": { + "type": "string" + }, + "deposit": { + "type": "number" + }, + "estateAddress": { + "type": "string" + }, + "attributes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "value": { + "anyOf": [ + { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "type": "string" + } + ] + }, + "attributeType": { + "type": "string" + }, + "group": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "displayGroupType": { + "type": "string" + } + }, + "additionalProperties": false + }, + "sortOrder": { + "type": "integer" + } + }, + "additionalProperties": false + } + }, + "depositElectronicPlatform": { + "type": "boolean" + }, + "depositRecipientName": { + "type": "string" + }, + "depositRecipientINN": { + "type": "string" + }, + "depositRecipientKPP": { + "type": "string" + }, + "depositBankName": { + "type": "string" + }, + "depositBIK": { + "type": "string" + }, + "depositPayAccount": { + "type": "string" + }, + "depositCorAccount": { + "type": "string" + }, + "depositPurposePayment": { + "type": "string" + }, + "hasAppeals": { + "type": "boolean" + }, + "isStopped": { + "type": "boolean" + }, + "auctionStartDate": { + "type": "string", + "format": "date-time" + }, + "biddStartTime": { + "type": "string", + "format": "date-time" + }, + "biddEndTime": { + "type": "string", + "format": "date-time" + }, + "versionId": { + "type": "string" + }, + "noticeSignedData": { + "type": "object", + "properties": { + "fileId": { + "type": "string" + }, + "signatureId": { + "type": "string" + }, + "fileName": { + "type": "string" + }, + "fileSize": { + "type": "integer" + }, + "uploadDate": { + "type": "string" + }, + "hash": { + "type": "string" + }, + "inactive": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "egrkns": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "number": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "egrnInfoRequestList": { + "type": "array", + "items": { + "type": "object", + "properties": { + "requestId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "hasRestrictions": { + "type": "boolean" + }, + "cadastralOrderNumber": { + "type": "integer" + }, + "objects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "objectTypeCode": { + "type": "string" + }, + "cadastralNumber": { + "type": "string" + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + } + }, + "noticeFirstVersionPublicationDate": { + "type": "string" + }, + "isAnnulled": { + "type": "boolean" + }, + "lotVat": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "npaHintCode": { + "type": "string" + }, + "typeTransaction": { + "type": "string" + }, + "priceFin": { + "type": "integer" + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/src/main/resources/schemas/shema.json b/src/main/resources/schemas/shema.json new file mode 100644 index 0000000..b4071aa --- /dev/null +++ b/src/main/resources/schemas/shema.json @@ -0,0 +1,268 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Generated schema for Root", + "type": "object", + "properties": { + "content": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "noticeNumber": { + "type": "string" + }, + "lotNumber": { + "type": "number" + }, + "lotStatus": { + "type": "string" + }, + "biddType": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "biddForm": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "lotName": { + "type": "string" + }, + "lotDescription": { + "type": "string" + }, + "biddEndTime": { + "type": "string" + }, + "lotImages": { + "type": "array", + "items": { + "type": "string" + } + }, + "characteristics": { + "type": "array", + "items": { + "type": "object", + "properties": { + "characteristicValue": {}, + "name": { + "type": "string" + }, + "code": { + "type": "string" + }, + "type": { + "type": "string" + }, + "unit": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "symbol": { + "type": "string" + } + } + } + } + } + }, + "currencyCode": { + "type": "string" + }, + "subjectRFCode": { + "type": "string" + }, + "category": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "createDate": { + "type": "string" + }, + "timeZoneName": { + "type": "string" + }, + "timezoneOffset": { + "type": "string" + }, + "hasAppeals": { + "type": "boolean" + }, + "isStopped": { + "type": "boolean" + }, + "attributes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "fullName": { + "type": "string" + }, + "attributeType": { + "type": "string" + }, + "group": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "displayGroupType": { + "type": "string" + } + } + }, + "sortOrder": { + "type": "number" + }, + "value": {} + } + } + }, + "isAnnulled": { + "type": "boolean" + }, + "noticeFirstVersionPublicationDate": { + "type": "string" + }, + "npaHintCode": { + "type": "string" + }, + "typeTransaction": { + "type": "string" + }, + "priceMin": { + "type": "number" + }, + "etpCode": { + "type": "string" + }, + "lotVat": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } + } + }, + "pageable": { + "type": "object", + "properties": { + "sort": { + "type": "object", + "properties": { + "unsorted": { + "type": "boolean" + }, + "sorted": { + "type": "boolean" + }, + "empty": { + "type": "boolean" + } + } + }, + "pageNumber": { + "type": "number" + }, + "pageSize": { + "type": "number" + }, + "offset": { + "type": "number" + }, + "paged": { + "type": "boolean" + }, + "unpaged": { + "type": "boolean" + } + } + }, + "categoryFacet": { + "type": "array", + "items": {} + }, + "totalPages": { + "type": "number" + }, + "totalElements": { + "type": "number" + }, + "last": { + "type": "boolean" + }, + "numberOfElements": { + "type": "number" + }, + "first": { + "type": "boolean" + }, + "size": { + "type": "number" + }, + "number": { + "type": "number" + }, + "sort": { + "type": "object", + "properties": { + "unsorted": { + "type": "boolean" + }, + "sorted": { + "type": "boolean" + }, + "empty": { + "type": "boolean" + } + } + }, + "empty": { + "type": "boolean" + } + } +} \ No newline at end of file diff --git a/src/main/resources/templates/uploadForm.html b/src/main/resources/templates/uploadForm.html new file mode 100644 index 0000000..8843ee5 --- /dev/null +++ b/src/main/resources/templates/uploadForm.html @@ -0,0 +1,22 @@ + + + +
+

+

+
+

Нужно загрузить эксель файл на 1 листе которого в первом столбце будут кадастровые или vin номера тех объектов, информацию по которым необходимо получить

+
+
+
+
+ + + + +
Выберите файл:
Почта для получения:
+
+
+ + + \ No newline at end of file