working code
commit
eff8275ff8
|
@ -0,0 +1,2 @@
|
|||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
|
@ -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/
|
|
@ -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.9/apache-maven-3.9.9-bin.zip
|
|
@ -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-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
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 "$@"
|
|
@ -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-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
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"
|
|
@ -0,0 +1,124 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.1</version>
|
||||
<relativePath/> <!-- Lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<groupId>com.email.classifier</groupId>
|
||||
<artifactId>email-classifier</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>email-classifier</name>
|
||||
<description>Gen AI Email Processing System</description>
|
||||
|
||||
<properties>
|
||||
<java.version>17</java.version> <!-- Spring Boot 3.4.1 supports Java 17 -->
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-email</artifactId>
|
||||
<version>1.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sun.mail</groupId>
|
||||
<artifactId>javax.mail</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.http-client</groupId>
|
||||
<artifactId>google-http-client-jackson2</artifactId>
|
||||
<version>1.43.3</version> <!-- Use the latest available version -->
|
||||
</dependency>
|
||||
<!-- Spring Boot Starters (No Version Needed) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Google API Client for Gmail -->
|
||||
<dependency>
|
||||
<groupId>com.google.api-client</groupId>
|
||||
<artifactId>google-api-client</artifactId>
|
||||
<version>1.35.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.oauth-client</groupId>
|
||||
<artifactId>google-oauth-client-jetty</artifactId>
|
||||
<version>1.34.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.apis</groupId>
|
||||
<artifactId>google-api-services-gmail</artifactId>
|
||||
<version>v1-rev110-1.25.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- OpenAI API Integration (REST API-Based) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok (for reducing boilerplate code) -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.36</version> <!-- Use the latest version -->
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Testing Dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-core</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mockito</groupId>
|
||||
<artifactId>mockito-junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,28 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AIProcessingService {
|
||||
|
||||
private final OpenAIClient openAIClient;
|
||||
|
||||
public String processEmail(String emailBody) {
|
||||
log.info("Sending email content to OpenAI for classification...");
|
||||
String aiResponse = null;
|
||||
try {
|
||||
aiResponse = openAIClient.sendRequest(emailBody);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
log.info("AI Classification Response: {}", aiResponse);
|
||||
return aiResponse;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
public class Application {
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.mail.Email;
|
||||
import org.apache.commons.mail.EmailException;
|
||||
import org.apache.commons.mail.SimpleEmail;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class CommonsEmailService {
|
||||
|
||||
@Value("${spring.mail.host}")
|
||||
private String smtpHost;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String senderEmail;
|
||||
|
||||
@Value("${spring.mail.password}")
|
||||
private String smtpPassword;
|
||||
|
||||
@Value("${spring.mail.port}")
|
||||
private int smtpPort;
|
||||
|
||||
public void sendEmail(String toEmail, String subject, String ticketUrl) {
|
||||
try {
|
||||
Email email = new SimpleEmail();
|
||||
email.setHostName(smtpHost);
|
||||
email.setSmtpPort(smtpPort);
|
||||
email.setAuthentication(senderEmail, smtpPassword);
|
||||
email.setSSLOnConnect(true);
|
||||
email.setFrom(senderEmail);
|
||||
email.setSubject("Support Ticket Created for " + subject);
|
||||
email.setMsg(getBody(subject, ticketUrl));
|
||||
email.addTo(toEmail);
|
||||
email.send();
|
||||
|
||||
log.info("Email sent successfully to {}", toEmail);
|
||||
} catch (EmailException e) {
|
||||
log.error("Error sending email via Apache Commons Email", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getBody(String subject, String ticketUrl) {
|
||||
return "Dear User,\n\nYour support request has been logged in Jira.\n" +
|
||||
"\nThis is in regard with your email with subject: " + subject +
|
||||
"\n\nYou can track the issue at: " + ticketUrl + "\n\nBest regards,\nSupport Team";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EmailProcessingService {
|
||||
|
||||
private final AIProcessingService aiProcessingService;
|
||||
private final JiraService jiraService;
|
||||
|
||||
public void processEmail(String emailBody) {
|
||||
log.info("Processing email...");
|
||||
|
||||
// Step 1: Send email content to AI for classification
|
||||
String aiResponse = aiProcessingService.processEmail(emailBody);
|
||||
Map<String, String> extractedData = extractKeyDetails(aiResponse);
|
||||
|
||||
String requestType = extractedData.get("requestType");
|
||||
String subRequestType = extractedData.get("subRequestType");
|
||||
|
||||
log.info("Email classified as: {} -> {}", requestType, subRequestType);
|
||||
|
||||
// Step 2: Create Jira ticket based on classification
|
||||
String summary = "New Service Request: " + requestType;
|
||||
String description = "Details: " + emailBody + "\n\nAI Classification: " + aiResponse;
|
||||
|
||||
// jiraService.createJiraTicket(summary, description);
|
||||
|
||||
log.info("Jira ticket created successfully.");
|
||||
}
|
||||
|
||||
private Map<String, String> extractKeyDetails(String aiResponse) {
|
||||
// Simulating AI response parsing
|
||||
return Map.of(
|
||||
"requestType", "Loan Modification",
|
||||
"subRequestType", "Interest Rate Change"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.mail.*;
|
||||
import javax.mail.internet.MimeMultipart;
|
||||
import javax.mail.search.FlagTerm;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class GmailIMAPService {
|
||||
|
||||
|
||||
@Value("${spring.mail.password}")
|
||||
private String gmailAppPassword;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String gmailUserName;
|
||||
|
||||
private static final String HOST = "imap.gmail.com";
|
||||
private static final String LABEL_NAME = "email-classification"; // Folder name for the label
|
||||
|
||||
private final AIProcessingService aiProcessingService;
|
||||
private final JiraService jiraService;
|
||||
private final CommonsEmailService commonsEmailService;
|
||||
|
||||
// @Scheduled(fixedRate = 60000) // Check every 60 seconds
|
||||
public void fetchUnreadEmails() {
|
||||
log.info("Checking for new unread emails in label '{}'", LABEL_NAME);
|
||||
Properties properties = new Properties();
|
||||
properties.put("mail.imap.host", HOST);
|
||||
properties.put("mail.imap.port", "993");
|
||||
properties.put("mail.imap.starttls.enable", "true");
|
||||
|
||||
try {
|
||||
Session session = Session.getDefaultInstance(properties);
|
||||
Store store = session.getStore("imaps");
|
||||
store.connect(HOST, gmailUserName, gmailAppPassword);
|
||||
|
||||
Folder labelFolder = store.getFolder(LABEL_NAME);
|
||||
labelFolder.open(Folder.READ_WRITE);
|
||||
|
||||
Message[] messages = labelFolder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false)); // Fetch unread emails
|
||||
log.info("Found {} unread emails in '{}'", messages.length, LABEL_NAME);
|
||||
|
||||
for (Message message : messages) {
|
||||
processEmail(message);
|
||||
message.setFlag(Flags.Flag.SEEN, true); // Mark email as read
|
||||
}
|
||||
|
||||
labelFolder.close(false);
|
||||
store.close();
|
||||
} catch (Exception e) {
|
||||
log.error("Error fetching emails from '{}': ", LABEL_NAME, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void processEmail(Message message) throws Exception {
|
||||
log.info("Processing email from: {}", message.getFrom()[0]);
|
||||
log.info("Subject: {}", message.getSubject());
|
||||
|
||||
String content = getTextFromMessage(message);
|
||||
log.info("Email Body: {}", content);
|
||||
|
||||
// Send email text to OpenAI for classification
|
||||
String classification = aiProcessingService.processEmail(content);
|
||||
log.info("AI Classification: {}", classification);
|
||||
Map<String, String> extractedData = extractCategoryFromAIResponse(classification);
|
||||
String category = extractedData.getOrDefault("category", "Unknown");
|
||||
String subCategory = extractedData.getOrDefault("subCategory", "Unknown");
|
||||
|
||||
log.info("Extracted Category: {} | Subcategory: {}", category, subCategory);
|
||||
|
||||
String reporterEmail = message.getFrom()[0].toString();
|
||||
String ticketUrl = jiraService.createJiraTicket(message.getSubject(), content, category, subCategory, reporterEmail);
|
||||
|
||||
if (ticketUrl != null) {
|
||||
commonsEmailService.sendEmail(reporterEmail, message.getSubject(), ticketUrl);
|
||||
}
|
||||
|
||||
log.info("Jira ticket created and auto-reply sent for email: {}", message.getSubject());
|
||||
}
|
||||
|
||||
private Map<String, String> extractCategoryFromAIResponse(String aiResponse) {
|
||||
try {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
Map<String, String> responseMap = objectMapper.readValue(aiResponse, new TypeReference<Map<String, String>>() {});
|
||||
|
||||
String category = responseMap.getOrDefault("category", "Unknown");
|
||||
String subCategory = responseMap.getOrDefault("subCategory", "Unknown");
|
||||
|
||||
log.info("Extracted Category: {} | Subcategory: {}", category, subCategory);
|
||||
return Map.of("category", category, "subCategory", subCategory);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error extracting category from AI response", e);
|
||||
return Map.of("category", "Unknown", "subCategory", "Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
private String getTextFromMessage(Message message) throws Exception {
|
||||
if (message.isMimeType("text/plain")) {
|
||||
return message.getContent().toString();
|
||||
} else if (message.isMimeType("multipart/*")) {
|
||||
MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
|
||||
return getTextFromMimeMultipart(mimeMultipart);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
public void listFolders() {
|
||||
try {
|
||||
Properties properties = new Properties();
|
||||
properties.put("mail.imap.ssl.enable", "true");
|
||||
|
||||
Session session = Session.getInstance(properties);
|
||||
Store store = session.getStore("imaps");
|
||||
store.connect("imap.gmail.com", gmailUserName, gmailAppPassword);
|
||||
|
||||
Folder[] folders = store.getDefaultFolder().list();
|
||||
for (Folder folder : folders) {
|
||||
System.out.println("Found folder: " + folder.getFullName());
|
||||
}
|
||||
|
||||
store.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private String getTextFromMimeMultipart(MimeMultipart mimeMultipart) throws Exception {
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < mimeMultipart.getCount(); i++) {
|
||||
BodyPart bodyPart = mimeMultipart.getBodyPart(i);
|
||||
if (bodyPart.isMimeType("text/plain")) {
|
||||
result.append(bodyPart.getContent());
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class JiraService {
|
||||
|
||||
@Value("${jira.token}")
|
||||
private String jiraToken;
|
||||
|
||||
@Value("${jira.user}")
|
||||
private String jiraUser;
|
||||
|
||||
@Value("${jira.host}")
|
||||
private String jiraHost;
|
||||
|
||||
private final RestTemplate restTemplate = new RestTemplate();
|
||||
|
||||
public String createJiraTicket(String subject, String emailBody, String category, String subCategory, String reporterEmail) {
|
||||
try {
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBasicAuth(jiraUser, jiraToken);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"fields", Map.of(
|
||||
"project", Map.of("key", "SCRUM"), // Replace with your Jira project key
|
||||
"summary", "[Support Request] " + subject,
|
||||
"description", String.format(
|
||||
"**Category:** %s\n**Subcategory:** %s\n**Reported By:** %s\n\n**Issue Details:**\n%s",
|
||||
category, subCategory, reporterEmail, emailBody
|
||||
),
|
||||
"issuetype", Map.of("name", "Task"), // Change to "Bug" or "Incident" if needed
|
||||
"labels", List.of(category.replace(" ", "_"), subCategory.replace(" ", "_")),
|
||||
"reporter", Map.of("accountId", "712020:2cf94bb8-d7c4-4557-8511-0bae7381f521")
|
||||
)
|
||||
);
|
||||
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(request, headers);
|
||||
ResponseEntity<Map> response = restTemplate.postForEntity(jiraHost, entity, Map.class);
|
||||
String ticketId = response.getBody().get("key").toString();
|
||||
String ticketUrl = jiraHost.replace("/rest/api/2/issue", "/browse/") + ticketId;
|
||||
|
||||
log.info("Jira ticket created successfully: {}", ticketId);
|
||||
return ticketUrl;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Error creating Jira ticket", e);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Data
|
||||
@RequiredArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class LLMQueryResponse {
|
||||
|
||||
private Boolean validSql;
|
||||
private String sql;
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class OpenAIClient {
|
||||
@Qualifier("openAiClient") // if you used a named bean
|
||||
private final WebClient webClient;
|
||||
|
||||
@Value("${spring.ai.openai.api-key}")
|
||||
private String openAiApiKey;
|
||||
|
||||
@Value("${spring.ai.openai.chat.model}")
|
||||
private String openAiModel;
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
public static final Map<String, List<String>> CATEGORY_MAP = new HashMap<>();
|
||||
|
||||
static {
|
||||
CATEGORY_MAP.put("Authentication Issues", Arrays.asList("Invalid Credentials", "Two-Factor Authentication (2FA) Issues", "Password Reset", "Account Lockout"));
|
||||
|
||||
CATEGORY_MAP.put("API Access Issues", Arrays.asList("401 Unauthorized", "403 Forbidden", "API Key Not Working", "Rate Limit Exceeded", "Missing Permissions"));
|
||||
|
||||
CATEGORY_MAP.put("Payment & Billing", Arrays.asList("Payment Failure", "Subscription Issues", "Invoice Requests", "Refund Requests"));
|
||||
|
||||
CATEGORY_MAP.put("Technical Issues", Arrays.asList("Server Downtime", "Slow Response Time", "Database Errors", "Integration Issues"));
|
||||
|
||||
CATEGORY_MAP.put("Product Support", Arrays.asList("Feature Requests", "Bug Reports", "Performance Optimization", "Usage Guidelines"));
|
||||
|
||||
CATEGORY_MAP.put("General Inquiries", Arrays.asList("Account Information", "Company Policies", "Partnership Opportunities"));
|
||||
}
|
||||
|
||||
private static final String OPENAI_VISION_URL = "https://api.openai.com/v1/chat/completions";
|
||||
|
||||
/**
|
||||
* Extracts text from an uploaded image using OpenAI Vision API.
|
||||
*/
|
||||
public String sendRequest(String emailContent) throws IOException {
|
||||
Map<String, Object> request = Map.of("model", openAiModel,
|
||||
"messages", List.of(
|
||||
Map.of("role", "system", "content", "You are an AI assistant that classifies emails into categories and subcategories."),
|
||||
Map.of("role", "user", "content", "Please classify the following email:\n\n" + emailContent),
|
||||
Map.of("role", "user", "content", "Please provide output in json format containing 2 keys: category and subCategory"),
|
||||
Map.of("role", "user", "content", "please provide optput in this format only and nothing more than that: {\\n \\\"category\\\": \\\"API Access Issues\\\",\\n \\\"subCategory\\\": \\\"API Key Not Working\\\"\\n}"),
|
||||
Map.of("role", "user", "content", "Please categorize among these mentioned category and subCategory: " + CATEGORY_MAP.toString())),
|
||||
"max_tokens", 500);
|
||||
String requestBody = objectMapper.writeValueAsString(request);
|
||||
log.info("Sending request to OpenAI: {}", requestBody); // ✅ Log the request JSON for debugging
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(openAiApiKey);
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// Send request to OpenAI
|
||||
ResponseEntity<String> responseEntity = restTemplate.exchange(OPENAI_VISION_URL, HttpMethod.POST, requestEntity, String.class);
|
||||
|
||||
if (responseEntity.getStatusCode() == HttpStatus.OK) {
|
||||
JsonNode jsonResponse = objectMapper.readTree(responseEntity.getBody());
|
||||
log.info(responseEntity.getBody());
|
||||
return jsonResponse.get("choices").get(0).get("message").get("content").asText();
|
||||
} else {
|
||||
log.error("OpenAI API Error: {}", responseEntity.getBody());
|
||||
return "Error extracting text from image.";
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getRequiredTables(String question, Set<String> tableNames) {
|
||||
String prompt = """
|
||||
You are an assistant that selects ALL relevant table names needed to answer a question.
|
||||
|
||||
Rules:
|
||||
- If the question refers to a "user", "customer", "employee", or similar, include the corresponding user-related table.
|
||||
- If the question refers to tasks, orders, payments, etc., include both that table AND any related user or organization tables.
|
||||
- If the question mentions attributes like "organization", "company", "department", or "team", include the organization-related table.
|
||||
- Always return only the minimal set of tables required, but never omit necessary join tables.
|
||||
|
||||
Available tables: %s
|
||||
|
||||
Question: %s
|
||||
|
||||
Return only a JSON array like: ["users", "tasks", "organizations"]
|
||||
""".formatted(String.join(", ", tableNames), question);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"model", openAiModel,
|
||||
"messages", List.of(Map.of("role", "user", "content", prompt))
|
||||
);
|
||||
|
||||
String rawResponse = webClient.post()
|
||||
.uri("/v1/chat/completions")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + openAiApiKey)
|
||||
.header(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.map(res -> {
|
||||
List<Map<String, Object>> choices = (List<Map<String, Object>>) res.get("choices");
|
||||
Map<String, Object> message = (Map<String, Object>) choices.get(0).get("message");
|
||||
return message.get("content").toString().trim();
|
||||
})
|
||||
.block();
|
||||
|
||||
try {
|
||||
return objectMapper.readValue(rawResponse, List.class);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse table list from LLM response: " + rawResponse, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String translateToSql(String question, String schema) {
|
||||
String prompt = """
|
||||
You are an expert SQL assistant. Convert the question into a SQL query.
|
||||
|
||||
### Rules:
|
||||
- Use only the tables and columns from the schema.
|
||||
- Always respect foreign key relationships when joining tables.
|
||||
- Never invent columns or tables not present in schema.
|
||||
- If filtering by a user, customer, or employee, always join to the related table via foreign key.
|
||||
- Use compact single-line SQL (no newlines).
|
||||
- Please validate the generated sql against the schema and make sure i do not get BadSQLGrammar exceptions
|
||||
|
||||
|
||||
Output must be in **strict JSON** format only mentioned as below (no explanations, no extra text):
|
||||
{
|
||||
"validSql": true/false,
|
||||
"sql": "the SQL query here (or empty if invalid)"
|
||||
}
|
||||
|
||||
### Schema:
|
||||
%s
|
||||
|
||||
### Question:
|
||||
%s
|
||||
""".formatted(schema, question);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"model", openAiModel,
|
||||
"messages", List.of(
|
||||
Map.of("role", "system", "content", "You are a SQL expert that strictly follows schema and foreign keys."),
|
||||
Map.of("role", "user", "content", prompt)
|
||||
)
|
||||
);
|
||||
|
||||
return webClient.post()
|
||||
.uri("https://api.openai.com/v1/chat/completions")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + openAiApiKey)
|
||||
.header(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.map(res -> {
|
||||
Map<String, Object> choice = ((List<Map<String, Object>>) res.get("choices")).get(0);
|
||||
Map<String, String> message = (Map<String, String>) choice.get("message");
|
||||
return message.get("content").trim();
|
||||
})
|
||||
.block();
|
||||
}
|
||||
public String repairSql(String question, String schema, String previousSql, String errorMessage) {
|
||||
String prompt = """
|
||||
You are an expert SQL assistant. The following SQL failed during execution.
|
||||
|
||||
### Original Question:
|
||||
%s
|
||||
|
||||
### Schema:
|
||||
%s
|
||||
|
||||
### Previous SQL:
|
||||
%s
|
||||
|
||||
### Error:
|
||||
%s
|
||||
|
||||
### Instructions:
|
||||
- Correct the SQL query so it works against the given schema.
|
||||
- Only use tables and columns that exist in the schema.
|
||||
- Respect foreign keys and relationships.
|
||||
- Do not invent columns or tables.
|
||||
- Output must be in **strict JSON** format only (no explanations, no extra text):
|
||||
|
||||
{
|
||||
"sql": "the corrected SQL query here"
|
||||
}
|
||||
""".formatted(question, schema, previousSql, errorMessage);
|
||||
|
||||
Map<String, Object> request = Map.of(
|
||||
"model", openAiModel,
|
||||
"messages", List.of(
|
||||
Map.of("role", "system", "content", "You are a SQL expert that strictly follows schema and foreign keys."),
|
||||
Map.of("role", "user", "content", prompt)
|
||||
)
|
||||
);
|
||||
|
||||
return webClient.post()
|
||||
.uri("https://api.openai.com/v1/chat/completions")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + openAiApiKey)
|
||||
.header(HttpHeaders.CONTENT_TYPE, "application/json")
|
||||
.bodyValue(request)
|
||||
.retrieve()
|
||||
.bodyToMono(Map.class)
|
||||
.map(res -> {
|
||||
Map<String, Object> choice = ((List<Map<String, Object>>) res.get("choices")).get(0);
|
||||
Map<String, String> message = (Map<String, String>) choice.get("message");
|
||||
return message.get("content").trim();
|
||||
})
|
||||
.block();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import com.google.common.net.HttpHeaders;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
@Configuration
|
||||
public class OpenAIConfig {
|
||||
|
||||
@Value("${spring.ai.openai.api-key}")
|
||||
private String openAiApiKey;
|
||||
|
||||
|
||||
@Bean
|
||||
public WebClient openAiClient() {
|
||||
return WebClient.builder()
|
||||
.baseUrl("https://api.openai.com")
|
||||
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + openAiApiKey)
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
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/query")
|
||||
@RequiredArgsConstructor
|
||||
public class QueryController {
|
||||
|
||||
private final QueryService queryService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<QueryResponse> query(@RequestBody QueryRequest request) {
|
||||
return ResponseEntity.ok(queryService.handleQuery(request.getQuestion()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class QueryRequest {
|
||||
private String question;
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@RequiredArgsConstructor
|
||||
@Builder
|
||||
public class QueryResponse {
|
||||
private String message;
|
||||
private String generatedSql;
|
||||
private int records_count;
|
||||
private List<Map<String, Object>> result;
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class QueryService {
|
||||
|
||||
private final OpenAIClient openAIClient;
|
||||
private final SchemaExtractor schemaExtractor;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
private static final int MAX_REPAIR_ATTEMPTS = 3;
|
||||
|
||||
public QueryResponse handleQuery(String question) {
|
||||
List<String> requiredTables = openAIClient.getRequiredTables(question, schemaExtractor.getAllTableNames());
|
||||
String schema = schemaExtractor.getRequiredSchemaData(requiredTables);
|
||||
|
||||
// Main LLM Repair loop
|
||||
String sql = null;
|
||||
LLMQueryResponse llmQueryResponse = null;
|
||||
String lastException = null;
|
||||
|
||||
for (int attempt = 1; attempt <= MAX_REPAIR_ATTEMPTS; attempt++) {
|
||||
log.info("Attempt {} to generate SQL for question: {}", attempt, question);
|
||||
|
||||
// Get SQL from LLM (first attempt = direct, retries = repair)
|
||||
if (attempt == 1) {
|
||||
sql = openAIClient.translateToSql(question, schema);
|
||||
} else {
|
||||
sql = openAIClient.repairSql(question, schema, sql, lastException);
|
||||
}
|
||||
|
||||
String sanitizedJson = sql
|
||||
.replaceAll("```json", "")
|
||||
.replaceAll("```", "");
|
||||
|
||||
try {
|
||||
llmQueryResponse = objectMapper.readValue(sanitizedJson, LLMQueryResponse.class);
|
||||
} catch (JsonProcessingException e) {
|
||||
String errorId = UUID.randomUUID().toString();
|
||||
log.error("Error id: " + errorId + " Failed to parse LLM response: {}", e.getMessage());
|
||||
return QueryResponse.builder()
|
||||
.message("Unable to process request, please try again later or contact support! Error Id: " + errorId)
|
||||
.records_count(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
if (llmQueryResponse.getValidSql() != null && !llmQueryResponse.getValidSql()) {
|
||||
return QueryResponse.builder()
|
||||
.message("I can only help you with Vider data related queries")
|
||||
.records_count(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
// Try executing SQL
|
||||
try {
|
||||
List<Map<String, Object>> result = jdbcTemplate.queryForList(llmQueryResponse.getSql());
|
||||
return QueryResponse.builder()
|
||||
.records_count(result.size())
|
||||
.generatedSql(llmQueryResponse.getSql())
|
||||
.result(result)
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
StringWriter sw = new StringWriter();
|
||||
e.printStackTrace(new PrintWriter(sw));
|
||||
lastException = sw.toString();
|
||||
log.error("SQL execution failed at attempt {}: {}", attempt, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
String errorId = UUID.randomUUID().toString();
|
||||
log.error("Error id: " + errorId + " Failed to parse LLM response: {}", lastException);
|
||||
return QueryResponse.builder()
|
||||
.message("Unable to process request, please try again later or contact support! Error Id: " + errorId)
|
||||
.records_count(0)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
//package com.email.classifier;
|
||||
//
|
||||
//import jakarta.annotation.PostConstruct;
|
||||
//import lombok.RequiredArgsConstructor;
|
||||
//import org.springframework.stereotype.Component;
|
||||
//
|
||||
//import javax.sql.DataSource;
|
||||
//import java.sql.*;
|
||||
//import java.util.*;
|
||||
//
|
||||
//@Component
|
||||
//@RequiredArgsConstructor
|
||||
//public class SchemaExtractor {
|
||||
//
|
||||
// private final DataSource dataSource;
|
||||
//
|
||||
// private final Map<String, List<String>> schemaMap = new HashMap<>();
|
||||
// private final Map<String, List<String>> foreignKeyMap = new HashMap<>();
|
||||
//
|
||||
// public Set<String> getAllTableNames() {
|
||||
// return schemaMap.keySet();
|
||||
// }
|
||||
//
|
||||
// public String getRequiredSchemaData(List<String> tableNames) {
|
||||
// StringBuilder schemaBuilder = new StringBuilder();
|
||||
//
|
||||
// tableNames.forEach(table -> {
|
||||
// schemaBuilder.append("Table: ").append(table).append("\nColumns: ");
|
||||
// List<String> columns = schemaMap.getOrDefault(table, new ArrayList<>());
|
||||
// schemaBuilder.append(String.join(", ", columns)).append("\n");
|
||||
//
|
||||
// List<String> fks = foreignKeyMap.getOrDefault(table, new ArrayList<>());
|
||||
// if (!fks.isEmpty()) {
|
||||
// schemaBuilder.append("Foreign Keys: ").append(String.join(", ", fks)).append("\n");
|
||||
// }
|
||||
//
|
||||
// schemaBuilder.append("\n");
|
||||
// });
|
||||
//
|
||||
// return schemaBuilder.toString();
|
||||
// }
|
||||
//
|
||||
// @PostConstruct
|
||||
// public void extractSchema() {
|
||||
// try (Connection conn = dataSource.getConnection()) {
|
||||
// DatabaseMetaData metaData = conn.getMetaData();
|
||||
//
|
||||
// // ✅ Extract Tables & Columns
|
||||
// ResultSet tables = metaData.getTables(conn.getCatalog(), null, "%", new String[]{"TABLE"});
|
||||
// while (tables.next()) {
|
||||
// String tableName = tables.getString("TABLE_NAME");
|
||||
//
|
||||
// ResultSet columns = metaData.getColumns(conn.getCatalog(), null, tableName, "%");
|
||||
// List<String> columnNames = new ArrayList<>();
|
||||
// while (columns.next()) {
|
||||
// columnNames.add(columns.getString("COLUMN_NAME"));
|
||||
// }
|
||||
// schemaMap.put(tableName, columnNames);
|
||||
// }
|
||||
//
|
||||
// // ✅ Extract Foreign Keys
|
||||
// for (String tableName : schemaMap.keySet()) {
|
||||
// ResultSet fks = metaData.getImportedKeys(conn.getCatalog(), null, tableName);
|
||||
// List<String> fkList = new ArrayList<>();
|
||||
// while (fks.next()) {
|
||||
// String fkColumn = fks.getString("FKCOLUMN_NAME");
|
||||
// String pkTable = fks.getString("PKTABLE_NAME");
|
||||
// String pkColumn = fks.getString("PKCOLUMN_NAME");
|
||||
// fkList.add(fkColumn + " -> " + pkTable + "." + pkColumn);
|
||||
// }
|
||||
// foreignKeyMap.put(tableName, fkList);
|
||||
// }
|
||||
//
|
||||
// } catch (SQLException e) {
|
||||
// throw new RuntimeException("Error extracting schema", e);
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
package com.email.classifier;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.*;
|
||||
import java.util.*;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class SchemaExtractor {
|
||||
|
||||
private final DataSource dataSource;
|
||||
|
||||
private final Map<String, TableSchema> schemaMap = new HashMap<>();
|
||||
|
||||
public Set<String> getAllTableNames() {
|
||||
return schemaMap.keySet();
|
||||
}
|
||||
|
||||
public TableSchema getTableSchema(String tableName) {
|
||||
return schemaMap.get(tableName);
|
||||
}
|
||||
|
||||
public String getRequiredSchemaData(List<String> tableNames) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
tableNames.forEach(table -> {
|
||||
TableSchema schema = schemaMap.get(table);
|
||||
if (schema != null) {
|
||||
sb.append(schema.toPrettyString()).append("\n\n");
|
||||
}
|
||||
});
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void extractSchema() {
|
||||
try (Connection conn = dataSource.getConnection()) {
|
||||
DatabaseMetaData metaData = conn.getMetaData();
|
||||
|
||||
ResultSet tables = metaData.getTables(conn.getCatalog(), null, "%", new String[]{"TABLE", "VIEW"});
|
||||
while (tables.next()) {
|
||||
String tableName = tables.getString("TABLE_NAME");
|
||||
String tableType = tables.getString("TABLE_TYPE");
|
||||
|
||||
TableSchema tableSchema = new TableSchema(tableName, tableType);
|
||||
|
||||
// Columns
|
||||
ResultSet columns = metaData.getColumns(conn.getCatalog(), null, tableName, "%");
|
||||
while (columns.next()) {
|
||||
ColumnSchema col = new ColumnSchema(
|
||||
columns.getString("COLUMN_NAME"),
|
||||
columns.getString("TYPE_NAME"),
|
||||
columns.getInt("COLUMN_SIZE"),
|
||||
"YES".equalsIgnoreCase(columns.getString("IS_NULLABLE"))
|
||||
);
|
||||
tableSchema.getColumns().add(col);
|
||||
}
|
||||
|
||||
// Primary Keys
|
||||
ResultSet pkRs = metaData.getPrimaryKeys(conn.getCatalog(), null, tableName);
|
||||
while (pkRs.next()) {
|
||||
tableSchema.getPrimaryKeys().add(pkRs.getString("COLUMN_NAME"));
|
||||
}
|
||||
|
||||
// Foreign Keys
|
||||
ResultSet fkRs = metaData.getImportedKeys(conn.getCatalog(), null, tableName);
|
||||
while (fkRs.next()) {
|
||||
String fkCol = fkRs.getString("FKCOLUMN_NAME");
|
||||
String pkTable = fkRs.getString("PKTABLE_NAME");
|
||||
String pkCol = fkRs.getString("PKCOLUMN_NAME");
|
||||
tableSchema.getForeignKeys().put(fkCol, pkTable + "." + pkCol);
|
||||
}
|
||||
|
||||
// Indexes
|
||||
ResultSet idxRs = metaData.getIndexInfo(conn.getCatalog(), null, tableName, false, false);
|
||||
while (idxRs.next()) {
|
||||
String idxName = idxRs.getString("INDEX_NAME");
|
||||
String colName = idxRs.getString("COLUMN_NAME");
|
||||
if (idxName != null && colName != null) {
|
||||
tableSchema.getIndexes().computeIfAbsent(idxName, k -> new ArrayList<>()).add(colName);
|
||||
}
|
||||
}
|
||||
|
||||
schemaMap.put(tableName, tableSchema);
|
||||
}
|
||||
|
||||
log.info("✅ Schema extracted for {} tables/views", schemaMap.size());
|
||||
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Error extracting schema", e);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Inner Classes ----------
|
||||
|
||||
@Data
|
||||
public static class TableSchema {
|
||||
private final String name;
|
||||
private final String type; // TABLE or VIEW
|
||||
private final List<ColumnSchema> columns = new ArrayList<>();
|
||||
private final List<String> primaryKeys = new ArrayList<>();
|
||||
private final Map<String, String> foreignKeys = new LinkedHashMap<>();
|
||||
private final Map<String, List<String>> indexes = new LinkedHashMap<>();
|
||||
|
||||
public String toPrettyString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(type).append(": ").append(name).append("\n");
|
||||
|
||||
sb.append(" Columns:\n");
|
||||
for (ColumnSchema col : columns) {
|
||||
sb.append(" - ").append(col).append("\n");
|
||||
}
|
||||
|
||||
if (!primaryKeys.isEmpty()) {
|
||||
sb.append(" Primary Keys: ").append(String.join(", ", primaryKeys)).append("\n");
|
||||
}
|
||||
if (!foreignKeys.isEmpty()) {
|
||||
sb.append(" Foreign Keys:\n");
|
||||
foreignKeys.forEach((col, ref) -> sb.append(" - ").append(col).append(" -> ").append(ref).append("\n"));
|
||||
}
|
||||
if (!indexes.isEmpty()) {
|
||||
sb.append(" Indexes:\n");
|
||||
indexes.forEach((idx, cols) -> sb.append(" - ").append(idx).append(": ").append(cols).append("\n"));
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ColumnSchema {
|
||||
private final String name;
|
||||
private final String type;
|
||||
private final int size;
|
||||
private final boolean nullable;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name + " (" + type + (size > 0 ? "(" + size + ")" : "") + (nullable ? ", NULL" : ", NOT NULL") + ")";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.email.classifier.controller;
|
||||
|
||||
import com.email.classifier.service.OpenAiVisionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/images")
|
||||
@RequiredArgsConstructor
|
||||
public class ImageUploadController {
|
||||
|
||||
private final OpenAiVisionService openAiVisionService;
|
||||
|
||||
@PostMapping("/extract-text")
|
||||
public ResponseEntity<String> extractTextFromImage(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
log.info("Processing image: {}", file.getOriginalFilename());
|
||||
String extractedText = openAiVisionService.extractTextFromImage(file);
|
||||
return ResponseEntity.ok(extractedText);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to process image", e);
|
||||
return ResponseEntity.status(500).body("Error processing image.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package com.email.classifier.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OpenAiVisionService {
|
||||
|
||||
@Value("${spring.ai.openai.api-key}")
|
||||
private String openAiApiKey;
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private static final String OPENAI_VISION_URL = "https://api.openai.com/v1/chat/completions";
|
||||
|
||||
/**
|
||||
* Extracts text from an uploaded image using OpenAI Vision API.
|
||||
*/
|
||||
public String extractTextFromImage(MultipartFile file) throws IOException {
|
||||
// byte[] imageBytes = file.getBytes();
|
||||
// String base64Image = java.util.Base64.getEncoder().encodeToString(imageBytes);
|
||||
String base64Image = "";
|
||||
// JSON payload for OpenAI Vision API
|
||||
String requestBody = """
|
||||
{
|
||||
"model": "gpt-4.5-preview",
|
||||
"messages": [{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{ "type": "text", "text": "Extract the text from the image, and output just that" }
|
||||
]
|
||||
}],
|
||||
"max_tokens": 500
|
||||
}""";
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBearerAuth(openAiApiKey);
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
|
||||
HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers);
|
||||
|
||||
// Send request to OpenAI
|
||||
ResponseEntity<String> responseEntity = restTemplate.exchange(OPENAI_VISION_URL, HttpMethod.POST, requestEntity, String.class);
|
||||
|
||||
if (responseEntity.getStatusCode() == HttpStatus.OK) {
|
||||
JsonNode jsonResponse = objectMapper.readTree(responseEntity.getBody());
|
||||
log.info(responseEntity.getBody());
|
||||
return jsonResponse.get("choices").get(0).get("message").get("content").asText();
|
||||
} else {
|
||||
log.error("OpenAI API Error: {}", responseEntity.getBody());
|
||||
return "Error extracting text from image.";
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package com.microsoft;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class LRU {
|
||||
|
||||
public LRU(int capacity) {
|
||||
this.capacity = capacity;
|
||||
this.cache = new HashMap<>();
|
||||
this.dll = new DoublyLinkedList();
|
||||
}
|
||||
|
||||
static class Node {
|
||||
public int key;
|
||||
public int val;
|
||||
public Node prev;
|
||||
public Node next;
|
||||
public Node(int key, int val) {
|
||||
this.key = key;
|
||||
this.val = val;
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
return "[key=" + key + ", val=" + val + "]";
|
||||
}
|
||||
}
|
||||
|
||||
static class DoublyLinkedList {
|
||||
Node head = new Node(0,0);
|
||||
Node tail = new Node(0,0);
|
||||
|
||||
public DoublyLinkedList() {
|
||||
head.next = tail;
|
||||
tail.prev = head;
|
||||
}
|
||||
|
||||
public void remove(Node node) {
|
||||
node.next.prev = node.prev;
|
||||
node.prev.next = node.next;
|
||||
}
|
||||
|
||||
public Node removeTail() {
|
||||
Node node = tail.prev;
|
||||
remove(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
public void addToFront(Node node) {
|
||||
node.next = head.next;
|
||||
node.prev = head;
|
||||
head.next.prev = node;
|
||||
head.next = node;
|
||||
}
|
||||
|
||||
public void moveToFront(Node node) {
|
||||
remove(node);
|
||||
addToFront(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
Node current = head.next;
|
||||
while (current != tail) {
|
||||
System.out.print("(" + current.key + "," + current.val + ") ");
|
||||
current = current.next;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private final int capacity;
|
||||
private final Map<Integer, Node> cache;
|
||||
private final DoublyLinkedList dll;
|
||||
|
||||
private int getValue(int key) {
|
||||
if (cache.containsKey(key)) {
|
||||
Node node = cache.get(key);
|
||||
dll.moveToFront(node);
|
||||
return node.val;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void add(int key, int val) {
|
||||
if (cache.containsKey(key)) {
|
||||
Node node = cache.get(key);
|
||||
node.val = val;
|
||||
dll.moveToFront(node);
|
||||
} else {
|
||||
if (cache.size() == capacity) {
|
||||
Node node = dll.removeTail();
|
||||
cache.remove(node.key);
|
||||
}
|
||||
Node node = new Node(key, val);
|
||||
cache.put(key, node);
|
||||
dll.addToFront(node);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
LRU lru = new LRU(2);
|
||||
lru.add(1, 2);
|
||||
lru.add(3, 4);
|
||||
|
||||
System.out.println(lru.cache);
|
||||
System.out.println(lru.dll);
|
||||
|
||||
System.out.println(lru.getValue(3));
|
||||
System.out.println(lru.cache);
|
||||
System.out.println(lru.dll);
|
||||
|
||||
System.out.println(lru.getValue(1));
|
||||
System.out.println(lru.cache);
|
||||
System.out.println(lru.dll);
|
||||
|
||||
lru.add(5, 6);
|
||||
System.out.println(lru.cache);
|
||||
System.out.println(lru.dll);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package com.email.classifier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class ApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue