commit eff8275ff81bd26b0586e3dbc84d345e50ae025f Author: spallya Date: Tue Aug 19 02:03:23 2025 +0530 working code diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..d58dfb7 --- /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.9/apache-maven-3.9.9-bin.zip diff --git a/mvnw b/mvnw new file mode 100755 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..fb904ab --- /dev/null +++ b/pom.xml @@ -0,0 +1,124 @@ + + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.1 + + + + com.email.classifier + email-classifier + 1.0.0 + email-classifier + Gen AI Email Processing System + + + 17 + + + + + org.apache.commons + commons-email + 1.5 + + + com.sun.mail + javax.mail + 1.6.2 + + + com.google.http-client + google-http-client-jackson2 + 1.43.3 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-logging + + + + + com.google.api-client + google-api-client + 1.35.2 + + + com.google.oauth-client + google-oauth-client-jetty + 1.34.1 + + + com.google.apis + google-api-services-gmail + v1-rev110-1.25.0 + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + org.projectlombok + lombok + 1.18.36 + provided + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.mockito + mockito-core + test + + + + org.mockito + mockito-junit-jupiter + test + + + com.mysql + mysql-connector-j + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file diff --git a/src/main/java/com/email/classifier/AIProcessingService.java b/src/main/java/com/email/classifier/AIProcessingService.java new file mode 100644 index 0000000..d21e279 --- /dev/null +++ b/src/main/java/com/email/classifier/AIProcessingService.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/Application.java b/src/main/java/com/email/classifier/Application.java new file mode 100644 index 0000000..9c52eb8 --- /dev/null +++ b/src/main/java/com/email/classifier/Application.java @@ -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(); + } + +} diff --git a/src/main/java/com/email/classifier/CommonsEmailService.java b/src/main/java/com/email/classifier/CommonsEmailService.java new file mode 100644 index 0000000..fb0b427 --- /dev/null +++ b/src/main/java/com/email/classifier/CommonsEmailService.java @@ -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"; + } +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/EmailProcessingService.java b/src/main/java/com/email/classifier/EmailProcessingService.java new file mode 100644 index 0000000..4a032da --- /dev/null +++ b/src/main/java/com/email/classifier/EmailProcessingService.java @@ -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 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 extractKeyDetails(String aiResponse) { + // Simulating AI response parsing + return Map.of( + "requestType", "Loan Modification", + "subRequestType", "Interest Rate Change" + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/GmailIMAPService.java b/src/main/java/com/email/classifier/GmailIMAPService.java new file mode 100644 index 0000000..df3d3bd --- /dev/null +++ b/src/main/java/com/email/classifier/GmailIMAPService.java @@ -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 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 extractCategoryFromAIResponse(String aiResponse) { + try { + ObjectMapper objectMapper = new ObjectMapper(); + + Map responseMap = objectMapper.readValue(aiResponse, new TypeReference>() {}); + + 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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/JiraService.java b/src/main/java/com/email/classifier/JiraService.java new file mode 100644 index 0000000..fcdf8f1 --- /dev/null +++ b/src/main/java/com/email/classifier/JiraService.java @@ -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 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> entity = new HttpEntity<>(request, headers); + ResponseEntity 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 ""; + } +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/LLMQueryResponse.java b/src/main/java/com/email/classifier/LLMQueryResponse.java new file mode 100644 index 0000000..3495ad6 --- /dev/null +++ b/src/main/java/com/email/classifier/LLMQueryResponse.java @@ -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; +} diff --git a/src/main/java/com/email/classifier/OpenAIClient.java b/src/main/java/com/email/classifier/OpenAIClient.java new file mode 100644 index 0000000..3d6a3f7 --- /dev/null +++ b/src/main/java/com/email/classifier/OpenAIClient.java @@ -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> 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 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 requestEntity = new HttpEntity<>(requestBody, headers); + + // Send request to OpenAI + ResponseEntity 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 getRequiredTables(String question, Set 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 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> choices = (List>) res.get("choices"); + Map message = (Map) 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 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 choice = ((List>) res.get("choices")).get(0); + Map message = (Map) 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 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 choice = ((List>) res.get("choices")).get(0); + Map message = (Map) choice.get("message"); + return message.get("content").trim(); + }) + .block(); + } +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/OpenAIConfig.java b/src/main/java/com/email/classifier/OpenAIConfig.java new file mode 100644 index 0000000..8dc9287 --- /dev/null +++ b/src/main/java/com/email/classifier/OpenAIConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/QueryController.java b/src/main/java/com/email/classifier/QueryController.java new file mode 100644 index 0000000..b748793 --- /dev/null +++ b/src/main/java/com/email/classifier/QueryController.java @@ -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 query(@RequestBody QueryRequest request) { + return ResponseEntity.ok(queryService.handleQuery(request.getQuestion())); + } +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/QueryRequest.java b/src/main/java/com/email/classifier/QueryRequest.java new file mode 100644 index 0000000..149d6f2 --- /dev/null +++ b/src/main/java/com/email/classifier/QueryRequest.java @@ -0,0 +1,8 @@ +package com.email.classifier; + +import lombok.Data; + +@Data +public class QueryRequest { + private String question; +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/QueryResponse.java b/src/main/java/com/email/classifier/QueryResponse.java new file mode 100644 index 0000000..225e401 --- /dev/null +++ b/src/main/java/com/email/classifier/QueryResponse.java @@ -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> result; +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/QueryService.java b/src/main/java/com/email/classifier/QueryService.java new file mode 100644 index 0000000..8ddbe0e --- /dev/null +++ b/src/main/java/com/email/classifier/QueryService.java @@ -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 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> 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(); + } + +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/SchemaExtractor.java b/src/main/java/com/email/classifier/SchemaExtractor.java new file mode 100644 index 0000000..afea042 --- /dev/null +++ b/src/main/java/com/email/classifier/SchemaExtractor.java @@ -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> schemaMap = new HashMap<>(); +// private final Map> foreignKeyMap = new HashMap<>(); +// +// public Set getAllTableNames() { +// return schemaMap.keySet(); +// } +// +// public String getRequiredSchemaData(List tableNames) { +// StringBuilder schemaBuilder = new StringBuilder(); +// +// tableNames.forEach(table -> { +// schemaBuilder.append("Table: ").append(table).append("\nColumns: "); +// List columns = schemaMap.getOrDefault(table, new ArrayList<>()); +// schemaBuilder.append(String.join(", ", columns)).append("\n"); +// +// List 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 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 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 schemaMap = new HashMap<>(); + + public Set getAllTableNames() { + return schemaMap.keySet(); + } + + public TableSchema getTableSchema(String tableName) { + return schemaMap.get(tableName); + } + + public String getRequiredSchemaData(List 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 columns = new ArrayList<>(); + private final List primaryKeys = new ArrayList<>(); + private final Map foreignKeys = new LinkedHashMap<>(); + private final Map> 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") + ")"; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/email/classifier/controller/ImageUploadController.java b/src/main/java/com/email/classifier/controller/ImageUploadController.java new file mode 100644 index 0000000..1b49cc0 --- /dev/null +++ b/src/main/java/com/email/classifier/controller/ImageUploadController.java @@ -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 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."); + } + } +} diff --git a/src/main/java/com/email/classifier/service/OpenAiVisionService.java b/src/main/java/com/email/classifier/service/OpenAiVisionService.java new file mode 100644 index 0000000..1fab957 --- /dev/null +++ b/src/main/java/com/email/classifier/service/OpenAiVisionService.java @@ -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 requestEntity = new HttpEntity<>(requestBody, headers); + + // Send request to OpenAI + ResponseEntity 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."; + } + } +} diff --git a/src/main/java/com/microsoft/LRU.java b/src/main/java/com/microsoft/LRU.java new file mode 100644 index 0000000..e3b1955 --- /dev/null +++ b/src/main/java/com/microsoft/LRU.java @@ -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 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); + } +} diff --git a/src/test/java/com/email/classifier/ApplicationTests.java b/src/test/java/com/email/classifier/ApplicationTests.java new file mode 100644 index 0000000..dd42924 --- /dev/null +++ b/src/test/java/com/email/classifier/ApplicationTests.java @@ -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() { + } + +}