diff --git a/jdk_21_maven/cs/rest/lovemining/.gitattributes b/jdk_21_maven/cs/rest/lovemining/.gitattributes new file mode 100644 index 000000000..3b41682ac --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/jdk_21_maven/cs/rest/lovemining/.gitignore b/jdk_21_maven/cs/rest/lovemining/.gitignore new file mode 100644 index 000000000..667aaef0c --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/.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/jdk_21_maven/cs/rest/lovemining/mvnw b/jdk_21_maven/cs/rest/lovemining/mvnw new file mode 100644 index 000000000..bd8896bf2 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/mvnw @@ -0,0 +1,295 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.4 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +scriptDir="$(dirname "$0")" +scriptName="$(basename "$0")" + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +actualDistributionDir="" + +# First try the expected directory name (for regular distributions) +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then + actualDistributionDir="$distributionUrlNameMain" + fi +fi + +# If not found, search for any directory with the Maven executable (for snapshots) +if [ -z "$actualDistributionDir" ]; then + # enable globbing to iterate over items + set +f + for dir in "$TMP_DOWNLOAD_DIR"/*; do + if [ -d "$dir" ]; then + if [ -f "$dir/bin/$MVN_CMD" ]; then + actualDistributionDir="$(basename "$dir")" + break + fi + fi + done + set -f +fi + +if [ -z "$actualDistributionDir" ]; then + verbose "Contents of $TMP_DOWNLOAD_DIR:" + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" + die "Could not find Maven distribution directory in extracted archive" +fi + +verbose "Found extracted Maven distribution directory: $actualDistributionDir" +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/jdk_21_maven/cs/rest/lovemining/mvnw.cmd b/jdk_21_maven/cs/rest/lovemining/mvnw.cmd new file mode 100644 index 000000000..92450f932 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/mvnw.cmd @@ -0,0 +1,189 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.4 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' + +$MAVEN_M2_PATH = "$HOME/.m2" +if ($env:MAVEN_USER_HOME) { + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" +} + +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null +} + +$MAVEN_WRAPPER_DISTS = $null +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" +} else { + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" +} + +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null + +# Find the actual extracted directory name (handles snapshots where filename != directory name) +$actualDistributionDir = "" + +# First try the expected directory name (for regular distributions) +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { + $actualDistributionDir = $distributionUrlNameMain +} + +# If not found, search for any directory with the Maven executable (for snapshots) +if (!$actualDistributionDir) { + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" + if (Test-Path -Path $testPath -PathType Leaf) { + $actualDistributionDir = $_.Name + } + } +} + +if (!$actualDistributionDir) { + Write-Error "Could not find Maven distribution directory in extracted archive" +} + +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/jdk_21_maven/cs/rest/lovemining/pom.xml b/jdk_21_maven/cs/rest/lovemining/pom.xml new file mode 100644 index 000000000..c13652f75 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/pom.xml @@ -0,0 +1,105 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.4.11 + + + + it.unipi + LoveMining + 0.0.1-SNAPSHOT + LoveMining + LoveMining project LSMSD + + + 21 + 1.18.36 + + + + + org.springframework.boot + spring-boot-starter-data-mongodb + + + org.springframework.boot + spring-boot-starter-data-neo4j + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-web + + + + org.projectlombok + lombok + true + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + + org.json + json + 20230227 + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + + + + + + \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/LoveMiningApplication.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/LoveMiningApplication.java new file mode 100644 index 000000000..56850b04f --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/LoveMiningApplication.java @@ -0,0 +1,13 @@ +package it.unipi.LoveMining; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class LoveMiningApplication { + + public static void main(String[] args) { + SpringApplication.run(LoveMiningApplication.class, args); + } + +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/config/MongoConfig.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/config/MongoConfig.java new file mode 100644 index 000000000..6ae92754a --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/config/MongoConfig.java @@ -0,0 +1,55 @@ +package it.unipi.LoveMining.config; + +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.ReadPreference; +import com.mongodb.WriteConcern; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.MongoDatabaseFactory; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.convert.*; // Import for conversions +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +@Configuration +public class MongoConfig extends AbstractMongoClientConfiguration { + + @Override + protected String getDatabaseName() { return "LoveMining"; } + + @Override + @Bean + public MongoClient mongoClient() { + String uri = "mongodb://10.1.1.14:27017,10.1.1.15:27017,10.1.1.16:27017/?replicaSet=lsmdb"; + + // Write and Read preferences + MongoClientSettings settings = MongoClientSettings.builder().applyConnectionString(new ConnectionString(uri)) + .writeConcern(WriteConcern.W1.withJournal(false)) // w:1 and j:false + .readPreference(ReadPreference.nearest()) // nearest + .build(); + + return MongoClients.create(settings); + } + + // This method overrides the default converter and removes the _class field from the documents saved in Mongo + @Override + @Bean + public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory, MongoCustomConversions customConversions, MongoMappingContext mappingContext) { + + // 1. Create the standard resolver + DbRefResolver dbRefResolver = new DefaultDbRefResolver(databaseFactory); + + // 2. Create the converter + MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContext); + + // 3. Set custom conversions (important to prevent breaking other features) + converter.setCustomConversions(customConversions); + + // 4. THE SOLUTION: Set the TypeMapper to null to remove the _class field + converter.setTypeMapper(new DefaultMongoTypeMapper(null)); + + return converter; + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/config/OpenAPIConfig.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/config/OpenAPIConfig.java new file mode 100644 index 000000000..6e8470607 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/config/OpenAPIConfig.java @@ -0,0 +1,29 @@ +package it.unipi.LoveMining.config; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenAPIConfig { + + @Bean + public OpenAPI customOpenAPI() { + return new OpenAPI() + .info(new Info() + .title("LoveMining API") + .version("1.0") + .description("Documentation for LoveMining Application")) + // 1. Define type of security (Basic Auth) + .components(new Components() + .addSecuritySchemes("basicAuth", new SecurityScheme() + .type(SecurityScheme.Type.HTTP) + .scheme("basic"))) + // 2. Apply security on all the APIs + .addSecurityItem(new SecurityRequirement().addList("basicAuth")); + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/config/SecurityConfig.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/config/SecurityConfig.java new file mode 100644 index 000000000..110471a4f --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/config/SecurityConfig.java @@ -0,0 +1,64 @@ +package it.unipi.LoveMining.config; + +import it.unipi.LoveMining.service.security.CustomUserDetailsService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final CustomUserDetailsService customUserDetailsService; + + public SecurityConfig(CustomUserDetailsService customUserDetailsService) { + this.customUserDetailsService = customUserDetailsService; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(auth -> auth + // 1. Add Swagger and OpenAPI to the list of public authorizations + .requestMatchers( + "/api/authentication/**", + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-ui.html" + ).permitAll() + .requestMatchers("/api/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + ) + .httpBasic(Customizer.withDefaults()); + + return http.build(); + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public DaoAuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(customUserDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/controller/AdminController.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/controller/AdminController.java new file mode 100644 index 000000000..ef0255e9c --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/controller/AdminController.java @@ -0,0 +1,121 @@ +package it.unipi.LoveMining.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestParam; + +import it.unipi.LoveMining.model.mongo.UserDocument; +import it.unipi.LoveMining.model.mongo.ReviewDocument; +import it.unipi.LoveMining.service.admin.AdminService; + +import java.util.Map; +import java.util.List; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/admin") +@RequiredArgsConstructor +public class AdminController { + + private final AdminService adminService; + + // Deletes a user by ID + @DeleteMapping("/users/{id}") + public ResponseEntity deleteUser(@PathVariable String id) { + adminService.deleteUser(id); + return ResponseEntity.ok("[OK] User deleted successfully"); + } + + // Search a user by ID + @GetMapping("/user/{id}") + public ResponseEntity getUser(@PathVariable String id) { + UserDocument user = adminService.getUserById(id); // Call the service to get the user + + // Check if the user exists + if (user != null) { + return ResponseEntity.ok(user); + } else { + return ResponseEntity.notFound().build(); // Return "404 Not Found" if the user doesn't exist + } + } + + // Search a review by ID + @GetMapping("/userReviews/{id}") + public ResponseEntity getReview(@PathVariable String id) { + ReviewDocument review = adminService.getReviewById(id); // Call the service to get the review + + // Check if the review exists + if (review != null) { + return ResponseEntity.ok(review); + } else { + return ResponseEntity.notFound().build(); // Return "404 Not Found" if the review doesn't exist + } + } + + //Analytics: Love Points + @GetMapping("/analytics/love-points") + public ResponseEntity>> getLovePointsAnalytic(@RequestParam String state) { + List> stats = adminService.getLovePointsStats(state); + if (stats.isEmpty()) { + return ResponseEntity.noContent().build(); + } + return ResponseEntity.ok(stats); + } + + //Analytics: Glow-up + @GetMapping("/analytics/glow-up") + public ResponseEntity>> getGlowUpAnalytics() { + + List> bestUsers = adminService.getBestGlowUpUsers(); + + if (bestUsers.isEmpty()) { + return ResponseEntity.noContent().build(); + } + + return ResponseEntity.ok(bestUsers); + } + + //Analytics: Unhappy Cities + @GetMapping("/analytics/unhappy-cities") + public ResponseEntity>> getUnhappyCitiesAnalytics() { + + List> bestCities = adminService.getUnhappyCities(); + + if (bestCities.isEmpty()) { + return ResponseEntity.noContent().build(); + } + + return ResponseEntity.ok(bestCities); + } + + // Analytics: Status by Age Group + @GetMapping("/analytics/status-by-age-group") + public ResponseEntity>> getStatusAnalytics() { + + List> stats = adminService.getStatusAnalytics(); + + if (stats.isEmpty()) { + return ResponseEntity.noContent().build(); + } + + return ResponseEntity.ok(stats); + } + + // Analytics: Orientation by Age Group + @GetMapping("/analytics/orientation-by-age-group") + public ResponseEntity>> getOrientationAnalytics() { + + List> stats = adminService.getOrientationAnalytics(); + + if (stats.isEmpty()) { + return ResponseEntity.noContent().build(); + } + + return ResponseEntity.ok(stats); + } +} + diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/controller/AuthenticationController.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/controller/AuthenticationController.java new file mode 100644 index 000000000..c49a3d729 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/controller/AuthenticationController.java @@ -0,0 +1,30 @@ +package it.unipi.LoveMining.controller; + +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; + +import it.unipi.LoveMining.model.mongo.UserDocument; +import it.unipi.LoveMining.service.authentication.AuthenticationService; +import lombok.RequiredArgsConstructor; + +@RestController +@RequestMapping("/api/authentication") +@RequiredArgsConstructor +public class AuthenticationController { + + private final AuthenticationService authenticationService; + + // Registers a new user + @PostMapping("/register") + public ResponseEntity registerUser(@RequestBody UserDocument user) { + try { + UserDocument savedUser = authenticationService.registerUser(user); + return ResponseEntity.ok("User registered succesfully" + savedUser); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().body(e.getMessage()); + } + } +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/controller/UserController.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/controller/UserController.java new file mode 100644 index 000000000..5efea737f --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/controller/UserController.java @@ -0,0 +1,229 @@ +package it.unipi.LoveMining.controller; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RequestBody; + +import it.unipi.LoveMining.model.mongo.UserDocument; +import it.unipi.LoveMining.model.mongo.ReviewDocument; +import it.unipi.LoveMining.service.user.UserService; +import lombok.RequiredArgsConstructor; + +import org.springframework.security.core.Authentication; + +import java.util.List; + +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class UserController { + + private final UserService userService; + + // Retrieves a user by ID + @GetMapping("/{id}") + public ResponseEntity getUser(@PathVariable String id) { + UserDocument user = userService.getUser(id); + if (user != null) { + return ResponseEntity.ok(user); + } else { + return ResponseEntity.notFound().build(); + } + } + + // Retrieves the authenticated user's profile + @GetMapping + public ResponseEntity getMe(Authentication authentication) { + // 1. Check if user is ADMIN + boolean isAdmin = authentication.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + + if (isAdmin) { + return ResponseEntity.status(403).body("ADMINs cannot access user profiles."); + } + + // 2. Retrieve user by Email (Principal in Basic Auth is the username/email) + String email = authentication.getName(); + UserDocument user = userService.getUserByEmail(email); + + if (user != null) { + return ResponseEntity.ok(user); + } else { + return ResponseEntity.notFound().build(); + } + } + + // Retrieves the matches for the authenticated user + @GetMapping("/matches") + public ResponseEntity getMatches(Authentication authentication) { + // 1. Check if user is ADMIN + boolean isAdmin = authentication.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + + if (isAdmin) { + return ResponseEntity.status(403).body("ADMINs cannot access user matches."); + } + + String email = authentication.getName(); + UserDocument user = userService.getUserByEmail(email); // Need ID from Mongo Doc first + if (user == null) { + return ResponseEntity.notFound().build(); + } + + java.util.Set matches = userService.getUserMatches(user.getId()); + return ResponseEntity.ok(matches); + } + + // Retrieves the reviews made by the authenticated user + @GetMapping("/reviews") + public ResponseEntity getReviews(org.springframework.security.core.Authentication authentication) { + // 1. Check if user is ADMIN + boolean isAdmin = authentication.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + + if (isAdmin) { + return ResponseEntity.status(403).body("ADMINs cannot access user reviews."); + } + + String email = authentication.getName(); + UserDocument user = userService.getUserByEmail(email); + if (user == null) { + return ResponseEntity.notFound().build(); + } + + java.util.List reviews = userService.getUserReviews(user.getId()); + + if (reviews == null) { + return ResponseEntity.ok(java.util.Collections.emptyList()); + } + return ResponseEntity.ok(reviews); + } + + // Update user profile + @PatchMapping("/me") + public ResponseEntity updateUser(Authentication authentication, + @RequestBody UserDocument inputUser) { + String email = authentication.getName(); + + // Retrieve current user to check role/id + UserDocument currentUser = userService.getUserByEmail(email); + + if (currentUser == null) { + return ResponseEntity.notFound().build(); + } + + if (currentUser.getIsAdmin() != null && currentUser.getIsAdmin()) { + return ResponseEntity.status(403).body("Admins cannot have a profile to update."); + } + + try { + userService.updateUser(currentUser.getId(), inputUser); + return ResponseEntity.ok("User profile updated successfully."); + } catch (Exception e) { + return ResponseEntity.badRequest().body("Update failed: " + e.getMessage()); + } + } + + // Create a review + @PostMapping("/{id}/review") + public ResponseEntity createReview(Authentication authentication, @PathVariable String id, @RequestBody ReviewDocument review) { + + // 1. Check + boolean isAdmin = authentication.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + if (isAdmin) { + return ResponseEntity.status(403).body("ADMINs cannot create reviews."); + } + + // 2. Author + String email = authentication.getName(); + UserDocument author = userService.getUserByEmail(email); + + if (author == null) { return ResponseEntity.notFound().build(); } + + try { + // 3. Call logic (Match check + Insert) + userService.addReview(author.getId(), id, review); + return ResponseEntity.ok("Review created successfully."); + } catch (Exception e) { + // Errors (es. "Not matched", "User not found") + return ResponseEntity.badRequest().body("Error creating review: " + e.getMessage()); + } + } + + // Likes + @PostMapping("/{id}/like") + public ResponseEntity likeUser(Authentication authentication, @PathVariable String id) { + + boolean isAdmin = authentication.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + if (isAdmin) return ResponseEntity.status(403).body("ADMINs cannot like users."); + + String email = authentication.getName(); + UserDocument author = userService.getUserByEmail(email); + if (author == null) return ResponseEntity.notFound().build(); + + try { + String result = userService.likeUser(author.getId(), id); + return ResponseEntity.ok(result); + } catch (Exception e) { + return ResponseEntity.badRequest().body("Error in like: " + e.getMessage()); + } + } + + // Dislikes + @PostMapping("/{id}/dislike") + public ResponseEntity dislikeUser(Authentication authentication, @PathVariable String id) { + + boolean isAdmin = authentication.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + if (isAdmin) return ResponseEntity.status(403).body("ADMINs cannot dislike users."); + + String email = authentication.getName(); + UserDocument author = userService.getUserByEmail(email); + if (author == null) return ResponseEntity.notFound().build(); + + try { + userService.dislikeUser(author.getId(), id); + return ResponseEntity.ok("Dislike send."); + } catch (Exception e) { + return ResponseEntity.badRequest().body("Error in dislike: " + e.getMessage()); + } + } + + // Recommendations with filters + @GetMapping("/recommendations/{filters}") + public ResponseEntity getRecommendations( + Authentication authentication, + @RequestParam String locationFilter, + @RequestParam int minAge, + @RequestParam int maxAge + ) { + + boolean isAdmin = authentication.getAuthorities().stream() + .anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN")); + if (isAdmin) return ResponseEntity.status(403).body("ADMINs cannot dislike users."); + + if (!locationFilter.equals("City") && !locationFilter.equals("State")) { // Expects "City" or "State" + return ResponseEntity.badRequest().body("Invalid filter. Use 'City' or 'State'."); + } + + String email = authentication.getName(); + UserDocument user = userService.getUserByEmail(email); + if (user == null) return ResponseEntity.notFound().build(); + + List recommendations = userService.getRecommendations(user.getId(), locationFilter, minAge, maxAge); + + if (recommendations.isEmpty()) { + return ResponseEntity.noContent().build(); + } + + return ResponseEntity.ok(recommendations); + } +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/.DS_Store b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/.DS_Store new file mode 100644 index 000000000..ce0e83946 Binary files /dev/null and b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/.DS_Store differ diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/mongo/ReviewDocument.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/mongo/ReviewDocument.java new file mode 100644 index 000000000..b8fbb8a68 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/mongo/ReviewDocument.java @@ -0,0 +1,31 @@ +package it.unipi.LoveMining.model.mongo; + +import java.time.LocalDateTime; + +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.FieldType; +import org.springframework.data.mongodb.core.mapping.MongoId; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Document(collection = "reviews") +public class ReviewDocument { + + @MongoId(FieldType.STRING) + private String id; + + @Field("target_id") + private String targetId; + private Integer rating; + private String comment; + + @Field("review_date") + private LocalDateTime date; +} + diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/mongo/UserDocument.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/mongo/UserDocument.java new file mode 100644 index 000000000..904152ec4 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/mongo/UserDocument.java @@ -0,0 +1,72 @@ +package it.unipi.LoveMining.model.mongo; + +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.FieldType; +import org.springframework.data.mongodb.core.mapping.MongoId; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Document(collection = "users") +public class UserDocument { + + @MongoId(FieldType.STRING) + private String id; + + @Field("Email") + private String email; + + @Field("Password") + private String password; + + @Field("is_admin") + private Boolean isAdmin; + + private Integer age; + private String status; + private String sex; + private String orientation; + + @Field("body_type") + private String bodyType; + + private String diet; + private String drinks; + private String education; + private String ethnicity; + private Integer height; + private Integer income; + private String job; + private String offspring; + private String pets; + private String religion; + private String smokes; + private String speaks; + private String city; + private String state; + + private String essay0; + + private java.util.List interests; + + @Field("reviews_made") + private java.util.List reviewsMade; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ReviewSummary { + @Field("review_id") + private String reviewId; + + @Field("target_id") + private String targetId; + + private Integer rating; + } +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/CityNode.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/CityNode.java new file mode 100644 index 000000000..3b509c69a --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/CityNode.java @@ -0,0 +1,22 @@ +package it.unipi.LoveMining.model.neo4j; + +import org.springframework.data.annotation.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +import lombok.Data; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Node("City") +public class CityNode { + + @Id + private String name; + + @Relationship(type = "LOCATED_IN", direction = Relationship.Direction.OUTGOING) + private StateNode state; +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/InterestNode.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/InterestNode.java new file mode 100644 index 000000000..259effbb8 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/InterestNode.java @@ -0,0 +1,17 @@ +package it.unipi.LoveMining.model.neo4j; + +import org.springframework.data.annotation.Id; +import org.springframework.data.neo4j.core.schema.Node; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Node("Interest") +public class InterestNode { + @Id + private String name; +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/StateNode.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/StateNode.java new file mode 100644 index 000000000..5ae9501a7 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/StateNode.java @@ -0,0 +1,17 @@ +package it.unipi.LoveMining.model.neo4j; + +import org.springframework.data.annotation.Id; +import org.springframework.data.neo4j.core.schema.Node; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Node("State") +public class StateNode { + @Id + private String name; +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/UserNode.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/UserNode.java new file mode 100644 index 000000000..132037b62 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/model/neo4j/UserNode.java @@ -0,0 +1,47 @@ +package it.unipi.LoveMining.model.neo4j; + +import org.springframework.data.neo4j.core.schema.Property; +import org.springframework.data.annotation.Id; +import org.springframework.data.neo4j.core.schema.Node; +import org.springframework.data.neo4j.core.schema.Relationship; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Node("User") +public class UserNode { + + @Id + @Property("_id") + private String id; + + private Integer age; + private String sex; + private String orientation; + + @lombok.EqualsAndHashCode.Exclude + @lombok.ToString.Exclude + @Relationship(type = "LIVES_IN", direction = Relationship.Direction.OUTGOING) + private CityNode cityNode; + + @lombok.EqualsAndHashCode.Exclude + @lombok.ToString.Exclude + @Relationship(type = "HAS_INTEREST", direction = Relationship.Direction.OUTGOING) + private java.util.Set interests; + + @com.fasterxml.jackson.annotation.JsonIgnore + @lombok.EqualsAndHashCode.Exclude + @lombok.ToString.Exclude + @Relationship(type = "LIKES", direction = Relationship.Direction.OUTGOING) + private java.util.Set likes; + + @com.fasterxml.jackson.annotation.JsonIgnore + @lombok.EqualsAndHashCode.Exclude + @lombok.ToString.Exclude + @Relationship(type = "DISLIKES", direction = Relationship.Direction.OUTGOING) + private java.util.Set dislikes; +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/mongo/ReviewMongoRepository.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/mongo/ReviewMongoRepository.java new file mode 100644 index 000000000..351facc94 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/mongo/ReviewMongoRepository.java @@ -0,0 +1,62 @@ +package it.unipi.LoveMining.repository.mongo; + +import it.unipi.LoveMining.model.mongo.ReviewDocument; +import org.springframework.data.mongodb.repository.Aggregation; +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Map; + +@Repository +public interface ReviewMongoRepository extends MongoRepository { + + void deleteByTargetId(String targetId); + + @Aggregation(pipeline = { + // 1: GROUP by user + // Sum ratings and count reviews for Recent (>= ?0) and Past (< ?0) periods. + "{ $group: { " + + " _id: '$target_id', " + + " recentRatingSum: { $sum: { $cond: [{ $gte: ['$review_date', ?0] }, '$rating', 0] } }, " + + " recentCount: { $sum: { $cond: [{ $gte: ['$review_date', ?0] }, 1, 0] } }, " + + " pastRatingSum: { $sum: { $cond: [{ $lt: ['$review_date', ?0] }, '$rating', 0] } }, " + + " pastCount: { $sum: { $cond: [{ $lt: ['$review_date', ?0] }, 1, 0] } }, " + + " totalCount: { $sum: 1 } " + + "} }", + + // 2: MATCH + // Filter users: must have activity in both periods and at least 3 reviews total. + "{ $match: { recentCount: { $gt: 0 }, pastCount: { $gt: 0 }, totalCount: { $gte: 3 } } }", + + // 3: PROJECT + // Calculate averages. 'glowUpIndex' is the improvement (Recent Avg - Past Avg). + "{ $project: { " + + " recentAvg: { $divide: ['$recentRatingSum', '$recentCount'] }, " + + " pastAvg: { $divide: ['$pastRatingSum', '$pastCount'] }, " + + " glowUpIndex: { $subtract: [ " + + " { $divide: ['$recentRatingSum', '$recentCount'] }, " + + " { $divide: ['$pastRatingSum', '$pastCount'] } " + + " ] } " + + "} }", + + // 4: SORT by highest first + "{ $sort: { glowUpIndex: -1 } }", + + // 5: LIMIT by 3 + "{ $limit: 3 }" + }) + List> findGlowUpRaw(Date cutoffDate); + + default List> findBestGlowUpUsers() { + Date sixMonthsAgo = Date.from(LocalDateTime.now() + .minusMonths(6) + .atZone(ZoneId.systemDefault()) + .toInstant()); + + return findGlowUpRaw(sixMonthsAgo); + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/mongo/UserMongoRepository.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/mongo/UserMongoRepository.java new file mode 100644 index 000000000..f9cf81ed9 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/mongo/UserMongoRepository.java @@ -0,0 +1,130 @@ +package it.unipi.LoveMining.repository.mongo; + +import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.Aggregation; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; + +import it.unipi.LoveMining.model.mongo.UserDocument; + +@Repository +// Repository interface for User documents in MongoDB +public interface UserMongoRepository extends MongoRepository { + java.util.Optional findByEmail(String email); + + @org.springframework.data.mongodb.repository.Query("{ 'reviewsMade.targetId': ?0 }") + @org.springframework.data.mongodb.repository.Update("{ '$pull': { 'reviewsMade': { 'targetId': ?0 } } }") + void removeReviewReferences(String targetId); + + // Analytics: Finds cities with the highest number of unhappy users + @Aggregation(pipeline = { + // 1. MATCH: Keep only users who have at least one review with a rating + "{ '$match': { 'reviews_made.rating': { '$gt': 0 } } }", + + // 2. PROJECT: Calculate the average rating for each user + "{ '$project': { 'city': 1, 'userPersonalAvg': { '$avg': '$reviews_made.rating' } } }", + + // 3. MATCH: Filter for "unhappy" users (average rating less than 3) + "{ '$match': { 'userPersonalAvg': { '$lt': 3 } } }", + + // 4. GROUP: Group by City. Count total unhappy users and split by severity + "{ '$group': { " + + "'_id': '$city', " + + "'totalUnhappyUsers': { '$sum': 1 }, " + + "'extremeHatersCount': { '$sum': { '$cond': [ { '$lt': ['$userPersonalAvg', 2] }, 1, 0 ] } }, " + + "'moderateUnhappyCount': { '$sum': { '$cond': [ { '$gte': ['$userPersonalAvg', 2] }, 1, 0 ] } }, " + + "'avgUnhappinessScore': { '$avg': '$userPersonalAvg' } } }", + + // 5. PROJECT: Format output and round the score to 2 decimal places + "{ '$project': { " + + "'totalUnhappyUsers': 1, " + + "'extremeHatersCount': 1, " + + "'moderateUnhappyCount': 1, " + + "'avgUnhappinessScore': { '$round': ['$avgUnhappinessScore', 2] } } }", + + // 6. SORT: Order by the highest number of unhappy users (descending) + "{ '$sort': { 'totalUnhappyUsers': -1 } }", + + // 7. LIMIT: Return only the top 5 cities + "{ '$limit': 5 }" + }) + List> findTopUnhappyCities(); + + default List> findUnhappyCities() { + return findTopUnhappyCities(); + } + + // Analytics: Groups users into 3 age buckets and calculates the percentage of singles + @Aggregation(pipeline = { + // 1. MATCH: Filter valid users with a known status + "{ '$match': { 'status': { '$in': ['available', 'single', 'seeing someone', 'married'] } } }", + + // 2. PROJECT: Determine Age Group using $cond + // Logic: IF (age <= 25) THEN '1. Young' ELSE (IF (age <= 40) THEN '2. Adult' ELSE '3. Senior') + "{ '$project': { " + + " '_id': 0, " + + " 'status': 1, " + + " 'ageGroup': { '$cond': [ { '$lte': ['$age', 25] }, '1. Young (18-25)', " + + "{ '$cond': [ { '$lte': ['$age', 40] }, '2. Adult (26-40)', " + + "'3. Senior (40+)' ] } ] } } }", + + // 3. GROUP: Group by the calculated 'ageGroup' + "{ '$group': { " + + " '_id': '$ageGroup', " + + " 'totalUsers': { '$sum': 1 }, " + + " 'singleCount': { '$sum': { '$cond': [ { '$in': ['$status', ['single', 'available']] }, 1, 0 ] } } } }", + + // 4. PROJECT: Calculate Percentage + "{ '$project': { " + + " '_id': 0, " + + " 'ageGroup': '$_id', " + + " 'totalUsers': 1, " + + " 'singlePercentage': { '$round': [ { '$multiply': [ { '$divide': ['$singleCount', '$totalUsers'] }, 100 ] }, 1 ] } } }", // Round to 1 decimal place + + // 5. SORT: Sort by group name (1 -> 2 -> 3) + "{ '$sort': { 'ageGroup': 1} }" + }) + List> findSinglesByAgeGroup(); + + default List> getStatusAnalytics() { + return findSinglesByAgeGroup(); + } + + // Analytics: Categorizes users into Groups by Orientation + AgeGroup + @Aggregation(pipeline = { + // 1. MATCH: Filter valid orientations + "{ '$match': { 'orientation': { '$in': ['straight', 'gay', 'bisexual'] } } }", + + // 2. PROJECT: Calculate Age Group (Covered Query: _id excluded) + "{ '$project': { " + + " '_id': 0, " + + " 'orientation': 1, " + + " 'ageGroup': { '$cond': [ { '$lte': ['$age', 25] }, '1. Young (18-25)', " + + "{ '$cond': [ { '$lte': ['$age', 40] }, '2. Adult (26-40)', " + + "'3. Senior (40+)' ] } ] } } }", + + // 3. GROUP #1: Group by combination of Orientation and AgeGroup + "{ '$group': { " + + " '_id': { 'orientation': '$orientation', 'ageGroup': '$ageGroup' }, " + + " 'count': { '$sum': 1 } } }", + + // 4. SORT: Sort by group name (1 -> 2 -> 3) + "{ '$sort': { '_id.ageGroup': 1 } }", + + // 5. GROUP #2: Group only by Orientation + "{ '$group': { " + + " '_id': '$_id.orientation', " + + " 'totalUsers': { '$sum': '$count' }, " + + " 'demographics': { '$push': { 'ageGroup': '$_id.ageGroup', 'usersCount': '$count' } } } }", + + // 6. SORT: Sort by total users + "{ '$sort': { 'totalUsers': -1 } }" + }) + List> findOrientationDemographics(); + + default List> getOrientationAnalytics() { + return findOrientationDemographics(); + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/CityNeo4jRepository.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/CityNeo4jRepository.java new file mode 100644 index 000000000..15d65cd14 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/CityNeo4jRepository.java @@ -0,0 +1,12 @@ +package it.unipi.LoveMining.repository.neo4j; + +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.stereotype.Repository; + +import it.unipi.LoveMining.model.neo4j.CityNode; + +@Repository +// Repository interface for City nodes in Neo4j +public interface CityNeo4jRepository extends Neo4jRepository { + +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/InterestNeo4jRepository.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/InterestNeo4jRepository.java new file mode 100644 index 000000000..2fb4f421a --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/InterestNeo4jRepository.java @@ -0,0 +1,12 @@ +package it.unipi.LoveMining.repository.neo4j; + +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.stereotype.Repository; + +import it.unipi.LoveMining.model.neo4j.InterestNode; + +@Repository +// Repository interface for Interest nodes in Neo4j +public interface InterestNeo4jRepository extends Neo4jRepository { + +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/StateNeo4jRepository.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/StateNeo4jRepository.java new file mode 100644 index 000000000..0497636b6 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/StateNeo4jRepository.java @@ -0,0 +1,12 @@ +package it.unipi.LoveMining.repository.neo4j; + +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.stereotype.Repository; + +import it.unipi.LoveMining.model.neo4j.StateNode; + +@Repository +// Repository interface for State nodes in Neo4j +public interface StateNeo4jRepository extends Neo4jRepository { + +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/UserNeo4jRepository.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/UserNeo4jRepository.java new file mode 100644 index 000000000..ac6c91858 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/repository/neo4j/UserNeo4jRepository.java @@ -0,0 +1,156 @@ +package it.unipi.LoveMining.repository.neo4j; + +import it.unipi.LoveMining.model.neo4j.UserNode; +import org.springframework.data.neo4j.repository.Neo4jRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +// Repository interface for User nodes in Neo4j +public interface UserNeo4jRepository extends Neo4jRepository { + + @org.springframework.data.neo4j.repository.query.Query("MATCH (u:User {_id: $id}) SET u.age = $age, u.sex = $sex, u.orientation = $orientation") + void updateBasicProfile(String id, Integer age, String sex, String orientation); + + @org.springframework.data.neo4j.repository.query.Query("MATCH (u:User {_id: $id}) " + + "OPTIONAL MATCH (u)-[r:LIVES_IN]->() " + + "DELETE r " + + "WITH u " + + "MERGE (s:State {name: $stateName}) " + + "MERGE (c:City {name: $cityName}) " + + "MERGE (c)-[:LOCATED_IN]->(s) " + + "MERGE (u)-[:LIVES_IN]->(c)") + void updateLocation(String id, String cityName, String stateName); + + @org.springframework.data.neo4j.repository.query.Query("MATCH (u:User {_id: $id}) " + + "OPTIONAL MATCH (u)-[r:HAS_INTEREST]->() DELETE r " + + "FOREACH (name IN $interestNames | " + + "MERGE (i:Interest {name: name}) " + + "MERGE (u)-[:HAS_INTEREST]->(i))") + void updateInterests(String id, java.util.List interestNames); + + // Check if User 'from' likes 'to' + @org.springframework.data.neo4j.repository.query.Query("MATCH (a:User {_id: $fromId})-[r:LIKES]->(b:User {_id: $toId}) RETURN count(r) > 0") + boolean hasLiked(String fromId, String toId); + + // Check if Matched + @org.springframework.data.neo4j.repository.query.Query("MATCH (a:User {_id: $userId})-[:MATCHED]-(b:User {_id: $targetId}) RETURN count(b) > 0") + boolean areMatched(String userId, String targetId); + + // Create LIKES (A -> B) + @org.springframework.data.neo4j.repository.query.Query("MATCH (a:User {_id: $actorId}) " + + "MATCH (b:User {_id: $targetId}) " + + "MERGE (a)-[r:LIKES]->(b) " + + "RETURN count(r)") + Long createLikeRelationship(String actorId, String targetId); + + // Create DISLIKES (A -> B) + @org.springframework.data.neo4j.repository.query.Query("MATCH (a:User {_id: $actorId}) " + + "MATCH (b:User {_id: $targetId}) " + + "MERGE (a)-[r:DISLIKES]->(b) " + + "RETURN count(r)") + Long createDislikeRelationship(String actorId, String targetId); + + // Cancel LIKE of 'target' to 'actor' and create MATCHED + @org.springframework.data.neo4j.repository.query.Query("MATCH (a:User {_id: $actorId}) " + + "MATCH (b:User {_id: $targetId}) " + + "OPTIONAL MATCH (a)-[r:LIKES]-(b) " + + "DELETE r " + + "MERGE (a)-[m:MATCHED]-(b) " + + "RETURN count(m)") + Long transformLikeToMatch(String actorId, String targetId); + + /* + * Custom Query to find recommendations based on: + * - Sex and Orientation + * - No existing relationships (Likes/Dislikes/Matched) + * - Geographic scope (City or State) + * - Age range + * - Ordered by common interests + */ + @org.springframework.data.neo4j.repository.query.Query( + "MATCH (me:User {_id: $userId}) " + + "MATCH (me)-[:LIVES_IN]->(myCity:City)-[:LOCATED_IN]->(myState:State) " + + "MATCH (candidate:User)-[:LIVES_IN]->(candCity:City)-[:LOCATED_IN]->(candState:State) " + + "WHERE me._id <> candidate._id " + + + // Constraint: No existing relationship between me and candidate + "AND NOT (me)-[:LIKES|DISLIKES|MATCHED]-(candidate) " + + + // Constraint: Age Range + "AND candidate.age >= $minAge AND candidate.age <= $maxAge " + + + // Constraint: Location Filter + // If input is 'City', we match the city. + // If input is 'State', we match the state. + "AND ( " + + " ($filter = 'City' AND myCity = candCity) OR " + // Same node city + " ($filter = 'State' AND myState = candState) " + // Same node state + ") " + + + // Constraint: Sex & Orientation Compatibility + "AND ( " + + " (me.orientation = 'straight' AND candidate.orientation = 'straight' AND me.sex <> candidate.sex) OR " + + " (me.orientation = 'straight' AND candidate.orientation = 'bisexual' AND me.sex <> candidate.sex) OR " + + " (me.orientation = 'gay' AND candidate.orientation = 'gay' AND me.sex = candidate.sex) OR " + + " (me.orientation = 'gay' AND candidate.orientation = 'bisexual' AND me.sex = candidate.sex) OR " + + " (me.orientation = 'bisexual' AND candidate.orientation = 'gay' AND me.sex = candidate.sex) OR" + + " (me.orientation = 'bisexual' AND candidate.orientation = 'straight' AND me.sex <> candidate.sex) OR " + + " (me.orientation = 'bisexual' AND candidate.orientation = 'bisexual')" + + ") " + + + // Calculation: Common Interests + "WITH me, candidate " + + "OPTIONAL MATCH (me)-[:HAS_INTEREST]->(i:Interest)<-[:HAS_INTEREST]-(candidate) " + + "WITH candidate, count(i) AS commonInterests " + + + // Return the top 10 candidates sorted by common interests + "RETURN candidate._id " + + "ORDER BY commonInterests DESC " + + "LIMIT 10") + List findRecommendations(@Param("userId") String userId, @Param("filter") String filter, + @Param("minAge") int minAge, @Param("maxAge") int maxAge); + + /* + * ANALYTIC: Love Points (input: State) + * 1. Finds Users in Cities of the State + * 2. Counts internal LIKES (User A in City X -> User B in City X) = 1 Point + * 3. Counts internal MATCHES (User A in City X <-> User B in City X) = 3 Points (check on ID to count the couple only once) + * 4. Returns JSON with City, UserCount, LovePoints, LoveRatio. + */ + @org.springframework.data.neo4j.repository.query.Query("MATCH (s:State {name: $stateName})<-[:LOCATED_IN]-(c:City)<-[:LIVES_IN]-(u:User) " + + + + // 1. Internal Likes (User -> Target in the same City) + // Aggregate by user to avoid duplicates + "OPTIONAL MATCH (u)-[l:LIKES]->(target:User)-[:LIVES_IN]->(c) " + + "WITH c, u, count(l) as userLikes " + + + // 2. Internal MATCHED (User <-> Partner in the same City) + // WHERE u._id < partner._id to count the couple only once + "OPTIONAL MATCH (u)-[m:MATCHED]-(partner:User)-[:LIVES_IN]->(c) " + + "WHERE u._id < partner._id " + + "WITH c, u, userLikes, count(m) as userMatches " + + + // 3. Aggregation for each City + "WITH c.name AS city, " + + " count(DISTINCT u) AS users, " + + " sum(userLikes) AS totalLikes, " + + " sum(userMatches) AS totalMatches " + + + // 4. Points count + "WITH city, users, " + + " (totalLikes * 1) + (totalMatches * 3) AS interactions " + + + "WITH city, users, interactions, " + + "CASE WHEN users > 0 THEN toFloat(interactions) / users ELSE 0.0 END AS loveRatio " + + "RETURN { city: city, users: users, interactions: interactions, loveRatio: loveRatio } " + + "ORDER BY loveRatio DESC") + List> getLovePointsAnalytic(@Param("stateName") String stateName); + + @org.springframework.data.neo4j.repository.query.Query("MATCH (u:User {_id: $id})-[:MATCHED]-(m:User) RETURN m") + java.util.List findMatches(@Param("id") String id); + +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/admin/AdminService.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/admin/AdminService.java new file mode 100644 index 000000000..eafea1a2d --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/admin/AdminService.java @@ -0,0 +1,105 @@ +package it.unipi.LoveMining.service.admin; + +import org.springframework.stereotype.Service; +import lombok.RequiredArgsConstructor; + +import it.unipi.LoveMining.repository.mongo.UserMongoRepository; +import it.unipi.LoveMining.repository.mongo.ReviewMongoRepository; +import it.unipi.LoveMining.repository.neo4j.UserNeo4jRepository; +import it.unipi.LoveMining.model.mongo.UserDocument; +import it.unipi.LoveMining.model.mongo.ReviewDocument; +import java.util.Map; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +// Service class for administrative operations like User deletion +public class AdminService { + + private final UserMongoRepository userMongoRepository; + private final UserNeo4jRepository userNeo4jRepository; + private final ReviewMongoRepository reviewMongoRepository; + + // Deletes a user by ID from both MongoDB (with associated reviews) and Neo4j + public void deleteUser(String id) { + + UserDocument user = userMongoRepository.findById(id) // Retrieve the user + .orElseThrow(() -> new IllegalArgumentException("User not found with ID: " + id)); + + // Delete form neo4j first + try { + if (userNeo4jRepository.existsById(id)) { + userNeo4jRepository.deleteById(id); + } + } catch (Exception e) { + System.err.println("Could not delete node from Neo4j."); + throw new RuntimeException("Deletion failed: Graph Database is unavailable."); + } + + // Delete from MongoDB + // 1. Delete Reviews MADE by the user + if (user.getReviewsMade() != null && !user.getReviewsMade().isEmpty()) { + try { + // Extract the list of Review IDs from the user's summary list + List reviewIdsToDelete = user.getReviewsMade().stream() + .map(UserDocument.ReviewSummary::getReviewId).collect(Collectors.toList()); + + // Delete all these reviews from the Review Collection in one go + if (!reviewIdsToDelete.isEmpty()) { + reviewMongoRepository.deleteAllById(reviewIdsToDelete); + } + } catch (Exception e) { + System.err.println("Warning: Failed to delete some reviews made by user: " + e.getMessage()); + } + } + + // 2. Delete Reviews RECEIVED by the user (and update references) + try { + reviewMongoRepository.deleteByTargetId(id); + // Remove references from authors' "reviewsMade" lists + userMongoRepository.removeReviewReferences(id); + } catch (Exception e) { + System.err.println("Warning: Failed to clean up received reviews: " + e.getMessage()); + } + + // 3. Delete the User Document + userMongoRepository.deleteById(id); + System.out.println("User successfully deleted from both databases: " + id); + } + + // Retrieves a user by their ID from MongoDB + public UserDocument getUserById(String id) { + return userMongoRepository.findById(id).orElse(null); + } + + // Retrieves a review by its ID from MongoDB + public ReviewDocument getReviewById(String id) { + return reviewMongoRepository.findById(id).orElse(null); + } + + // Analytics: Love Points + public List> getLovePointsStats(String state) { + return userNeo4jRepository.getLovePointsAnalytic(state); + } + + // Analytics: Glow-up + public List> getBestGlowUpUsers() { + return reviewMongoRepository.findBestGlowUpUsers(); + } + + //Analytics: Unhappy Cities + public List> getUnhappyCities() { + return userMongoRepository.findUnhappyCities(); + } + + // Analytics: Status by Age Group + public List> getStatusAnalytics() { + return userMongoRepository.getStatusAnalytics(); + } + + // Analytics: Orientation by Age Group + public List> getOrientationAnalytics() { + return userMongoRepository.getOrientationAnalytics(); + } +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/authentication/AuthenticationService.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/authentication/AuthenticationService.java new file mode 100644 index 000000000..5c2e1e5bc --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/authentication/AuthenticationService.java @@ -0,0 +1,149 @@ +package it.unipi.LoveMining.service.authentication; + +import org.springframework.stereotype.Service; +import org.springframework.security.crypto.password.PasswordEncoder; + +import it.unipi.LoveMining.model.mongo.UserDocument; +import it.unipi.LoveMining.model.neo4j.CityNode; +import it.unipi.LoveMining.model.neo4j.InterestNode; +import it.unipi.LoveMining.model.neo4j.StateNode; +import it.unipi.LoveMining.model.neo4j.UserNode; +import it.unipi.LoveMining.repository.mongo.UserMongoRepository; +import it.unipi.LoveMining.repository.neo4j.CityNeo4jRepository; +import it.unipi.LoveMining.repository.neo4j.InterestNeo4jRepository; +import it.unipi.LoveMining.repository.neo4j.StateNeo4jRepository; +import it.unipi.LoveMining.repository.neo4j.UserNeo4jRepository; +import it.unipi.LoveMining.service.utility.InterestExtractorService; // Interests +import lombok.RequiredArgsConstructor; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Set; + +@Service +@RequiredArgsConstructor +// Service class for User registration and authentication +public class AuthenticationService { + + private final UserMongoRepository userMongoRepository; + private final UserNeo4jRepository userNeo4jRepository; + private final InterestNeo4jRepository interestNeo4jRepository; + private final CityNeo4jRepository cityNeo4jRepository; + private final StateNeo4jRepository stateNeo4jRepository; + private final PasswordEncoder passwordEncoder; + private final InterestExtractorService interestExtractorService; + + // Registers a new user in both MongoDB and Neo4j databases + public UserDocument registerUser(UserDocument user) { + + user.setId(null); // id null + user.setIsAdmin(false); // isAdmin false + user.setReviewsMade(null); // reviews null + user.setInterests(null); // interests null + if (user.getEmail() == null || !user.getEmail().contains("@")) { // email + throw new IllegalArgumentException("Email is missing or invalid."); + } + if (userMongoRepository.findByEmail(user.getEmail()).isPresent()) { + throw new IllegalArgumentException("Email already in use."); + } + if (user.getPassword() == null || user.getPassword().length() < 4) { // password + throw new IllegalArgumentException("Password must be at least 4 characters long."); + } + if (user.getAge() == null || user.getAge() < 18 || user.getAge() > 100) { // age + throw new IllegalArgumentException("Age must be between 18 and 100."); + } + if (user.getSex() == null || (!user.getSex().equals("m") && !user.getSex().equals("f"))) { // ( "m" / "f") + throw new IllegalArgumentException("Sex must be exactly 'm' or 'f'."); + } + if (user.getOrientation() == null || // ("straight", "gay", "bisexual") + (!user.getOrientation().equals("straight") && + !user.getOrientation().equals("gay") && + !user.getOrientation().equals("bisexual"))) { + throw new IllegalArgumentException("Orientation must be 'straight', 'gay', or 'bisexual'."); + } + // status: not mandatory (unknown if is null), but if it is present: one of the following + if(user.getStatus() != null) { + if (!user.getStatus().equalsIgnoreCase("available") && + !user.getStatus().equalsIgnoreCase("single") && + !user.getStatus().equalsIgnoreCase("seeing someone") && + !user.getStatus().equalsIgnoreCase("married") && + !user.getStatus().equalsIgnoreCase("unknown")) + throw new IllegalArgumentException("Status must be 'available', 'single', 'seeing someone', 'married' or 'unknown'"); + } + else + user.setStatus("unknown"); + + if (user.getCity() == null || user.getCity().trim().isEmpty()) { // City + throw new IllegalArgumentException("The 'city' field is mandatory."); + } + if (user.getState() == null || user.getState().trim().isEmpty()) { // State + throw new IllegalArgumentException("The 'state' field is mandatory."); + } + + // city-state lowercase + user.setCity(user.getCity().trim().toLowerCase()); + user.setState(user.getState().trim().toLowerCase()); + + // Interest Extraction from essay0 + Set totalInterests = new HashSet<>(); + + // Analyze Essay0 to find keyword + if (user.getEssay0() != null && !user.getEssay0().isEmpty()) { + Set extractedInterests = interestExtractorService.extractInterestsFromText(user.getEssay0()); + totalInterests.addAll(extractedInterests); + } + user.setInterests(new ArrayList<>(totalInterests)); + + user.setPassword(passwordEncoder.encode(user.getPassword())); // password Bcrypt + user.setId(new org.bson.types.ObjectId().toHexString()); // id mongo assign + + // MongoDB + UserDocument savedUser = userMongoRepository.save(user); + + // Sync to Neo4j + try { + UserNode userNode = new UserNode(); + userNode.setId(savedUser.getId()); + userNode.setAge(savedUser.getAge()); + userNode.setSex(savedUser.getSex()); + userNode.setOrientation(savedUser.getOrientation()); + + // Handle Interests + if (savedUser.getInterests() != null) { + Set interestNodes = new HashSet<>(); + for (String interestName : savedUser.getInterests()) { + InterestNode interestNode = interestNeo4jRepository.findById(interestName) + .orElse(new InterestNode(interestName)); + interestNodes.add(interestNode); + } + userNode.setInterests(interestNodes); + } + + // Handle City and State + if (savedUser.getCity() != null && !savedUser.getCity().isEmpty()) { + CityNode cityNode = cityNeo4jRepository.findById(savedUser.getCity()) + .orElse(new CityNode(savedUser.getCity(), null)); + + if (savedUser.getState() != null && !savedUser.getState().isEmpty()) { + StateNode stateNode = stateNeo4jRepository.findById(savedUser.getState()) + .orElse(new StateNode(savedUser.getState())); + + stateNode = stateNeo4jRepository.save(stateNode); + cityNode.setState(stateNode); + } + + cityNode = cityNeo4jRepository.save(cityNode); + userNode.setCityNode(cityNode); + } + + userNeo4jRepository.save(userNode); + + } catch (Exception e) { + // Rollback + System.err.println("Rollback: Neo4j sync failed. Deleting user from MongoDB. Error: " + e.getMessage()); + userMongoRepository.deleteById(savedUser.getId()); + throw new RuntimeException("Registration failed due to database synchronization error. Please try again."); + } + return savedUser; + } +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/security/CustomUserDetailsService.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/security/CustomUserDetailsService.java new file mode 100644 index 000000000..3607fff57 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/security/CustomUserDetailsService.java @@ -0,0 +1,34 @@ +package it.unipi.LoveMining.service.security; + +import it.unipi.LoveMining.model.mongo.UserDocument; +import it.unipi.LoveMining.repository.mongo.UserMongoRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + @Autowired + private UserMongoRepository userRepo; + + @Override + public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException { + // 1. Find the user in the DB using the email + UserDocument appUser = userRepo.findByEmail(email) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email)); + + // 2. Define the role: If isAdmin is true -> "ADMIN", otherwise "USER" + String role = Boolean.TRUE.equals(appUser.getIsAdmin()) ? "ADMIN" : "USER"; + + // 3. Return the user to Spring Security + return User.builder() + .username(appUser.getEmail()) + .password(appUser.getPassword()) // The password in the DB must be encrypted (BCrypt) + .roles(role) + .build(); + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/user/UserService.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/user/UserService.java new file mode 100644 index 000000000..628f293bd --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/user/UserService.java @@ -0,0 +1,286 @@ +package it.unipi.LoveMining.service.user; + +import org.springframework.stereotype.Service; + +import it.unipi.LoveMining.model.mongo.ReviewDocument; +import it.unipi.LoveMining.model.mongo.UserDocument; +import it.unipi.LoveMining.repository.mongo.ReviewMongoRepository; +import it.unipi.LoveMining.repository.mongo.UserMongoRepository; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +// Service class for managing User retrieval operations +public class UserService { + + private final UserMongoRepository userMongoRepository; + private final ReviewMongoRepository reviewMongoRepository; + private final it.unipi.LoveMining.repository.neo4j.UserNeo4jRepository userNeo4jRepository; + private final it.unipi.LoveMining.service.utility.InterestExtractorService interestExtractorService; + private final org.springframework.security.crypto.password.PasswordEncoder passwordEncoder; + + // Retrieves a user by their ID from MongoDB + public UserDocument getUser(String id) { + return userMongoRepository.findById(id).orElse(null); + } + + // Retrieves a user by their Email from MongoDB + public UserDocument getUserByEmail(String email) { + return userMongoRepository.findByEmail(email).orElse(null); + } + + // Retrieves matches (UserNodes) from Neo4j for a given User ID + public java.util.Set getUserMatches(String id) { + return new java.util.HashSet<>(userNeo4jRepository.findMatches(id)); + } + + // Retrieves reviews made by the user from MongoDB + public java.util.List getUserReviews(String id) { + UserDocument user = getUser(id); + if (user != null) { + return user.getReviewsMade(); + } + return java.util.Collections.emptyList(); + } + + // Updates a user in MongoDB and Neo4j + @org.springframework.transaction.annotation.Transactional + public void updateUser(String id, UserDocument inputUser) throws Exception { + + UserDocument userDoc = getUser(id); + if (userDoc == null) + throw new Exception("User not found in MongoDB"); + + // Check if user exists in Neo4j + if (!userNeo4jRepository.existsById(id)) { + throw new Exception("User node not found in Graph"); + } + + // Flags to track what needs to be synced to Neo4j + boolean updateBasicInfo = false; + boolean updateInterests = false; + boolean updateLocation = false; + + // Validation (Age, Sex, Orientation, Status) + // Use inputUser as the source of "new" values + Integer newAge = inputUser.getAge(); + String newSex = inputUser.getSex(); + String newOrientation = inputUser.getOrientation(); + + if (newAge != null) { // age validation + if (newAge < 18 || newAge > 100) + throw new Exception("Age must be between 18 and 100"); + userDoc.setAge(newAge); + updateBasicInfo = true; + } + + if (newSex != null) { // sex validation + if (!newSex.equalsIgnoreCase("m") && !newSex.equalsIgnoreCase("f")) + throw new Exception("Sex must be 'm' or 'f'"); + userDoc.setSex(newSex); + updateBasicInfo = true; + } + + if (newOrientation != null) { // orientation validation + if (!newOrientation.equalsIgnoreCase("straight") && + !newOrientation.equalsIgnoreCase("gay") && + !newOrientation.equalsIgnoreCase("bisexual")) + throw new Exception("Orientation must be 'straight', 'gay', or 'bisexual'"); + userDoc.setOrientation(newOrientation); + updateBasicInfo = true; + } + + if (inputUser.getStatus() != null) { // status validation + if (!inputUser.getStatus().equalsIgnoreCase("available") && + !inputUser.getStatus().equalsIgnoreCase("single") && + !inputUser.getStatus().equalsIgnoreCase("seeing someone") && + !inputUser.getStatus().equalsIgnoreCase("married") && + !inputUser.getStatus().equalsIgnoreCase("unknown")) + throw new Exception("Status must be 'available', 'single', 'seeing someone', 'married' or 'unknown'"); + userDoc.setStatus(inputUser.getStatus().toLowerCase().trim()); + } + + // Password update + if (inputUser.getPassword() != null && !inputUser.getPassword().isEmpty()) { + if (inputUser.getPassword().length() < 4) { + throw new Exception("New password must be at least 4 characters long."); + } + userDoc.setPassword(passwordEncoder.encode(inputUser.getPassword())); + } + + // Simple Fields Update (Mongo only) + if (inputUser.getBodyType() != null) + userDoc.setBodyType(inputUser.getBodyType()); + if (inputUser.getDiet() != null) + userDoc.setDiet(inputUser.getDiet()); + if (inputUser.getDrinks() != null) + userDoc.setDrinks(inputUser.getDrinks()); + if (inputUser.getEducation() != null) + userDoc.setEducation(inputUser.getEducation()); + if (inputUser.getEthnicity() != null) + userDoc.setEthnicity(inputUser.getEthnicity()); + if (inputUser.getHeight() != null) + userDoc.setHeight(inputUser.getHeight()); + if (inputUser.getIncome() != null) + userDoc.setIncome(inputUser.getIncome()); + if (inputUser.getJob() != null) + userDoc.setJob(inputUser.getJob()); + if (inputUser.getOffspring() != null) + userDoc.setOffspring(inputUser.getOffspring()); + if (inputUser.getPets() != null) + userDoc.setPets(inputUser.getPets()); + if (inputUser.getSmokes() != null) + userDoc.setSmokes(inputUser.getSmokes()); + if (inputUser.getSpeaks() != null) + userDoc.setSpeaks(inputUser.getSpeaks()); + + // Essay & Interest Matching Logic + String newEssay = inputUser.getEssay0(); + if (newEssay != null && !newEssay.trim().isEmpty()) { + userDoc.setEssay0(newEssay); // Essay + + // Extract new interests + java.util.List foundInterests = new java.util.ArrayList<>( + interestExtractorService.extractInterestsFromText(newEssay)); + + userDoc.setInterests(foundInterests); + updateInterests = true; + } + + // City & State Logic + String newCity = inputUser.getCity(); + String newState = inputUser.getState(); + boolean cityChanged = newCity != null && !newCity.trim().isEmpty(); + boolean stateChanged = newState != null && !newState.trim().isEmpty(); + + if (cityChanged || stateChanged) { + String targetCityName = cityChanged ? newCity : userDoc.getCity(); + String targetStateName = stateChanged ? newState : userDoc.getState(); + + if (targetCityName == null || targetStateName == null) { + throw new Exception("City and State must be both present to update location."); + } + + userDoc.setCity(targetCityName); + userDoc.setState(targetStateName); + updateLocation = true; + } + + // Update Neo4j first + try { + if (updateBasicInfo) { + userNeo4jRepository.updateBasicProfile(id, userDoc.getAge(), userDoc.getSex(), + userDoc.getOrientation()); + } + if (updateInterests) { + userNeo4jRepository.updateInterests(id, userDoc.getInterests()); + } + if (updateLocation) { + userNeo4jRepository.updateLocation(id, userDoc.getCity(), userDoc.getState()); + } + } catch (Exception e) { + System.err.println("CRITICAL: Graph Sync Failed. Aborting MongoDB update. Error: " + e.getMessage()); + throw new Exception("Failed to update profile due to Graph Database error."); + } + // Save to Mongo + userMongoRepository.save(userDoc); + } + + // Add a review in MongoDB + @org.springframework.transaction.annotation.Transactional + public void addReview(String authorId, String targetId, ReviewDocument reviewInput) throws Exception { + + // Check input + if (authorId.equals(targetId)) { + throw new Exception("Cannot review himself."); + } + + // Check if targetId is in MATCHEDs + boolean isMatched = userNeo4jRepository.areMatched(authorId, targetId); + + if (!isMatched) { + throw new Exception("Cannot review an User not matched."); + } + + // Check if target user is already been reviewed + UserDocument authorDoc = userMongoRepository.findById(authorId) + .orElseThrow(() -> new Exception("MongoDB User profile not found.")); + + if (authorDoc.getReviewsMade() != null) { + for (UserDocument.ReviewSummary summary : authorDoc.getReviewsMade()) { + if (summary.getTargetId().equals(targetId)) { + throw new Exception("Already reviewed. Cannot review a user twice."); + } + } + } + + Integer score = reviewInput.getRating(); + + // Check if rating is valid + if (score == null || score < 1 || score > 5) { + throw new Exception("Rating must be between 1 and 5."); + } + + // Save on Collection 'reviews' + reviewInput.setId(null); // Genera nuovo ID + reviewInput.setTargetId(targetId); + reviewInput.setDate(LocalDateTime.now()); + + ReviewDocument savedReview = reviewMongoRepository.save(reviewInput); + + // Update UserDocument (add a 'reviewsMade') + UserDocument.ReviewSummary summary = new UserDocument.ReviewSummary(); + summary.setReviewId(savedReview.getId()); + summary.setTargetId(targetId); + summary.setRating(savedReview.getRating()); + + if (authorDoc.getReviewsMade() == null) { + authorDoc.setReviewsMade(new java.util.ArrayList<>()); + } + + authorDoc.getReviewsMade().add(summary); + + // Save Mongo + userMongoRepository.save(authorDoc); + } + + // Likes a User + @org.springframework.transaction.annotation.Transactional + public String likeUser(String actorId, String targetId) throws Exception { + if (actorId.equals(targetId)) { + throw new Exception("Cannot like himself."); + } + if (userNeo4jRepository.areMatched(actorId, targetId)) { + return "Already matched."; + } + + boolean targetLikesActor = userNeo4jRepository.hasLiked(targetId, actorId); // bi-directional like ? + if (targetLikesActor) { + // MATCH! + userNeo4jRepository.transformLikeToMatch(actorId, targetId); + return "It's a Match!"; + } else { + userNeo4jRepository.createLikeRelationship(actorId, targetId); + return "Like send."; + } + } + + // Dislikes a User + @org.springframework.transaction.annotation.Transactional + public void dislikeUser(String actorId, String targetId) throws Exception { + if (actorId.equals(targetId)) { + throw new Exception("Cannot dislike himself."); + } + + userNeo4jRepository.createDislikeRelationship(actorId, targetId); + } + + // Recommendations with filters on location ('City' or 'State') and age + public List getRecommendations(String userId, String filter, int minAge, int maxAge) { + // We pass the filter string directly ('City' or 'State') to the query + return userNeo4jRepository.findRecommendations(userId, filter, minAge, maxAge); + } +} diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/utility/InterestExtractorService.java b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/utility/InterestExtractorService.java new file mode 100644 index 000000000..7a91d80ca --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/java/it/unipi/LoveMining/service/utility/InterestExtractorService.java @@ -0,0 +1,83 @@ +package it.unipi.LoveMining.service.utility; + +import jakarta.annotation.PostConstruct; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +@Service +public class InterestExtractorService { + + private final Map interestsMap = new HashMap<>(); + + @PostConstruct + public void loadInterestsFromCsv() { + try { + ClassPathResource resource = new ClassPathResource("interessi.csv"); + BufferedReader reader = new BufferedReader( new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)); + String line; + // skip first line if present: (Display_Name;Original_Variants) + // reader.readLine(); + + while ((line = reader.readLine()) != null) { + if (line.trim().isEmpty()) continue; + + // 1. Split Display_Name from Original_Variants by ; + String[] parts = line.split(";"); + if (parts.length < 2) continue; + + String displayName = parts[0].trim(); + String variantsString = parts[1].trim(); + + // 2. Slit variants by , + String[] variants = variantsString.split(","); + + for (String variant : variants) { + String cleanVariant = variant.trim().toLowerCase(); + if (!cleanVariant.isEmpty()) { + // map variant->display_name + interestsMap.put(cleanVariant, displayName); + } + } + } + + } catch (Exception e) { + System.err.println("ERROR loading file interessi.csv: " + e.getMessage()); + } + } + + public Set extractInterestsFromText(String text) { + Set foundInterests = new HashSet<>(); + + if (text == null || text.isEmpty()) { + return foundInterests; + } + + String lowerCaseText = text.toLowerCase(); + + for (Map.Entry entry : interestsMap.entrySet()) { + String variant = entry.getKey(); + String displayName = entry.getValue(); + + // "\\b" for "Word Boundary" + String regex = "\\b" + Pattern.quote(variant) + "\\b"; + + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(lowerCaseText); + + if (matcher.find()) { + foundInterests.add(displayName); + } + } + return foundInterests; + } +} \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/resources/application.properties b/jdk_21_maven/cs/rest/lovemining/src/main/resources/application.properties new file mode 100644 index 000000000..dba8b3801 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/resources/application.properties @@ -0,0 +1,6 @@ +spring.application.name=LoveMining + +# Neo4j VM Configuration +spring.neo4j.uri=bolt://10.1.1.14:7687 +spring.neo4j.authentication.username=neo4j +spring.neo4j.authentication.password=LoveMining \ No newline at end of file diff --git a/jdk_21_maven/cs/rest/lovemining/src/main/resources/interessi.csv b/jdk_21_maven/cs/rest/lovemining/src/main/resources/interessi.csv new file mode 100644 index 000000000..22015c34d --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/main/resources/interessi.csv @@ -0,0 +1,432 @@ +Display_Name;Original_Variants +academy;academy +activist;activist +actor;actor +adult;adult +adventure;adventure +african;african +alcohol;alcohol +ambition;ambition +american;american +amusement;amusement +animal;anima, animal +anthropology;anthropology +apple;apple +aquarium;aquarium +archery;archery +architecture;architecture +art;art, artist, art gallery +asian;asian +attraction;attraction +bachelor;bachelor +bacon;bacon +badminton;badminton +ball;ball +band;band +banjo;banjo +bar;bar +barcelona;barcelona +bartender;bartender +baseball;baseball +basketball;basketball +bass;bass +batman;batman +bay;bay +bear;bear +beauty;beauty +bed;bed +beer;beer +bicycle;bicycle +bike;bike, bike rid, bike riding +biology;biology +bird;bird +bisexual;bisexual +boat;boat +bolivia;bolivia +book;book, good book, bookstore, read book, comic book +boss;boss +bourbon;bourbon +brain;brain +brand;brand +brazilian;brazilian +bread;bread +breakfast;breakfast +bridge;bridge +brilliant;brilliant +brunch;brunch +buddhism;buddhism +buddhist;buddhist +bungee;bungee +butterfly;butterfly +cake;cake +calm;calm +camera;camera +camp;camp +cancer;cancer +candy;candy +capitalism;capitalism +car;car +caribbean;caribbean +carpenter;carpenter +cartoon;cartoon +cat;cat +catholic;catholic +challenge;challenge +character;character +charm;charm +cheese;cheese +chef;chef +chemistry;chemistry +chess;chess +chicken;chicken +childhood;childhood +chill;chill +chivalry;chivalry +chocolate;chocolate, dark chocolate +christian;christian +christmas;christmas +church;church +cigarette;cigarette +cinema;cinema +classy;classy +cliff;cliff +clothe;clothe, clothing +cloud;cloud +club;club +coach;coach +coast;coast +cocktail;cocktail +coffee;coffee +collection;collection +comedy;comedy +commitment;commitment +communication;communicator, communication +community;community +company;company +computer;computer, computer science +concert;concert +conversation;conversation +countryside;countryside +cow;cow +cream;cream +creativity;creativity, creativo writing +crime;crime +culture;culture, new culture +cupid;cupid +cycling;cycle, cycling, cyclist +dancing;dancer, dancing, dance floor +degree;degree +desert;desert +design;design, designer, graphic design, graphic designer +devil;devil +dining;dine, dining +dinner;dinner +dinosaur;dinosaur +diversity;diversity +doctor;doctor +documentary;documentary +dog;dog +drama;drama +drug;drug +drum;drum +economy;economy, economist +education;educator, education +ego;ego +electronic music;electronic music +energy;energy +engineer;engineer, engineering, software engineer +english;english +enigma;enigma +entertainment;entertain, entertainment +entrepreneur;entrepreneur +environment;environment +essence;essence +european;european +everyday life;everyday life +exchange;exchange +exercise;exercise +explore;explore, explorer, exploration +fairy;fairy +fantasy;fantasy +farm;farm, farmer +fashion;fashion +feminist;feminist +festival;festival +fiction;fiction +fighter;fighter +finance;finance +fishing;fish, fishing +fitness;fitness +flight;flight +florence;florence +flower;flower +folk;folk +food;food, new food +fool;fool +foot;foot, football +forest;forest +freak;freak +free spirit;free spirit +freedom;freedom +friendship;friendship +fruit;fruit +funk;funk +gadget;gadget +games;game, games +garden;garden +geek;geek +generosity;generosity +gentleman;gentleman +geography;geography +german;german +ghost;ghost +glass;glass +golf;golf +good friend;good friend +good medicina;good medicina +good sense of humor;good sense of humor +government;government +graphic;graphic +grass;grass +gun;gun +gym;gym +hard working;hard work, hard worker, hard working +harmony;harmony +health;health +heaven;heaven +high school;high school +hiker;hiker +hill;hill +hip hop;hip hop +history;history +hockey;hockey +holiday;holiday +horse;horse, horseback +hospital;hospital, hospitality +hot tub;hot tub +hug;hug +human;human, humanity +humility;humility +humour;humour +hunt;hunt +ice;ice +ideal match;ideal match +indian;indian +industry;industry, tech industry +inspiration;inspiration +intimacy;intimacy +irony;irony +island;island +italian;italian +japanese;japanese +jazz;jazz +jet;jet +jewelry;jewelry +jewish;jewish +joke;joke +journal;journal, journalist, journalism +journey;journey +judge;judge +jungle;jungle +justice;justice +karaoke;karaoke +kinky;kinky +kitchen;kitchen +lake;lake +landscape;landscape +language;language +latin;latin +law;law +lawyer;lawyer +leader;leader +leather;leather +lesbian;lesbian +library;library +linguistic;linguistic +lion;lion +live music;live music +long run;long run +lover;lover +magic;magic +mall;mall +management;manager, management +manga;manga +marathon;marathon +marina;marina +marketing;market, marketing +martial art;martial art +massage;massage +math;math, mathematics +meat;meat +mechanical;mechanic, mechanical +meditation;meditation +meet new people;meet new people +meet people;meet people +mexican;mexican +military;military +mirror;mirror +model;model +moderation;moderation +monkey;monkey +motorcycle;motorcycle +mountain;mountain +movie;movie, movies +museum;museum, art museum +music;music, musician +nap;nap +natural world;natural world +nepal;nepal +new friend;new friend +new job;new job +new people;new people +new place;new place +new recipe;new recipe +new yorker;new yorker +news;news +nonfiction;nonfiction +nonsmoker;nonsmoker +novel;novel +nurse;nurse, nursing +ocean;ocean +office;office +open mind;open mind +opera;opera +outdoor activity;outdoor activity +paint;paint, painter +parking;parking, golden gate park +party;party +peace;peace +peninsula;peninsula +performance;performer, performance +personality;dog person, personality, well person, happy person, social person +pet;pet +philosophy;philosophy +photo;photo, photograph, photography, photographer +physics;physics, physical activity +piano;piano +picnic;picnic +pig;pig +pilot;pilot +pizza;pizza +plane;plane, planet +plant;plant +playfulness;playfulness +poet;poet, poetry +poker;poker +politic;politic +pool;pool +pop cultura;pop cultura +positivity;positivity, positivo attitude +prince;prince +programmer;programmer +psychology;psychology +pub;pub +punk;punk +puppy;puppy +puzzle;puzzle +quiet night;quiet night +radio;radio +rain;rain +rap;rap +real life;real life +realist;realist +redwood;redwood +religion;religion +republic;republic, republican +researcher;research, researcher +restaurant;restaurant +rhythm;rhythm +river;river +robot;robot +rock climbing;rock climbing +romance;romance +runner;runner +russian;russian +sailing;sail, sailor, sailing +san franciscan;san franciscan +sarcastic sense of humor;sarcastic sense of humor +science;science +scientist;scientist +scorpio;scorpio +scotch;scotch +sculpture;sculpture +sea;sea +security;security +series;series +sex;sex +sexuality;sexuality +shark;shark +shoe;shoe +shop;shop, coffee shop +sign;sign +sincerity;sincerity +singer;singer +ski;ski, skier +skill;skill +sleep;sleep +small group;small group +smoker;smoker +snow;snow +soccer;soccer +sociology;sociology +softball;softball +song;song +space;space +spanish;spanish +spice;spice +spicy;spicy +sport;sport, sport event +star;star +stone;stone +store;store +street;street +stress;stress +study;study +style;style +stylish;stylish +suit;suit +sunset;sunset +surf;surf +symphony;symphony +tea;tea +tech;tech, technology +television;television +tell story;tell story +tennis;tennis, play tennis +thai;thai +theater;theater +thunderstorm;thunderstorm +trade;trade +traffic;traffic +travel;travel, traveler +treasure;treasure +tree;tree +trip;trip +truck;truck +ultimato frisbee;ultimato frisbee +universe;universe +vacation;vacation +vegetarian;vegetarian +video;video, video game +vintage;vintage +violin;violin +vodka;vodka +volleyball;volleyball +walking;walking +war;war +warcraft;warcraft +waterfall;waterfall +weather;weather +web;web +weed;weed +whiskey;whiskey +wife;wife +wilderness;wilderness +wind;wind +wine;wine, winery +wisdom;wisdom +wood;wood +workout;workout +writer;writer +yoga;yoga +zoo;zoo diff --git a/jdk_21_maven/cs/rest/lovemining/src/test/java/it/unipi/LoveMining/LoveMiningApplicationTests.java b/jdk_21_maven/cs/rest/lovemining/src/test/java/it/unipi/LoveMining/LoveMiningApplicationTests.java new file mode 100644 index 000000000..d07967f73 --- /dev/null +++ b/jdk_21_maven/cs/rest/lovemining/src/test/java/it/unipi/LoveMining/LoveMiningApplicationTests.java @@ -0,0 +1,13 @@ +package it.unipi.LoveMining; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class LoveMiningApplicationTests { + + @Test + void contextLoads() { + } + +}